android jni 作用,JNI解析以及在Android中的实际应用

原标题:JNI解析以及在Android中的实际应用

本文作者

作者:stormWen

掘金主页:

https://juejin.im/user/5743cb0ec26a38006c3b5c75

本文由作者投稿发布。

1

简述

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原理以及会有一个实际的例子来说明加深理解。

2

如何使用

在目前的Android开发中,一般情况下有2种方法来使用JNI编程,就是传统的需要手动生成h文件和新版的CMake,Cmake的是利用配置文件来完成一些配置,实际上只是简化了流程,用CMakeLists.txt文件来进行一些类库的配置而已,这里以Cmake为例子,下面是步骤:

● 首先新建一个项目,并且勾选上C++的支持,如图:

e5ec94d94c88cacf951021f36111cfe3.png

然后默认就好,最后来到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,下面来写实际的代码:

publicclassJniDemo{

static{

System.loadLibrary( "native-lib");

}

//静态注册

publicstaticnativeObject getPackage();

//静态注册

publicstaticnativeintaddTest(inta, intb);

//需要动态注册的方法

publicstaticnativeApplication getApplicationObject();

}

首先我们在静态代码块加载so库,我们已经知道了是native-lib,然后定义3个方法,这里前面2个方法是静态注册,后面的这个方法是动态注册,这里为什么要区分呢.

在AndroidStudio中,用Alt+Enter弹出的菜单就可以自动生成方法了,我们来看一下:

extern"C"

JNIEXPORT jObject JNICALL

Java_com_jni_JniDemo_getPackage(JNIEnv *env, jclass type){

std:: stringhello = "com.example.test";

// TODO

returnenv->NewStringUTF(hello.c_str());

}

extern"C"

JNIEXPORT jint JNICALL

Java_com_jni_JniDemo_addTest(JNIEnv *env, jclass type, jint a, jint b){

// TODO

returna + b;

}

可以看到静态注册的方法的格式为Java_包名_类名_方法名,参数来看其中JNIEnv * 是一个指向全部JNI方法的指针,该指针只在创建它的线程有效,不能跨线程传递,就是说每个线程都有自己的JNIEnv,jclass是JNI的数据类型,对应Java的java.lang.Class实例。

jobject同样也是JNI的数据类型,对应于Java的Object,系统在调用native方法的时候会根据方法名,将Java方法和JNI方法建立关联,但是它有一些明显的缺点:

JNI层的方法名称过长,特别是包名比较深的话,就更加明显了

声明Native方法的类需要用javah生成头文件, 在以前的开发中需要自己手动生成,现在是工具帮我们生成了而已

初次调用JIN方法时需要建立关联,影响效率,在建立关系的时候是全局搜索的,这样效率上大打折扣。

不够灵活,因为有些需要在运行的时候才决定注册需要的方法。

因为以上的不方便,所以才有了动态注册的机制存在,下面简单分析一下:

JNI_函数

在调用了

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, constJNINativeMethod* methods,

jint nMethods)

{ returnfunctions->RegisterNatives( this, clazz, methods, nMethods); }

其中第一个参数为:需要动态注册的Java类(以/来隔开,比如com/example/等),第二个参数是一个JNINativeMethod指针,定义如下:

typedefstruct{

constchar* name; //java层对应的方法全名

constchar* signature; //方法的签名

void* fnPtr; //对应的在c++里面的方法

} JNINativeMethod;

注释已经有了,其中第二个参数是方法的签名,我们回顾一下,Java是如何判断2个方法是相同的呢,是方法的签名,换句话说,每个方法都有自己的签名,每个签名对应一个方法,用javap -s -p 就可以获取了,下面是一张截图就可以看明白:

f165c2b9c46e39f1a10c32bab72167f3.png

可以看到了吧,deion:后面的就是对应的方法的签名了,这个后面会用到

//TODO 动态注册的方法集合

staticJNINativeMethod gMethods[] = {

{ "getApplicationObject", "()Landroid/app/Application;", ( void*) getApplicationObject}

};

这是下面要讲的例子,这个例子是在JNI中获取application对象,是用反射获取

好了,有了这些,那么就可以动态注册了,全部代码如下:

#include

#include

#include"log.h"

//TODO 这个表示需要动态注册的函数所在的类文件

staticconstchar* constCLASSNAME = "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);

returnjobject1;

}

