文章目录
- 1. jni的注册以及编译
- 1.1 静态注册jni
- 1.1.1 使用Android Studio创建NDK开发工程
- 1.1.2 手动创建jni源文件
- 2.1 Jni的特殊描述符以及主要变量
- 2.1.1 Jni中的特殊描述符
- 2.1.2 JNIEnv和jobject类型总结
- 2.4.1 关于编码方式
- 2.4.2 需要用到的函数
- 3.1 访问成员属性
- 3.2 访问成员方法
1. jni的注册以及编译
1.1 静态注册jni
1.1.1 使用Android Studio创建NDK开发工程
使用Android Studio直接创建jni接口比较简单,集成开发工具为我们做了大量工作,且默认为静态注册,下面首先简单梳理一下使用最新版本Android Studio(4.0.1)创建NDK开发工程的方法:
-
首先新建工程,选择C++工程;
-
填写工程信息后选择C++可支持的版本以及编译选项,这里暂时选择默认的toolchain;
-
完成后即可看到AS为我们自动生成NDK开发的c++目录,对应的CMakeLists.txt以及cpp源文件示例。接下来只需要在cpp目录下创建需要的其他c++源文件即可。在Android Studio自动配置的静态注册中,Java文件新定义任何接口都可以自动在cpp文件中生成对应的接口声明。
Android Studio自动生成的c++接口声明示例:
#include <jni.h>#include <string>extern \"C\" JNIEXPORT jstring JNICALLJava_com_example_jnitest_jniutil_JniManager_stringFromJNI(JNIEnv* env,jobject /* this */) {std::string hello = \"Hello from C++\";return env->NewStringUTF(hello.c_str());}extern \"C\"JNIEXPORT jint JNICALLJava_com_example_jnitest_jniutil_JniManager_getIDFromJni(JNIEnv *env, jobject thiz) {// TODO: implement getIDFromJni()return 0;}
1.1.2 手动创建jni源文件
由于IDE为我们完成了过多工作使我们无法看清jni在创建过程中具体做了什么,因此这里简单尝试一下手动进行静态注册jni的过程,稍微降低对IDE的依赖。
创建jni主要需要三个步骤:创建必要的源文件、添加CMakeLists.txt、配置gradle。
1) 创建相关源文件:在原有的普通Android工程中,创建需要调用Native方法的java源文件,如:
package com.example.myappdemo.nativeManager;public class NativeManager {public native String getName(int id); // 本地方法声明}
然后针对这个Java文件创建cpp的头文件,可以使用jdk指令:
step1: javac NativeManage.java // 生成class文件
step2: javah -jni com.example.jnitest2.nativeManager.NativeManager // 生成jni头文件
但是实验发现以上命令无法再适用于jdk10以上了,jdk10以后javah被包含在javac中,因此以上命令可以合并为:
javac -h -jni NativeManager.java // 直接在当前目录下创建jni目录并生成头文件,根据项目不同要求,可以将路径更改为c++目录下
生成的头文件如下:
/* DO NOT EDIT THIS FILE - it is machine generated */#include <jni.h>/* Header for class com_example_myappdemo_nativeManager_NativeManager */#ifndef _Included_com_example_myappdemo_nativeManager_NativeManager#define _Included_com_example_myappdemo_nativeManager_NativeManager#ifdef __cplusplusextern \"C\" {#endif/** Class: com_example_myappdemo_nativeManager_NativeManager* Method: getName* Signature: (I)Ljava/lang/String;*/JNIEXPORT jstring JNICALL Java_com_example_myappdemo_nativeManager_NativeManager_getName(JNIEnv *, jobject, jint);#ifdef __cplusplus}#endif#endif
注意到h文件方法的名字结构是比较规则的,是以Java_包名_类名_方法名的结构出现,这也是静态注册的特点之一。
然后创建同名的cpp文件(与h文件同名方便识别),如下:
#include \"com_example_myappdemo_nativeManager_NativeManager.h\"JNIEXPORT jstring JNICALL Java_com_example_myappdemo_nativeManager_NativeManager_getName(JNIEnv *env, jobject obj, jint id) {std::string hello = \"Hello from C++\";return env->NewStringUTF(hello.c_str());}
- 创建CMakeLists.txt
CMakeLists.txt是用来生成makefile(或者project文件)的工具,用于编译c++文件。具体CMakeLists的细节不在这里介绍,这里主要介绍基本的CMakeLists.txt如何创建和实现。
首先创建CMakeLists.txt文件,然后列出编译基本规则,如下:
// 这里是要求的最低cmake版本,但不是指定版本,指定版本在gradle中说明cmake_minimum_required(VERSION 3.4.1)// 这里是指定include文件夹路径,是CMakeLists.txt的相对路径,include由开发者根据情况自行创建include_directories(include)// 需要添加的library,native-lib为so库的名字;SHARED表示类型为共享库;add_library( native-lib SHARED// 编译该库需要的源文件com_example_myappdemo_nativeManager_NativeManager.cppcom_example_myappdemo_nativeManager_NativeManager.h )
- 在gradle中配置CMakeLists.txt
Android Studio是通过gradle完成代码编译的,因此CMakeLists.txt也需要配置在gradle中才能实现动态库的编译和加载。app/build.gradle中需要配置CMakeLists的位置有两处,一处为defaultConfig选项,一处为android选项中.
其中在defaultConfig中externalNativeBuild的cmake参数为编译选项,如下所示:
defaultConfig {applicationId \"com.example.myappdemo\"minSdkVersion 26targetSdkVersion 29versionCode 1versionName \"1.0\"testInstrumentationRunner \"androidx.test.runner.AndroidJUnitRunner\"externalNativeBuild {cmake {cppFlags \"-std=c++11\" // 此处表示使用c++11标准}}}
在android中的externalNativeBuild需要标时CMakeLists.txt的路径和使用的cmake版本:
externalNativeBuild {cmake {// 这里的路径是相对build.gradle的path \"src/main/cpp/CMakeLists.txt\"version \"3.10.2\"}}
1.2 动态注册jni
动态注册jni不需要静态注册那样严格地要求方法名,但是需要手动将native方法和java方法关联起来,并手动注册。具体步骤如下:
- 创建java源文件,定义需要的native方法:
public final class DynamicJniRegister {private DynamicJniRegister() {}private static class SingleInstHolder {private static DynamicJniRegister sInstance = new DynamicJniRegister();}public static DynamicJniRegister getInstance() {return SingleInstHolder.sInstance;}// 定义的native方法public native String getGreetingsFromDynamicJni();}
-
创建jni的cpp文件,可以自行命名,但方便后续开发,依然推荐取名为java类包名。源文件需要包含jni的基础功能头文件jni.h;
-
在cpp文件中定义native接口的实现,如:
jstring getGreetingsFromDynamicJni(JNIEnv* env, jobject obj) {std::string hello = \"Greetings from dynamic C++\";return env->NewStringUTF(hello.c_str());}
- 定义jni方法与java方法的映射数组JNINativeMethod。这个数据类型主要包含三部分:java方法名、方法签名、jni函数指针。有关该数据结构和签名含义会在后面详细总结。
static const JNINativeMethod jniNativeMethod[] = {{\"getGreetingsFromDynamicJni\", \"()Ljava/lang/String;\", (jstring *)getGreetingsFromDynamicJni}};
- 实现JNI_Onload函数。该函数会在java中加载so库时自动调用。由于动态注册的jni无法根据命名查找到正确的jni函数,需要手动在该方法中确定映射规则。因此方法的主要功能是使用RegisterNatives:
static const char* mClassName = \"com/example/myappdemo/nativemanager/DynamicJniRegister\";jint JNI_OnLoad(JavaVM *vm, void *unused) {JNIEnv* env = NULL;int res = vm->GetEnv((void**)&env, JNI_VERSION_1_4);if (res != JNI_OK) {return -1;}jclass mainClass = env->FindClass(mClassName);res = env->RegisterNatives(mainClass, jniNativeMethod, 1);if (res != JNI_OK) {return -1;}return JNI_VERSION_1_4;}
- 在CMakeLists.txt中增加c++源文件,编译动态库:
add_library( dynamic_native-lib SHAREDjni_dynamic_register.cpp)
- 在java文件中加载该动态库:
static {System.loadLibrary(\"dynamic_native-lib\");}
8)最后,使用1.1.2中的3)相同的方法配置build.gradle。至此就完成了动态注册jni.
1.3 jni编译的重要文件CMakeLists.txt
CMakeLists.txt是用来生成makefile并编译c++/c源文件的,可以通过简单的指令输入对源文件的编译需求。在使用Android Studio时,CMakeLists.txt是在修改后sync时执行生成makefile的,编译时自动读取externalNativeBuild中的makefile,并编译c/c++代码。
这里总结一下CMakeLists的基本编写方法。
1) 首先使用cmake_minimum_required限定cmake的最低版本,否则会产生警告:
cmake_minimum_required(VERSION 3.4.1)
2) 构建新的静态/动态库
使用add_library可以根据已有的cpp源文件编译新的静态/动态库,命令的参数为:
add_library(
dynamic_native-lib // 该参数为库的名字,如果是动态库,最终文件名为:libdynamic_native_lib.so
SHARED // 库的类型,SHARED为共享动态库,STATIC为静态库
jni_dynamic_register.cpp) // 对应的源文件,所有的源文件都可以罗列在这里,无需标点分割。
3)使用预编译库中的函数
Android中预制了一些NDK的库供开发者使用,比如log库。这类的原生库可以通过find_library将该库与变量关联,再通过target_link_libraries将变量连接在希望使用它的库中,以便于使用该库中的函数。
find_library的参数结构为:
find_library( log-lib // 希望连接到的变量名,即定义一个变量与需要使用的库对应
log) // 希望用到的NDK预制库
target_link_libraries的参数结构为:
target_link_libraries( dynamic_native-lib // 希望用到NDK预制库方法的目标库
${log-lib} // find_library定义好的变量,这里可以添加多个
4) 引入第三方so库
引入第三方库的方法与创建一个新的native库类似,区别在于最后一个参数,我们通过IMPORTED标志告知CMake只希望将库导入到项目中。
关于目标库的路径有几点需要说明:
a. CMAKE_SOURCE_DIR表示的是CMakeLists.txt所在的路径,当指定第三方so所在路径时,应当以这个常量为起点。
b, 在具体项目中可以为每种ABI提供单独的软件包,就可以在jinLibs(如果是project结构的目录则是libs)下建立多个目录,每个目录对应一种ABI接口类型,然后再通过${ANDROID_ABI}来泛化这一层目录的结构,这样有助于充分利用特定的CPU架构。第三方的库关联到原生库与NDK库关联到原生库的原理是一样的。
c. 为了确保CMake可以在编译时定位我们的头文件,需要将include_directories() 命令添加到 CMake构建脚本中并指定头文件的路径
add_library(
#指定目标导入库
imported-lib
#设置导入库的类型(静态或动态)为shared library.
SHARED
#告知 CMake imported-lib 是导入的库
IMPORTED )
set_target_properties(
#指定目标导入库
imported-lib
#指定属性(本地导入的已有库)
PROPERTIES IMPORTED_LOCATION
#指定你要导入库的路径. 比如:
${CMAKE_SOURCE_DIR}/libs/${ANDROID_ABI}/libimported-lib.so )
#为了确保 CMake 可以在编译时定位到我们的 头文件,我们需要使用include_directories()命令,
#并包含头文件的路径
include_directories(libs/include/)
#要将预构建库关联到我们的原生库,需要将其添加到CMake构建脚本的target_link_libraries()命令中
target_link_libraries(
#这里指定了三个库,分别是native-lib、imported-lib和log-lib.
native-lib
imported-lib
#log-lib是包含在NDK中的一个日志库
${log-lib} )
2. Jni接口编写
Jni为C/C++编程,但是具体使用风格还是会有稍许不同(比如在字符串编码以及常用数据类型等),这里我会简单总结一下jni编程中的主要关键词、主要方法以及其他需要主意的事项。
2.1 Jni的特殊描述符以及主要变量
在1.1.1和1.1.2中我们看到静态注册的jni的c++和h文件中有一些特殊的描述符、宏以及数据结构,这里主要总结一下这些描述符的含义:
#ifdef __cplusplus
extern “C” {
#endif
/*
- Class: com_example_jnitest2_nativeManager_NativeManager
- Method: getName
- Signature: (I)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_example_jnitest2_nativeManager_StaticRegisterJniManager_getName
(JNIEnv *, jobject);
#ifdef __cplusplus
}
2.1.1 Jni中的特殊描述符
- extern “C”:
jni静态注册属于C/C++混合编程,是通过函数名来寻找函数入口的。C与C++在编译中对函数名字的处理有所区别。C++存在重载,因此不能用函数名作为唯一标识,会将参数列表与返回值引入参考。使用extern \”C\”以后,可以使被extern \”C\”标识的部分使用C的编译方法,函数名不会做特殊处理,以方便jni正确识别。
为什么在jdk自动生成的h文件中增加了#ifdef __cplusplus条件限制呢?因为h文件不可避免有可能被C程序引入,而C程序本身是不识别extern \”C\”修饰符的,因此需要确认当前引入该头文件的是C++文件,才使该修饰符生效。 - JNIEXPORT:
这个关键字表明这个函数是一个可导出函数。每一个C/C++库都有一个导出函数列表,只有在这个列表里面的函数才可以被外部直接调用,类似Java的public函数和private函数的区别。 - JNICALL:
说明这个函数是一个JNI函数,用来和普通的C/C++函数进行区别。
2.1.2 JNIEnv和jobject类型总结
- JNIEnv
JNIEnv类型实际上代表了Java环境,通过这个JNIEnv* 指针,就可以对Java端的代码进行操作。例如,创建Java类中的对象,调用Java对象的方法,获取Java对象中的属性等等。JNIEnv的指针会被JNI传入到本地方法的实现函数中来对Java端的代码进行操作。因此每个函数中都有JNIEnv的指针参数。
JNIEnv的部分源码如下:
typedef _JNIEnv JNIEnv;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); }jclass FindClass(const char* name){ return functions->FindClass(this, name); }jmethodID FromReflectedMethod(jobject method){ return functions->FromReflectedMethod(this, method); }......
通过该源码可以看到,JNIEnv中包含JNINativeInterface结构,而后面的所有函数均是对JNINativeInterface中函数的封装,并对参数进行了修改。并且又看到了熟悉的宏:#if defined(__cplusplus)。这里可以看出C和C++对JNIEnv的处理有所不同。对于C编写的Jni程序,JNIEnv等同于JNINativeInterface,而对于C++来说,JNIEnv则对其进行了一次封装。
2. jobject
如果native方法不是static的话,jobject就代表这个native方法的类实例。
如果native方法是static的话,jobject就代表这个native方法的类的class对象实例(static方法不需要类实例的,所以就代表这个类的class对象)。
3. JavaVM
JavaVM的源码如下:
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的主要成员函数都是围绕虚拟机进行的,包括:销毁虚拟机、归入线程、作为守护线程归入某线程、获取环境参数等等。因此这个变量是针对整个线程的性质进行描述和控制的。
在jni动态注册的初始时,就是通过GetEnv获取虚拟机环境参数的。
4. JNINativeMethod 源码如下:
typedef struct {const char* name;const char* signature;void* fnPtr;} JNINativeMethod;
JNINativeMethod是用来联系Java方法与本地方法的数据,一般会定义成数组(因为有多个本地方法需要注册)。其中name为Java方法,signature是用来描述参数以及返回值的签名,fnPtr是C++的函数指针。
2.2 Jni的主要函数
这里所讲的函数,是指jni.h中的函数,包括JNIEnv以及JavaVM中的部分成员函数。
- JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved);
这个是jni,h中声明的外部函数,由JNIEXPORT声明。该函数如果实现,则会在加载库成功后调用,提供Java虚拟机参数(第二个参数预留)。一般动态注册需要在这个函数中做一些自定义初始化工作,如寻找关联的Java类、动态注册本地函数等。 - JNIEXPORT void JNI_OnUnload(JavaVM* vm, void* reserved);
当虚拟机释放该C库时,则会调用JNI_OnUnload()函数来进行善后清除动作。 - jint GetEnv(void** env, jint version)
用于获取环境参数,是JavaVM的成员函数。一般用于在库刚刚加载的初始化工作中。 - jint RegisterNatives(jclass clazz, const JNINativeMethod* methods,
jint nMethods)
注册本地函数,是JNIEnv的成员函数,是用来动态注册Jni的函数。其中jclass需要额外获取相关类,JNINativeMethod中记录了Java函数和C++函数的映射关系,也是预先写好的。 - jclass FindClass(const char* name)
用来根据类的全命名寻找类的class对象,也是JNIEnv的成员函数:
static const char* mClassName = \"com/example/jnitest2/nativeManager/DynamicRegisterJniManager\";jclass nativeManagerCls = env->FindClass(mClassName);
一般是用来做反射使用(调用某Java类的成员方法),或者在动态注册jni的初始时,需要用它来寻找关联的Java类。
2.3 Jni的函数签名
在1.2 动态注册jni中,以及2.1.2的第4条JNINativeMethod结构 中可以看到函数关系表都是有
函数签名参与的,函数签名用来描述函数的参数以及返回值,当需要使用可以通过jdk直接查询签名表,如:javap -s -p StaticRegisterJniManager.class
结果参考下图:
常见的签名如下表所示:
数据类型 | 签名 |
---|---|
boolean | Z |
byte | B |
char | C |
short | S |
int | I |
long | J |
float | F |
double | D |
void | V |
object | L开头,以/分隔包的完整类型,结尾加‘;’ 比如String为:Ljava/lang/String; |
Array | 以[开头,加上数组元素类型的签名。如int[]应为[I,int[][]为[[I |
如何组织JNINativeMethod的签名格式呢?比如当我的Java方法是这样的:
String foo(int a, boolean b, Date[] c), 对应的签名应该为:
(参数1 参数2 参数3 …)返回值
因此应该为:
(I Z [Ljava/util/Date;)Ljava/lang/String;
2.4 Jni访问字符串
2.4.1 关于编码方式
在Java的JVM内码中,String的编码是utf16编码格式,(也就是说每个字符的大小为2~4个字节),但是在jni中一般使用utf8编码格式,而在C/C++中则普遍使用原始数据(ASCII码),1个字节,中文使用GB2312(2个字节)。因此在使用jni进行字符串传递时,一定需要进行编码的转换,否则会出现乱码情况。
编码的转换过程参考下图:
2.4.2 需要用到的函数
JNIEnv中有关String的函数参考下表:
函数定义 | 函数功能 |
---|---|
jstring (NewString)(JNIEnv, const jchar*, jsize) | 创建新的String |
jsize (GetStringLength)(JNIEnv, jstring) | 返回Unicode字符串的字符数。 |
const jchar* (GetStringChars)(JNIEnv, jstring, jboolean*) | 获取以Unicode格式编码的字符串 |
void (ReleaseStringChars)(JNIEnv, jstring, const jchar*) | 释放字符串的空间 |
jstring (NewStringUTF)(JNIEnv, const char*) | 创建新的jstring字符串 |
jsize (GetStringUTFLength)(JNIEnv, jstring) | 返回UTF-8字符串的字节数,不包含末尾’\\0’。 |
const char* (GetStringUTFChars)(JNIEnv, jstring, jboolean*) | 把jstring指针(指向JVM内部Unicode序列)转化成UTF-8格式的C字符串 |
void (ReleaseStringUTFChars)(JNIEnv, jstring, const char*) | 释放UTF-8编码的字符串 |
值得一提的是,GetStringLength与GetStringUTFChars两个函数的表现效果是不确定的,具体由可能会将提供的参数jstring指针直接强转编码形势后返回,也可能会开辟一段空间并将内容copy过去。第三个参数jboolean*会反应本次操作是否发生了拷贝。为了保险起见,返回的字符串不应该做任何修改,且使用完成应该对应使用ReleaseStringChars和ReleaseStringUTFChars进行释放,防止内存泄漏。
3 Jni中的反射
3.1 访问成员属性
Jni中每个函数都具有JNIEnv*和jobject两个参数,其中jobject在前文中介绍到,是调用者类的对象或者其class。如果当前native方法不是static时,欲访问其成员需要以下步骤(此处假设目标类的某成员属性为 int property = 0):
- 获得其class引用:
jclass clazz = env->GetObjectClass(jobj); // 此处以C++举例 - 获取成员属性ID:
jfieldID jfid = env->GetfieldID(clazz, “property”, “I”) //property为属性名,“I”为其类型签名 - 获取成员属性的值:
jint val = env->GetIntField(jobj, jfid); - 修改成员属性值:
env->SetIntField(jobj, jfid, val + 100);note:当操作私有成员时,需要设置setAccessible(true)后才可以哦!
如果访问的是静态变量,获取/设置字段需要更换为:
val = env->GetStaticIntField(clazz, jobj, jfid);
env->SetStaticIntField(clazz, jobj, jfid, val+100);
3.2 访问成员方法
访问成员方法一般需要按照以下流程(获得类的引用流程基本类似,因此这里我们将前提条件那个了更换,在3.1中假设调用者类就是需要反射的类,此处我们将假定反射任意一个类,例如类名为:FooClass,目标方法为:void methodName(String, int) ):
- 获得class引用,对于任意一个存在的类,需要使用FindClass来查找:
jclass clz = env->FindClass(“FooClass”); - 查找需要使用的 (假设是静态方法,与普通方法流程一致,但调用函数名有区别)
jmethodID jmeid = env->GetStaticMethodID(clz, “methodName”, \”
(Ljava/lang/String;I)V\”); - 调用该静态方法
env->CallStaticVoidMethod(clz,jmeid,“test”,100);