NDK开发入门
date
Apr 3, 2022
slug
ndk101
status
Published
tags
ndk
android
summary
ndk101
type
Post
用途NDK构建系统JNIEnv如何获取当前线程的 JNIEnv那如何在 native 部分创建的线程中使用特定的 JNIEnv 呢?静态注册和动态注册静态注册方法动态注册方法静态函数和普通函数区别在 c/c++ 中使用 java 的参数和函数实用 ndk-gdb 进行调试使用NDK开发时导入第三方动态库需要注意的步骤jbyteArray 和 jbyte 的应用signatures 规范Type Signatures异常处理DirectByteBuffer使用(NIO support)引用的管理在native使用自定义java类的限制 TODOndk build 命令行使用参考
用途
JNI 指 Java Native Interface,用途是让Java能够与 C/C++ 代码进行交互,能够调用调用用C/C++的软件库。
NDK构建系统
NDK提供了ndk-build和CMake的支持。
能够通过NDK与其它项目的构建系统结合,来跨平台编译出目标平台的so或者.a
详细参考参照:
JNIEnv
代表了在 Native 中的 java 环境,JNI 函数的第一个参数都是
JNIEnv* 通过JNIEnv* 指针就能够对java端的代码进行操作,这个类的一些基本函数NewObject: 创建Java类中的对象。
NewString: 创建Java类中的String对象。
NewArray: 创建类型为Type的数组对象。
GetField: 获取类型为Type的字段。
SetField: 设置类型为Type的字段的值。
GetStaticField: 获取类型为Type的static的字段。
SetStaticField: 设置类型为Type的static的字段的值。
CallMethod: 调用返回类型为Type的方法。
CallStaticMethod: 调用返回值类型为Type的static 方法。
JNIEnv 只在创建它的线程有效,无法跨线程传递,各个线程的 JNIEnv 也是独立的。还有一个JavaVM的结构体,则是进程共享的。
如何获取当前线程的 JNIEnv
需要通过JavaVM来获取,如下
那如何在 native 部分创建的线程中使用特定的 JNIEnv 呢?
因为JNIEnv是线程之间独立的,如果要在另外的 native 线程中获取到对应的 JNIEnv,可以通过传递 JavaVM ,之后通过 JavaVM 来获取 JNIEnv,这也是能够在新的 thread 中调用 java 函数的基础。
静态注册和动态注册
要将 java 中的 native 关键字修饰的方法和 native 中的函数绑定起来,有静态和动态两种方式。
静态注册方法
静态注册需要遵循以下几个规则
- native函数定义的返回值部分,需要有JNIEXPORT和JNICALL来修饰
- native函数名的命名方式应该是
Java_包名_类名_java函数名,其中包名中的.都需要替换成_,如果java函数名含有下划线,native函数名应该用下划线加数字来代替。
例子如下
动态注册方法
使用动态注册方式,不需要像静态一样严格按照命名方式来,需要做的事情主要是:
- 定义
JNINativeMethod数组记录native和java函数的对应关系
- 实现
JNI_OnLoad函数,这是在动态加载后第一个执行的函数
- 调用
FindClass函数,获取到java对象
- 调用
RegisterNatives,传入java对象和JNINativeMethod数组以及数组长度。
以android_media_MediaPlayer.cpp的实现为例
静态函数和普通函数区别
在 c/c++ 中使用 java 的参数和函数
关键是要获取到有效的JNIEnv
实用 ndk-gdb 进行调试
//todo
使用NDK开发时导入第三方动态库需要注意的步骤
- CMakeList 中的代码修改,参考https://cmake.org/documentation/会有更详细的解释
- app级别的build.grade文件的修改
jbyteArray 和 jbyte 的应用
signatures 规范
Type Signatures
The JNI uses the Java VM’s representation of type signatures. Table 3-2 shows these type signatures.
Type Signature | Java Type | 备注 |
Z | boolean | ㅤ |
B | byte | ㅤ |
C | char | ㅤ |
S | short | ㅤ |
I | int | ㅤ |
J | long | ㅤ |
F | float | ㅤ |
D | double | ㅤ |
L fully-qualified-class ; | fully-qualified-class | 类 |
[ type | type[] | 数组类型 |
( arg-types ) ret-type | method type | 函数类型 |
For example, the Java method:
long f (int n, String s, int[] arr);has the following type signature:
(ILjava/lang/String;[I)J异常处理
调用java函数的时候可能会发生异常,可以用以下的函数来判断异常是否发生以及之后的处理。
- ExceptionOccurred
有异常发生,需要通过ExceptionClear来清理或者java部分来catch,返回的是jthrowable,所以需要清理局部引用,可以用在这个函数可能抛出多个异常的情况下
- ExceptionDescribe
打印异常信息
- ExceptionClear
清除当前抛出中的异常
- ExceptionCheck
检查当前是否有异常发生,返回的是jboolean
最佳实践最好是先用
ExceptionCheck检查是否有异常发生,ExceptionOccurred作为可选操作,用于捕获识别出异常的类型,ExceptionDescribe和ExceptionClear最好在ExceptionCheck之后立刻调用,如果晚一点就可能让上层出现crashDirectByteBuffer使用(NIO support)
提供了三个函数
NewDirectByteBuffer
创建DirectByteBuffer,返回jobject
GetDirectBufferAddress
获取buffer内存起始地址指针。
GetDirectBufferCapacity
获取 capcity
引用的管理
JNI的引用有两种,全局引用和局部引用,通过
NewGlobalRef创建的全局引用不会被GC回收,需要手动释放。而FindClass,NewObject创建的都为局部引用。传递给原生方法的每个参数,以及 JNI 函数返回的几乎每个对象都属于“局部引用”。
请注意,
jfieldID 和 jmethodID 属于不透明类型,不是对象引用,且不应传递给 NewGlobalRef。函数返回的 GetStringUTFChars 和 GetByteArrayElements 等原始数据指针也不属于对象。(这些指针可以在线程之间传递,并且在匹配的 Release 调用完成之前一直有效。)在java调用native函数时创建的局部引用,会在这个函数返回时全部无效,所以这时候用全局变量的形式来保存是不行的。
在新创建的线程中创建的局部引用,一定要手动释放。不然会超过jvm中的最大引用数量(按谷歌的说法默认是16个,可以扩展),引发问题,同时也要避免过多的局部引用同时存在,使用完需要尽早释放。
在 Android 8.0 之前的 Android 版本中,局部引用的数量上限取决于版本特定的限制。从 Android 8.0 开始,Android 支持无限制的局部引用。
jobject和jclass可以用Deletexxx的函数来释放,有一些不同的数据结构要用特定的函数。
获取函数 | 释放函数 |
GetStringUTFChars | ReleaseStringUTFChars |
ㅤ | ㅤ |
ㅤ | ㅤ |
在native使用自定义java类的限制 TODO
正常来说,只要有JNIEnv就可以通过FindClass来获取到Java类的反射对象,但是如果这个Java类是自定义的类,就会有限制。
ndk build 命令行使用
这些是参数
