For begin

Android的so开发中,其他基本与C/C++一致,而与Java交互需要用到jni,也主要掌握jni的内容

jni,Java native interface,允许Java代码和其他语言进行交互

NDK,交叉编译工具链(PC开发,Android使用,就是交叉编译),AS里装NDK和Cmake就可以,如果使用ndk-build的话不需要cmake(一般不这样做),LLDB是调试用的(AS4.0之后不需要装)

cmakelist.txt里,配置了编译成哪个so、so的类型、引用了哪些库、哪些cpp编译成so

ABI,就是平台,包括armeabi-v7a、arm64-v8a、x86、x86_64,现在不支持mips

第一个NDK工程

NDK工程与纯Java工程的区别

创建NDK工程

build.gradle中与Java工程的区别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
defaultConfig {
...
externalNativeBuild {
cmake {
// 指定C++的版本
cppFlags "-std=c++11"
}
}
// 指定编译成哪些平台的so,可以省略,默认就是编译全部
ndk {
abiFilters 'armeabi-v7a','arm64-v8a','x86','x86_64'
}
}
externalNativeBuild {
cmake {
// 指定CMakeLists.txt的路径
path "src/main/cpp/CMakeLists.txt"
version "3.10.2"
}
}

New Project -> Native C++,C++标准选C++11

可以看到MainActivity.java里多出来了一些代码

1
2
3
4
5
6
7
8
    // Used to load the 'native-lib' library on application startup.
// 加载so文件,实际上加载的so文件名叫libnative-lib.so
static {
System.loadLibrary("native-lib");
}
...
// 声明函数,native代表函数的实现在C里
public native String stringFromJNI();

CMakeList.txt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html

# Sets the minimum version of CMake required to build the native library.

cmake_minimum_required(VERSION 3.10.2)

# Declares and names the project.

project("testcplus")

# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.

add_library( # Sets the name of the library.指定so文件的名字
native-lib

# Sets the library as a shared library.
SHARED

# Provides a relative path to your source file(s).编译哪些cpp成目标so
native-lib.cpp )

# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.

find_library( # Sets the name of the path variable.
log-lib

# Specifies the name of the NDK library that
# you want CMake to locate.
log )

# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.

target_link_libraries( # Specifies the target library.
native-lib

# Links the target library to the log library
# included in the NDK.
${log-lib} )

JNI函数的静态注册规则

当MainActivity.java里调用stringFromJNI()函数的时候,怎么知道去哪个so文件里找这个函数呢?

1
public native String stringFromJNI();

一种方法是静态注册,静态注册下,对应so文件的函数命名需要遵守特定的规则

native-lib.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <jni.h>
#include <string>

// extern "C"告诉编译器以C进行编译而不是以C++,因为C++编译的时候会修饰函数符号导致函数名变了
// JNIEXPORT将函数定义为导出函数,在导出表里可以找到,默认就是可见(可以在导出表找到)
// jstring定义返回值
// JNICALL在这里没有用
extern "C" JNIEXPORT jstring JNICALL
// Java_包名_类名_方法名
Java_com_yourfff_testcplus_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}

一般不会用静态注册

JNIEnvjobject,也就是上面的Java_com_yourfff_testcplus_MainActivity_stringFromJNI(...)的两个参数,这两个参数是每个和Java层关联的函数都要有的参数,并且必须是第0个和第1个

JNIEnv是JNI环境,通过JNIEnv可以去调用相关的函数来完成C层调用Java、Java数据类型和C数据类型之间的转换

jobject代表这个函数是被哪个对象调用的,这里的参数不一定是jobject,也有可能是jclass(当函数是静态函数的时候)

so中常用的log输出

1
2
3
4
5
6
7
8
9
10
#include <android/log.h>

#define TAG "thisisalog"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__);
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__);
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__);

#define中的...和__VA_ARGS__
...表示接受任意个数参数
__VA_ARGS__表示...处不管接受多少参数,都会天道这个位置

当然反编译的时候是看不到这些define的,编译的时候都给替换成实际调用的__android_log_print函数了,参数也一并编译之后填进去了

NDK多线程

简介

每个进程中只有一份JavaVM

每个线程中都有一份JNIEnv

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 线程id,其实就是long
pthread_t thread;
// 线程id 线程属性 线程要执行的函数(其实是函数指针,看pthread_create函数定义就知道了) 传给函数的参数
pthread_create(&thread, nullptr, myThread, nullptr);
// 等待线程执行完毕
// 默认的线程属性是joinable 随着主线程结束而结束
// 将线程的属性设置为deattach 可以分离执行
pthread_join(thread, nullptr);
// 子线程中使用它来退出线程
pthread_exit(0);

