暴力突破 JNI

語言: CN / TW / HK

theme: smartblue highlight: androidstudio


一、前言


JNI 簡單來説就是 java 調用本地 C/C++ 的技術,JVM 就是通過大量的 JNI 技術使得 Java 能在不同平台上運行。它允許 Java 類的某些方法用原生方法實現,這些原生方法也可以使用 Java 對象,使用方法與 Java 代碼調用 Java 對象的方法相同。原生方法可以創建新的 Java 對象或者使用 Java 應用程序創建的對象。

二、簡單示例


現在我們來看下官方示例,在 Android Studio 上創建一個 Native C++ 工程:

image.png 創建好後的項目結構如下:

image.png 可以看到比普通的 Android 項目多了一個 cpp 目錄,並且在 MainActivity 中多了個一個 native 方法 stringFromJNI(),這個方法 在 Java 層是個空實現,它的具體實現在 native-lib.cpp 中:

image.png 該方法返回一個字符串,現在我們運行這個項目:

三、JNIEnv 和 jobject 解釋


我們繼續來看 stringFromJNI 方法: cpp extern "C" JNIEXPORT jstring JNICALL Java_com_tencent_learnndk_MainActivity_stringFromJNI(JNIEnv *env, jobject instance) { std::string hello = "Hello from C++"; return env->NewStringUTF(hello.c_str()); } 其中 JNIEXPORT 與 JNICALL 都是 JNI 的關鍵字,表示此函數是要被 JNI 調用的。接下來來看看方法參數。

3.1 JNIEnv *env

JNIEnv 表示 Java 調用 native 語言的環境,是一個封裝了幾乎全部 JNI 方法的指針。JNI 中有哪些方法可以參考 JNI 方法大全及使用示例

JNIEnv 只在創建它的線程生效,不能跨線程傳遞,不同線程的 JNIEnv 彼此獨立。native 環境中創建的線程,如果需要訪問 JNI,必須要調用 AttachCurrentThread 關聯,並使用 DetachCurrentThread 解除鏈接。

除此之外,JNIEnv 在 C 和 C++ 環境下的調用也是有區別的:

cpp //C風格: (*env)->NewStringUTF(env, “Hellow World!”); //C++風格: env->NewStringUTF(“Hellow World!”);

3.2 jobject instance

如果 native 方法不是 static 的話,這個 instance 就代表這個 native 方法的類實例。如果 native 方法是 static 的話就代表這個 native 方法的類的 class 對象實例。示例如下:

java 代碼: ```java public native String test();

public static native String testStatic(); ```

jni 代碼:

```java extern "C" JNIEXPORT jstring JNICALL Java_com_tencent_learnndk_MainActivity_test(JNIEnv *env, jobject instance) { }

extern "C" JNIEXPORT jstring JNICALL Java_com_tencent_learnndk_MainActivity_testStatic(JNIEnv *env, jclass type) { } ```

四、Java 和 C/C++ 中類型的映射關係


現在我們在 MainActivity 中新增一個 native 方法,參數為 name:

image.png 並根據代碼提示自動生成 JNI 方法:

image.png 可以看到 Java 中 String 類型的參數,在 native 方法中變成了 jstring 類型。那是因為 JNI 是接口語言,所以會有一箇中間的轉型過程,數據類型的轉變是這個過程中重要的類型對接方式。下面我們來看看各個類型的映射。

基本數據類型:

| Java 類型 | JNI 類型 | C/C++ 類型 | | --- | --- | --- | | boolean | jboolean | unsigned char | |byte |jbyte |char |char |jchar |unsingned short |short |jshort |short |int |jint |int |long |jlong |long |float |jfloat |float |double |jdouble |double

引用類型:

| Java 類型 | 原生類型 | Java 類型 | 原生類型 | | --- | --- | --- | --- | | | | | | |Java.lang.Class| jclass| char[]| jcharArray| |Java.lang.Throwable| jthrowable| short[]| jshortArray| |Java.lang.String| jstring| int[]| jintArray| |Other object| jobject| long[]| jlongArray| |Java.lang.Object[]| jobjectArray| float[]| jfloatArray| |boolean[]| jbooleanArray| double[]| jdoubleArray| |byte[]| jbyteArray| Other arrays| jarray|

五、JNI 調用 Java 代碼


上面描述的都是 java 調用 C/C++ 端代碼,然而在 JNI 中還有一個非常重要的內容,即在 C/C++ 本地代碼中訪問 Java 端代碼。我們先來看下例子。

```java package com.tencent.learnndk;

public class Person {

private String name;

private int age;

public int getAge() {
    return age;
}

public void setAge(int age) {
    this.age = age;
}

public Person(String name, int age) {
    this.name = name;
    this.age = age;
}

public void setName(String name) {
    this.name = name;
}

public String getName() {
    return name;
}

@Override
public String toString() {
    return "Person{name='" + name + "', age=" + age + '}';
}

} ```

