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++的支援,如圖:
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiIwIjNx8CX39CXy8CXycXZpZVZnFWbp9zZlBnauYTOzATO0cjZlVjYyEWNlRTOlJzMwkTZ4UjM0UWZ3MmNvwFN5MTO5MzNtUGall3LcVmdhNXLwRHdo9CXt92YucWbpRWdvx2Yx5yazF2Lc9CX6MHc0RHaiojIsJye.jpeg)
然後預設就好,最後來到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檔案裡面,比如
大家可以多去看看那些方法,基本上各種類型的方法都有,運作如下:
3
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開頭的,比如:
其他的基本都是類似的,在用的是時候注意下就好。
4
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等等,這個在用到的時候再說,大家也可以多去研究,今天的文章就寫到這裡,感謝大家閱讀.。