Android开发学习笔记——NDK开发

1、NDK介绍

1.1、app为什么把代码放到so中

a) C语言历史悠久,有很多现成的代码可用

b) C代码执行效率比Java高

c) Java代码很容易被反编译,而且反编译以后的逻辑很清晰

1.2、为什么要学习NDK开发

在安卓的so开发中,基本与C/C++开发一致,而与Java交互需要用到JNI

在本部分的NDK开发讲解中,主要就是介绍JNI相关内容,so中会接触的:系统库函数、jni调用、加密算法、魔改算法、系统调用、自定义算法

1.3、什么是JNI

JNI是Java Native Interface的缩写。从Java1.1开始,JNI标准成为Java平台的一部分,允许Java代码和其他语言写的代码进行交互

1.4、什么是NDK

NDK是一套工具,使我们能够在 Android 应用中使用 C 和 C++ 代码

交叉编译工具链

NDK的配置

NDK、CMake、LLDB的作用 https://developer.android.com/ndk/guides

1.5、ABI与指令集

不同的 Android 设备使用不同的 CPU,而不同的 CPU 支持不同的指令集。CPU 与指令集的每种组合都有专属的应用二进制接口 (ABI)。

https://developer.android.com/ndk/guides/abis

2、NDK与Java工程的区别

1、Java代码中多了加载so和声明所需使用的so中的函数代码

2、编写CMakeLists.txt和C文件

3、build.gradle中添加一些代码

3、第一个NDK工程

1、CMakeLists介绍

2、so的加载

static {
    System.loadLibrary("javaandso");
}

3、native函数的声明

public native String stringFromJNI();

4、JNI函数的静态注册规则

#include <jni.h>
#include <string>

extern "C" JNIEXPORT jstring JNICALL
Java_com_example_javaandso_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}

Java_包名_类_方法名

5、JNIEnv、jobject/jclass

jobject 在native函数的声明改为static时,使用jclass,因为静态方法可以通过类直接调用

6、NewstringUTF

Java的数据和so的数据不互通,如果so的数据最后要转到java层处理就需要NewstringUTF,因此NewstringUTF可以成为一个hook点。

7、在NDK开发中,一定要注意哪些是Java的类型,哪些是C/C++的类型,在适当的时候需要转换

hello.c_str()获取C字符串的类型,jstring是JAVA字符串类型

8、extern “C” JNIEXPORT jstring JNICALL

JNICALL是空的,JNIEXPORT代表把函数导出,给函数添加了默认的可见属性

  1. 指定只编译arm64的so

    ndk {
        abiFilters 'arm64-v8a'
    }

  2. 指定编译后的so名字

修改地方如下

4、so中常用的Log输出

#include <jni.h>
#include <string>
#include <android/log.h>
#define TAG "whitebird"
#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__);


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

    std::string hello = "Hello from C++";
    LOGD("LOGD test%d",1);
    LOGI("LOGI test%d,%d",1,2);
    LOGE("LOGE test%d,%d,%d",1,2,3);
    return env->NewStringUTF(hello.c_str());
}

5、NDK多线程

int pthread_create(pthread_t* __pthread_ptr, pthread_attr_t const* __attr, void* (*__start_routine)(void*), void*);

第一个是指向pthread的指针,也是线程id,第二个是线程属性,第三个是线程执行的函数,第四个是函数参数

#include <jni.h>
#include <string>
#include <android/log.h>
#define TAG "whitebird"
#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__);
void myThread(){
    std::string test="this is a thread function";
    LOGD(test.c_str());
}

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

    std::string hello = "Hello from C++";
    pthread_t pthread;
    pthread_create(&pthread, nullptr, reinterpret_cast<void *(*)(void *)>(myThread), nullptr);

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

默认的线程属性是joinable 随着主线程结束而结束

#include <jni.h>
#include <string>
#include <android/log.h>
#define TAG "whitebird"
#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__);

void myThread(){
    std::string test="this is a thread function";
    LOGD(test.c_str());
    //子线程中使用它来退出线程
    pthread_exit(0);
}

extern "C" JNIEXPORT jstring JNICALL
Java_com_example_javaandso_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */) {
    pthread_t pthread;
    pthread_create(&pthread, nullptr, reinterpret_cast<void *(*)(void *)>(myThread), nullptr);
    //线程属性是dettach,可以分离执行
    pthread_join(pthread, nullptr);
    return env->NewStringUTF(hello.c_str());
}

