JNI解析以及在Android中的實際應用

stormWen發表於2017-12-29

簡述

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++的支援,如圖:

JNI解析以及在Android中的實際應用
然後預設就好,最後來到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 就可以獲取了,下面是一張截圖就可以看明白:

JNI解析以及在Android中的實際應用
可以看到了吧,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解析以及在Android中的實際應用
大家可以多去看看那些方法,基本上各種型別的方法都有,執行如下:

JNI解析以及在Android中的實際應用

JNI資料型別

上面我們提到JNI定義了一些自己的資料型別。這些資料型別是銜接Java層和C/C++層的,如果有一個物件傳遞下來,那麼對於C/C++來說是沒辦法識別這個物件的,同樣的如果C/C++的指標對於Java層來說它也是沒辦法識別的,那麼就需要JNI進行匹配,所以需要定義一些自己的資料型別,分為原始型別和引用型別,匹配的規則如下:

●.原始資料型別

JNI解析以及在Android中的實際應用

● 引用型別

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 也可以看的出來的,一般對應規則如下:

JNI解析以及在Android中的實際應用

對於陣列的話,舉列如下:其他的都是類似的,有規律可循

JNI解析以及在Android中的實際應用

資料型別描述符

上面說的是方法描述符,實際上資料型別也是有描述符的,如下表所示:

JNI解析以及在Android中的實際應用

而對於引用型別,用L開頭的,比如:

JNI解析以及在Android中的實際應用

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

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等等,這個在用到的時候再說,大家也可以多去研究,今天的文章就寫到這裡,感謝大家閱讀.。

相關文章