简述
JNI 是 Java Native Interface 的缩写,它提供了若干的 API 实现了 Java 和其他语言的通信(在 Android 里面主要是 C&C++)。从 Java1.1 开始,JNI 标准成为 java 平台的一部分,它允许 Java 代码和其他语言写的代码进行动态交互, JNI 标准保证本地代码能工作在任何 Java 虚拟机环境, 目前的很多热修复补的开源项目,比如——Depoxed(阿里)、AnFix(阿里)、DynamicAPK(携程) 等,它们都用到了 JNI 编程,并且 JNI 编程也贯穿了 Android 系统,实际上 JNI 是 Android 系统中底层和框架层通信的重要方式、JNI 对于 Android 安全以及 Android 安全加固等都是有所帮助的, 一般情况下,在 Android 应用层,大部分时间都是在使用 Java 编程,很少使用 C/C++ 编程, 在一些比较特殊的情况下会用到,比如加密等等, 下面我将详细分析 JNI 原理以及会有一个实际的例子来说明加深理解。
如何使用
在目前的 Android 开发中,一般情况下有 2 种方法来使用 JNI 编程,就是传统的需要手动生成 h 文件和新版的 CMake,Cmake 的是利用配置文件来完成一些配置,实际上只是简化了流程,用 CMakeLists.txt 文件来进行一些类库的配置而已,这里以 Cmake 为例子,下面是步骤:
● 首先新建一个项目,并且勾选上 C++ 的支持,如图:
然后默认就好,最后来到 C++ 有关的选项,可以 2 个都勾上。
● 第一个步骤完成之后,会在项目的 build.gradle 文件里面生成下面的几个选项,
- defaultConfig {
- //省略一些代码
- externalNativeBuild {
- cmake {
- cppFlags "-frtti -fexceptions" //这里指定了编译的一些C++选项
- }
- }
- }
- externalNativeBuild {
- cmake {
- path "CMakeLists.txt" //这里指定了配置文件的路径在项目目录下,文件名叫做CMakeLists.text,
- 这个路径可以自己修改为自己想要的路径,只需要在这里修改,并且把文件移动到相应的目录下就可以了
- }
- }
然后就可以在项目的目录下看到 CMakeLists.text 这个文件了,我们来看一下其中生成的代码, 这里会省略掉注释,占篇幅啊:
- cmake_minimum_required(VERSION 3.4.1)// 指定CMake的版本
- //add_library是添加类库,下面3个分别表示类库的名字叫做native-lib.so,SHARED这个选项表示共享类库的意思(就是以so结尾)
- // src/main/cpp/native-lib.cpp表示native-lib.so对应的C++源代码位置
- //这个add_library很重要,因为如果要添加其他类库,那么都是这样的方法来的,比如
- 添加这个 wlffmpeg类库
- add_library( # Sets the name of the library.
- wlffmpeg
- # Sets the library as a shared library.
- SHARED
- # Provides a relative path to your source file(s).
- src/main/jni/player.cpp )
- add_library(
- native-lib
- SHARED
- src/main/cpp/native-lib.cpp )
- //表示系统的日志库,只需要导入一个就可以了
- find_library(
- log-lib
- log )
- //链接库,要跟上面的类库名字保持一致
- target_link_libraries(
- native-lib
- ${log-lib} )
好了,上面是关于 CMakeLists.text 内容的一些分析,实际项目中,会更加复杂,特别是导入第三方 so 库的时候,这个有机会再讲,我们知道了,这个 so 库的名字就叫做 native-lib.so,下面来写实际的代码:
- public class JniDemo {
- static {
- System.loadLibrary("native-lib");
- }
- //静态注册
- public static native Object getPackage();
- //静态注册
- public static native int addTest(int a, int b);
- //需要动态注册的方法
- public static native Application getApplicationObject();
- }
首先我们在静态代码块加载 so 库,我们已经知道了是 native-lib, 然后定义 3 个方法,这里前面 2 个方法是静态注册,后面的这个方法是动态注册,这里为什么要区分呢,在 AndroidStudio 中,用 Alt+Enter 弹出的菜单就可以自动生成方法了,我们来看一下:
- extern "C"
- JNIEXPORT jObject JNICALL
- Java_com_jni_JniDemo_getPackage(JNIEnv *env, jclass type) {
- std::string hello = "com.example.test";
- // TODO
- return env->NewStringUTF(hello.c_str());
- }
- extern "C"
- JNIEXPORT jint JNICALL
- Java_com_jni_JniDemo_addTest(JNIEnv *env, jclass type, jint a, jint b) {
- // TODO
- return a + b;
- }
可以看到静态注册的方法的格式为 Java_包名_类名_方法名, 参数来看 其中 JNIEnv * 是一个指向全部 JNI 方法的指针,该指针只在创建它的线程有效,不能跨线程传递, 就是说每个线程都有自己的 JNIEnv, jclass 是 JNI 的数据类型,对应 Java 的 java.lang.Class 实例。jobject 同样也是 JNI 的数据类型,对应于 Java 的 Object, 系统在调用 native 方法的时候会根据方法名,将 Java 方法和 JNI 方法建立关联,但是它有一些明显的缺点:
● JNI 层的方法名称过长, 特别是包名比较深的话,就更加明显了
● 声明 Native 方法的类需要用 javah 生成头文件, 在以前的开发中需要自己手动生成,现在是工具帮我们生成了而已
● 初次调用 JIN 方法时需要建立关联,影响效率, 在建立关系的时候是全局搜索的,这样效率上大打折扣。
● 不够灵活,因为有些需要在运行的时候才决定注册需要的方法。
因为以上的不方便,所以才有了动态注册的机制存在, 下面简单分析一下:
JNI_OnLoad 函数
在调用了
- System.loadLibrary("native-lib");
方法加载 so 库的时候,Java 虚拟机就会找到这个函数并调用该函数,因此可以在该函数中做一些初始化的动作,其实这个函数就是相当于 Activity 中的 onCreate() 方法。该函数前面有三个关键字,分别是 JNIEXPORT、JNICALL 和 jint,其中 JNIEXPORT 和 JNICALL 是两个宏定义,用于指定该函数是 JNI 函数。jint 是 JNI 定义的数据类型,因为 Java 层和 C/C++ 的数据类型或者对象不能直接相互的引用或者使用,JNI 层定义了自己的数据类型,用于衔接 Java 层和 JNI 层,至于这些数据类型我们在后面介绍。这里的 jint 对应 Java 的 int 数据类型,该函数返回的 int 表示当前使用的 JNI 的版本,其实类似于 Android 系统的 API 版本一样,不同的 JNI 版本中定义的一些不同的 JNI 函数。该函数会有两个参数,其中 * jvm 为 Java 虚拟机实例,JavaVM 结构体定义了以下函数
- DestroyJavaVM
- AttachCurrentThread
- DetachCurrentThread
- GetEnv
我们前面已经说过了,JNIEnv 是线程范围内的 JNI 环境,在动态注册的时候首先需要获取, 一般用下面的代码:
- JNIEnv * env = NULL;
- if (vm - >GetEnv((void * *) & env, JNI_VERSION_1_4) != JNI_OK) {
- return - 1;
- }
好了,获取到了 JNIEnv 了,既然是动态注册,那么就会有对应的方法,方法为:
- jint RegisterNatives(jclass clazz, const JNINativeMethod * methods, jint nMethods) {
- return functions - >RegisterNatives(this, clazz, methods, nMethods);
- }
其中第一个参数为: 需要动态注册的 Java 类 (以 / 来隔开,比如 com/example / 等),第二个参数是一个 JNINativeMethod 指针,定义如下:
- typedef struct {
- const char* name; //java层对应的方法全名
- const char* signature;//方法的签名
- void* fnPtr;//对应的在c++里面的方法
- } JNINativeMethod;
注释已经有了,其中第二个参数是方法的签名,我们回顾一下,Java 是如何判断 2 个方法是相同的呢,是方法的签名,换句话说,每个方法都有自己的签名,每个签名对应一个方法,用 javap -s -p 就可以获取了,下面是一张截图就可以看明白:
可以看到了吧, description: 后面的就是对应的方法的签名了,这个后面会用到
- //TODO 动态注册的方法集合
- static JNINativeMethod gMethods[] = {
- {"getApplicationObject", "()Landroid/app/Application;", (void *) getApplicationObject}
- };
- 这是下面要讲的例子,这个例子是在JNI中获取application对象,是用反射获取
好了,有了这些,那么就可以动态注册了,全部代码如下:
- #include <jni.h>
- #include <string>
- #include "log.h"
- //TODO 这个表示需要动态注册的函数所在的类文件
- static const char *const CLASSNAME = "com/jni/JniDemo";
- extern "C"
- JNIEXPORT jobject JNICALL
- Java_com_jni_JniDemo_getPackage(JNIEnv *env, jclass type) {
- // TODO 获取包名,一样可以反射获取,这里我们获取主线程里面的currentPackageName()方法就好
- jclass jclass1 = env->FindClass("android/app/ActivityThread");
- jmethodID jmethodID1 = env->GetStaticMethodID(jclass1, "currentPackageName",
- "()Ljava/lang/String;");
- jobject jobject1 = (jstring ) env->CallStaticObjectMethod(jclass1, jmethodID1);
- return jobject1;
- }
- extern "C"
- JNIEXPORT jint JNICALL
- Java_com_jni_JniDemo_addTest(JNIEnv *env, jclass type, jint a, jint b) {
- // TODO
- return a + b;
- }
- extern "C"
- JNIEXPORT jstring JNICALL
- Java_com_example_hadoop_testproject_MainActivity_stringFromJNI(
- JNIEnv *env,
- jobject /* this */) {
- std::string hello = "Hello from C++";
- return env->NewStringUTF(hello.c_str());
- }
- //TODO 获取application对象
- jobject getApplicationObject(JNIEnv *env, jobject thiz) {
- jobject mApplicationObj = NULL;
- //找到ActivityThread类
- jclass jclass1 = env->FindClass("android/app/ActivityThread");
- //找到currentActivityThread方法
- jmethodID jmethodID2 = env->GetStaticMethodID(jclass1, "currentActivityThread", "()Landroid/app/ActivityThread;");
- //获取ActivityThread对象
- jobject mCurrentActivity = env->CallStaticObjectMethod(jclass1, jmethodID2);
- //找到currentApplication方法
- jmethodID jmethodID1 = env->GetMethodID(jclass1, "getApplication",
- "()Landroid/app/Application;");
- //获取Application对象
- mApplicationObj = env->CallObjectMethod(mCurrentActivity, jmethodID1);
- if (mApplicationObj == NULL) {
- return NULL;
- }
- return mApplicationObj;
- }
- //TODO 动态注册的方法集合
- static JNINativeMethod gMethods[] = {
- {"getApplicationObject", "()Landroid/app/Application;", (void *) getApplicationObject}
- };
- /*
- * System.loadLibrary("lib")时调用
- * 如果成功返回JNI版本, 失败返回-1
- * 这个方法一般都是固定的
- */
- extern "C"
- JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
- JNIEnv *env = NULL;
- if (vm->GetEnv((void **) &env, JNI_VERSION_1_4) != JNI_OK) {
- return -1;
- }
- if (env == NULL) {
- return -1;
- }
- // 需要注册的类
- jclass clazz = env->FindClass(CLASSNAME);
- if (clazz == NULL) {
- return -1;
- }
- //TODO 这里是重点,动态注册方法
- if (env->RegisterNatives(clazz, gMethods, sizeof(gMethods) / sizeof(gMethods[0])) < 0) {
- return -1;
- }
- LOGD("dynamic success is %d", JNI_VERSION_1_4);
- return JNI_VERSION_1_4;
- }
- 日志文件代码如下:
- #ifndef FINENGINE_LOG_H
- #define FINENGINE_LOG_H
- #include <android/log.h>
- static const char* kTAG = "JNIDEMO";
- #define LOGI(...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__)
- #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)
- #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,kTAG,__VA_ARGS__)
- #endif
注释也已经很清楚了,我们需要知道 C 语言中调用 Java 的一些函数,实际上也是反射获取的,步骤跟 Java 层的是一样的,换句话说在 Java 反射能做到的,在 JNI 中通过类似的反射也是可以做到的,这些方法原型在 jni.h 文件里面,比如
大家可以多去看看那些方法,基本上各种类型的方法都有,运行如下:
JNI 数据类型
上面我们提到 JNI 定义了一些自己的数据类型。这些数据类型是衔接 Java 层和 C/C++ 层的,如果有一个对象传递下来,那么对于 C/C++ 来说是没办法识别这个对象的,同样的如果 C/C++ 的指针对于 Java 层来说它也是没办法识别的,那么就需要 JNI 进行匹配,所以需要定义一些自己的数据类型,分为原始类型和引用类型,匹配的规则如下:
●. 原始数据类型
● 引用类型
- jobject (all Java objects)
- |
- |-- jclass (java.lang.Class objects)
- |-- jstring (java.lang.String objects)
- |-- jarray (array)
- | |--jobjectArray (object arrays)
- | |--jbooleanArray (boolean arrays)
- | |--jbyteArray (byte arrays)
- | |--jcharArray (char arrays)
- | |--jshortArray (short arrays)
- | |--jintArray (int arrays)
- | |--jlongArray (long arrays)
- | |--jfloatArray (float arrays)
- | |--jdoubleArray (double arrays)
- |
- |--jthrowable
方法描述符
我们前面说了,在调用方法的时候需要提供一个方法的签名,动态注册 native 方法的时候结构体 JNINativeMethod 中含有方法描述符,就是确定 native 方法的参数和返回值,我们这里定义的 getApplication() 方法没有参数,返回值为空所以对应的描述符为:"()Landroid/app/Application;",括号类为参数,其他的表示返回值,通过 javap -s -p 也可以看的出来的, 一般对应规则如下:
对于数组的话,举列如下: 其他的都是类似的,有规律可循
数据类型描述符
上面说的是方法描述符,实际上数据类型也是有描述符的,如下表所示:
而对于引用类型,用 L 开头的,比如:
其他的基本都是类似的,在用的是时候注意下就好。
JNI 在 Android 中的实际应用
前面说了,JNI 在整个 Android 系统中发挥了重要的作用,是连接底层和框架层的桥梁, 在 Android 源码中更是大量的 JNI 代码,我们来说一个实际的例子:获取签名并且校验签名,原理是: 获取当前的签名信息并且跟期待的签名信息是否一致,如果是一致,则通过,否则失败, 代码原理跟上面的反射是一个道理. 这个工作在 JNI_OnLoad 中完成,如下代码:
- JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM * vm, void * reserved) {
- JNIEnv * evn;
- if (vm - >GetEnv((void * *)( & evn), JNI_VERSION_1_6) != JNI_OK) {
- return - 1;
- }
- jclass appClass = evn - >FindClass("com/***/App");
- jmethodID getAppContextMethod = evn - >GetStaticMethodID(appClass, "getContext", "()Landroid/content/Context;");
- //获取APplication定义的context实例
- jobject appContext = evn - >CallStaticObjectMethod(appClass, getAppContextMethod);
- // 获取应用当前的签名信息
- jstring signature = loadSignature(evn, appContext);
- // 期待的签名信息
- jstring keystoreSigature = evn - >NewStringUTF("31BC77F998CB0D305D74464DAECC2");
- const char * keystroreMD5 = evn - >GetStringUTFChars(keystoreSigature, NULL);
- const char * releaseMD5 = evn - >GetStringUTFChars(signature, NULL);
- // 比较两个签名信息是否相等
- int result = strcmp(keystroreMD5, releaseMD5);
- if (DEBUG_MODE) LOGI("strcmp %d", result);
- // 这里记得释放内存
- evn - >ReleaseStringUTFChars(signature, releaseMD5);
- evn - >ReleaseStringUTFChars(keystoreSigature, keystroreMD5);
- // 得到的签名一样,验证通过
- if (result == 0) {
- return JNI_VERSION_1_6;
- }
- return - 1;
- }
loadSignature(evn, appContext) 也是反射调用 Java 代码实现的,是系统自带的功能,代码如下:
- jstring loadSignature(JNIEnv *env, jobject context)
- {
- // 获取Context类
- jclass contextClass = env->GetObjectClass(context);
- if (DEBUG_MODE)
- LOGI("获取Context类");
- // 得到getPackageManager方法的ID
- jmethodID getPkgManagerMethodId = env->GetMethodID(contextClass, "getPackageManager", "()Landroid/content/pm/PackageManager;");
- if (DEBUG_MODE)
- LOGI("得到getPackageManager方法的ID");
- // PackageManager
- jobject pm = env->CallObjectMethod(context, getPkgManagerMethodId);
- if (DEBUG_MODE)
- LOGI("PackageManager");
- // 得到应用的包名
- jmethodID pkgNameMethodId = env->GetMethodID(contextClass, "getPackageName", "()Ljava/lang/String;");
- jstring pkgName = (jstring) env->CallObjectMethod(context, pkgNameMethodId);
- if (DEBUG_MODE)
- LOGI("get pkg name: %s", getCharFromString(env, pkgName));
- // 获得PackageManager类
- jclass cls = env->GetObjectClass(pm);
- // 得到getPackageInfo方法的ID
- jmethodID mid = env->GetMethodID(cls, "getPackageInfo", "(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;");
- // 获得应用包的信息
- jobject packageInfo = env->CallObjectMethod(pm, mid, pkgName, 0x40); //GET_SIGNATURES = 64;
- // 获得PackageInfo 类
- cls = env->GetObjectClass(packageInfo);
- // 获得签名数组属性的ID
- jfieldID fid = env->GetFieldID(cls, "signatures", "[Landroid/content/pm/Signature;");
- // 得到签名数组
- jobjectArray signatures = (jobjectArray) env->GetObjectField(packageInfo, fid);
- // 得到签名
- jobject signature = env->GetObjectArrayElement(signatures, 0);
- // 获得Signature类
- cls = env->GetObjectClass(signature);
- // 得到toCharsString方法的ID
- mid = env->GetMethodID(cls, "toByteArray", "()[B");
- // 返回当前应用签名信息
- jbyteArray signatureByteArray = (jbyteArray) env->CallObjectMethod(signature, mid);
- return ToMd5(env, signatureByteArray);
- }
注释已经很明显了,获取签名信息并且转换为 MD5 格式的,如下:
- jstring ToMd5(JNIEnv * env, jbyteArray source) {
- // MessageDigest类
- jclass classMessageDigest = env - >FindClass("java/security/MessageDigest");
- // MessageDigest.getInstance()静态方法
- jmethodID midGetInstance = env - >GetStaticMethodID(classMessageDigest, "getInstance", "(Ljava/lang/String;)Ljava/security/MessageDigest;");
- // MessageDigest object
- jobject objMessageDigest = env - >CallStaticObjectMethod(classMessageDigest, midGetInstance, env - >NewStringUTF("md5"));
- // update方法,这个函数的返回值是void,写V
- jmethodID midUpdate = env - >GetMethodID(classMessageDigest, "update", "([B)V");
- env - >CallVoidMethod(objMessageDigest, midUpdate, source);
- // digest方法
- jmethodID midDigest = env - >GetMethodID(classMessageDigest, "digest", "()[B");
- jbyteArray objArraySign = (jbyteArray) env - >CallObjectMethod(objMessageDigest, midDigest);
- jsize intArrayLength = env - >GetArrayLength(objArraySign);
- jbyte * byte_array_elements = env - >GetByteArrayElements(objArraySign, NULL);
- size_t length = (size_t) intArrayLength * 2 + 1;
- char * char_result = (char * ) malloc(length);
- memset(char_result, 0, length);
- // 将byte数组转换成16进制字符串,发现这里不用强转,jbyte和unsigned char应该字节数是一样的
- ByteToHexStr((const char * ) byte_array_elements, char_result, intArrayLength);
- // 在末尾补\0
- * (char_result + intArrayLength * 2) = '\0';
- jstring stringResult = env - >NewStringUTF(char_result);
- // release
- env - >ReleaseByteArrayElements(objArraySign, byte_array_elements, JNI_ABORT);
- // 释放指针使用free
- free(char_result);
- return stringResult;
- }
这个也是系统的 MD5 加密功能, 可以看到先获取了系统自带的签名信息,然后跟一个预期的信息进行 strcmp 比较,如果是一致的话,那么通过,如果不一样,有可能程序被篡改了,就不能通过,然后采取其他的措施,比如杀掉进程等等方法来处理,这个需要在实际的业务中根据实际情况决定。
在实际中,JNI 还有很多的应用,比如 FFMPEG,OpenGL 等等,这个在用到的时候再说,大家也可以多去研究,今天的文章就写到这里,感谢大家阅读.。
来源: https://juejin.im/post/5a449b1a6fb9a045055e6eff