6、JNI_OnLoad

在使用native方法前都会先加载该native方法的so文件,通常在一个类的静态代码块中进行加载,当然也可以在构造函数,或者调用前加载。jvm在加载so时都会先调用so中的JNI_OnLoad函数,如果你没有重写该方法,那么系统会给你自动生成一个。我们先来测试一下JNI_OnLoad的调用时机

JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) {
    JNIEnv *env = nullptr;
    if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
        LOGD("GetEnv failed");
        return -1;
    }
    return JNI_VERSION_1_6;
  //如果我们的*.so中没有提供JNI_OnLoad()函数,VM会默认该*.so档是使用最老的JNI 1.1版本。
}

注意事项

1、一个so中可以不定义JNI_OnLoad,一旦定义了JNI_OnLoad,在so被加载的时候会自动执行,必须返回JNI版本 JNI_VERSION_1_6

2、在so被成功卸载时,会回调另一个JNI方法:JNI_UnOnLoad。这两个方法声明如下:

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved);
JNIEXPORT void JNICALL JNI_OnUnload(JavaVM* vm, void* reserved);

其中第一个参数vm表示DVM虚拟机,该vm在应用进程中仅有一个,可以保存在native的静态变量中,供其他函数或其他线程使用。其返回值表示当前需要native library需要的版本。

7、JavaVM

7.1、JavaVM是什么

JavaVM是虚拟机在JNI中的表示,一个JVM中只有一个JavaVM实例,这个实例是线程共享的。通过JNIEnv可以获取一个Java虚拟机实例,其函数如下:

jint GetJavaVM(JNIEnv *env, JavaVM **vm);

vm用来存放获得的虚拟机指针的指针,成功返回0,失败返回其它值。

#if defined(__cplusplus)
typedef _JNIEnv JNIEnv;
typedef _JavaVM JavaVM;
#else
typedef const struct JNINativeInterface* JNIEnv;
typedef const struct JNIInvokeInterface* JavaVM;
#endif

如果是C++,则用_JavaVM定义,如果是C,就用JNIInvokeInterface定义。

_JavaVM和JNIInvokeInterface分别如下:

struct JNIInvokeInterface {
    void*       reserved0;
    void*       reserved1;
    void*       reserved2;

    jint        (*DestroyJavaVM)(JavaVM*);
    jint        (*AttachCurrentThread)(JavaVM*, JNIEnv**, void*);
    jint        (*DetachCurrentThread)(JavaVM*);
    jint        (*GetEnv)(JavaVM*, void**, jint);
    jint        (*AttachCurrentThreadAsDaemon)(JavaVM*, JNIEnv**, void*);
};

struct _JavaVM {
    const struct JNIInvokeInterface* functions;

#if defined(__cplusplus)
    jint DestroyJavaVM()
    { return functions->DestroyJavaVM(this); }
    jint AttachCurrentThread(JNIEnv** p_env, void* thr_args)
    { return functions->AttachCurrentThread(this, p_env, thr_args); }
    jint DetachCurrentThread()
    { return functions->DetachCurrentThread(this); }
    jint GetEnv(void** env, jint version)
    { return functions->GetEnv(this, env, version); }
    jint AttachCurrentThreadAsDaemon(JNIEnv** p_env, void* thr_args)
    { return functions->AttachCurrentThreadAsDaemon(this, p_env, thr_args); }
#endif /*__cplusplus*/
};

_JavaVM其实是对JNIInvokeInterface的封装,底层还是调用JNIInvokeInterface的函数。

JavaVM中的常用方法:GetEnv和AttachCurrentThread(在子线程中获取JNIEnv)

7.2、JavaVM的获取方式

JNI_OnLoad的第一个参数

JNI_OnUnload的第一个参数

env->GetJavaVM

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

8、JNIEnv

8.1、JNIEnv是什么

JNIEnv一般是是由虚拟机传入,而且与线程相关的变量,也就说线程A不能使用线程B的JNIEnv。而作为一个结构体,它里面定义了JNI系统操作函数。JNIEnv的定义如下:

#if defined(__cplusplus)
typedef _JNIEnv JNIEnv;
#else
typedef const struct JNINativeInterface* JNIEnv;
#endif

