暴力突破 JNI
theme: smartblue highlight: androidstudio
一、前言
JNI 简单来说就是 java 调用本地 C/C++ 的技术,JVM 就是通过大量的 JNI 技术使得 Java 能在不同平台上运行。它允许 Java 类的某些方法用原生方法实现,这些原生方法也可以使用 Java 对象,使用方法与 Java 代码调用 Java 对象的方法相同。原生方法可以创建新的 Java 对象或者使用 Java 应用程序创建的对象。
二、简单示例
现在我们来看下官方示例,在 Android Studio 上创建一个 Native C++ 工程:
创建好后的项目结构如下:
可以看到比普通的 Android 项目多了一个 cpp 目录,并且在 MainActivity 中多了个一个 native 方法 stringFromJNI(),这个方法 在 Java 层是个空实现,它的具体实现在 native-lib.cpp 中:
该方法返回一个字符串,现在我们运行这个项目:
三、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:
并根据代码提示自动生成 JNI 方法:
可以看到 Java 中 String 类型的参数,在 native 方法中变成了 jstring 类型。那是因为 JNI 是接口语言,所以会有一个中间的转型过程,数据类型的转变是这个过程中重要的类型对接方式。下面我们来看看各个类型的映射。
基本数据类型:
引用类型:
五、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;
} ```
运行如下:
我们来具体看下 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 字节码。
除了根据命令行来查看,其实也可以根据一定的规律自己来写,类型描述符的格式可以参考下表:
六、动态注册
在此之前我们一直在 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
return JNI_VERSION_1_6;
} ```