NDK开发学习笔记

一、NDK项目新增内容

使用Android Studio直接创建一个默认的NDK项目,会发现新增了如下部分:

  1. 多了加载本地库的代码和本地库中方法的声明代码。

  2. 在项目的main目录下多了cpp目录,包含CMakeLists.txt配置文件以及native-lib.cpp代码实现文件。

  3. buid.gradle配置文件中多了native层的编译配置。

二、NDK模板代码分析

借助上面这个模板项目来理解基本的NDK开发:

  1. so库加载

    1
    2
    3
    static {
    System.loadLibrary("nativetest");
    }

    它的加载时机有两种,

    • 1、在类静态初始化中: 如果只在一个类或者很少类中使用到该 so 库,则最常见的方式是在类的静态初始化块中调用。
    • 2、在 Application 初始化时调用: 如果有很多类需要使用到该 so 库,则可以考虑在 Application 初始化等场景中提前加载。
  2. Java层native方法声明

    1
    public native String stringFromJNI();
  3. native方法的实现与静态注册

    1
    2
    3
    4
    5
    6
    extern "C" JNIEXPORT jstring JNICALL Java_com_example_nativetest_MainActivity_stringFromJNI(
    JNIEnv* env,
    jobject /* this */) {
    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
//Unicode编码
jstring NewString(const jchar* unicodeChars, jsize len)//将native层的字符数组转成Unicode编码的java字符串对象,以'$'结尾
jsize GetStringLength(jstring string)//返回Unicode编码的字符串的长度,由于结尾符不是'\0',所以不可通过strlen函数获取
const jchar* GetStringChars(jstring string, jboolean* isCopy)//将Java字符串对象转成字native层的字符串数组
void ReleaseStringChars(jstring string, const jchar* chars)//释放native层的字符串数组(第二个参数)
void GetStringRegion(jstring str, jsize start, jsize len, jchar* buf)//复制str的指定部分到buf中
//UTF-8编码
jstring NewStringUTF(const char* bytes)//将native层的字符数组转成UTF-8编码的java字符串对象,以'\0'结尾
jsize GetStringUTFLength(jstring string)//返回UTF-8编码的字符串的长度,这里也可以通过strlen函数获取
const char* GetStringUTFChars(jstring string, jboolean* isCopy)//将Java字符串对象转成字native层的字符串数组
void ReleaseStringUTFChars(jstring string, const char* utf)//释放native层的字符串数组(第二个参数)
void GetStringUTFRegion(jstring str, jsize start, jsize len, char* buf)//复制str指定部分到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)//返回数组中的元素个数

//引用类型数组
//创建引用类型的数组,length指定数组大小,elementClass指定元素的引用类型,initialElement指定元素的初始值,一般为null
jobjectArray NewObjectArray(jsize length, jclass elementClass, jobject initialElement)
//获取array数组中指定下标index的元素的值
jobject GetObjectArrayElement(jobjectArray array, jsize index)
//设置array数组中指定下标index的元素的值
void SetObjectArrayElement(jobjectArray array, jsize index, jobject value)

//基本类型数组
//创建Type类型的数组,length指定数组大小
j<type>Array New<Type>Array(jsize length)
//将Java层的数组转成native层的数组
j<type>* Get<Type>ArrayElements(j<type>Array array, j<Type>* isCopy)
/*
释放array数组,elems由Get<Type>ArrayElements函数获取的,mode指定操作模式:
0 更新数组并释放elems 缓冲区
JNI_COMMIT 更新但不释放elems 缓冲区
JNI_ABORT 不作更新但释放elems缓冲区
*/
void Release<Type>ArrayElements(j<type>Array array, j<type>* elems,jint mode)
//复制array数组中指定范围内的元素到buf数组中
void Get<Type>ArrayRegion(j<type>Array array, jsize start, jsize len, j<type>* buf)
//将buf数组中的元素赋值到array数组中指定范围内
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
//java层
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();
}
//native层
extern "C" JNIEXPORT jintArray JNICALL
Java_com_example_nativetest_MainActivity_getIntArray(JNIEnv *env, jobject thiz) {
//获取MainActivity的int数组 a
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);
//转成native使用的int数组
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;
}
//更新java层的数组并释放native层的对应的数组
env->ReleaseIntArrayElements(aArray, intArray,0);
//返回给Java层输出
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 JNICALL
Java_com_example_nativetest_MainActivity_accessField(JNIEnv *env, jobject thiz) {
// TODO: implement accessField()
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) {
// TODO: implement accessField()
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);
//获取要调用的方法ID
jmethodID addmethodId = env->GetMethodID(clz,"add", "(II)I");
int result = env->CallIntMethod(DemoObj,addmethodId,1,2);
LOGD("result = %d", result);
/*
//获取父类
jclass origindemoclz = env->FindClass("com/example/nativetest/OriginDemo");
jmethodID addmethodID = env->GetMethodID(origindemoclz,"add", "(II)I");//实际上是减法

int result = env->CallNonvirtualIntMethod(DemoObj,origindemoclz,addmethodID,1,2);
LOGD("result = %d", result);//结果为-1
*/
}

4.2.3 CallMethod、CallMethodA、CallMethodV的区别