JNIEnv在C语言环境和C++语言环境中的实现是不一样。在C中定义为JNINativeInterface,在C++中定义为_JNIEnv。

_JNIEnv结构体的定义如下:

struct _JNIEnv {
    /* do not rename this; it does not seem to be entirely opaque */
    const struct JNINativeInterface* functions;

#if defined(__cplusplus)

    jint GetVersion()
    { return functions->GetVersion(this); }

    jclass DefineClass(const char *name, jobject loader, const jbyte* buf,
        jsize bufLen)
    { return functions->DefineClass(this, name, loader, buf, bufLen); }

    jclass FindClass(const char* name)
    { return functions->FindClass(this, name); }
    
    
    ...
    ...
    
 #endif   
    }

在_JNIEnv中定义了一个functions变量,这个变量是指向JNINativeInterface的指针。所以如果我们在写native函数时,当接收到类型为JNIEnv*的变量env时,可以使用如下方式调用JNIEnv中的函数(准确说是通过函数指针来调用函数,因为JNIEnv的数据结构聚合了所有 JNI 函数的函数指针),我们可在C++中通过如下方式调用:

env->FindClass("java/lang/String")   

而在C中可以通过如下方式调用:

(*env)->FindClass(env, "java/lang/String") // C中的写法 

由于变量functions是定义在结构体_JNIEnv的第1个变量,所以我们通过*env就能获取到functions变量的值,然后通过JNINativeInterface中的函数指针来调用对应的函数。

JNINativeInterface结构体的定义如下

struct JNINativeInterface {
    void*       reserved0;
    void*       reserved1;
    void*       reserved2;
    void*       reserved3;

    jint        (*GetVersion)(JNIEnv *);

    jclass      (*DefineClass)(JNIEnv*, const char*, jobject, const jbyte*,
                        jsize);
    jclass      (*FindClass)(JNIEnv*, const char*);


...
}

8.2、JNIEnv的获取方式

1、函数静态/动态注册,传的第一个参数

2、vm->GetEnv

3、globalVM->AttachCurrentThread

用于子线程中获取Env

JNIEnv是每个线程单独拥有一个,对于JNI_OnLoad和Java_com_example_javaandso_MainActivity_stringFromJNI都在主线程中,所以他们的值相等,myThread是一个新开的线程,所以打印出来的JNIEnv值不一样。

9、JNI函数的注册

9.1、静态注册

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

系统会通过dlopen加载对应的so,通过dlsym来获取指定名字的函数地址,然后调用静态注册的jni函数,必然在导出表里

9.2、动态注册

通过env->RegisterNatives注册函数,通常在JNI_OnLoad中注册

JNINativeMethod是一个结构体,第一个成员是Java层的函数名,第二个是签名,通常格式为(参数类型)返回值类型,第三个参数是Native层的函数指针。

jstring FunctionTest(JNIEnv* env,jobject,int a,jstring b, jobjectArray c){
    return env->NewStringUTF("this is FunctionTest");
}

JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) {
    JNIEnv *env = nullptr;
    globalvm =vm;
    if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
        LOGD("GetEnv failed");
        return -1;
    }
    jclass MainActivityClazz = env->FindClass("com/example/javaandso/MainActivity");
    JNINativeMethod methods[] = {
            {"stringFromJNI1", "(ILjava/lang/String;[Ljava/lang/Byte;)Ljava/lang/String;", (void *)FunctionTest},
    };
    env->RegisterNatives(MainActivityClazz, methods, sizeof(methods)/sizeof(JNINativeMethod));
    return JNI_VERSION_1_6;
}

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

10、多个cpp文件编译成一个so

新建了一个cpp文件

如果想要把多个cpp文件编译成一个so,就需要修改CMakeLists.txt文件

然后我们在JNI_OnLoad中调用这个函数

11、编译多个so

1、编写多个cpp文件

2、修改CMakeLists.txt

3、Java静态代码块加载多个so

# 编译so的CMAKE的最低版本
cmake_minimum_required(VERSION 3.22.1)
# Declares and names the project.
project("javaandso")

# 设置so的名字,属性为共享库,so的源文件为native-lib.cpp
add_library( # Sets the name of the library.
        whitebirdA
        # Sets the library as a shared library.
        SHARED
        # Provides a relative path to your source file(s).
        native-lib.cpp
        )