// 定义线程函数
// 函数指针必须是void* (*__start_routine)(void*)
void* myThread(void* a) {
...
}

传递单个参数

void* (*__start_routine)(void*) 里面的参数void*其实是代表可以传任意参数

1
2
3
4
5
6
7
8
9
10
void* myThread(void* a){
// 转换成原来的参数类型
int *num = static_cast<int *>(a);
...
}

...

int num = 1;
pthread_create(&thread, nullptr, myThread, &num);

传递多个参数

就用指针就行,数组指针、结构体指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void* myThread(void* a){
// 转换成原来的参数类型
test* abc = static_cast<test *>(a);
abc->name...;
abc->age...;
...
}

struct test {
string name;
int age;
};
test abc;
abc.name = "john";
abc.age = "5";

pthread_create(&thread, nullptr, myThread, &abc);

接收返回值

要取返回值的话,必须有这两句

1
2
3
4
// 将线程的属性设置为deattach,等待子线程执行结束才结束
pthread_join(thread, nullptr);
// 子线程中使用它来退出线程
pthread_exit(0);

返回值就通过pthread_exit()函数来取

1
2
int pthread_join(pthread_t __pthread, void** __return_value_ptr);
void pthread_exit(void* __return_value) __noreturn;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
void* myThread(void* a){
...
// 这里可以通过pthread_exit返回字符串常量类型作为返回值
// 不能用局部变量的地址,否则函数返回之后成了野指针
// C里面很少取返回值,一般都是把参数当返回值,比如传入一个结构体的指针作为参数
// int a = 100; pthread_exit((void*)&a); 这种不合法
pthread_exit((void*)"testttttttt");
}

main {
void * resPtr;
pthread_join(thread, &resPtr);
LOGD("stringFromJNI: %s", (char*)resPtr);
}

JNI_OnLoad

so中各种函数的执行时机

按先后顺序 initinit_arrayJNI_OnLoad

JNI_OnLoad的定义

1
2
3
4
5
6
7
8
9
10
11
// 第二个参数目前是保留的,不用管
JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env = nullptr;
// 如果GetEnv成功执行,会返回JNIEnv
if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
LOGD("GetEnv failed");
return -1;
}
// 目前的安卓机型都支持JNI_VERSION_1_6
return JNI_VERSION_1_6;
}

注意事项

一个so中可以不定义JNI_OnLoad

一旦定义了JNI_OnLoad,在so首次加载的时候会自动执行

通常执行成功必须返回JNI版本JNI_VERSION_1_6

JavaVM

JavaVM实际上就是一个结构体,里面封装了各种方法

JavaVM中的常用方法

GetEnv(用来获取JNIEnv的)

1
2
3
4
5
JNIExport jint JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env = nullptr;
// 如果GetEnv成功执行,会返回JNIEnv
if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
...

除此之外还有 AttachCurrentThread(用来在子线程中获取JNIEnv)、DetachCurrentThread

JavaVM的获取方法:JNI_OnLoad的第一个参数、JNI_OnUnload的第一个参数、env->GetJavaVM ,各种方式获取的JavaVM的地址应该一样,因为进程只有一份JavaVM

JNIEnv

也是一个结构体

获取方式

函数静态/动态注册,传入的第一个参数,如下

1
2
3
4
5
6
7
extern "C" JNIEXPORT jstring JNICALL
Java_com_yourfff_testcplus_MainActivity_stringFromJNI(
JNIEnv* env, // 第一个参数默认就是JNIEnv
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}

或者vm->GetEnvglobalVM->AttachCurrentThread(globalVM是定义的全局JavaVM结构体)

demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#include <jni.h>
#include <string>
#include <android/log.h>
#include <pthread.h>

#define TAG "testttttttt"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__);
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO , TAG, __VA_ARGS__);
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__);


JavaVM* globalVM = nullptr;

void* myThread(void* a){

// JNIEnv* env = nullptr;
// if(globalVM->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6) != JNI_OK){
// LOGD("myThread GetEnv failed");
// }else {
// LOGD("myThread JNIEnv: %p", env);
// }

JNIEnv* env = nullptr;
if(globalVM->AttachCurrentThread(reinterpret_cast<JNIEnv **>(&env), nullptr) != JNI_OK){
LOGD("myThread GetEnv failed");
}else {
LOGD("myThread JNIEnv: %p", env);
}

pthread_exit(0);
}

extern "C" JNIEXPORT jstring JNICALL
Java_com_testttttttt_ndkdemo_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */) {

LOGD("stringFromJNI JNIEnv: %p", env);
std::string hello = "Hello from C++";

pthread_t thread;
pthread_create(&thread, nullptr, myThread, nullptr);
pthread_join(thread, nullptr);

return env->NewStringUTF(hello.c_str());
}

JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) {
globalVM = vm;
JNIEnv *env = nullptr;
if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
LOGD("GetEnv failed");
return -1;
}
JavaVM *vm2;
env->GetJavaVM(&vm2);
LOGD("JNI_OnLoad JavaVM1: %p", vm);
LOGD("JNI_OnLoad JavaVM2: %p", vm2);
LOGD("JNI_OnLoad JNIEnv: %p", env);
return JNI_VERSION_1_6;
}

so相关的一些概念

出现在导入导出表的函数,一般可以通过frida相关API直接得到函数地址,或者自己计算

没有出现在导入导出表的函数,都需要计算函数地址

要完成so层的hook,都需要函数的地址

Java层中的native函数,被调用之后会找到so中对应的函数,简单地说就是Java调用C需要先完成函数注册

so层函数注册

JNI函数的静态注册

必须遵循一定的命名规则,一般是Java_包名_类名_方法名

系统会通过dlopen加载对应的so,通过dlsym来获取指定名字的函数地址,然后调用

静态注册的JNI函数,必然出现在导出表里

JNI函数的动态注册

通过env->RegisterNatives注册函数,通常在JNI_OnLoad中注册,所以动态注册在so加载的时候就注册了,导出表里找不到,但是Java里又能逆向出来的native函数,就有可能是动态注册的函数,这个时候就需要定位RegisterNatives函数

可以给同一个Java函数注册多个native函数,以最后一次为准

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 首先要找到函数所在的类    
jclass MainActivityClazz = env->FindClass("com/john/ndkdemo/MainActivity");
JNINativeMethod methods[] = {
//public native String stringFromJNI2(int a, byte[] b, String c);
{"stringFromJNI2", "(I[BLjava/lang/String;)Ljava/lang/String;", (void *)encodeFromC},
{"stringFromJNI2", "(I[BLjava/lang/String;)Ljava/lang/String;", (void *)encodeFromC1},
};
// 第一个参数是类
//jint RegisterNatives(jclass clazz, const JNINativeMethod* methods, jint nMethods)
env->RegisterNatives(MainActivityClazz, methods, sizeof(methods) / sizeof(JNINativeMethod));

--------------------------------------------------------
typedef struct {
const char* name; // 函数名
const char* signature; // 函数签名,可以理解为就是包含了函数的参数和返回值
void* fnPtr; // C层的函数名
} JNINativeMethod;

C代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// 假设Java里的MainActivity.java定义的native函数是
// public native String stringFromJNI2(int a, byte[] b, String c);

jstring encodeFromC(JNIEnv* env, jobject obj, jint a, jbyteArray b, jstring c) {
return env->NewStringUTF("encodeFromC");
}

jstring encodeFromC1(JNIEnv* env, jobject obj, jint a, jbyteArray b, jstring c) {
return env->NewStringUTF("encodeFromC1");
}

JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) {
globalVM = vm;
JNIEnv *env = nullptr;
if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
LOGD("GetEnv failed");
return -1;
}

jclass MainActivityClazz = env->FindClass("com/newdemo/ndkdemo/MainActivity");
// typedef struct {
// const char* name;
// const char* signature;
// void* fnPtr;
// } JNINativeMethod;
JNINativeMethod methods[] = {
//public native String stringFromJNI2(int a, byte[] b, String c);
// (I[BLjava/lang/String;)Ljava/lang/String;解释
// ()里是参数,I代表int,[B代表Byte数组,[代表数组
// Ljava/lang/String; L代表对象, ;号代表引用类型java/lang/String的结束
// ()外的Ljava/lang/String;代表返回类型
{"stringFromJNI2", "(I[BLjava/lang/String;)Ljava/lang/String;", (void *)encodeFromC},
{"stringFromJNI2", "(I[BLjava/lang/String;)Ljava/lang/String;", (void *)encodeFromC1},
};
//jint RegisterNatives(jclass clazz, const JNINativeMethod* methods, jint nMethods)
// methods是要注册的函数的数组
// nMethods要注册的函数的个数
env->RegisterNatives(MainActivityClazz, methods, sizeof(methods) / sizeof(JNINativeMethod));

JavaVM *vm2;
env->GetJavaVM(&vm2);
LOGD("JNI_OnLoad JavaVM1: %p", vm);
LOGD("JNI_OnLoad JavaVM2: %p", vm2);
LOGD("JNI_OnLoad JNIEnv: %p", env);
return JNI_VERSION_1_6;
}

异常处理

1
2
env->ExceptionDescribe(); //打印错误描述
env->ExceptionClear(); // 清除错误

子线程中的ClassLoader与主线程中的ClassLoader是不一样的

