一、NDK项目新增内容 使用Android Studio直接创建一个默认的NDK项目,会发现新增了如下部分:
多了加载本地库的代码和本地库中方法的声明代码。
在项目的main目录下多了cpp目录,包含CMakeLists.txt配置文件以及native-lib.cpp代码实现文件。
buid.gradle配置文件中多了native层的编译配置。
二、NDK模板代码分析 借助上面这个模板项目来理解基本的NDK开发:
so库加载
1 2 3 static { System.loadLibrary("nativetest" ); }
它的加载时机有两种,
1、在类静态初始化中: 如果只在一个类或者很少类中使用到该 so 库,则最常见的方式是在类的静态初始化块中调用。
2、在 Application 初始化时调用: 如果有很多类需要使用到该 so 库,则可以考虑在 Application 初始化等场景中提前加载。
Java层native方法声明
1 public native String stringFromJNI () ;
native方法的实现与静态注册
1 2 3 4 5 6 extern "C" JNIEXPORT jstring JNICALL Java_com_example_nativetest_MainActivity_stringFromJNI ( JNIEnv* env, jobject ) { std::string hello = "Hello from C++" ; return env->NewStringUTF (hello.c_str ()); }
函数名采用以字符串 “Java” 为前缀,用 “_” 下划线将全限定类名以及方法名连接起来,这个就是JNI函数静态注册约定的函数命名规则。
extern "C"
表示编译器使用C语言的方式进行编译或者链接。
宏定义JNIEXPORT
表示将该函数导出,可供外部使用。宏定义JNICALL
表示该函数是一个JNI函数,它们的定义如下(Linux平台下):
jstring
是函数的返回类型。
jobject
类型是 JNI 层对于 Java 层应用类型对象的表示。每一个从 Java 调用的 native 方法,在 JNI 函数中都会传递一个当前对象的引用。区分 2 种情况:
静态Native方法: 第二个参数为 jclass
类型,指向 native 方法所在类的 Class 对象。
实例Native方法: 第二个参数为 jobject
类型,指向调用 native 方法的对象。
参数 env 是Native层中Java环境的代表,通过 JNIEnv* 指针就可以在 Native 层中访问 Java 层的代码并进行操作,比如调用 Java 的方法、操作 Java 的变量和对象等。
三、数据类型转换 3.1 数据类型映射 Java的数据类型与JNI中的数据类型映射见JNI原理 - gla2xy’s blog (gal2xy.github.io) 。由于Java层数据类型和C/C++层的数据类型不尽相同,因此在使用时,需要做相应的转换:
基础数据类型: 会直接转换为 C/C++ 的基础数据类型,例如 int 类型映射为 jint 类型。由于 jint 是 C/C++ 类型,所以可以直接当作普通 C/C++ 变量使用,而不需要依赖 JNIEnv 环境对象。
引用数据类型: 对象只会转换为一个 C/C++ 指针,例如 Object 类型映射为 jobject 类型。由于指针指向 Java 虚拟机内部的数据结构,所以不可能直接在 C/C++ 代码中操作对象,而是需要依赖 JNIEnv 环境对象。另外,为了避免对象在使用时突然被回收,在本地方法返回前,虚拟机会固定(pin)对象,阻止其 GC。
转换的方法见JNI源码:jni.h (aospxref.com) 。
3.2 字符串 字符串的转换由于编码的原因分为两套:一套是用于Unicode编码,另一套是用于UTF-8编码,具体函数如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 jstring NewString (const jchar* unicodeChars, jsize len) jsize GetStringLength (jstring string) const jchar* GetStringChars (jstring string, jboolean* isCopy) void ReleaseStringChars (jstring string, const jchar* chars) void GetStringRegion (jstring str, jsize start, jsize len, jchar* buf) jstring NewStringUTF (const char * bytes) jsize GetStringUTFLength (jstring string) const char * GetStringUTFChars (jstring string, jboolean* isCopy) void ReleaseStringUTFChars (jstring string, const char * utf) void GetStringUTFRegion (jstring str, jsize start, jsize len, char * buf) const jchar* GetStringCritical (jstring string, jboolean* isCopy) void ReleaseStringCritical (jstring string, const jchar* carray)
3.3 数组 Native层中有如下数组:ObjectArray、BooleanArray、ByteArray、CharArray、ShortArray、IntArray、LongArray、FloatArray、DoubleArray
。每种类型的数组都有一套相似的对应函数来操作数组,函数主要如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 <Type>、<type>代指基本类型jsize GetArrayLength (jarray array) jobjectArray NewObjectArray (jsize length, jclass elementClass, jobject initialElement) jobject GetObjectArrayElement (jobjectArray array, jsize index) void SetObjectArrayElement (jobjectArray array, jsize index, jobject value) j<type>Array New<Type>Array (jsize length) j<type>* Get<Type>ArrayElements (j<type>Array array, j<Type>* isCopy) void Release<Type>ArrayElements (j<type>Array array, j<type>* elems,jint mode) void Get<Type>ArrayRegion (j<type>Array array, jsize start, jsize len, j<type>* buf) void Set<Type>ArrayRegion (j<type>Array array, jsize start, jsize len, const j<type>* buf)
示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 public class MainActivity extends AppCompatActivity { static { System.loadLibrary ("nativetest" ); } public static int [] a = {1 ,2 ,3 ,4 ,5 }; protected void onCreate (Bundle savedInstanceState) { ... int [] bArray = getIntArray (); for (int i = 0 ; i < bArray.length ; i++) { Log.d ("gal2xy" , "bArray[" + i + "]: " + bArray[i]); } } public native int [] getIntArray (); }extern "C" JNIEXPORT jintArray JNICALL Java_com_example_nativetest_MainActivity_getIntArray (JNIEnv *env, jobject thiz) { jclass clz = env->GetObjectClass (thiz); jfieldID aFieldID = env->GetStaticFieldID (clz,"a" , "[I" ); jintArray aArray = static_cast <jintArray>(env->GetStaticObjectField (clz, aFieldID)); int aAarryLen = env->GetArrayLength (aArray); LOGD ("Array Length: %d" , aAarryLen); jint* intArray = nullptr ; intArray = env->GetIntArrayElements (aArray, NULL ); for (int i = 0 ; i < aAarryLen; ++i) { LOGD ("Array[%d] = %d" , i, intArray[i]); intArray[i] *= 2 ; } env->ReleaseIntArrayElements (aArray, intArray,0 ); return aArray; }
输出如下图所示:
四、JNI层访问Java层 4.1 访问类
FindClass(classname)
:获取classname
对应的类,classname
中的.
需要用/
替换。
GetSuperclass(clz)
:获取clazz
的父类。
GetObjectClass(clzObj)
:获取类实例clzObj
的类。
IsInstanceOf(clzObj, clz)
:判断类实例clzObj
对象是否是clz
类类型。
NewObject(clz, methodID, args...)
:调用构造方法创建实例对象。
AllocObject(clz)
:创建实例对象(仅仅为类对象分配内存空间而已),不初始成员变量,也不调用构造方法。
示例:
1 2 3 4 5 6 jclass clz = env->FindClass ("com/example/nativetest/Demo" ); jmethodID initmethodID = env->GetMethodID (clz,"<init>" , "()V" ); jobject DemoObj = env->NewObject (clz, initmethodID);
4.2 访问方法(包括构造方法) 4.2.1 静态方法
GetStaticMethodId(clz, methodname, sig)
:获取静态方法ID。
CallStatic<Type>Method(clz, methodID, args...)
:调用静态方法,类型<Type>
取决于方法的返回值类型,如Object、Boolean、Byte、Char、Double、Float、Int、Short、Long、Void
。
示例:
1 2 3 4 5 6 7 8 9 10 11 extern "C" JNIEXPORT void JNICALLJava_com_example_nativetest_MainActivity_accessField (JNIEnv *env, jobject thiz) { LOGD("come into native method: accessField" ); jclass clz = env->FindClass("com/example/nativetest/Demo" ); jmethodID mulmethodID = env->GetStaticMethodID(clz,"mul" , "(II)I" ); int result = env->CallStaticIntMethod(clz, mulmethodID,2 ,3 ); LOGD("result = %d" , result); }
4.2.2 实例方法 需要先通过NewObject
方法创建实例对象才能调用。
GetMethodID(clz, methodname, sig)
:获取实例方法ID。
Call<Type>Method(clzObj, methodID, args...)
:调用实例方法。
CallNonvirtual<Type>Method(clzObj,clz,methodID,args...)
:调用父类clz
的方法(如果不想用自身的重载方法)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 extern "C" JNIEXPORT void JNICALL Java_com_example_nativetest_MainActivity_accessField (JNIEnv *env, jobject thiz) { LOGD ("come into native method: accessField" ); jclass clz = env->FindClass ("com/example/nativetest/Demo" ); jmethodID initmethodID = env->GetMethodID (clz,"<init>" , "()V" ); jobject DemoObj = env->NewObject (clz, initmethodID); jmethodID addmethodId = env->GetMethodID (clz,"add" , "(II)I" ); int result = env->CallIntMethod (DemoObj,addmethodId,1 ,2 ); LOGD ("result = %d" , result); }
4.2.3 CallMethod、CallMethodA、CallMethodV的区别 1 2 3 4 5 6 7 8 9 10 11 12 13 void CallVoidMethod (jobject obj, jmethodID methodID, ...) { va_list args; va_start (args, methodID); functions->CallVoidMethodV (this , obj, methodID, args); va_end (args); }void CallVoidMethodV (jobject obj, jmethodID methodID, va_list args) { functions->CallVoidMethodV (this , obj, methodID, args); }void CallVoidMethodA (jobject obj, jmethodID methodID, const jvalue* args) { functions->CallVoidMethodA (this , obj, methodID, args); }
Call<Type>Method
方法中调用了Call<Type>MethodV
方法,并且在此之前将methodID
后面的参数封装成va_list
类型的数组(数组的长度可变)。
Call<Type>MethodV
方法接收va_list
类型的数组。
Call<Type>MethodA
方法接收jvalue
类型的指针,jvalue
联合体如下:
1 2 3 4 5 6 7 8 9 10 11 typedef union jvalue { jboolean z; jbyte b; jchar c; jshort s; jint i; jlong j; jfloat f; jdouble d; jobject l; } jvalue;
4.3 访问Java字段 4.3.1 静态字段
GetStaticFieldID(clz,FieldName,sig)
:获取静态字段ID。
GetStatic<Type>Field(clz,FieldID)
:获取静态字段,类型<Type>
取决于字段的类型,如Object、Boolean、Byte、Char、Double、Float、Int、Short、Long
。
SetStatic<Type>Field(clz,FieldID,value)
:设置静态字段值。
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 extern "C" JNIEXPORT void JNICALL Java_com_example_nativetest_MainActivity_accessField (JNIEnv* env, jobject thiz) { jclass clz = env->GetObjectClass (thiz); jfieldID staticstringFieldID = env->GetStaticFieldID (clz,"staticstring" , "Ljava/lang/String;" ); jstring staticstring = static_cast <jstring>(env->GetStaticObjectField (clz,staticstringFieldID)); const char * staticchars = env->GetStringUTFChars (staticstring, nullptr ); LOGD ("static String from Java: %s" , staticchars); env->ReleaseStringChars (staticstring, reinterpret_cast <const jchar *>(staticchars)); }
4.3.2 实例字段 需要先通过NewObject
方法创建实例对象才能调用。
GetFieldID(clz,FieldName,sig)
:获取实例字段ID。
Get<Type>Field(clzObj,FieldID)
:获取实例字段,类型<Type>
取决于字段的类型,如Object、Boolean、Byte、Char、Double、Float、Int、Short、Long
。
Set<Type>Field(clzObj,FieldID,value)
:设置实例字段值。
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 extern "C" JNIEXPORT void JNICALL Java_com_example_nativetest_MainActivity_accessField (JNIEnv *env, jobject thiz) { LOGD ("come into native method: accessField" ); jclass clz = env->FindClass ("com/example/nativetest/Demo" ); jmethodID initmethodID = env->GetMethodID (clz,"<init>" , "()V" ); jobject DemoObj = env->NewObject (clz, initmethodID); jfieldID aFieldID = env->GetFieldID (clz, "a" , "I" ); int avalue = env->GetIntField (DemoObj,aFieldID); LOGD ("value = %d" , avalue); }
4.4 缓存ID 访问 Java 层字段或方法时,需要先利用字段名 / 方法名和描述符进行检索,获得 jfieldID / jmethodID。这个检索过程比较耗时,优化方法是将字段 ID 和方法 ID 缓存起来,减少重复检索。
提示: 从不同线程中获取同一个字段或方法 的 ID 是相同的,缓存 ID 不会有多线程问题。
缓存字段ID和方法ID的方法主要有 2 种:
1、使用时缓存: 使用时缓存是指在首次访问字段或方法时,将字段 ID 或方法 ID 存储在静态变量中。这样将来再次调用本地方法时,就不需要重复检索 ID 了。例如:
2、类初始化时缓存: 静态初始化时缓存是指在 Java 类初始化的时候,提前缓存字段 ID 和方法 ID。可以选择在 JNI_OnLoad
方法中缓存,也可以在加载 so 库后调用一个 native 方法进行缓存。
两种缓存 ID 方式的主要区别在于缓存发生的时机和时效性:
1、时机不同: 使用时缓存是延迟按需缓存,只有在首次访问 Java 时才会获取 ID 并缓存,而类初始化时缓存是提前缓存;
2、时效性不同: 使用时缓存的 ID 在类卸载后失效,在类卸载后不能使用,而类加载时缓存在每次加载 so 动态库时会重新更新缓存,因此缓存的 ID 是保持有效的。
五、对象引用管理 5.1 引用类型 JNI中存在三种引用:局部引用、全局引用、弱全局引用。
5.1.1 局部引用 JNIEnv提供的函数所返回的引用基本上都是本地引用,它具有以下特点:
当Native函数返回时,这个本地引用就会被自动释放。
只在创建它的线程中有效,不能够跨线程使用。
局部引用是JVM负责的引用类型,受JVM管理。
局部引用可以通过NewLocalRef
函数创建,通过DeleteLocalRef
函数来释放。
5.1.2 全局引用 全局引用与本地引用相反,它具有以下特点:
在native函数返回时不会被自动释放,因此全局引用需要手动释放,并且不会被GC回收。
全局引用是可以跨线程使用的。
全局引用不受到JVM管理。
全局引用通过JNIEnv
的NewGlobalRef
函数创建,通过JNIEnv
的DeleteGlobalRef
函数释放。
示例代码如下:
1 2 3 4 jclass clazz = env->GetObjectClass (classname); jcalss mClass = (jcalss)env->NewGlobalRef (clazz); ... env->DeleteGlobalRef (mClass)
5.1.3 弱全局引用 弱全局引用是一种特殊的全局引用,它和全局引用的特点相似,但是弱全局引用是可以被GC回收的 。弱全局引用被GC回收之后会指向NULL,因此在调用前需要判断它是否被回收了(通过JNIEnv
的IsSameObject
函数来判断)。
弱全局引用通过JNIEnv
的NewWeakGlobalRef
函数来创建弱全局引用,通过JNIEnv
的DeleteWeakGlobalRef
函数释放。
示例代码如下:
1 2 3 4 5 6 7 8 9 jclass clazz = env->GetObjectClass (classname); jcalss mClass = (jcalss)env->NewWeakGlobalRef (clazz); env->DeleteWeakGlobalRef (mClass)if (env->IsSameObject (mClass,NULL )){ }else { }
5.2 比较引用是否指向相同对象 使用 IsSameObject
判断两个引用是否指向相同对象,示例代码同上。
另外,当引用与 NULL
比较时含义略有不同:
局部引用和全局引用与 NULL 比较: 用于判断引用是否指向 NULL 对象;
弱全局引用与 NULL 比较: 用于判断引用指向的对象是否被回收。
六、异常处理 6.1 JNI异常处理机制 JNI 中的异常机制与 Java 和 C/C++ 的处理机制都不同:
Java 和 C/C++: 程序使用关键字 throw
抛出异常,虚拟机会中断当前执行流程,转而去寻找匹配的catch块,或者继续向外层抛出寻找匹catch块。
JNI : 程序使用 JNI 函数 ThrowNew
抛出异常,发生异常时程序不会中断当前执行流程 ,而是返回到Java层后,虚拟机才会抛出这个异常。
6.2 检测异常的方法 JNI检测异常的方法有两种:
通过函数返回值错误码,大部分 JNI 函数和库函数都会有特定的返回值来标示错误,例如 -1、NULL 等。在程序流程中可以多检查函数返回值来判断异常。
通过 JNI 函数 ExceptionOccurred
或 ExceptionCheck
检查当前是否有异常发生。
6.3 异常发生时的处理方法 在 JNI 层出现异常时,有两种处理选择:
直接 return
当前方法,让 Java 层去处理这个异常(类似Java中向方法外层抛出异常)。
通过 JNI 函数 ExceptionClear
清除这个异常,再执行异常处理程序。
6.4 JNI的异常处理函数 JNI 提供了以下与异常处理相关的函数:
Throw(jthrowable obj)
:向Java层抛出java.lang.Throwable
异常对象,该异常对象可以通过ExceptionOccurred()
获取。
ThrowNew(jclass clazz, const char* message)
:利用message
构造异常对象并向Java层抛出异常。
ExceptionDescribe()
:打印异常描述信息。
ExceptionOccurred()
:检查当前环境是否发生异常,如果存在异常则返回该异常对象。
ExceptionCheck()
:检查当前环境是否发生异常。
ExceptionClear()
:清除当前环境的异常。
FatalError(const char* msg)
:抛出致命错误并且不希望虚拟机进行修复。
示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 extern "C" JNIEXPORT void JNICALL Java_com_example_nativetest_MainActivity_accessField (JNIEnv *env, jobject thiz) { jclass DemoClass = env->FindClass ("com/example/nativetest/Demo" ); jfieldID aFieldID = env->GetStaticFieldID (DemoClass,"a" , "I" ); if (env->ExceptionCheck ()){ env->ExceptionDescribe (); env->ExceptionClear (); LOGD ("异常处理完毕" ); } }
没有异常处理的话,程序直接崩溃;加了异常处理之后,程序能正常运行,且打印出了信息。
七、多线程 7.1 不能跨线程的引用 在JNI中,有两类引用是无法跨线程调用的:
JNIEnv
JNI只在创建它的线程中有效,不能跨线程传递,不同线程的JNIEnv是彼此独立的。
如需获取JNIEnv,可通过JavaVM的AttachCurrentThread
方法将当前线程依附到JavaVM上,获取属于当前线程的JNIEnv指针,事后需调用DetachCurrentThread
方法解除依附。
如果当前线程已经依附到JavaVM上,可直接使用GetEnv
方法。
局部引用
局部引用只在创建的线程和方法中有效,不能跨线程使用。可以将局部引用升级为全局引用后跨线程使用。
7.2 线程创建 线程创建的方法有两种
通过Java API创建:反射调用Java的Thread类创建。
通过C/C++ API创建:使用pthread_create()
或std::thread
创建线程。
1 int pthread_create (pthread_t * __pthread_ptr, pthread_attr_t const * __attr, void * (*__start_routine)(void *), void *) ;
第一个是指向pthread的指针,也是线程id,第二个是线程属性,第三个是线程执行的函数,第四个是函数参数。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 void myThread (int a) { LOGD ("come into Thread, a = %d" , a); }extern "C" JNIEXPORT void JNICALL Java_com_example_nativetest_MainActivity_accessField (JNIEnv *env, jobject thiz) { jclass DemoClass = env->FindClass ("com/example/nativetest/Demo" ); pthread_t pthread; pthread_create (&pthread, nullptr , reinterpret_cast <void *(*)(void *)>(myThread), reinterpret_cast <void *>(5 )); LOGD ("complete" ); }
涉及到多参数传递,就需要使用结构体来封装参数,将结构体指针传给线程。
7.3 监视器同步 在JNI中,也存在多个线程访问同一资源的情况,所以也需要互斥锁来保证并发安全。在Java层中,我们通过使用synchronized
关键字来实现互斥,在JNI层中,提供了如下函数实现互斥:
MonitorEnter(jobject obj)
:进入同步块。如果另一个线程已经进入该 jobject 的监视器,则当前线程会阻塞。
MonitorExit(jobject obj)
:退出同步块。如果当前线程未进入该 jobject 的监视器,则会抛出 IllegalMonitorStateException
异常。
7.4 等待与唤醒 JNI 没有像Java一样提供 wait/notify 相关功能的函数,所以需要反射 Java 方法的方式来实现。
八、JNI_OnLoad 当Java层通过System.loadLibrary
方法加载so库时,首先会自动调用JNI_OnLoad
函数,且系统会帮我们实现JNI_OnLoad
函数。如果我们需要重实现JNI_OnLoad
函数,则函数的返回值必须时JNI版本 JNI_VERSION_1_6
。
在JNI_OnLoad
函数中,我们可以动态注册native方法。
示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 JNIEXPORT jint JNICALL JNI_OnLoad (JavaVM* vm, void * reserved) { JNIEnv* env = nullptr ; jint result = -1 ; if (vm->GetEnv ((void **)&env,JNI_VERSION_1_6) != JNI_OK){ return result; } JNINativeMethod jmethods[] = { {"DynamicRegistration" , "()V" , (void *)(DynamicRegistrationNative)} }; jclass clz = env->FindClass ("com/example/nativetest/MainActivity" ); env->RegisterNatives (clz,jmethods,sizeof (jmethods)/sizeof (JNINativeMethod)); result = JNI_VERSION_1_6; return result; }
九、日志输出
导入<android/log.h>
库。
定义宏TAG。
利用宏定义来定义LOGD或LOGI或LOGE函数。
示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 #include <jni.h> #include <string> #include <android/log.h> #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,TAG ,__VA_ARGS__) #define LOGI(...) __android_log_print(ANDROID_LOG_INFO , TAG, __VA_ARGS__); #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__); #define TAG "gal2xy" extern "C" JNIEXPORT void JNICALL Java_com_example_nativetest_MainActivity_accessField (JNIEnv *env, jobject thiz) { LOGD ("come into native method: accessField" ); }
十、Native方法注册 Native方法的注册可分为两种:静态注册和动态注册。它们的原理分析详见JNI原理#三、Native注册方法 ,这里只讲如何使用。
10.1 静态注册 静态注册采用的是基于「约定」的命名规则,由于C/C++无法实现重载函数,所以根据有无重载分为两种规则:
短名称规则 :Java_[类的全限定名 (带下划线)]_[方法名]
,其中类的全限定名中的 .
改为 _
;
长名称规则 :在短名称的基础上后追加两个下划线(__
)和参数描述符,以区分函数重载。
静态注册的函数需要暴露到符号表,也就是需要添加宏定义JNIEXPORT
。
10.2 动态注册 动态注册需要使用 RegisterNatives
函数,因为是动态注册,所以函数并不需要暴露到符号表。
1 2 3 4 5 6 7 8 jint RegisterNatives (jclass clazz, const JNINativeMethod* methods, jint nMethods) jint UnregisterNatives (jclass clazz) typedef struct { const char * name; const char * signature; void * fnPtr; } JNINativeMethod;
示例代码如下:
在Java层的MainActivity中,声明了一个native方法:
1 public native void DynamicRegistration () ;
并且调用了该方法。然后native层的实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 void DynamicRegistrationNative (JNIEnv *env, jobject thiz) { LOGD ("call DynamicRegistration method" ); } JNINativeMethod jmethods[] = { {"DynamicRegistration" , "()V" , (void *)(DynamicRegistrationNative)} };JNIEXPORT jint JNICALL JNI_OnLoad (JavaVM* vm, void * reserved) { JNIEnv* env = nullptr ; jint result = -1 ; if (vm->GetEnv ((void **)&env,JNI_VERSION_1_6) != JNI_OK){ return result; } jclass clz = env->FindClass ("com/example/nativetest/MainActivity" ); env->RegisterNatives (clz,jmethods,sizeof (jmethods)/sizeof (JNINativeMethod)); result = JNI_VERSION_1_6; return result; }
10.3 注册时机
注册时机
注册方式
描述
第一次调用native 方法时
静态注册
虚拟机会在 JNI 函数库中搜索该函数指针并记录下来,后续调用不需要重复搜索。
加载 so 库时
动态注册
加载 so 库时会自动回调 JNI_OnLoad 函数,在其中调用 RegisterNatives 注册。
提前注册
动态注册
在加载 so 库后,调用该 native 方法前,通过静态注册的 native 函数 触发 RegisterNatives 注册。例如在 App 启动时,很多系统源码会提前做一次注册。
十一、JavaVM 11.1 JavaVM是什么 JavaVM
是 Java
虚拟机在 JNI
层的代表,在一个虚拟机进程中只有一个 JVM
,因此该进程的所有线程都可以使用这个 JVM
。JavaVM
的定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 struct _JavaVM ;#if defined(__cplusplus) typedef _JavaVM JavaVM;#else typedef const struct JNIInvokeInterface * JavaVM;#endif struct JNIInvokeInterface { void * reserved0; void * reserved1; void * reserved2; jint (*DestroyJavaVM)(JavaVM*); jint (*AttachCurrentThread)(JavaVM*, JNIEnv**, void *); jint (*DetachCurrentThread)(JavaVM*); jint (*GetEnv)(JavaVM*, void **, jint); jint (*AttachCurrentThreadAsDaemon)(JavaVM*, JNIEnv**, void *); };struct _JavaVM { const struct JNIInvokeInterface * functions;#if defined(__cplusplus) jint DestroyJavaVM () { return functions->DestroyJavaVM (this ); } jint AttachCurrentThread (JNIEnv** p_env, void * thr_args) { return functions->AttachCurrentThread (this , p_env, thr_args); } jint DetachCurrentThread () { return functions->DetachCurrentThread (this ); } jint GetEnv (void ** env, jint version) { return functions->GetEnv (this , env, version); } jint AttachCurrentThreadAsDaemon (JNIEnv** p_env, void * thr_args) { return functions->AttachCurrentThreadAsDaemon (this , p_env, thr_args); }#endif };
_JavaVM
其实是对JNIInvokeInterface
的一个封装结构体,里面的方法最终还是调用JNIInvokeInterface
的方法实现。
在以上方法中,我们通常使用GetEnv()
方法或者AttachCurrentThread()
方法(子线程中)来获取JNIEnv
。
由于JavaVm
在C和C++中的宏定义不同(C中是结构体指针,C++中是结构体),因此使用时是有区别的:
1 2 3 4 5 6 JavaVM* vm; JNIEnv* env = nullptr ; vm->GetEnv ((void **)&env,JNI_VERSION_1_6); (*vm)->GetEnv ((void **)&env,JNI_VERSION_1_6);
11.2 JavaVM的获取 通过JNIEnv
的GetJavaVM(JavaVM** vm)
方法来获取JavaVM
。
十二、JNIEnv 12.1 JNIEnv是什么 JNIEnv
是 Native 层中 Java 环境的代表,通过 JNIEnv*
指针就可以在 Native 层中访问 Java 层的代码并进行操作,比如调用 Java 的方法、操作 Java 的变量和对象等。但是它只在创建它的线程中有效,不能跨线程传递, 因此不同线程的 JNIEnv 是彼此独立的。
JNIEnv
的定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 struct _JNIEnv ;#if defined(__cplusplus) typedef _JNIEnv JNIEnv;#else typedef const struct JNINativeInterface * JNIEnv;#endif struct _JNIEnv { const struct JNINativeInterface * functions;#if defined(__cplusplus) jint GetVersion () { return functions->GetVersion (this ); } jclass DefineClass (const char *name, jobject loader, const jbyte* buf, jsize bufLen) { return functions->DefineClass (this , name, loader, buf, bufLen); } ... }struct JNINativeInterface { void * reserved0; void * reserved1; void * reserved2; void * reserved3; jint (*GetVersion)(JNIEnv *); jclass (*DefineClass)(JNIEnv*, const char *, jobject, const jbyte*, jsize); ... }
同样的_JNIEnv
其实是对JNINativeInterface
的一个封装结构体,里面的方法最终还是调用JNINativeInterface
的方法实现。
由于JNIEnv在C和C++中的宏定义不同(C中是结构体指针,C++中是结构体),因此使用时是有区别的:
1 2 3 4 5 JNIEnv* env; env->FindClass ("java/lang/String" ); (*env)->FindClass (env, "java/lang/String" );
12.2 JNIEnv的获取 通过JavaVM
的GetEnv()
方法或者AttachCurrentThread()
方法(子线程中)来获取JNIEnv
。
十三、SO文件生成的相关事项 13.1 Native层创建CPP文件 在main/cpp
目录下创建的新cpp文件,需要在CMakeLists.txt
中的add_library
中声明,具体如下:
13.2 多个cpp编译成多个so库/一个so库 默认情况(只有一个add_library
标签)是无论多少cpp文件,只编译成一个so库。
想要编译成多个so库,可以通过额外添加add_library
标签,在其中指定so库的名字和所涉及到的cpp文件。
13.3 自定义so文件名 在add_library
标签中直接写明,见上图所示。
13.4 添加第三方库或者系统库 如果想要添加第三方库或者系统库,则需按如下步骤进行:
将so库复制到src/main/jniLibs/${架构名称}
目录下,如下图所示:
如果需要相关的.h
头文件,复制到cpp目录下或则新建目录。
在CMakeLists.txt
中声明,如下图所示:
生成的apk的so库如下所示:
明显将第三方库链接进来了。
13.5 生成指定架构的so库 如果想要生成指定应用二进制接口(ABI)的so库,可以在buid.gradle.kts
文件中声明:
13.6 Native层调用so库 一种方法是通过dlopen
函数打开so库,通过dlsym
函数获取指定函数的句柄来使用,以上方法需要导入dlfcn.h
才能使用。
示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 #define OpenMemory "_ZN3art7DexFile10OpenMemoryEPKhjRKNSt3__112basic_stringIcNS3_11char_traitsIcEENS3_9allocatorIcEEEEjPNS_6MemMapEPKNS_10OatDexFileEPS9_" typedef void *(*org_artDexFileOpenMemory)(const uint8_t *base, size_t size, const std::string &location, uint32_t location_checksum, void *mem_map, std::string *error_msg);void * artHandle = (void *)dlopen ("libart.so" , RTLD_LAZY);auto func = (org_artDexFileOpenMemory) dlsym (artHandle, OpenMemory);
另一种方法就是 13.4 所述,导入后库后可直接使用。
参考:NDK 系列(5):JNI 从入门到实践,万字爆肝详解! - 掘金 (juejin.cn)
NDK 系列(6):说一下注册 JNI 函数的方式和时机 - 掘金 (juejin.cn)
Android开发学习笔记——NDK开发 | Whitebird’s Home (whitebird0.github.io)
JNI/NDK入门指南之JNI字符串处理
JNI/NDK入门指南之JNI访问数组
Android jni调用第三方so库和.h文件 - 掘金 (juejin.cn)
CMakeList编译报错ninja: error: missing and no known rule to make it解决方法-CSDN博客