NDK
NDK开发优点
- NDK是一系列工具的集合
- NDK提供了一份稳定、功能有限的API库
- C/C++甚至汇编的代码可以通过NDK的工具链最终编译生成动态库,最后通过JNI完成和Dalvik/ART虚拟机环境中的Java代码的交互。
使用NDK开发的so不再具有跨平台特性,需要编译提供不同平台支持ABI:ApplicationBinary Interface。
NDK开发缺点
- 开发效率低
- 开发难度大
- 稳定性难以保证
编写第一个含ndk开发的app
new Project选择
Native C++
Next
Next
Finsh。
可以看到项目的默认全貌是这样的。
cpp包下就是我们要编写的JNI代码存放的地方,native-lib.cpp是默认生成的cpp文件,先挨个解释一下
JNIEXPORT
JNIEXPORT是一个宏定义,其作用为告知编译器保留其函数名称,即保留导出函数名。只要设置这个之后我们可以通过IDA可以直接的搜索到其函数名。
extern “C”
告诉编译器该函数按照C函数的形式进行编译,为什么强调是按照C函数的形式进行编译?因为C++函数名会进行name mangling
操作,该函数名被编译的时候会跟我们编写的时候不一样,以下给出一个简单的示例
c++:
c:
android studio帮我们生成的函数是一个静态注册的函数,静态注册的函数有几个特征。
其函数名为Java_包名_类名_函数名为函数的名称
,并且在相应的Java类当中还能找到其加载的相应so与静态注册的函数。
static native
和非静态函数不同的是,由于静态函数可以直接通过类名进行直接的调用,所以和非静态函数相比其第二个参数不再是this,也就是jobject,而应该是jclass
对于一个JNI函数来说,其参数肯定是不少于两个的,从第三个开始才是JNI函数自己的参数。
JNIEnv*
Java Native Interface Environment
,JNI接口指针,指向本地方法的一个函数表,该函数表中的每一个成员指向了一个JNI函数![]()
JNIEnv表示Java调用native语言的环境,是一个封装了大量JNI方法的指针。
JNIEnv只在创建它的线程生效,如需要访问JNI,必须调用AttachCurrentThread关联,并使用DetachCurrentThread解除链接
引入C头文件
引入一个C写的头文件是有讲究的,前面有提到函数要编译成C函数需要添加extern “C”,而引入C的.h(即头文件)有要注意的点么?有!
以下给出一个示例
1 | extern "C"{ |
日志的打印
- 先引入
#include <android/log.h>
- 找到
int __android_log_print(int prio, const char* tag, const char* fmt, ...)
该函数用于直接的日志打印
第一个参数: 按需从以下参数当中选择(包含在<android/log.h>当中):
1 | /** For internal use only. */ |
第二个参数: tag
第三个参数则为要输出的内容
编写一个含引入C头文件的程序并调用
以下结合以上的知识点给出示例截图
tips:
添加一个源码文件需要让其在CMakeLists.txt当中的add_library
里添加具体的源码文件名.后缀
JNI的基本数据类型
一个小demo
JNI引用数据类型
JNI引用数据类型的继承关系
JNI常用的字符串操作
jstring NewStringUTF(const char* bytes)
函数使用给定的C字符串创建一个新的JNI字符串(jstring),不能为构造的java.lang.String分配足够的内存,则会抛出OutOfMemoryError异常,并返回null
const char* GetStringUTFChars(jstring string,jboolean* isCopy)
函数可用于从给定的Java的jstring创建新的C字符串并且要为新诞生的UTF-8字符串分配内存,如果无法分配内存,则该函数返回NULL。该操作有可能因为内存太少失败,失败时会返回NULL并抛出OutOfMemoryError异常,在不使用该函数返回的字符串时,需要释放内存和引用以便对其进行垃圾回收,因此需要始终调用ReleaseStringUTFChars()
jsize GetStringUTFLength(jstring string)
用于获取jstring的长度。
常用字符串操作释义
demo
JNI调用Java
JNI创建Java对象
有两种方法
- NewObject
示例1
2
3
4
5
6jclass jclazz = env->FindClass("com/example/juziss/ReflectionClass");
jmethodID jmethodId = env->GetMethodID(jclazz, "<init>", "()V");
jobject obj = env->NewObject(jclazz, jmethodId);
if (obj != nullptr){
__android_log_print(ANDROID_LOG_INFO, "juzissJNI", "JNI init Java class sucess");
}
- 使用AllocObject创建Java对象
使用AllocObject可以根据传入的jclass创建一个Java对象,但是该对象的状态是非初始化的,在使用这个对象之前必须要用CallNovirtualVoidMethod来进行初始化,这样可以延迟构造函数的调用,该方式使用的很少。
1 | jclass jclazz = env->FindClass("com/example/juziss/ReflectionClass"); |
类描述符
将原报名+类名改成原包名的.改成/ + 类名
,例如com/example/juziss/ReflectionClass
函数描述符
将参数类型的域描述符按照申明顺序放入一对括号中后跟返回值类型的域描述符例如public static void publicStaticMethod() -> ()V ;例如int func(int a, Object obj) -> (ILjava/lang/Object;)I
JNI调用Java其实符合反射调用的步骤流程
找到需要调用的类
env-> FindClass(“xx/xx/xx”)获取想要调用的类的属性id
获取想要调用的类的方法ID
调用
方法调用时env->Call开头的函数,属性调用env->GetxxField,属性调用和函数调用无需考虑Java反射当中的安全检查!
示例:
tips:
要想调用一个jobject对象的属性可以使用GetObjectXXX/SetObjectXXX
获取Java数组长度
env->GetArrayLength(jarray)
得到数组的指针
env->GetIntArrayElements(jarray, 0);
遍历数组中的每一个元素
1 | for (int i = 0;i < length;i++){ |
CallXXMethodA与CallXXMethodV与CallXXMethod函数的区别
CallXXMethodA:
CallXXMethod与CallXXMethodV:
可以看到CallXXMethod最终调用的是“V”,而“A”跟前两个的区别在于其传参的形式不一样而已。
动态注册
JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *unused)
动态注册主要依赖于JNI_Onload
函数进行函数的注册,该函数被调用的时机早于静态注册的函数,其在System.loadLibary/load这两个函数执行时将被执行,所以时机早于静态注册。
- 定义
通过RegisterNatives
方法手动完成native方法和so中的方法的绑定,这样虚拟机就可以通过这个函数映射关系直接找到相应的方法了。
RegisterNatives函数的使用
返回值:成功则返回JNI_OK(0),失败则返回一个负值。
需要三个参数:要注册的jni函数所属的jclass, JNINativeMethod数组指针,注册的jni函数个数。
is_fast
该变量的意思是jni函数在运行过程中是否要进行线程的切换。
ArtMethod
- PrettyMethod():
该函数可以获取JNI函数名。
比JNI_OnLoad执行还早的两个函数
constructor标记的是定义该函数是在init_array当中注册的。init_array比init执行慢一点,可以在init_array可以放一堆函数,而_init只能注册一个函数。
constructor还可以标记注册顺序。
JavaVM
JavaVM是虚拟机在JNI层的代表,一个进程只有一个JavaVM,所有的线程共用一个JavaVM。
- JNIInvokeInterface_结构封装和JVM相关的功能函数,如销毁JVM,获得当前线程的Java执行环境。
- 在C和C++中JavaVM的定义有所不同,在C中JavaVM是JNIInvokeInterface_类型指针,而在C++中又对JNIInvokeInterface_进行了一次封装。
获取JavaVM
在JNI_OnLoad中作为参数获得,该函数由ART负责自动化查找和传入参数进行调用。
通过JNIEnv的
GetJavaVM
函数来获取。
获取JNIEnv
- 如果当前线程已绑定了一个JNIEnv,可以通过JavaVM获取
1
2
3
4JNIEnv* env;
if (vm->GetEnv((void**)&env, JNI_VERSION_1_6)==JNI_OK){
...
} - 通过JNI函数的参数传入获取,对于任何一个JNI函数来说,第一个参数都是JNIEnv指针
- 在非主线程中时,需要通过当前进程的JavaVM调用AttachCurrentThread(&env, NULL)来获取。(获取代码可参考第一个方法)
tips:
- JNIEnv是与一个ClassLoader绑定的,当使用env->FindClass()进行类的查询和加载时,便是使用这个ClassLoader。
- JNIEnv是当前Java线程的执行环境,一个JVM对应一个JavaVM结构,而一个JVM中可能创建多个Java线程,使用pthread_create新建的线程当使用AttachCurrentThread来获取JNIEnv后,该JNIEnv的ClassLoader并不是主线程的ClassLoader,因此无法加载App自己的Class
全局引用、局部引用、弱全局引用
局部引用
通过NewLocalRef和各种JNI接口创建(FindClass、NewObject、GetObjectClass、NewCharArray等)。会阻止GC回收所引用的对象。局部引用只能在当前函数中使用,函数返回后局部引用所引用的对象会被JVM自动释放,或调用DeleteLocalRef手动释放。 因此,局部引用不能跨函数使用,不能跨线程使用。
tips:
Java层函数在调用本地jni代码的时候,会维护一个局部引用表(该引用表并不是无限的),一般jni函数调用结束后,ART会释放这个引用,如果是简单的函数就不需要注意这些问题,让他自己释放,基本没有什么问题,但是如果函数里面有诸如大量的循环的操作的话,那么程序可能就会因为局部引用太多而出现异常情况。
局部引用的管理
- 函数返回时自动释放
- 手动调用deleteLocalRef释放
- 使用JNI提供的一系列函数来管理局部引用的生命周期。(这些函数包括EnsureLocalCapacity、NewLocalRef、PushLocalFrame、PopLocalFrame、DeleteLocalRef)。
全局引用
调用NewGlobalRef基于局部引用创建,会阻止GC回收所引用的对象。全局引用可以跨函数、跨线程使用。ART不会自动释放,必须调用DeleteGlobalRef手动释放DeleteGlobalRef(g_cls_string),否则会出现内存泄漏。
弱全局引用
3、弱全局引用:调用NewWeakGlobalRef基于局部或全局引用创建,不会阻止Gc回收所引用的对象,可以跨方法、跨线程使用。但与全局引用很重要不同的一点是,弱引用不会阻止GC回收它引用的对象。但是引用也不会自动释放,在ART认为应该回收它的时候(比如内存紧张的时候)进行回收而被释放,或调用DeleteWeakGlobalRef手动释放。