子线程中是找不到主线程中的类的,比如我们在子线程中调用FindClass去获取类,是获取不了的,jclass MainActivityClazz = env->FindClass("com/newdemo/ndkdemo/MainActivity");

so文件编译

多个cpp文件编译成一个so

改CMakeLists.txt里的add_library就行

不同的cpp之间互相调用函数,使用extern关键字声明之后调用

编译多个so

编写多个cpp文件

修改CMakeLists.txt,加上add_library块和target_link_libraries块就行

Java静态代码块加载多个so

so之间相互调用

使用dlopendlsymdlclose获取函数地址,然后调用,需要导入dlfcn.hdlopen在Android 7.0和7.1使用不了,但是现在这个版本的Android很少了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
jstring encodeFromC1(JNIEnv* env, jobject obj, jint a, jbyteArray b, jstring c) {
// 把传入的jstring转换成const char*类型
const char* soPath = env->GetStringUTFChars(c, nullptr);
// dlopen的第一个参数是so文件的路径,要求必须是const char*类型
// 第二个参数是RTLD_NOW,指定加载完成之后立刻初始化
void *soinfo = dlopen(soPath, RTLD_NOW);
void (*def)() = nullptr;
// dlsym的第一个参数是dlopen的返回值
// dlsym的第二个参数是函数名,以反汇编的函数名为准,有时候编译的时候会符号修饰改函数名
// dlsym返回的是函数地址,需要转换成函数指针再用
// void (*def)()定义函数指针,返回值是void,无参数的这一类函数,void (*)()才是函数指针的类型
def = reinterpret_cast<void (*)()>(dlsym(soinfo, "_Z4testv"));
def();
return env->NewStringUTF("encodeFromC1");
}

或者使用extern关键字声明之后调用,这个和多个cpp编译成一个so的调用方法不一样

如果是多个cpp编译成一个so的话,a.cpp调用b.cpp里面定义的test()函数,只需要声明一下就可以了

1
extern void test();

但是不同的so之间调用,除了声明之外,还需要修改CMakeList.txt,修改target_link_libraries块,把两个so文件添加进去

1
2
3
4
5
6
7
target_link_libraries( # Specifies the target library.
newdemoA

newdemoB
# Links the target library to the log library
# included in the NDK.
${log-lib} )

so路径动态获取

32和64的so存放路径不一样,为了更加通用,可以使用代码动态获取so文件的路径

app使用的so文件都存放在/data/app/包名下面

1
2
3
4
5
6
7
8
9
10
11
12
13
public String getPath(Context cxt){
PackageManager pm = cxt.getPackageManager();
List<PackageInfo> pkgList = pm.getInstalledPackages(0);
if (pkgList == null || pkgList.size() == 0) return null;
for (PackageInfo pi : pkgList) {
if (pi.applicationInfo.nativeLibraryDir.startsWith("/data/app/")
&& pi.packageName.startsWith("com.newdemo.ndkdemo")) {
Log.e("newdemo", pi.applicationInfo.nativeLibraryDir);
return pi.applicationInfo.nativeLibraryDir;
}
}
return null;
}

JNI常用方法

本质都是通过JNI的函数去和Java进行交互

类的代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class NDKDemo {
public static String publicStaticStringField = "this is publicStaticStringField";
public String publicStringField = "this is publicStringField";

private static String privateStaticStringField = "this is privateStaticStringField";
private String privateStringField = "this is privateStringField";
private byte[] byteArray = new byte[]{1,2,3,4,5,6,7,8,9,10};

public NDKDemo(){
Log.d("test", "this is ReflectDemo()");
}

public NDKDemo(String str){
Log.d("test", "this is ReflectDemo(String str)");
}

public NDKDemo(String str, int i){
Log.d("test", i + " " + str);
Log.d("test", "this is ReflectDemo(String str, int i)");
}

public static void publicStaticFunc(){
Log.d("johnTest", "this is publicStaticFunc");
}

public void publicFunc(){
Log.d("test", "this is publicFunc");
}

private static int[] privateStaticFunc(String[] str){
StringBuilder retval = new StringBuilder();
for(String i : str) {
retval.append(i);
}
Log.d("test", "this is privateStaticFunc: " + retval.toString());
return new int[]{0,1,2,3,4,5,6,7,8,9};
}

private String privateFunc(String str, int i){
Log.d("test", i + " this is privateFunc: " + str);
return "this is from java";
}

}

通过jni创建Java对象

大致都是找到类、找到方法、调用方法,和反射的逻辑差不多

NewObject创建对象