1
2
3
4
5
6
7
8
9
10
11
12
13
//以CallVoidMethod为例
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 静态字段

  1. GetStaticFieldID(clz,FieldName,sig):获取静态字段ID。
  2. GetStatic<Type>Field(clz,FieldID):获取静态字段,类型<Type>取决于字段的类型,如Object、Boolean、Byte、Char、Double、Float、Int、Short、Long
  3. 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) {

//1、通过对象对应的类
jclass clz = env->GetObjectClass(thiz);
//2、通过类获取静态字段ID
jfieldID staticstringFieldID = env->GetStaticFieldID(clz,"staticstring", "Ljava/lang/String;");

//set
//jstring newString = env->NewStringUTF("newString alter by Native");
//env->SetStaticObjectField(clz, staticstringFieldID, newString);

//3、获取字段的值
jstring staticstring = static_cast<jstring>(env->GetStaticObjectField(clz,staticstringFieldID));
//GetStringChars方法获取的字符串以$结尾,不能以%s格式化输出
//4、转成可供Native层直接使用的类型
const char* staticchars = env->GetStringUTFChars(staticstring, nullptr);
LOGD("static String from Java: %s", staticchars);
//5、释放资源
env->ReleaseStringChars(staticstring, reinterpret_cast<const jchar *>(staticchars));

}

4.3.2 实例字段

需要先通过NewObject方法创建实例对象才能调用。

  1. GetFieldID(clz,FieldName,sig):获取实例字段ID。
  2. Get<Type>Field(clzObj,FieldID):获取实例字段,类型<Type>取决于字段的类型,如Object、Boolean、Byte、Char、Double、Float、Int、Short、Long
  3. 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) {
// TODO: implement accessField()
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);
//获取字段ID
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管理。

全局引用通过JNIEnvNewGlobalRef函数创建,通过JNIEnvDeleteGlobalRef函数释放。

示例代码如下:

1
2
3
4
jclass clazz = env->GetObjectClass(classname);
jcalss mClass = (jcalss)env->NewGlobalRef(clazz);
...
env->DeleteGlobalRef(mClass)

5.1.3 弱全局引用

弱全局引用是一种特殊的全局引用,它和全局引用的特点相似,但是弱全局引用是可以被GC回收的。弱全局引用被GC回收之后会指向NULL,因此在调用前需要判断它是否被回收了(通过JNIEnvIsSameObject函数来判断)。

弱全局引用通过JNIEnvNewWeakGlobalRef函数来创建弱全局引用,通过JNIEnvDeleteWeakGlobalRef函数释放。

示例代码如下:

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 函数 ExceptionOccurredExceptionCheck 检查当前是否有异常发生。

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");//a不是静态字段
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));
/*
可以调用pthread_join(pthread_t, returnresultpointer)让主线程进入阻塞,
等待子线程执行完后才能继续往下执行
*/
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)}//DynamicRegistrationNative为native方法
};
//获取动态注册的函数所在的类
jclass clz = env->FindClass("com/example/nativetest/MainActivity");
env->RegisterNatives(clz,jmethods,sizeof(jmethods)/sizeof(JNINativeMethod));

result = JNI_VERSION_1_6;
return result;
}

九、日志输出

  1. 导入<android/log.h>库。
  2. 定义宏TAG。
  3. 利用宏定义来定义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; // Java方法名
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结构体记录要动态注册的函数
JNINativeMethod jmethods[] = {
{"DynamicRegistration", "()V", (void *)(DynamicRegistrationNative)}
};
//在JNI_OnLoad函数中实现动态注册
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是什么

JavaVMJava 虚拟机在 JNI 层的代表,在一个虚拟机进程中只有一个 JVM,因此该进程的所有线程都可以使用这个 JVMJavaVM的定义如下:

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)
//C++的宏定义
typedef _JavaVM JavaVM;
#else
//C的宏定义
typedef const struct JNIInvokeInterface* JavaVM;
#endif

/*
* JNI invocation interface.
*/
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*);
};

/*
* C++ version.
*/
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 /*__cplusplus*/
};

_JavaVM其实是对JNIInvokeInterface的一个封装结构体,里面的方法最终还是调用JNIInvokeInterface的方法实现。

在以上方法中,我们通常使用GetEnv()方法或者AttachCurrentThread()方法(子线程中)来获取JNIEnv

由于JavaVm在C和C++中的宏定义不同(C中是结构体指针,C++中是结构体),因此使用时是有区别的:

1
2
3
4
5
6
JavaVM* vm;
JNIEnv* env = nullptr;
//C++
vm->GetEnv((void**)&env,JNI_VERSION_1_6);
//C
(*vm)->GetEnv((void**)&env,JNI_VERSION_1_6);

11.2 JavaVM的获取

通过JNIEnvGetJavaVM(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 {
/* do not rename this; it does not seem to be entirely opaque */
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;
//C++
env->FindClass("java/lang/String");
//C
(*env)->FindClass(env, "java/lang/String");

12.2 JNIEnv的获取

通过JavaVMGetEnv()方法或者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 添加第三方库或者系统库

如果想要添加第三方库或者系统库,则需按如下步骤进行:

  1. 将so库复制到src/main/jniLibs/${架构名称}目录下,如下图所示:

  2. 如果需要相关的.h头文件,复制到cpp目录下或则新建目录。

  3. 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);
//打开libart库
void* artHandle = (void*)dlopen("libart.so", RTLD_LAZY);
//获取OpenMemory函数指针
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博客


NDK开发学习笔记
http://example.com/2023/12/26/Android安全/NDK开发学习笔记/
作者
gla2xy
发布于
2023年12月26日
许可协议