修改 MainActivity 的代碼如下: ```java public class MainActivity extends AppCompatActivity {

static {
    System.loadLibrary("native-lib");
}

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    // Example of a call to a native method
    TextView tv = findViewById(R.id.sample_text);
    tv.setText(getPerson().toString());
}

public native Person getPerson();

} 修改 native-lib.cpp 的代碼如下:cpp

include

include

include

define LOG_TAG "learnNdk"

define LOGD(...) android_log_print(ANDROID_LOG_DEBUG,LOG_TAG,__VA_ARGS)

extern "C" JNIEXPORT jobject JNICALL Java_com_tencent_learnndk_MainActivity_getPerson(JNIEnv *env, jobject instance) {

//1. 拿到 Java 類的全路徑
const char *person_java = "com/tencent/learnndk/Person";
const char *method = "<init>"; // Java構造方法的標識

//2. 找到需要處理的 Java 對象 class
jclass j_person_class = env->FindClass(person_java);

//3. 拿到空參構造方法
jmethodID person_constructor = env->GetMethodID(j_person_class, method, "()V");

//4. 創建對象
jobject person_obj = env->NewObject(j_person_class, person_constructor);

//5. 拿到 setName 方法的簽名,並拿到對應的 setName 方法
const char *nameSig = "(Ljava/lang/String;)V";
jmethodID nameMethodId = env->GetMethodID(j_person_class, "setName", nameSig);

//6. 拿到 setAge 方法的簽名,並拿到 setAge 方法
const char *ageSig = "(I)V";
jmethodID ageMethodId = env->GetMethodID(j_person_class, "setAge", ageSig);

//7. 正在調用 Java 對象函數
const char *name = "lerendan";
jstring newStringName = env->NewStringUTF(name);
env->CallVoidMethod(person_obj, nameMethodId, newStringName);
env->CallVoidMethod(person_obj, ageMethodId, 28);

const char *sig = "()Ljava/lang/String;";
jmethodID jtoString = env->GetMethodID(j_person_class, "toString", sig);
jobject obj_string = env->CallObjectMethod(person_obj, jtoString);
jstring perStr = static_cast<jstring >(obj_string);
const char *itemStr2 = env->GetStringUTFChars(perStr, NULL);
LOGD("Person: %s", itemStr2);
return person_obj;

} ```

運行如下:

![image.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c845bf3f642d40b39a012a25bf8da4c8~tplv-k3u1fbpfcp-watermark.image?)

我們來具體看下 native-lib.cpp 中的 GetMethodId() 方法: ```cpp 説明:獲取類中某個非靜態方法的ID

參數: clazz:指定對象的類 name:這個方法在 Java 類中定義的名稱,構造方法為 ““ sig:這個方法的類型描述符,例如 “()V”,其中括號內是方法的參數,括號後是返回值類型

jmethodID GetMethodID(jclass clazz, const char name, const char sig) ``` 在上面代碼中有一個新的問題,那便是類型描述符 sig。我們也可以通過使用 javap 命令來查看類型描述符,具體可參考我之前的文章 暴力突破 Android 編譯插樁(八)- class 字節碼

除了根據命令行來查看,其實也可以根據一定的規律自己來寫,類型描述符的格式可以參考下表:

Java 類型 | 簽名 (描述符) | Java 類型 | 簽名 (描述符) | | ------- | -------- | ----------- | -------- | | boolean | Z | byte | B | | char | C | short | S | | int | I | long | J | | float | F | double | D | | void | V | 其他引用類型 | L+全類名+; | | type[] | [ | method type | (參數)返回值

六、動態註冊


在此之前我們一直在 JNI 中使用 Java_packagename_classname_methodname 來進行與 Java 方法的匹配,這種方式我們稱之為靜態註冊。動態註冊則意味着方法名可以不用這麼長了,在 android aosp 源碼中就大量使用了動態註冊的方式。現在我們來看下動態註冊使用的例子:

在 MainActivity 中增加:

```java public native void dynamicNativeTest1();

public native String dynamicNativeTest2(); ```

native-lib.cpp: ```cpp void dynamicNative1(JNIEnv *env, jobject jobj) { LOGD("dynamicNative1 動態註冊"); }

jstring dynamicNative2(JNIEnv *env, jobject jobj, jint i) { return env->NewStringUTF("我是動態註冊的dynamicNative2方法"); }

//需要動態註冊的方法數組 static const JNINativeMethod jniNativeMethod[] = { {"dynamicNative", "()V", (void ) dynamicNative1}, {"dynamicNative", "(I)Ljava/lang/String;", (jstring ) dynamicNative2} };

//需要動態註冊native方法的類名 static const char *mClassName = "com/tencent/learnndk/MainActivity";

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM javaVm, void pVoid) { //通過虛擬機 創建愛你全新的 evn JNIEnv *jniEnv = nullptr; jint result = javaVm->GetEnv(reinterpret_cast(&jniEnv), JNI_VERSION_1_6); if (result != JNI_OK) { return JNI_ERR; // 主動報錯 } jclass mainActivityClass = jniEnv->FindClass(mClassName); jniEnv->RegisterNatives(mainActivityClass, jniNativeMethod, sizeof(jniNativeMethod) / sizeof(JNINativeMethod));//動態註冊的數量

return JNI_VERSION_1_6;

} ```