1
2
3
4
5
jclass clazz = env->FindClass("com/test/ndkdemo/NDKDemo");
// GetMethodID第一个参数是类,第二个参数是函数名,第三个参数是函数的参数(AS里自动补全)
jmethod methodID = env->GetMethodID(clazz, "<init>", "()V");
jobject ReflectDemoObj = env->NewObject(clazz, methodID);
LOGD("ReflectDemoObj %p", ReflectDemoObj);

AllocObject创建对象

1
2
3
4
5
6
7
jclass clazz = env->FindClass("com/test/ndkdemo/NDKDemo");
jmethod methodID2 = env->GetMethodID(clazz, "<init>", "(Ljava/lang/String;I)V");
jobject ReflectDemoObj2 = env->AllocObject(clazz);
jstring jstr = env->NewStringUTF("thisisatest");
// Nonvirtual指调用父类的该函数,Void指的是函数的返回值
// 第一个参数是对象,第二个参数是类,第三个参数是方法,后面是方法的参数
env->CallNonvirtualVoidMethod(ReflectDemoObj2, clazz, methodID2, jstr, 100);

如果不想填函数的返回值类型,或者jclassjmethod这些分不清的话,直接全部填auto类型就行

通过JNI访问Java属性

不需要管属性的修饰符,public static private都不影响

有很多方法,比如GetBooleanFieldGetIntField ,取决于属性的类型

获取静态字段

1
2
3
4
5
6
// 获取静态字段ID
jfieldID privateStaticStringFieldID = env->GetStaticFieldID(clazz, "privateStaticStringField", "Ljava/lang/String;");
jstring privateStaticString = static_cast<jstring>(env->GetStaticObjectField(clazz,privateStaticStringFieldID));
// 直接打印jstring是无法打印的,要转成cstr才可以,jstring是java层的
const char* privatecstr = env->GetStringUTFChars(privateStaticString, nullptr);
LOGD("privateStaticString %s", privatecstr);

获取对象字段

1
2
3
4
5
6
//获取对象字段ID
jfieldID publicStringFieldID = env->GetFieldID(clazz, "publicStringField", "Ljava/lang/String;");
//读取对象字段,注意要先实例化出一个对象ndkobj
jstring publicString = static_cast<jstring>(env->GetObjectField(ndkobj, publicStringFieldID));
const char* publiccstr = env->GetStringUTFChars(publicString, nullptr);
LOGD("publicString %s", publiccstr);

设置字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//AllocObject创建对象
auto NDKDemoClazz = env->FindClass("com/johnTest/ndkdemo/NDKDemo");
auto init_MethodID = env->GetMethodID(NDKDemoClazz, "<init>", "(Ljava/lang/String;I)V");
jobject ndkobj = env->AllocObject(NDKDemoClazz);
LOGD("ndkobj %p", ndkobj);
jstring johnTest = env->NewStringUTF("johnTest");
env->CallNonvirtualVoidMethod(ndkobj, NDKDemoClazz, init_MethodID, johnTest, 100);
LOGD("ndkobj %p", ndkobj);

//获取静态字段
jfieldID privateStaticStringFieldID = env->GetStaticFieldID(NDKDemoClazz, "privateStaticStringField", "Ljava/lang/String;");
jstring jstr1 = static_cast<jstring>(env->GetStaticObjectField(NDKDemoClazz,
privateStaticStringFieldID));
LOGD("jstr1 %p", jstr1);
const char* cstr1 = env->GetStringUTFChars(jstr1, nullptr);
LOGD("cstr1 %s", cstr1);
// 调用GetStringUTFChars之后一般需要释放
env->ReleaseStringUTFChars(jstr1, cstr1);

//获取对象字段ID
jfieldID privateStringFieldID = env->GetFieldID(NDKDemoClazz, "privateStringField", "Ljava/lang/String;");
//修改对象字段
env->SetObjectField(ndkobj, privateStringFieldID, env->NewStringUTF("johnTest"));
//读取对象字段
jstring jstr2 = static_cast<jstring>(env->GetObjectField(ndkobj, privateStringFieldID));
const char* cstr2 = env->GetStringUTFChars(jstr2, nullptr);
LOGD("cstr2 %s", cstr2);
env->ReleaseStringUTFChars(jstr2, cstr2);

通过JNI访问Java数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
auto NDKDemoClazz = env->FindClass("com/johnTest/ndkdemo/NDKDemo");
auto init_MethodID = env->GetMethodID(NDKDemoClazz, "<init>", "(Ljava/lang/String;I)V");
jobject ndkobj = env->AllocObject(NDKDemoClazz);
LOGD("ndkobj %p", ndkobj);
jstring johnTest = env->NewStringUTF("johnTest");
env->CallNonvirtualVoidMethod(ndkobj, NDKDemoClazz, init_MethodID, johnTest, 100);
LOGD("ndkobj %p", ndkobj);