extern"C"

JNIEXPORT jint JNICALL

Java_com_jni_JniDemo_addTest(JNIEnv *env, jclass type, jint a, jint b){

// TODO

returna + b;

}

extern"C"

JNIEXPORT jstring JNICALL

Java_com_example_hadoop_testproject_MainActivity_stringFromJNI(

JNIEnv *env,

jobject /* this */){

std:: stringhello = "Hello from C++";

returnenv->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) {

returnNULL;

}

returnmApplicationObj;

}

//TODO 动态注册的方法集合

staticJNINativeMethod gMethods[] = {

{ "getApplicationObject", "()Landroid/app/Application;", ( void*) getApplicationObject}

};

/*

* System.loadLibrary("lib")时调用

* 如果成功返回JNI版本, 失败返回-1

* 这个方法一般都是固定的

*/

extern"C"

JNIEXPORT jint JNICALL JNI_(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);

returnJNI_VERSION_1_4;

}

日志文件代码如下:

#ifndefFINENGINE_LOG_H

#defineFINENGINE_LOG_H

#include

staticconstchar* kTAG = "JNIDEMO";

#defineLOGI(...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__)

#defineLOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)

#defineLOGD(...) __android_log_print(ANDROID_LOG_DEBUG,kTAG,__VA_ARGS__)

#endif

注释也已经很清楚了,我们需要知道C语言中调用Java的一些函数,实际上也是反射获取的,步骤跟Java层的是一样的,换句话说在Java反射能做到的,在JNI中通过类似的反射也是可以做到的,这些方法原型在jni.h文件里面,比如

8e56df0c3b63750c5c746279bbe7811a.png

大家可以多去看看那些方法,基本上各种类型的方法都有,运行如下:

fbdc011dfcc9330d9bca1b70aeb07436.png

3

JNI数据类型

上面我们提到JNI定义了一些自己的数据类型。这些数据类型是衔接Java层和C/C++层的,如果有一个对象传递下来,那么对于C/C++来说是没办法识别这个对象的,同样的如果C/C++的指针对于Java层来说它也是没办法识别的,那么就需要JNI进行匹配,所以需要定义一些自己的数据类型,分为原始类型和引用类型,匹配的规则如下:

●.原始数据类型

d61883a05cdac95a82da8dd25cecf4f5.png

● 引用类型

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 也可以看的出来的,一般对应规则如下:

49f043df3ce14263f8d2f520d7aa6299.png

对于数组的话,举列如下:其他的都是类似的,有规律可循

c85a083f56c4251497f4561c8dc90221.png

数据类型描述符

上面说的是方法描述符,实际上数据类型也是有描述符的,如下表所示:

bbca959d720e677ae6141e40aba721eb.png

而对于引用类型,用L开头的,比如:

a7db83a615d25cb46a7c1253e5fc624a.png

其他的基本都是类似的,在用的是时候注意下就好。

4

JNI在Android中的实际应用

前面说了,JNI在整个Android系统中发挥了重要的作用,是连接底层和框架层的桥梁,在Android源码中更是大量的JNI代码,我们来说一个实际的例子:获取签名并且校验签名,原理是:获取当前的签名信息并且跟期待的签名信息是否一致,如果是一致,则通过,否则失败,代码原理跟上面的反射是一个道理.这个工作在JNI_中完成,如下代码:

JNIEXPORT jint JNICALL JNI_(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");

constchar *keystroreMD5 = evn->GetStringUTFChars(keystoreSigature, NULL);

constchar *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){

returnJNI_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);

returnToMd5(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(( constchar*)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);

returnstringResult;

}

这个也是系统的MD5加密功能,可以看到先获取了系统自带的签名信息,然后跟一个预期的信息进行strcmp比较,如果是一致的话,那么通过,如果不一样,有可能程序被篡改了,就不能通过,然后采取其他的措施,比如杀掉进程等等方法来处理,这个需要在实际的业务中根据实际情况决定。

在实际中,JNI还有很多的应用,比如FFMPEG,OpenGL等等,这个在用到的时候再说,大家也可以多去研究,今天的文章就写到这里,感谢大家阅读.。

最后推荐一下我做的网站,玩Android: wanandroid.com,包含详尽的知识体系、好用的工具,还有本公众号文章合集,欢迎体验和收藏!返回搜狐,查看更多

责任编辑:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值