简述java
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
如何使用c++
在目前的Android开发中,通常状况下有2种方法来使用JNI编程,就是传统的须要手动生成h文件和新版的CMake,Cmake的是利用配置文件来完成一些配置,实际上只是简化了流程,用CMakeLists.txt文件来进行一些类库的配置而已,这里以Cmake为例子,下面是步骤:编程
● 首先新建一个项目,而且勾选上C++的支持,如图:数组
● 第一个步骤完成以后,会在项目的build.gradle文件里面生成下面的几个选项,安全
defaultConfig {
//省略一些代码
externalNativeBuild {
cmake {
cppFlags "-frtti -fexceptions"//这里指定了编译的一些C++选项
}
}
}
externalNativeBuild {
cmake {
path "CMakeLists.txt"//这里指定了配置文件的路径在项目目录下,文件名叫作CMakeLists.text,
这个路径能够本身修改成本身想要的路径,只须要在这里修改,而且把文件移动到相应的目录下就能够了
}
}
复制代码
而后就能够在项目的目录下看到CMakeLists.text这个文件了,咱们来看一下其中生成的代码,这里会省略掉注释,占篇幅啊:bash
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,下面来写实际的代码:app
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方法创建关联,可是它有一些明显的缺点:jvm
● 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 就能够获取了,下面是一张截图就能够看明白:
//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等等,这个在用到的时候再说,你们也能够多去研究,今天的文章就写到这里,感谢你们阅读.。