//获取数组字段和数组长度
jfieldID byteArrayID = env->GetFieldID(NDKDemoClazz, "byteArray", "[B");
jbyteArray jbyteArray = static_cast<_jbyteArray *>(env->GetObjectField(ndkobj, byteArrayID));
jsize jbyteArrayLength = env->GetArrayLength(jbyteArray);
//修改数组字段,需要自己新生成一个数组,然后把新的数组给拷贝到目标数组里去
jbyte charArray[jbyteArrayLength];
for(int i = 0; i < jbyteArrayLength; i++){
charArray[i] = i * 10;
}
// 不止SetByteArrayRegion,还有SetFloatArrayRegion等等,跟类型有关
env->SetByteArrayRegion(jbyteArray, 0, jbyteArrayLength, reinterpret_cast<const jbyte *>(&charArray));
//获取数组字段
//不能直接操作jbyteArray,这是Java的类型,需要转换成C的类型
char* cbyteArray = reinterpret_cast<char *>(env->GetByteArrayElements(jbyteArray, nullptr));
for(int i = 0; i < jbyteArrayLength; i++){
LOGD("jbyteArray[%d]=%d", i, cbyteArray[i]);
}
// 转换类型之后都需要释放
env->ReleaseByteArrayElements(jbyteArray, reinterpret_cast<jbyte *>(cbyteArray), 0);

通过JNI访问Java方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
auto NDKDemoClazz = env->FindClass("com/johnTest/ndkdemo/NDKDemo");
auto init_MethodID = env->GetMethodID(NDKDemoClazz, "<init>", "(Ljava/lang/String;I)V");
jobject ndkobj = env->AllocObject(NDKDemoClazz);
LOGD("ndkobj %p", ndkobj);
jstring johnTest = env->NewStringUTF("johnTest");
env->CallNonvirtualVoidMethod(ndkobj, NDKDemoClazz, init_MethodID, johnTest, 100);
LOGD("ndkobj %p", ndkobj);

//调用静态函数
jmethodID publicStaticFuncID = env->GetStaticMethodID(NDKDemoClazz, "publicStaticFunc", "()V");
env->CallStaticVoidMethod(NDKDemoClazz, publicStaticFuncID);

//调用对象函数
jmethodID privateFuncID = env->GetMethodID(NDKDemoClazz, "privateFunc", "(Ljava/lang/String;I)Ljava/lang/String;");

jvalue args[2];
// .l代表参数是对象,.i代表参数是int
args[0].l = env->NewStringUTF("johnTest");
args[1].i = 100;
// CallObjectMethodA,参数不是单个传,而是把参数都放到jvalue数组里传参,其实用的也少
// CallObjectMethodV一般不用
// CallObjectMethod,CallObjectMethod底层就是调用的CallObjectMethodV
jstring jresult = static_cast<jstring>(env->CallObjectMethodA(ndkobj, privateFuncID, args));
// 或者用如下也是一样的
// jstring jresult = static_cast<jstring>(env->CallObjectMethod(ndkobj, privateFuncID, env->NewStringUTF("johnTest"), 100));
char* cresult = const_cast<char *>(env->GetStringUTFChars(jresult, nullptr));
LOGD("cresult: %s", cresult);
env->ReleaseStringUTFChars(jresult, cresult);

//参数是数组,返回值是数组的函数
// 构造参数 String _jstringArray[3];,创建对象使用NewObjectArray
jclass _jstring = env->FindClass("java/lang/String");
jobjectArray _jstringArray = env->NewObjectArray(3, _jstring, nullptr);
for(int i = 0; i < 3; i++){
// 通过SetObjectArrayElement设置单个数组成员
env->SetObjectArrayElement(_jstringArray, i, env->NewStringUTF("johnTest;"));
}
jmethodID privateStaticFuncID = env->GetStaticMethodID(NDKDemoClazz, "privateStaticFunc", "([Ljava/lang/String;)[I");
// 这里函数的返回值是jObject,转成jintArray,后续的处理和上面访问Java数组是一样的
jintArray _jintArray = static_cast<jintArray>(env->CallStaticObjectMethod(NDKDemoClazz, privateStaticFuncID, _jstringArray));
int *cintArr = env->GetIntArrayElements(_jintArray, nullptr);
LOGD("cintArr[0]=%d", cintArr[0]);
env->ReleaseIntArrayElements(_jintArray, cintArr, JNI_ABORT);

通过JNI访问Java的父类方法

CallNonvirtualxxxx指调用父类的该函数