add_library( # Sets the name of the library.
        whitebirdB
        # Sets the library as a shared library.
        SHARED
        # Provides a relative path to your source file(s).
        test.cpp
        )
#查找依赖库,并且设置依赖库的名字log-lib
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)

#链接依赖库,名字就是上面定义的log-lib
target_link_libraries( # Specifies the target library.
        whitebirdA

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

target_link_libraries( # Specifies the target library.
        whitebirdB

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

12、so路径的动态获取

先来探讨一下apk中的so文件将来要放在安卓哪一个目录下

放在/data/app/包名/lib下

public String getPath(Context cxt){
       //包管理器
       PackageManager pm = cxt.getPackageManager();
       //得到已经安装的APP的信息
       List<PackageInfo> pkgList = pm.getInstalledPackages(0);
       if (pkgList == null || pkgList.size() == 0) return null;
       //遍历
       for (PackageInfo pi : pkgList) {
           //判断是否为/data/app/开头
           if (pi.applicationInfo.nativeLibraryDir.startsWith("/data/app/")
                   && pi.packageName.startsWith("com.example.javaandso")) {
               Log.e("whitetird", pi.applicationInfo.nativeLibraryDir);
               return pi.applicationInfo.nativeLibraryDir;
           }
       }
       return null;
   }

13、so之间相互调用

需要用到dlopen函数,需要导入dlfcn.h

两个参数,第一个是so的路径,第二个是下面定义的常量,表示解析符号,常用RTLD_NOW

void *soinfo = dlopen(nativePath, RTLD_NOW);//加载打开so
void (*funptr)() = nullptr;//定义函数指针
funptr = reinterpret_cast<void (*)()>(dlsym(soinfo,"TestMain"));
funptr();

另外一种方法就是通过link so实现

然后声明后就可以调用了

打印了两次this is TestMain for many cpp

14、通过JNI创建Java对象

有两种方法可以实现

NewObject创建对象

jclass clazz = env->FindClass("com/xiaojianbang/ndk/NDKDemo");
jmethodID methodID = env->GetMethodID(clazz, "<init>", "()V");
jobject ReflectDemoObj = env->NewObject(clazz, methodID);
LOGD("ReflectDemoObj %p", ReflectDemoObj);

AllocObject创建对象

jclass clazz = env->FindClass("com/xiaojianbang/ndk/NDKDemo");
jmethodID methodID2 = env->GetMethodID(clazz, "<init>", "(Ljava/lang/String;I)V");
jobject ReflectDemoObj2 = env->AllocObject(clazz);
jstring jstr = env->NewStringUTF("from jni str");
env->CallNonvirtualVoidMethod(ReflectDemoObj2, clazz, methodID2, jstr, 100);

15、通过JNI访问Java属性

a、获取静态字段

1、先调用获取静态字段ID的方法:GetStaticFieldID

jfieldID privateStaticStringField =
           env->GetStaticFieldID(clazz, "privateStaticStringField", "Ljava/lang/String;");

2、根据字段属性选择对应的Get方法

//    env->GetStaticBooleanField();
//    env->GetStaticIntField();
//    env->GetStaticShortField();
//    env->GetStaticByteField();
//    env->GetStaticCharField();
//    env->GetStaticFloatField();
//    env->GetStaticDoubleField();
//    env->GetStaticLongField();
//    env->GetStaticObjectField();

我们的privateStaticStringField是String类型,其实也就是Object,所以选择env->GetStaticObjectField()

3、获取jstring的静态字段privateStaticString

jstring privateStaticString =
        static_cast<jstring>(env->GetStaticObjectField(clazz, privateStaticStringField));

这里把jobject强转jstring类型

4、由于我们在so层,所以要转化成C语言类型

const char* privatecstr =
         env->GetStringUTFChars(privateStaticString, nullptr);

5、打印

LOGD("privateStaticString: %s", privatecstr);

6、释放

env->ReleaseStringUTFChars(privateStaticString, privatecstr);

b、获取对象字段

1、先获取FieldId

jfieldID publicStringField =
           env->GetFieldID(clazz, "publicStringField", "Ljava/lang/String;");

2、获取jstring

jclass clazz = env->FindClass("com/example/javaandso/NDKDemo");
jmethodID methodID = env->GetMethodID(clazz, "<init>", "()V");
jobject ReflectDemoObj = env->NewObject(clazz, methodID);  

jstring publicString =
            static_cast<jstring>(env->GetObjectField(ReflectDemoObj, publicStringField));

由于我们是获取对象字段,所以一定要有个对象,因此GetObjectField的第一个参数是对象

3、转化成c语言字符串

const char* publiccstr =
            env->GetStringUTFChars(publicString, nullptr);

4、打印输出

LOGD("publicStringField: %s", publiccstr);

5、对字符串进行释放

env->ReleaseStringUTFChars(publicString, publiccstr);

c、设置对象字段

env->SetObjectField(ReflectDemoObj, privateStringFieldID, env->NewStringUTF("whitebird"));

通过SetObjectField就行了,第一个参数是对象,第二个参数是字段ID,第三个参数是要修改的值

进行修改前后对比

jfieldID privateStringFieldID =
          env->GetFieldID(clazz, "privateStringField", "Ljava/lang/String;");  

jstring privateString =
        static_cast<jstring>(env->GetObjectField(ReflectDemoObj, privateStringFieldID));
const char* privateCstr =
        env->GetStringUTFChars(privateString, nullptr);
LOGD("privateStringField old: %s", privateCstr);
env->ReleaseStringUTFChars(privateString, privateCstr);

env->SetObjectField(ReflectDemoObj, privateStringFieldID, env->NewStringUTF("whitebird"));

privateString = static_cast<jstring>(env->GetObjectField(ReflectDemoObj, privateStringFieldID));
privateCstr = env->GetStringUTFChars(privateString, nullptr);
LOGD("privateStringField new: %s", privateCstr);
env->ReleaseStringUTFChars(privateString, privateCstr);

同时我们也能知道不管是public还是private我们都能获取

16、通过JNI访问和修改Java数组

 //获取Java对象的class和byte数组的字段ID。  
	jfieldID byteArrayID = env->GetFieldID(clazz, "byteArray", "[B");
//通过GetObjectField获取byte数组对象,并且通过GetArrayLength获取byte数组的长度。
   jbyteArray byteArray = static_cast<jbyteArray>(env->GetObjectField(ReflectDemoObj, byteArrayID));
   int _byteArrayLength = env->GetArrayLength(byteArray);
//在C++中创建一个char类型的数组cByteArray,并且为其赋值。
   char cByteArray[10];
   for(int i = 0; i < 10; i++){
       cByteArray[i] = static_cast<char>(100 - i);
   }
//将cByteArray中的元素转换成jbyte类型的数组。
   const jbyte *java_array = reinterpret_cast<const jbyte *>(cByteArray);
//使用SetByteArrayRegion函数将java数组元素设置为cByteArray中的元素。
   env->SetByteArrayRegion(byteArray, 0, _byteArrayLength, java_array);

//再次获取Java对象的byte数组,并且获取byte数组的元素。
   byteArray = static_cast<jbyteArray>(env->GetObjectField(ReflectDemoObj, byteArrayID));
   _byteArrayLength = env->GetArrayLength(byteArray);
//使用GetByteArrayElements函数将byte数组的元素转换到C++中的char数组CBytes中。
   char* CBytes = reinterpret_cast<char *>(env->GetByteArrayElements(byteArray, nullptr));
//遍历CBytes数组元素,输出数组内容。
   for(int i = 0; i < _byteArrayLength; i++) {
       LOGD("CArray: %d", CBytes[i]);
   }
//释放CBytes数组元素,将其转移回byte数组中。
   env->ReleaseByteArrayElements(byteArray, (jbyte*)CBytes, 0);

17、通过jni访问Java方法

1、调用静态函数

//    env->CallBooleanMethod();
//    env->CallVoidMethod();
//    env->CallByteMethod();
//    env->CallShortMethod();
//    env->CallIntMethod();
//    env->CallCharMethod();
//    env->CallDoubleMethod();
//    env->CallLongMethod();
//    env->CallFloatMethod();
//    env->CallObjectMethod();
    jmethodID publicStaticFuncID =
            env->GetStaticMethodID(clazz, "publicStaticFunc", "()V");
    env->CallStaticVoidMethod(clazz, publicStaticFuncID);

静态方法很简单,只要调用CallStaticVoidMethod就行了

2、调用对象函数

jmethodID privateFuncID =
          env->GetMethodID(clazz,"privateFunc","(Ljava/lang/String;I)Ljava/lang/String;");
  jstring str1 = env->NewStringUTF("this is from JNI");
  jstring retval_jstring = static_cast<jstring>(env->CallObjectMethod(ReflectDemoObj, privateFuncID, str1, 1000));
  const char* retval_cstr = env->GetStringUTFChars(retval_jstring, nullptr);
  LOGD("privateStaticString: %s", retval_cstr);
  env->ReleaseStringUTFChars(retval_jstring, retval_cstr);

通过CallObjectMethod调用对象函数,需要先创建一个对象

18、CallVoidMethod、A、V版本区别

可以看到CallVoidMethod底层调用的是CallVoidMethodV,他们两个的区别在于CallVoidMethod会帮我们封装参数,而CallVoidMethodV需要我们自己封装参数,可以看成它只能有一个参数。而对于CallVoidMethodA,它的参数是jvalue*

jvalue是一个联合体

jmethodID privateFuncID =
        env->GetMethodID(clazz,"privateFunc","(Ljava/lang/String;I)Ljava/lang/String;");
jstring str2 = env->NewStringUTF("this is from JNI2");
jvalue args[2];
args[0].l = str2;
args[1].i = 1000;
jstring retval = static_cast<jstring>(env->CallObjectMethodA(ReflectDemoObj, privateFuncID, args));
const char* cpp_retval = env->GetStringUTFChars(retval, nullptr);
LOGD("cpp_retval: %s", cpp_retval);
env->ReleaseStringUTFChars(retval, cpp_retval);

通过jvalue里面放参数,然后进行调用

19、通过jni访问Java父类方法

这里的onCreate就是调用父类方法,所以我们现在尝试把onCreate Native化

直接快捷操作生成JNI函数

我们需要用到这个JNI函数帮我们调用父类方法

extern "C"
JNIEXPORT void JNICALL
Java_com_example_javaandso_MainActivity_onCreate(JNIEnv *env, jobject thiz,
                                                 jobject saved_instance_state) {
    // TODO: implement onCreate()
    jclass ComponentActivityzz=env->FindClass("androidx/activity/ComponentActivity");
    jmethodID onCreateMethodId= env->GetMethodID(ComponentActivityzz,"onCreate", "(Landroid/os/Bundle;)V");
    env->CallNonvirtualVoidMethod(thiz,ComponentActivityzz,onCreateMethodId,saved_instance_state);

}

可以正常运行

20、内存管理

1、局部引用

1、大多数的jni函数,调用以后返回的结果都是局部引用,因此,env->NewLocalRef 基本不用

2、一个函数内的局部引用数量是有限制的,在早期的安卓系统中,体现的更为明显

3、当函数体内需要大量使用局部引用时,比如大循环中,最好及时删除不用的局部引用,可以使用 env->DeleteLocalRef 来删除局部引用

4、局部引用和局部变量不同

2、局部引用相关的其他函数

env->EnsureLocalCapacity(num) 判断是否有足够的局部引用可以使用,足够则返回0,需大量使用局部引用时,手动删除太过麻烦,可使用以下两个函数来批量管理局部引用

env->PushLocalFrame(num)

env->PopLocalFrame(nullptr)

env->PushLocalFrame(100);
if(env->EnsureLocalCapacity(100) == 0) {
    for(int i = 0; i < 3; i++){
        jstring tempString = env->NewStringUTF("whitebird");
        env->SetObjectArrayElement(_jstringArray, i, tempString);
        //env->DeleteLocalRef(tempString);
        sleep(1);
        LOGD("env->EnsureLocalCapacity");
    }
}
env->PopLocalFrame(nullptr);

3、全局引用

在jni开发中,需要跨函数使用变量时,直接定义全局变量是没用的,需要使用以下两个方法,来创建和删除全局引用

env->NewGlobalRef

jobject tempClassLoaderObj = env->CallObjectMethod(MainActivityClazz, getClassLoaderID);
ClassLoaderObj = env->NewGlobalRef(tempClassLoaderObj);

env->DeleteGlobalRef

4、弱全局引用

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

env->NewWeakGlobalRef

env->DeleteWeakGlobalRef

21、子线程中获取Java类

  1. 在子线程中,findClass可以直接获取系统类
jclass ClassLoaderClazz = env->FindClass("java/lang/ClassLoader");
LOGD("myThread ClassLoaderClazz: %p", ClassLoaderClazz);
jclass StringClasszz=env->FindClass("java/lang/String");
LOGD("myThread StringClasszz: %p", StringClasszz);

可以看到系统类是有值的,但是自己的类就没有值

  1. 在主线程中获取类,使用全局引用来传递到子线程中
  jclass MainActivityClazz = env->FindClass("com/example/javaandso/MainActivity");
    globalClass= static_cast<jclass>(env->NewGlobalRef(MainActivityClazz));

子线程中调用
  LOGD("myThread globalClass: %p", globalClass);

3、在主线程中获取正确的ClassLoader,在子线程中去加载类

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

Demo.class.getClassLoader()

new Demo().getClass().getClassLoader()

Class.forName(...).getClassLoader()

在native层进行转换,获取ClassLoader

//MainActivity.class.getClassLoader
  jclass MainActivityClazz = env->FindClass("com/example/javaandso/MainActivity");
  jclass classClasszz=env->FindClass("java/lang/Class");
  jmethodID getClassLoaderMethoid=env->GetMethodID(classClasszz,"getClassLoader", "()Ljava/lang/ClassLoader;");
  jobject tempClassLoader = env->CallObjectMethod(MainActivityClazz,getClassLoaderMethoid);
  globalClassLoader=env->NewGlobalRef(tempClassLoader);

在jni的子线程中loadClass

//jclass MainActivityClasszz=FindClass("com/example/javaandso/MainActivity")
jclass LoadClassClazz=env->FindClass("java/lang/ClassLoader");
jmethodID LoadClassmMethoid=env->GetMethodID(LoadClassClazz,"loadClass", "(Ljava/lang/String;)Ljava/lang/Class;");
jclass MainActivityClasszz= static_cast<jclass>(env->CallObjectMethod(globalClassLoader,
                                                                              LoadClassmMethoid,
                                                                              env->NewStringUTF(
                                                                                      "com.example.javaandso.MainActivity")));

我们要得到的MainActivityClasszz其实在主线程已经获取过一次了,所以第三种方法其实有点麻烦,不如第二种使用全局引用

22、init与initarray

1、so在执行JNI_Onload之前,还会执行两个构造函数init、initarray

2、so加固、so中字符串加密等等,一般会把相关代码放到这里,所以JNI_Onload的时候一般so都是已经被解密了

3、init的使用

extern "C" void _init(){ //函数名必须为_init

    LOGD("_init ");

}

4、initarray的使用

__attribute__ ((constructor)) void initArrayTest3(){
    LOGD("initArrayTest3");
}
__attribute__ ((constructor)) void initArrayTest1(){
    LOGD("initArrayTest1 ");
}
__attribute__ ((constructor)) void initArrayTest2(){
    LOGD("initArrayTest2 ");
}

如果不加任何修饰,会安装我们代码写的顺序执行

constructor后面的值,较小的先执行,最好从100以后开始用

__attribute__ ((constructor(101))) void initArrayTest3(){
    LOGD("initArrayTest3");
}
__attribute__ ((constructor(303))) void initArrayTest1(){
    LOGD("initArrayTest1 ");
}
__attribute__ ((constructor(202))) void initArrayTest2(){
    LOGD("initArrayTest2 ");
}

__attribute__ ((constructor(101))) void initArrayTest3(){
    LOGD("initArrayTest3");
}
__attribute__ ((constructor)) void initArrayTest5(){
    LOGD("initArrayTest5");
}
__attribute__ ((constructor(303))) void initArrayTest1(){
    LOGD("initArrayTest1 ");
}
__attribute__ ((constructor)) void initArrayTest2(){
    LOGD("initArrayTest2 ");
}
__attribute__ ((constructor(202))) void initArrayTest4(){
    LOGD("initArrayTest4");
}

对于这种有值和没值都有的,先执行有值的顺序,然后再按没值的顺序

__attribute__ ((constructor, visibility("hidden"))) void initArrayTest6(){
    LOGD("initArrayTest6");
}

反编译so时会抹去initArrayTest6符号

反编译so

_init在ida反编译后为.init_proc

通过段表,可以看到init_array段,跟过去看看

存放顺序已经排好序了,而且initArrayTest6符号已经没了,变成了sub_1F824


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 767778848@qq.com