可以尝试在父类调用ActivityonCreate()方法,重点是调用super.onCreate(savedInstanceState);这一句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
extern "C"
JNIEXPORT void JNICALL
Java_com_johnTest_ndkdemo_MainActivity_onCreate(JNIEnv *env, jobject thiz,
jobject saved_instance_state) {
// TODO: implement onCreate()
//super.onCreate(savedInstanceState);
jclass AppCompatActivityClazz = env->FindClass("androidx/appcompat/app/AppCompatActivity");
jmethodID onCreateID = env->GetMethodID(AppCompatActivityClazz, "onCreate", "(Landroid/os/Bundle;)V");
// CallNonvirtualVoidMethod
// 第一个参数是当前 Java 对象(即调用方法的对象)
// 第二个参数是父类
// 例如,MainActivity的onCreate通过super.onCreate(savedInstanceState) 调用父类AppCompatActivity的onCreate方法
env->CallNonvirtualVoidMethod(thiz,AppCompatActivityClazz, onCreateID, saved_instance_state);

}

也就是说,其实所有的Java层代码都可以放到so层去实现

JNI中的内存管理

局部引用

不能等价成局部变量和全局变量

1
2
3
比如在全局定义了NDKDemoClazz,并且在onLoad加载函数里调用JNI函数给这个变量赋值了
但是在别的函数里无法使用这个变量
因为JNI函数返回的结果都是局部引用,函数执行完返回就回收了

大多数JNI函数,调用以后返回的结果都是局部引用

因此,env->NewLocalRef基本不用,这个函数就是创建局部引用的

一个函数内的局部变量引用数量是有限的,早期Android里更明显

当函数体内需要大量使用局部变量的时候,最后及时删掉不用的局部引用

可以使用env->DeleteLocalRef(变量名)来删除局部引用

局部引用相关的其他函数

1
2
3
env->EnsureLocalcapacity(num) 判断是否有足够的局部变量可以使用,足够则返回0

需要大量使用局部引用时,可以使用env->PushLocalFrame(num)和env->PopLocalFrame(nullptr)来批量管理局部引用

全局引用

全局引用是不会被回收的,除非程序结束或者so文件卸载了

在JNI开发中,需要跨函数使用变量时,直接定义全局变量是没用的

需要使用以下两个方法来创建和删除全局引用

1
2
3
4
5
env->NewGlobalRef(jclass 变量名)
jclass tmpClass = env->FindClass(...);
NDKDemoClazz = static_cast<jclass>(env->NewGlobalRef(tmpClass));

env->DeleteGlobalRef()

弱全局引用

与全局引用基本相同,区别是弱全局引用有可能被回收

1
2
env->NewWeakGlobalRef
env->DeleteWeakGlobalRef

子线程中获取Java类

在子线程中,findClass可以直接获取系统类,但是直接findClass获取不到自写的类比如MainActivity这种,因为子线程的类加载器默认为系统加载器,只能找到系统类,主线程的类加载器是应用加载器,可以找到自写类,加载器就是classLoader,不同的classLoader即使加载相同的类名也会被视为不同的类

方法一,想要在子线程中使用主线程的类,定义全局引用,并在主线程中获取类,使用全局引用来传递到子线程中

方法二,在主线程中获取正确的ClassLoader,在子线程中去加载类

在Java中,可以先获取类字节码,然后使用getClassLoader来获取

在Java里是这样实现的

1
2
DemoClass.class.getClassLoader()
new DemoClass().getClass().getClassLoader()

转换成C++

1
2
3
4
5
6
7
8
9
10
11
12
13
// 在jni的主线程中获取ClassLoader
// MainActivity.class.getClassLoader();
jclass MainActivityClazz = env->FindClass("com/johnTest/ndkdemo/MainActivity");
jclass classClazz = env->FindClass("java/lang/Class");
jmethodID getClassLoaderID = env->GetMethodID(classClazz, "getClassLoader", "()Ljava/lang/ClassLoader;");
jobject tempClassLoaderObj = env->CallObjectMethod(MainActivityClazz, getClassLoaderID);
ClassLoaderObj = env->NewGlobalRef(tempClassLoaderObj); // 定义好全局变量jobject ClassLoaderObj;

// 在jni的子线程中获取loadClass,先获取loadClass方法
jclass ClassLoaderClazz = env->FindClass("java/lang/ClassLoader");
jmethodID loadClassID = env->GetMethodID(ClassLoaderClazz, "loadClass", "(Ljava/lang/String;)Ljava/lang/Class;");
jclass MainActivityClazz = static_cast<jclass>(env->CallObjectMethod(ClassLoaderObj, loadClassID, env->NewStringUTF("com.johnTest.ndkdemo.MainActivity")));
LOGD("myThread MainActivityClazz: %p", MainActivityClazz);

init与initarray

so在执行JNI_Onload之前,还会先后执行两个构造函数initinitarray,并且initarray可以存放多个函数,且这些函数按序执行

so加固、so中字符串加密等等,一般会把相关代码放到这里,也就是说当系统执行initinitarray的时候,so里的代码部分可能是加密的

init的定义及使用

1
2
3
extern "C" void _init() { // 函数名必须为 _init
...
}

initarray的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
__attribute__ ((constructor)) void initArrayTest1() {...};
__attribute__ ((constructor(200))) void initArrayTest2() {...};
__attribute__ ((constructor(101))) void initArrayTest3() {...};
// hidden会隐藏函数名字,导致函数在反编译的时候在导出表看不到函数名,只有地址
__attribute__ ((constructor, visibility("hidden")) void initArrayTest4() {...};



demo:
__attribute__ ((constructor, visibility("hidden"))) void initArrayTest3(){
LOGD("initArrayTest3");
}

__attribute__ ((constructor(101))) void initArrayTest1(){
LOGD("initArrayTest1");
}

__attribute__ ((constructor(300))) void initArrayTest2(){
LOGD("initArrayTest2");
}

extern "C" void _init(){ //函数名必须为_init //.init_proc
LOGD("_init");
}

constructor代表构造函数,必须是俩括号,后面的值,较小的先执行,最好从100之后开始用

如果constructor后面没有值,那么按定义的顺序,从上往下执行(就是写代码的顺序)

如果既有写了值的,还有没写值的,没写值的最后执行,写了值的按数值的顺序执行

PS:init函数在编译的时候会被重命名为 .init_proc,在IDA里要搜的话得搜这个

onCreate方法Native化

java代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());

// Example of a call to a native method
TextView tv = binding.sampleText;
tv.setText(stringFromJNI());

String soPath = new Utils().getPath(getApplicationContext()) + "/libjohnTestB.so";
Log.d("johnTest", stringFromJNI2(1, new byte[]{1}, soPath));

testJniFunc();
}

C++

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
extern "C"
JNIEXPORT void JNICALL
Java_com_johnTest_ndkdemo_MainActivity_onCreate(JNIEnv *env, jobject thiz, jobject saved_instance_state) {
// TODO: implement onCreate()
// super.onCreate(savedInstanceState);
jclass AppCompatActivityClazz = env->FindClass("androidx/appcompat/app/AppCompatActivity");
jmethodID onCreateID = env->GetMethodID(AppCompatActivityClazz, "onCreate", "(Landroid/os/Bundle;)V");
env->CallNonvirtualVoidMethod(thiz,AppCompatActivityClazz, onCreateID, saved_instance_state);

//getLayoutInflater()
//android.app.Activity getLayoutInflater
jclass ActivityClazz = env->FindClass("android/app/Activity");
jmethodID getLayoutInflaterID = env->GetMethodID(ActivityClazz, "getLayoutInflater", "()Landroid/view/LayoutInflater;");
jobject LayoutInflater = env->CallObjectMethod(thiz, getLayoutInflaterID);

// binding = ActivityMainBinding.inflate(getLayoutInflater());
// com.johnTest.ndkdemo.databinding.ActivityMainBinding inflate
// public static ActivityMainBinding inflate(LayoutInflater inflater)
jclass ActivityMainBindingClazz = env->FindClass("com/johnTest/ndkdemo/databinding/ActivityMainBinding");
jmethodID inflateID = env->GetStaticMethodID(ActivityMainBindingClazz, "inflate", "(Landroid/view/LayoutInflater;)Lcom/johnTest/ndkdemo/databinding/ActivityMainBinding;");
LOGD("ActivityMainBindingClazz %p", ActivityMainBindingClazz);
LOGD("inflateID %p", inflateID);
jobject ActivityMainBindingObj = env->CallStaticObjectMethod(ActivityMainBindingClazz, inflateID, LayoutInflater);

// binding.getRoot()
// com.johnTest.ndkdemo.databinding.ActivityMainBinding
// public ConstraintLayout getRoot()
jmethodID getRootID = env->GetMethodID(ActivityMainBindingClazz, "getRoot", "()Landroidx/constraintlayout/widget/ConstraintLayout;");
jobject ConstraintLayout = env->CallObjectMethod(ActivityMainBindingObj, getRootID);

// setContentView(binding.getRoot());
jmethodID setContentViewID = env->GetMethodID(AppCompatActivityClazz, "setContentView", "(Landroid/view/View;)V");
env->CallVoidMethod(thiz, setContentViewID, ConstraintLayout);

}

函数签名怎么写,比如 inflate(LayoutInflater inflater), 签名 (Landroid/view/LayoutInflater;)Lcom/johnTest/ndkdemo/databinding/ActivityMainBinding; ,()括号里写参数,L代表对象,;代表参数或者返回值结束