安卓逆向_6 --- JNI、NDK开发、jni静态注册、jni_onload动态注册

JNI 官方文档:https://docs.oracle.com/en/java/javase/19/docs/specs/jni/index.html

JNI官方文档(中文):https://blog.csdn.net/yishifu/article/details/52180448

NDK 官方文档:https://developer.android.google.cn/training/articles/perf-jni

Android JNI学习(1、2、3、4、5 ):https://www.jianshu.com/p/b4431ac22ec2

JNI 开发总结:https://cloud.tencent.com/developer/article/1356493

Android JNI 原理分析:http://gityuan.com/2016/05/28/android-jni/

1、jni 简介

jni 是什么 ?

JNI 全称是Java Native Interface,为Java本地接口,并提供了若干的 API 连接Java层与Native层。通俗来说,JNI 相当于一个桥梁,实现了 Java 和 C++ 之间互相访问调用。

在 Android 进行 JNI 开发时,可能会遇到 couldn't find "xxx.so" 问题,或者内存泄漏问题,或者令人头疼的 JNI 底层崩溃问题。Java 层如何调用 Native 方法?Java 方法的参数如何传递给 Native层?而 Native 层又如何反射调用 Java 方法?这些问题在本文将得到答案,带着问题去阅读会事半功倍,接下来我们开始全方位介绍与最佳代码实践。

关于ndk编译脚本:https://blog.csdn.net/u011686167/article/details/106458899

关于JNI开发规范:https://blog.csdn.net/u011686167/article/details/81784979

jni 有什么用 ?

JNI 最常见的两个作用:

  • 从 Java 程序调用 C/C++
  • 从 C/C++ 程序调用Java代码。

JNI 是一个双向的接口:通过 JNI 可以在 Java 代码中访问 Native 模块,还可以在 Native 代码中嵌入一个 JVM 并通过 JNI 访问运行于其中的 Java 模块。JNI 将 JVM 与 Native 模块联系起来,从而实现了 Java 代码与 Native 代码的互访

2、Android 的 JNI 开发全面介绍与最佳实践

一、JNI整体设计

1、库的加载

在 Android 提供 System.loadLibrary() 或者 System.load() 来加载库。示例如下:

    static {
        try {
            System.loadLibrary("hello");
        } catch (UnsatisfiedLinkError error) {
            Log.e(TAG, "load library error=" + error.getMessage());
        }
    }

需要注意的是,如果 .so 动态库或 .a 静态库不存在时,会抛出 couldn't find "libxxx.so" 异常:

如果期待加载的是 64bit 的库,却加载到 32bit 的,会报错如下:

java.lang.UnsatisfiedLinkError: dlopen failed: "xxx.so" is 32-bit instead of 64-bit

 System.loadLibrary() 内部调用 Runtime.getRuntime().loadLibrary0(),源码如下:

    synchronized void loadLibrary0(ClassLoader loader, String libraryName) {
        if (loader != null) {
            // 1、调用classLoader查找库
            String filename = loader.findLibrary(libraryName);
            if (filename == null) {
                throw new UnsatisfiedLinkError(loader + " couldn't find \"" +
                        System.mapLibraryName(libraryName) + "\"");
            }
            // 2、调用native方法来加载
            String error = nativeLoad(filename, loader);
            if (error != null) {
                throw new UnsatisfiedLinkError(error);
            }
            return;
        }
        // 3、拼接完整库名,比如由hello拼接成libhello.so
        String filename = System.mapLibraryName(libraryName);
        List candidates = new ArrayList();
        String lastError = null;
        for (String directory : getLibPaths()) {
            String candidate = directory + filename;
            candidates.add(candidate);
            if (IoUtils.canOpenReadOnly(candidate)) {
                // 4、调用native方法来加载
                String error = nativeLoad(candidate, loader);
                if (error == null) {
                    return; // 加载library成功
                }
                lastError = error;
            }
        }
 
        if (lastError != null) {
            throw new UnsatisfiedLinkError(lastError);
        }
        throw new UnsatisfiedLinkError("Library " + libraryName + " not found; tried " + candidates);
    }

这里的 nativeLoad() 属于 runtime 底层的 jni 方法,接着调用 art/runtime/java_vm_ext.cc 的load_NativeLibrary(),最终调用 dlopen() 来打开 so 库或 a 库。 调用过程如下图:

安卓逆向_6 --- JNI、NDK开发、jni静态注册、jni_onload动态注册_第1张图片

2、动态注册、静态注册

java 层调用带 native 关键字的 JNI 方法,需要注册 java层 与 native层 的对应关系,有静态注册和动态注册两种方式。

  • 静态注册一般是应用层使用,绑定包名+类名+方法名,在调用JNI方法时,通过类加载器查找对应的函数。静态注册的缺点是包名、类名或方法名发生修改时,native层的jni方法名也得对应修改。
  • 动态注册一般是 framework 层使用,在JNI_OnLoad() 回调时,把 JNINativeMethod 注册到函数表。

示例:java 层声明的函数名为 hello 的 JNI 方法 :private native void hello(int num);

静态注册的示例(如果是c++文件(.cpp/.cc/.cxx),需要加extern "C"关键字):

#ifdef __cplusplus
extern "C" {
#endif
JNIEXPORT void JNICALL Java_com_frank_ffmpeg_FFmpegCmd_hello(JNIEnv *env, jclass thiz, jint num) {
 
}
#ifdef __cplusplus
}
#endif

如果觉得每个JNI方法都这样写比较麻烦,我们可以写个宏定义:

#define FFMPEG_FUNC(RETURN_TYPE, FUNC_NAME, ...) \
    JNIEXPORT RETURN_TYPE JNICALL Java_com_frank_ffmpeg_FFmpegCmd_ ## FUNC_NAME \
    (JNIEnv *env, jclass thiz, ##__VA_ARGS__)\

动态注册的示例:

JNINativeMethod nativeMethods[] {
        {"hello", "(I)V", (void *)"native_hello"},
        {"world", "(J)V", (void *)"native_world"}
};
 
jint JNI_OnLoad(JavaVM* vm, void* reserved) {
    JNIEnv *env = NULL;
    vm->GetEnv((void **)&env, JNI_VERSION_1_6);
    jclass clazz = env->FindClass("com/frank/ffmpeg/handler/FFmpegHandler");
    int numMethods = sizeof(nativeMethods) / sizeof(nativeMethods[0]);
    // 注册本地方法到函数表
    env->RegisterNatives(clazz, nativeMethods, numMethods);
    env->DeleteLocalRef(clazz);
    return JNI_VERSION_1_6;
}

JNINativeMethod的结构体位于jni.h,定义如下:

typedef struct {
    const char* name;
    const char* signature;
    void*       fnPtr;
} JNINativeMethod;

3、JNI 方法参数

JNI方法前两个参数分别是 JNIEnv 和 jclass,其中 JNIEnv 是上下文环境,而 jclass 是类的实例对象。其他参数为带 j 开头,比如 jint、jstring。

4、全局引用与局部引用

JNI 提供局部引用和全局引用,还有全局弱引用。顾名思义,局部引用的作用域为局部,在本地方法返回时被GC主动回收,通过如下方法创建:jobject NewLocalRef(JNIEnv *env, jobject ref);

全局引用的作用域为全局,不会被GC回收,需要手动释放引用资源,否则导致内存泄漏。全局引用的创建与释放如下:

// new global reference
jobject NewGlobalRef(JNIEnv *env, jobject obj);
// delete global reference
void DeleteGlobalRef(JNIEnv *env, jobject globalRef);

全局弱引用与全局引用不同的是,它可以被GC回收。另外,它关联到虚引用,用于感知何时被GC回收。全局弱引用的创建与释放如下:

// new weak global reference
jweak NewWeakGlobalRef(JNIEnv *env, jobject obj);
// delete weak global reference
void DeleteWeakGlobalRef(JNIEnv *env, jweak obj);

5、异常检测与异常处理

JNI提供检测异常、抛出异常和清除异常。使用ExceptionOccurred()进行异常检测。在检测到异常后通过ThrowNew()抛出异常,方法如下:

jint ThrowNew(JNIEnv *env, jclass clazz, const char *message);

最后是清除异常,使用ExceptionClear()。完整的示例代码如下:

    // 检测异常
    if (env->ExceptionOccurred() != NULL) {
        // 抛出异常
        jclass clazz = env->FindClass("java/lang/NullPointerException");
        env->ThrowNew(clazz, "This is a null pointer...");
        // 清除异常
        env->ExceptionClear();
    }

JavaVM  与 JNIEnv

  • JavaVM 是虚拟机在 JNI 层的代表,一个进程只有一个 JavaVM,所有的线程共用一个 JavaVM。JavaVM 是一个全局变量,一个进程只有一个 JavaVM 对象。
  • JNIEnv 是一个线程相关的结构体,该结构体代表了 Java 在本线程的运行环境 。JNIEnv 是一个线程拥有一个,不同线程的 JNIEnv 彼此独立。

JNIEnv 作用

  • 调用 Java 函数: JNIEnv 代表 Java 运行环境,可以使用 JNIEnv 调用 Java 中的代码。
  • 操作 Java 对象:Java 对象传入 JNI 层就是 Jobject 对象, 需要使用 JNIEnv 来操作这个 Java 对象。

JNIEnv 体系结构 

  • JNIEnv 是线程相关 : JNIEnv 是线程相关的,即在每个线程中都有一个 JNIEnv 指针,每个 JNIEnv 都是线程专有的, 其它线程不能使用本线程中的 JNIEnv, 线程 A 不能调用 线程 B 的 JNIEnv。
  • JNIEnv 不能跨线程
            --- 当前线程有效 : JNIEnv 只在当前线程有效, JNIEnv 不能在 线程之间进行传递, 在同一个线程中, 多次调用 JNI层方法, 传入的 JNIEnv 是相同的;
            --- 本地方法匹配多 JNIEnv : 在 Java 层定义的本地方法, 可以在不同的线程调用, 因此 可以接受不同的 JNIEnv;
  • JNIEnv 结构 : 由上面的代码可以得出,,JNIEnv 是一个指针,  指向一个线程相关的结构, 线程相关结构指向 JNI 函数指针数组, 这个数组中存放了大量的 JNI 函数指针,这些指针指向了具体的 JNI 函数; 
  • 注意:JNIEnv 只在当前线程中有效。本地方法不能将 JNIEnv 从一个线程传递到另一个线程中。相同的 Java 线程中对本地方法多次调用时,传递给该本地方法的 JNIEnv 是相同的。但是,一个本地方法可被不同的 Java 线程所调用,因此可以接受不同的 JNIEnv。

安卓逆向_6 --- JNI、NDK开发、jni静态注册、jni_onload动态注册_第2张图片

关于 UTF-8 编码:JNI 使用改进的 UTF-8 字符串来表示不同的字符类型。Java 使用 UTF-16 编码。UTF-8 编码主要使用于 C 语言,因为它的编码用 \u000 表示为 0xc0,而不是通常的 0×00。非空 ASCII 字符改进后的字符串编码中可以用一个字节表示。

关于错误:JNI不会检查 NullPointerException、IllegalArgumentException 这样的错误,原因是:导致性能下降。在绝大多数 C 的库函数中,很难避免错误发生。JNI 允许用户使用 Java 异常处理。大部分 JNI 方法会返回错误代码但本身并不会报出异常。因此,很有必要在代码本身进行处理,将异常抛给 Java。在 JNI 内部,首先会检查调用函数返回的错误代码,之后会调用 ExpectOccurred() 返回一个错误对象。

jthrowable ExceptionOccurred(JNIEnv *env);  

例如:一些操作数组的 JNI 函数不会报错,因此可以调用 ArrayIndexOutofBoundsException 或 ArrayStoreExpection 方法报告异常。  

二、JNI类型与数据结构

因为 jni 扮演了 Java 和 C、C++ 之间的 "桥梁" 作用,所以 jni 也有自己的数据类型,用来连接 Java 和 C、C++ 之间的相互转换和调用

1、基本类型、引用类型

JNI 类型包括

  • "基本类型"
  • "引用(对象)类型"

基本类型包括:jint、jbyte、jshort、jlong、jdouble、jboolean、jchar、jfloat 等。

如下图所示: jni、java 基本数据类型对比

安卓逆向_6 --- JNI、NDK开发、jni静态注册、jni_onload动态注册_第3张图片

引用类型的父类是 jobject,包含 jclass、jstring、jarray,而 jarray 又包含各种基本类型对应的数组。层级关系如下图所示:

安卓逆向_6 --- JNI、NDK开发、jni静态注册、jni_onload动态注册_第4张图片

"Java不同的引用类型" 在 "JNI当中也有对应的引用类型" 如上图。当在 C 语言中使用时,所有的 JNI 引用类型都被定义为 jobject 类型。typedef jobject jclass;

2、变量 id、方法 id

使用场景为反射Java变量或Java方法。

  • 变量 id 用 jfieldID 表示,
  • 方法 id 用 jmethodID 表示。比如,在反射Java方法时,先获取对应的jmethodID,再调用对应的method。

3、函数签名、JNI中类签名

函数签名由参数类型和返回值组成,用参数个数、参数类型和返回值来区分同名方法,即解决方法重载问题。JNI 和 java 基本类型对应的签名如下:

安卓逆向_6 --- JNI、NDK开发、jni静态注册、jni_onload动态注册_第5张图片

至于引用对象类型,使用类的全限定名作为签名。 比如String对应签名为Ljava/lang/String;

  • (1):类和接口的描述符在 java 当中使用 ".",如:java.lang.String。而在 JNI 当中是用 "/",如:java/lang/String
  • (2):数组类型的引用类型用 "[" 表示。如  int[] ( java中的表示法 )    [I ( [ 大写的 i 是 JNI 中的表示法,[ 的个数表示数组的维数  二维则是  [[ I )
  • (3):引用类型的域 用L开头,并且以”;”作为结尾。数组类型和class说明的一样。

安卓逆向_6 --- JNI、NDK开发、jni静态注册、jni_onload动态注册_第6张图片

  • (4):Method 说明JNI 中的方法的声明规则:先写参数列表,再写返回类型,以下是例子。

安卓逆向_6 --- JNI、NDK开发、jni静态注册、jni_onload动态注册_第7张图片

引用类型则为 " L + 该类型类描述符 ",数组 为 " [ + 其类型的域描述符 " 。

int[ ]         描述符为  [I  
float[ ]       描述符为  [F  

String[ ]      描述符为  [Ljava/lang/String;  
String         描述符为  Ljava/lang/String;    

Object[ ]      描述符为  [Ljava/lang/Object;  
int  [ ][ ]    描述符为  [[I  
float[ ][ ]    描述符为  [[F  

将参数类型的域描述符按照申明顺序放入一对括号中后跟返回值类型的域描述符,规则如下: (参数的域描述符的叠加)返回类型描述符。对于,没有返回值的,用V(表示void型)表示。

举例如下:( 函数签名 就是 " 参数 + 返回值 " )

Java层方法                               JNI函数签名  
    String test ( )                         Ljava/lang/String;  
    int f (int i, Object object)            (ILjava/lang/Object;)I  
    void set (byte[ ] bytes)                ([B)V  

三、JNI 函数

jni 的常用方法和类型:https://blog.csdn.net/qinjuning/article/details/7595104

1、获取类的实例对象

我们该如何反射调用java方法呢?首先要获取类的实例对象,然后获取方法id,最后根据方法id来调用方法。获取类的实例对象有两种方式:GetObjectClass()和FindClass(),示例如下:

void get_class(JNIEnv *env, jobject object) {
    // 通过类的实例获取
    jclass clazz = env->GetObjectClass(object);
    // 通过类加载器查找指定的类
    jclass claxx = env->FindClass("java/lang/NullPointerException");
}

2、对象的操作

我们可以通过GetObjectRefType()获取引用类型,包括如下引用类型:

JNIInvalidRefType    = 0
JNILocalRefType      = 1
JNIGlobalRefType     = 2
JNIWeakGlobalRefType = 3

如果要判断是否属于某个类的实例,方法如下:

jboolean IsInstanceOf(JNIEnv *env, jobject obj, jclass clazz);

如果要判断两个对象是否相同,方法如下:

jboolean IsSameObject(JNIEnv *env, jobject ref1, jobject ref2);

3、反射调用Java变量

反射Java的变量分为两步,首先获取变量的jfieldID,然后获取/设置变量值,示例如下:

    jclass clazz = env->GetObjectClass(object);
    jfieldID fieldId = env->GetFieldID(clazz, "level", "I");
    env->SetIntField(object, fieldId, 8);

4、反射调用Java方法

反射Java的方法也分为两步,首先获取方法的jmethodID,然后调用方法,示例如下:

    jclass clazz = env->GetObjectClass(object);
    jmethodID methodId = env->GetMethodID(clazz, "setLevel", "(I)V");
    env->CallIntMethod(object, methodId, 8);

5、字符串的操作

如果要读取来自Java层的字符串,可以调用GetStringUTFChars(),使用完毕不要忘记释放资源,否则导致内存泄漏。示例代码如下:

void get_string_from_java(JNIEnv *env, jobject object, jstring jstr) {
    const char *str = env->GetStringUTFChars(jstr, JNI_FALSE);
    int len = env->GetStringUTFLength(jstr);
    printf("from java str=%s, len=%d", str, len);
    env->ReleaseStringUTFChars(jstr, str);
}

如果要返回字符串给Java层,使用NewStringUTF(),示例代码如下:

jstring set_string_to_java(JNIEnv *env, jobject object) {
    const char *str = "hello, world";
    return env->NewStringUTF(str);
}

6、数组的操作

如果要读取来自Java层的数组,可以调用GetXxxArrayElements()。也可以调用GetXxxArrayRegion(),该函数比较灵活,支持指定数组区间。还有没有第三种方式呢?答案是有的,可以调用GetPrimitiveArrayCritical()获取原始数组,采用内存映射实现。示例代码如下:

void get_array_from_java(JNIEnv *env, jobject object, jintArray jarray) {
    int len = env->GetArrayLength(jarray);
    // 1、使用GetIntArrayElements,使用完释放内存
    jint *array = env->GetIntArrayElements(jarray, JNI_FALSE);
    for (int i = 0; i < len; ++i) {
        printf("from java array=%d", array[i]);
    }
    env->ReleaseIntArrayElements(jarray, array, JNI_ABORT);
    // 2、使用GetIntArrayRegion,内部会释放内存
    env->GetIntArrayRegion(jarray, 0, len, array);
    // 3、使用GetPrimitiveArrayCritical获取原始数组
    array = (jint*) env->GetPrimitiveArrayCritical(jarray, JNI_FALSE);
}

如果要返回数组给Java层,先创建JNI数组,然后把数据拷贝给数据,示例代码如下:

jintArray set_array_to_java(JNIEnv *env, jobject object) {
    jint data[] = {1, 2, 3, 4, 5, 6};
    int size = sizeof(data)/sizeof(data[0]);
    jintArray array = env->NewIntArray(size);
    env->SetIntArrayRegion(array, 0, size, data);
    return array;
}

7、NIO的创建与处理

我们可以在本地方法访问java.nio的DirectBuffer。先科普一下,DirectBuffer为堆外内存,实现零拷贝,提升Java层与native层的传输效率。而HeapBuffer为堆内存,在native层多一次拷贝,效率相对低。两者对比如下:

内存位置 使用场景 优点 缺点
DirectBuffer 堆外内存 调用频率高、数据多 零拷贝,效率高 创建耗时
HeapBuffer 堆内存 调用频率低、数据少 创建相对快 存在拷贝,效率低

DirectBuffer在Native层的使用,可以在Java层创建,然后把对象传递到Native层。获取到内存地址后,把数据拷贝给DirectBuffer。整个过程如下:

void copy_to_directBuffer(JNIEnv *env, jobject object, jobject buf) {
    uint8_t data[] = {1, 2, 3, 4, 5, 6};
    uint8_t *buf_addr = (uint8_t *) (env->GetDirectBufferAddress(buf));
    int buf_size = env->GetDirectBufferCapacity(buf);
    int data_size = sizeof(data)/sizeof(data[0]);
    int size = data_size > buf_size ? buf_size : data_size;
    memcpy(buf_addr, data, size);
}

8、方法 与 ID 的转换

上面提及到反射调用Java方法,如果要根据method去获取对应id,API方法如下:

jmethodID FromReflectedMethod(JNIEnv *env, jobject method);

相反地,如果要根据id去获取对应method,API方法如下:

jobject ToReflectedMethod(JNIEnv *env, jclass cls, jmethodID methodID, jboolean isStatic);

so 的入口函数:JNI_OnLoad()JNI_OnUnload()

当 Android 的 VM(Virtual Machine) 执行到 System.loadLibrary() 函数时,首先会去执行 C 组件里的 JNI_OnLoad() 函数。它的用途有二:

  • (1)  告诉 VM 此 C 组件使用那一个 JNI 版本。如果你的 *.so 没有提供 JNI_OnLoad() 函数,VM 会默认该 *.so 是使用最老的JNI 1.1 版本。由于新版的 JNI 做了许多扩充,如果需要使用 JNI 的新版功能,例如 JNI 1.4 的 java.nio.ByteBuffer,就必须藉由 JNI_OnLoad() 函数来告知 VM 。
  • (2)  由于 VM 执行到 System.loadLibrary() 函数时,就会立即先呼叫 JNI_OnLoad(),所以 C 组件的开发者可以藉由JNI_OnLoad() 来进行 C 组件内的初期值之设定 (Initialization) 。

JNI 返回值

jstring str = env->newStringUTF("HelloJNI");  //直接使用该JNI构造一个jstring对象返回    
return str ;    

示例:

jobjectArray ret = 0;  
jsize len = 5;  
jstring str;  
string value("hello");  
   
ret = (jobjectArray)(env->NewObjectArray(len, env->FindClass("java/lang/String"), 0));  
for(int i = 0; i < len; i++)  
{  
    str = env->NewStringUTF(value..c_str());  
    env->SetObjectArrayElement(ret, i, str);  
}  
return ret; 返回数组  

示例:

jclass    m_cls   = env->FindClass("com/ldq/ScanResult");      
jmethodID m_mid   = env->GetMethodID(m_cls,"","()V");    
    
jfieldID  m_fid_1 = env->GetFieldID(m_cls,"ssid","Ljava/lang/String;");    
jfieldID  m_fid_2 = env->GetFieldID(m_cls,"mac","Ljava/lang/String;");    
jfieldID  m_fid_3 = env->GetFieldID(m_cls,"level","I");    

jobject   m_obj   = env->NewObject(m_cls,m_mid);    
                    env->SetObjectField(m_obj,m_fid_1,env->NewStringUTF("AP1"));    
                    env->SetObjectField(m_obj,m_fid_2,env->NewStringUTF("00-11-22-33-44-55"));    
                    env->SetIntField(m_obj,m_fid_3,-50);    
return m_obj;  返回自定义对象  

示例:

jclass list_cls = env->FindClass("Ljava/util/ArrayList;");//获得ArrayList类引用        
if(listcls == NULL)    
{    
    cout << "listcls is null \n" ;    
}
//获得得构造函数Id  
jmethodID list_costruct = env->GetMethodID(list_cls , "","()V");   

//创建一个Arraylist集合对象    
jobject list_obj = env->NewObject(list_cls , list_costruct); 

//或得Arraylist类中的 add()方法ID,其方法原型为: boolean add(Object object) ;    
jmethodID list_add  = env->GetMethodID(list_cls,"add","(Ljava/lang/Object;)Z");     
  
//获得Student类引用 
jclass stu_cls = env->FindClass("Lcom/feixun/jni/Student;");  
 
//获得该类型的构造函数  函数名为 返回类型必须为 void 即 V    
jmethodID stu_costruct = env->GetMethodID(stu_cls , "", "(ILjava/lang/String;)V");    

for(int i = 0 ; i < 3 ; i++)    
{    
    jstring str = env->NewStringUTF("Native");    
    //通过调用该对象的构造函数来new 一个 Student实例    
    //构造一个对象    
    jobject stu_obj = env->NewObject(stucls , stu_costruct , 10,str);  

    //执行Arraylist类实例的add方法,添加一个stu对象   
    env->CallBooleanMethod(list_obj , list_add , stu_obj);  
}    

return list_obj ;   //返回对象集合  

JNI 操作 Java层 的类

//获得jfieldID 以及 该字段的初始值    
jfieldID  nameFieldId ;    

//获得Java层该对象实例的类引用,即HelloJNI类引用    
jclass cls = env->GetObjectClass(obj);  

//获得属性句柄  
nameFieldId = env->GetFieldID(cls , "name" , "Ljava/lang/String;");   
if(nameFieldId == NULL)    
{    
    cout << " 没有得到name 的句柄Id \n;" ;    
}    

// 获得该属性的值   
jstring javaNameStr = (jstring)env->GetObjectField(obj ,nameFieldId);   

//转换为 char *类型  
const char * c_javaName = env->GetStringUTFChars(javaNameStr , NULL);    
string str_name = c_javaName ;      
cout << "the name from java is " << str_name << endl ; //输出显示    
env->ReleaseStringUTFChars(javaNameStr , c_javaName);  //释放局部引用    
 
//构造一个jString对象    
char * c_ptr_name = "I come from Native" ;        
jstring cName = env->NewStringUTF(c_ptr_name); //构造一个jstring对象     
env->SetObjectField(obj , nameFieldId , cName); // 设置该字段的值   

jni 回调 Java层 方法

jstring str = NULL;    
jclass clz = env->FindClass("cc/androidos/jni/JniTest");    
//获取clz的构造函数并生成一个对象    
jmethodID ctor = env->GetMethodID(clz, "", "()V");    
jobject obj = env->NewObject(clz, ctor);    

// 如果是数组类型,则在类型前加[, 如整形数组int[] intArray, 则对应类型为[I, 即整形数组。
// String[] strArray 对应为 [Ljava/lang/String;    
jmethodID mid = env->GetMethodID(clz, "sayHelloFromJava", "(Ljava/lang/String;II[I)I");    
if (mid)    
{    
    LOGI("mid is get");    
    jstring str1 = env->NewStringUTF("I am Native");    
    jint index1 = 10;    
    jint index2 = 12;    
    //env->CallVoidMethod(obj, mid, str1, index1, index2);    

    // 数组类型转换 testIntArray能不能不申请内存空间    
    jintArray testIntArray = env->NewIntArray(10);    
    jint *test = new jint[10];    
    for(int i = 0; i < 10; ++i)    
    {    
        *(test+i) = i + 100;    
    }    
    env->SetIntArrayRegion(testIntArray, 0, 10, test);    


    jint javaIndex = env->CallIntMethod(obj, mid, str1, index1, index2, testIntArray);    
    LOGI("javaIndex = %d", javaIndex);    
    delete[] test;    
    test = NULL;    
}

示例代码:

static void event_callback(int eventId,const char* description) {  //主进程回调可以,线程中回调失败。  
    if (gEventHandle == NULL)  
        return;  
      
    JNIEnv *env;  
    bool isAttached = false;  
  
    if (myVm->GetEnv((void**) &env, JNI_VERSION_1_2) < 0) { //获取当前的JNIEnv  
        if (myVm->AttachCurrentThread(&env, NULL) < 0)  
            return;  
        isAttached = true;  
    }  
  
    jclass cls = env->GetObjectClass(gEventHandle); //获取类对象  
    if (!cls) {  
        LOGE("EventHandler: failed to get class reference");  
        return;  
    }  
  
    jmethodID methodID = env->GetStaticMethodID(cls, "callbackStatic",  
        "(ILjava/lang/String;)V");  //静态方法或成员方法  
    if (methodID) {  
        jstring content = env->NewStringUTF(description);  
        env->CallVoidMethod(gEventHandle, methodID,eventId,  
            content);  
        env->ReleaseStringUTFChars(content,description);  
    } else {  
        LOGE("EventHandler: failed to get the callback method");  
    }  
  
    if (isAttached)  
        myVm->DetachCurrentThread();  
}  

线程中回调。把 c/c++ 中所有线程的创建,由 pthread_create 函数替换为由 Java 层的创建线程的函数 AndroidRuntime::createJavaThread。

static pthread_t create_thread_callback(const char* name, void (*start)(void *), void* arg)    
{    
    return (pthread_t)AndroidRuntime::createJavaThread(name, start, arg);    
}   
  
  
static void checkAndClearExceptionFromCallback(JNIEnv* env, const char* methodName) {  //异常检测和排除  
    if (env->ExceptionCheck()) {    
        LOGE("An exception was thrown by callback '%s'.", methodName);    
        LOGE_EX(env);    
        env->ExceptionClear();    
    }    
}    
    
static void receive_callback(unsigned char *buf, int len)  //回调  
{    
    int i;    
    JNIEnv* env = AndroidRuntime::getJNIEnv();    
    jcharArray array = env->NewCharArray(len);    
    jchar *pArray ;    
        
    if(array == NULL){    
        LOGE("receive_callback: NewCharArray error.");    
        return;     
    }    
    
    pArray = (jchar*)calloc(len, sizeof(jchar));    
    if(pArray == NULL){    
        LOGE("receive_callback: calloc error.");    
        return;     
    }    
    
    //copy buffer to jchar array    
    for(i = 0; i < len; i++)    
    {    
        *(pArray + i) = *(buf + i);    
    }    
    //copy buffer to jcharArray    
    env->SetCharArrayRegion(array,0,len,pArray);    
    //invoke java callback method    
    env->CallVoidMethod(mCallbacksObj, method_receive,array,len);    
    //release resource    
    env->DeleteLocalRef(array);    
    free(pArray);    
    pArray = NULL;    
        
    checkAndClearExceptionFromCallback(env, __FUNCTION__);    
}  
  
  
public void Receive(char buffer[],int length){  //java层函数  
    String msg = new String(buffer);    
    msg = "received from jni callback" + msg;    
    Log.d("Test", msg);    
}  

示例代码:

//获得Java类实例  
jclass cls = env->GetObjectClass(obj); 

//或得该回调方法句柄   
jmethodID callbackID = env->GetMethodID(cls , "callback" , "(Ljava/lang/String;)V") ;  
  
if(callbackID == NULL)    
{    
     cout << "getMethodId is failed \n" << endl ;    
}    
  
jstring native_desc = env->NewStringUTF(" I am Native");    

//回调该方法
env->CallVoidMethod(obj , callbackID , native_desc); 

传对象到 JNI 调用

//或得Student类引用    
jclass stu_cls = env->GetObjectClass(obj_stu); 
  
if(stu_cls == NULL)    
{    
    cout << "GetObjectClass failed \n" ;    
}    
//下面这些函数操作,我们都见过的。O(∩_∩)O~    
jfieldID ageFieldID = env->GetFieldID(stucls,"age","I"); //获得得Student类的属性id     
jfieldID nameFieldID = env->GetFieldID(stucls,"name","Ljava/lang/String;"); // 获得属性ID    

jint age = env->GetIntField(objstu , ageFieldID);  //获得属性值    
jstring name = (jstring)env->GetObjectField(objstu , nameFieldID);//获得属性值    

const char * c_name = env->GetStringUTFChars(name ,NULL);//转换成 char *    
 
string str_name = c_name ;     
env->ReleaseStringUTFChars(name,c_name); //释放引用    
    
cout << " at Native age is :" << age << " # name is " << str_name << endl ;     

与 C++ 互转

jbytearray 转 c++byte 数组

jbyte * arrayBody = env->GetByteArrayElements(data,0);     
jsize theArrayLengthJ = env->GetArrayLength(data);     
BYTE * starter = (BYTE *)arrayBody;     

jbyteArray 转 c++ 中的 BYTE[] 

jbyte * olddata = (jbyte*)env->GetByteArrayElements(strIn, 0);    
jsize  oldsize = env->GetArrayLength(strIn);    
BYTE* bytearr = (BYTE*)olddata;    
int len = (int)oldsize;    

C++ 中的 BYTE[] 转 jbyteArray 

jbyte *by = (jbyte*)pData;    
jbyteArray jarray = env->NewByteArray(nOutSize);    
env->SetByteArrayRegin(jarray, 0, nOutSize, by);    

jbyteArray 转 char * 

char* data = (char*)env->GetByteArrayElements(strIn, 0);    

char* 转 jstring

jstring WindowsTojstring(JNIEnv* env, char* str_tmp)    
{    
    jstring rtn=0;    
    int slen = (int)strlen(str_tmp);    
    unsigned short* buffer=0;    
    if(slen == 0)    
    {    
        rtn = env->NewStringUTF(str_tmp);    
    }    
    else    
    {    
        int length = MultiByteToWideChar(CP_ACP, 0, (LPCSTR)str_tmp, slen, NULL, 0);    
        buffer = (unsigned short*)malloc(length*2+1);    
        if(MultiByteToWideChar(CP_ACP, 0, (LPCSTR)str_tmp, slen, (LPWSTR)buffer, length) > 0)    
        {    
            rtn = env->NewString((jchar*)buffer, length);    
        }    
    }    
    if(buffer)    
    {    
        free(buffer);    
    }    
    return rtn;    
}    

char* jstring 互转

JNIEXPORT jstring JNICALL Java_com_explorer_jni_SambaTreeNative_getDetailsBy    
  (JNIEnv *env, jobject jobj, jstring pc_server, jstring server_user, jstring server_passwd)    
{    
    const char *pc = env->GetStringUTFChars(pc_server, NULL);    
    const char *user = env->GetStringUTFChars(server_user, NULL);    
    const char *passwd = env->GetStringUTFChars(server_passwd, NULL);    
    const char *details = smbtree::getPara(pc, user, passwd);    
    jstring jDetails = env->NewStringUTF(details);    
    return jDetails;    
}    

四、库加载回调与JavaVM调用

1、库加载回调

调用System.loadLibrary()时,系统在加载库成功后,会回调JNI_OnLoad(JavaVM *vm, void *reserved)。带有JavaVM参数可以保存为全局变量,返回值为JNI版本号。示例代码如下:

jint JNI_OnLoad(JavaVM *vm, void *reserved) {
    javaVM = vm;
    return JNI_VERSION_1_6;
}

当类加载器包含的本地库已经被垃圾回收器回收了,虚拟机会回调JNI_OnUnload()方法。 在该方法回调时,我们可以做内存清理工作。

2、JavaVM调用

2.1 创建JavaVM

创建JavaVM需要传入JavaVM指针、JNIEnv指针和VM参数。当前线程变成主线程,得到的env作为主线程的上下文环境。创建JVM方法为JNI_CreateJavaVM()。

2.2 关联JavaVM

当工作线程需要使用env时,必须先调用AttachCurrentThread()方法来关联JVM,因为env是线程私有的上下文环境。如果已经关联,不执行任何操作。需要注意的是,一个本地线程不能关联两个JVM。

2.3 脱离JavaVM

当使用完env时,调用DetachCurrentThread()方法来脱离JVM。

2.4 销毁JavaVM

当不再需要使用JavaVM时,调用DestroyJavaVM()方法用于卸载JVM和清除内存。任何线程,不管有没关联JVM,都可以调用该方法。

JavaVM 的完整使用过程如下:

void callJVM() {
    JNIEnv *env = nullptr;
    JavaVM *jvm = nullptr;
    // 1、创建jvm
    JNI_CreateJavaVM(&jvm, &env, nullptr);
    // 2、关联jvm
    jvm->AttachCurrentThread(&env, nullptr);
    // 3、do something with env
    // 4、脱离jvm
    jvm->DetachCurrentThread();
    // 5、销毁jvm
    jvm->DestroyJavaVM();
}

五、堆栈崩溃排查

做JNI/NDK开发时,经常遇到堆栈崩溃问题,只有一堆杂乱地址,实在让人摸不着头脑。堆栈信息包括:ABI架构、pid进程号、出错信号、崩溃原因、寄存器状态、堆栈地址。空指针引起的崩溃如下图所示:

安卓逆向_6 --- JNI、NDK开发、jni静态注册、jni_onload动态注册_第8张图片

字符串编码不同而引起的崩溃如下:

Abort message: 'JNI DETECTED ERROR IN APPLICATION: input is not valid Modified UTF-8: illegal continuation byte 0
string: '�'
input: '0xf4'

1、ndk-stack查看堆栈

遇到native层崩溃时,我们可用ndk-stack查看堆栈地址,命令如下:

adb logcat | ndk-stack -sym xxx/libxxx.so

2、addr2line查看代码位置

// 0x12345678为堆栈地址,替换为实际崩溃地址
aarch64-linux-android-addr2line -e libxxx.so 0x12345678

3、objdump查看符号表

objdump可以用-syms查看符号表,命令如下:

objdump -syms libxxx.so

4、readelf查看依赖库与符号表

readelf是用来查看ELF文件的工具,ELF(Executable and Linkable Format)是一种可执行、可重定向的二进制目标文件。命令参数选项如下:

安卓逆向_6 --- JNI、NDK开发、jni静态注册、jni_onload动态注册_第9张图片

使用readelf -d libxxx.so查看其依赖库:

安卓逆向_6 --- JNI、NDK开发、jni静态注册、jni_onload动态注册_第10张图片

使用readelf -s libxxx.so查看其符号表:

安卓逆向_6 --- JNI、NDK开发、jni静态注册、jni_onload动态注册_第11张图片

3、Windows 下 JNI 的使用教程

参考:IntelliJ idea 2018 平台下JNI编程调用 C++ 算法(一):https://www.cnblogs.com/lucychen/p/9771236.html

JNI 的使用大致有以下4个步骤:

  1. 在 Java 中写 native 方法
  2. javah 命令生成 C/C++ 头文件。( 注意:windows 系统生成的动态链接库是 .dll 文件,Linux 是 .so 文件。JDK10 中将 javah 工具取消了,需要使用 javac -h 替代,这是与 jdk8 不同的地方。 )
  3. 写对应的 C/C++ 程序,实现头文件中声明的方法,并编译成库文件
  4. 在 Java 中加载这个库文件并使用

注意:Windows 平台需要注意操作系统位数,32 位 dll 无法在 64位 上被调用。

在 Java 中写 native 方法

主要步骤

  1. 创建一个 java 项目,在其中编写一个带有 native 方法的类
  2. 利用 idea 生成 .h 头文件。  
  3. 在 vs 中创建一个动态链接库应用程序的解决方案
  4. 在解决方案中创建 C++ 文件,实现头文件中的方法
  5. 生成 动态 链接库
  6. 回到 idea,运行 java 项目,排错重复以上步骤直到运行成功

1. 在 idea 创建 java 项目

实现一个简单的 testHello_1() 函数 和 静态的 testHell0_2() 函数,在 C++ 中实现  testHello_1() 和 testHell0_2()。

注意:java 代码都不要放到默认包下(就是不写 package 语句就会放到默认包),默认包下的方法在其他地方都不能调用!!

步骤如下:

  • 在 idea 创建 java 项目(例如:jni_demo),在 src 目录下新建一个 package(示例 包名  com.jni.test )。
  • 在包下创建一个类,用来编写 native 方法和 main 函数。示例 类名 JNIDemo
  • 声明  native 方法,native 方法就是一个非 java 实现的方法,比如用 C/C++ 实现。本地方法可以是静态的,也可以不声明为静态的。

图示:

安卓逆向_6 --- JNI、NDK开发、jni静态注册、jni_onload动态注册_第12张图片

示例代码:

package com.jni.test;

public class JNIDemo {
    public native void testHello_1();
    public static native int testHello_2();

    public static void main(String[] args) {
        try {
            // System.loadLibrary("JNIPROJECT.dll");
            System.load("D:\\jni_demo\\src\\com\\jni\\test\\JNIPROJECT.dll");

            JNIDemo jniDemo =new JNIDemo();
            jniDemo.testHello_1();

            int retVal = testHello_2();
            System.out.println("retVal : " + retVal);
        }
        catch (Exception ex) {
            ex.printStackTrace();
        }
    }
}

其中 testHello_1 是一个类方法,testHello_2 是一个静态方法,前面都有 native 代表是一个本地函数。
main 函数中,调用 testHello_1 函数 和 testHello_2  函数。下面的 static 代码块暂且不谈。
代码写好后,build 一下项目,生成 class文件,build 后,可在左侧目录看到 out/production 目录下生成了对应 class 文件。

load 和 loadLibrary 区别

  1. 它们都可以用来装载库文件,不论是 JNI 库文件还是非 JNI 库文件。在任何本地方法被调用之前必须先用这个两个方法之一把相应的 JNI 库文件装载。
  2. System.load 参数为库文件的绝对路径,可以是任意路径。例如,你可以这样载入一个 windows 平台下 JNI 库 文件:System.load("C:\\Documents and Settings\\TestJNI.dll");
  3. System.loadLibrary 参数为库文件名,不包含库文件的扩展名。例如,你可以这样载入一个 windows 平台下 JNI 库 文件System. loadLibrary ("TestJNI"); 这里,TestJNI.dll 必须是在 java.library.path 这一 jvm 变量所指向的路径中。
    可以通过如下方法来获得该变量的值:System.getProperty("java.library.path");
     默认情况下,在 Windows 平台下,该值包含如下位置:
            1)和 jre 相关的一些目录
            2)程序当前目录
            3)Windows 目录
            4)系统目录(system32)
            5)系统环境变量 path 指定目录。

classpath 与 java.library.path 区别

classpath 路径下,只能是 jar 或者 class 文件,否者会报错,因为他们会被 load 到 JVM 

build ---> build project,

安卓逆向_6 --- JNI、NDK开发、jni静态注册、jni_onload动态注册_第13张图片

2.生成 头文件 ( 静态注册动态注册 )

为什么需要注册?其实就是给 Java 的 native 函数找到底层 C/C++ 实现的函数指针。

  • 静态注册:通过包名、类名一致来确认,Java 有一个命令 javah,专门生成某一个 JAVA 文件所有的 native 函数的头文件(h文件), 静态方法注册 JNI 有哪些缺点?1:必须遵循某些规则。 2:名字过长。 3:多个 class 需 Javah 多遍。 4:运行时去找效率不高
  • 动态注册 :在 JNI 层实现的,JAVA 层不需要关心,因为在 system.load 时就会去掉 JNI_OnLoad,有就注册,没有就不注册。
  • 区别:静态注册是用到时加载,动态注册一开始就加载好了,这个可以从 DVM 的源代码看出来。

生成 JNI 头文件。(此处有两种方法:2.1手动输入 javah 命令生成头文件、2.2 一键生成头文件)

2.1 手动输入 javah 命令生成头文件

打开 cmd,进入 src 目录,运行 javah 命令,生成 C/C++ 头文件,注意:要带上 java 包名

命令格式:javah -classpath 要加载的类的路径 -jni 包名.类名

安卓逆向_6 --- JNI、NDK开发、jni静态注册、jni_onload动态注册_第14张图片

执行完命令之后,会在 src  目录生成一个 .h 文件:

安卓逆向_6 --- JNI、NDK开发、jni静态注册、jni_onload动态注册_第15张图片

在 IntelliJ IDEA 图示:

安卓逆向_6 --- JNI、NDK开发、jni静态注册、jni_onload动态注册_第16张图片

头文件完整代码:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include 
/* Header for class com_jni_test_JNIDemo */

#ifndef _Included_com_jni_test_JNIDemo
#define _Included_com_jni_test_JNIDemo
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_jni_test_JNIDemo
 * Method:    testHello_1
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_com_jni_test_JNIDemo_testHello_11
  (JNIEnv *, jobject);

/*
 * Class:     com_jni_test_JNIDemo
 * Method:    testHello_2
 * Signature: ()I
 */
JNIEXPORT jint JNICALL Java_com_jni_test_JNIDemo_testHello_12
  (JNIEnv *, jclass);

#ifdef __cplusplus
}
#endif
#endif

头文件 说明:

  1. 包含了 jni.h 头文件。
  2. 在类中 声明 的常量(static final)类型会在头文件中以宏的形式出现,这一点还是很方便的。

  3. 函数的注释还是比较全的,包括了:
    1. 对应的 class
    2. 对应的 java 方法名
    3. 对应 java 方法 的 签名
  4. 方法的声明显得有点奇怪,由以下及部分组成:
    1. JNIEXPORT 这是函数的导出方式
    2. jint 返回值类型( jint 由 jni.h定义,对应 int
    3. JNICALL 函数的调用方式也就是汇编级别参数的传入方式
    4.  Java_com_jni_test_JNIDemo_testHello_11  超级长的函数名!!!
      格式是 :Java_ + 类全名 + _ + JAVA中声明的 native 方法名。其中会把包名中的点(.)替换成下划线(_),同时为了避免冲突把 下划线 替换成 _1
    5. 方法的参数,上面的这个方法在 JAVA 的声明中实际上是没有参数的,其中的 JNIENV 顾名思义是 JNI 环境,和具体的线程绑定。而第二个参数 jclass 其实是 java 中的 Class 因为上面是一个 static 方法,因此第二个参数是jclass。如果是一个实例方法则对应第二个参数是 jobject,相当于 java 中的 this 

2.2 一键生成头文件

头文件可以使用命令行生成(见参考文献),或者熟悉格式后自己手写。但是如果希望能够随便点一下就生成头文件,于是,找到了一种 用idea工具生成头文件的方法,那就是 External Tools。External Tools 其实就是将手动输入的命令存下来,本质也是运行 javah,后面跟着配置参数,这些参数存在 External Tools,避免每次手动输入。

  • 添加 External Tools。File -> Settings -> Tools -> ExternalTools,点击添加
  • 编辑 Tools
              Name: Generate Header File   
              Program: $JDKPath$/bin/javah 
              Arguments: -jni -classpath $OutputPath$ -d ./jni $FileClass$
              Working directory: $ProjectFileDir$

        Name:External Tools 的名称,喜欢什么起什么,只要自己明白
        Program是javah工具所在地址,即jdk所在路径下的bin,该参数是指tool采用的运行工具是javah
        Arguments设置的是javah的参数,具体可在命令行中查看javah的帮助,查看每个函数含义
        Working directory:项目名称

  • 生成头文件
    保存工具后,右击需要生成头文件的类,即我们的SimpleHello,选择External Tool,点击我们刚刚创建的tool。
    然后你就会发现我们的目录中多了一个jni文件夹,jni文件夹里面有一个名字长长的.h文件,成功!

提示:该方法适用于 jdk8,jdk10 中取消了 javah,的使用 javac -h。

jni.h 是什么 ?

  • jni.h 头文件一般位于 $JAVA_HOME/jd{jdk-version}/include 目录内下面的一个文件,jni.h 里面存储了大量的函数和对象,这是 JNI 中所有的 类型、函数、宏 等定义的地方。C/C++ 世界的 JNI 规则就是由他制定的。它有个很好的方法就是通过 native 接口名来获取 C/C++ 函数。
  • 另外还有个 %JAVA_HOME%\bin\include\win32 下的 jni_md.h 

打个比方类似如下:public static String getCMethod(String javaMethodName);

它可以根据你的 java接口,找到 C函数并调用。但这就意味着你不能在 C 里随意写函数名,因为如果你写的 java 方法叫 native aaa(); C函数也叫 aaa(); 但 jni.h 通过 getCMethod(String javaMethodName) 去找的结果是 xxx(); 那这样就无法调用了。

既然不能随意写,怎么办?

没事,jdk 提供了一个通过 java 方法生成 C/C++ 函数接口名的工具 javah。

javah 是什么?

javah 就是提供具有 native method 的 java 对象的 C/C++ 函数接口。javah  命令可以提供一个 C/C++ 函数的接口。

C/C++ 实现 Java 中 native 方法

然后就是在 C/C++ 中实现这个方法就可以了。

但是在动手前现大致了解以下 jni.h 制定的游戏规则。javah 生成的头文件里面使用的类型都是 jni.h 定义的,目的是做到 平台无关,比如保证在所有平台上 jint 都是 32位 的有符号整型。

基本对应关系如下:

jni 类型 JAVA 类型 对应 本地类型 类型签名
jboolean boolean uint8_t Z
jbyte byte char B
jcahr char uint16_t C
jshort short int16_t S
jint int int32_t I
jlong long int64_t J
jfloat float float F
jdouble double double D
void void void V

引用类型对应关系:

java 类型 JNI 类型 java 类型 JNI 类型
所有的实例引用 jobject java.lang.Class jclass
java.lang.String jstring Ocject[] jobjectArray
java.lang.Throwable jthrowable 基本类型[] jxxxArray

通过表格发现,除了上面定义的 StringClassThrowable,其他的类(除了数组)都是以 jobject 的形式出现的!事实上jstring, jclass 也都是 object 的子类。所以这里还是和 java 层一样,一切皆 jobject。(当然,如果 jni 在 C 语言中编译的话是没有继承的概念的,此时 jstring,jclass 等其实就是 jobject !用了 typedef 转换而已!!)

接下来是 JNIEnv * 这个指针,他提供了 JNI 中的一系列操作的接口函数。

JNI 中操作 jobject

其实也就是在 native 层操作 java 层的实例。 要操作一个实例无疑是:

  1. 获取/设置 (即 get/set )成员变量(field)的值

  2. 调用成员方法(method)

怎么得到 field 和 method?

通过使用 jfieldID jmethodID: 在 JNI 中使用类似于放射的方式来进行 field 和 method 的操作。JNI 中使用 jfieldID 和jmethodID 来表示成员变量和成员方法,获取方式是:

jfieldID GetFieldID(jclass clazz, const char *name, const char *sig);
jfieldID GetStaticFieldID(jclass clazz, const char *name, const char *sig);
jmethodID GetMethodID(jclass clazz, const char *name, const char *sig);
jmethodID GetStaticMethodID(jclass clazz, const char *name, const char *sig) ;

其中最后一个参数是签名。 获取 jclass 的方法 除了实用上面静态方法的第二个参数外,还可以手动获取。 jclass FindClass(const char *name) 需要注意的是 name 参数,他是一个类包括包名的全称,但是需要把包名中的点.替换成斜杠/

有了 jfieldID 和 jmethodID 就知道狗蛋住哪了,现在去狗蛋家找他玩 ♪(^∇^*)

成员变量:

get:

  • GetField(jobject , jfieldID);即可获得对应的field,其中field的类型是type,可以是上面类型所叙述的任何一种。
  • GetStaticField(jobject , jfieldID);同1,唯一的区别是用来获取静态成员。

set:

  • void SetField(jobject obj, jfieldID fieldID, val)
  • void SetStaticField(jclass clazz, jfieldID fieldID, value);

成员方法:

调用方法自然要把方法的参数传递进去,JNI中实现了三种参数的传递方式:

  1. CallMethod(jobject obj, jmethod jmethodID, ...)其中...是C中的可变长参数,类似于printf那样,可以传递不定长个参数。于是你可以把java方法需要的参数在这里面传递进去。

  2. CallMethodV(jobject obj, jmethodID methodID, va_list args)其中的va_list也是C中可变长参数相关的内容(我不了解,不敢瞎说。。。偷懒粘一下Oracle的文档)Programmers place all arguments to the method in an args argument of type va_list that immediately follows the methodID argument. The CallMethodV routine accepts the arguments, and, in turn, passes them to the Java method that the programmer wishes to invoke.

  3. CallMethodA(jobject obj, jmethodID methodID, const jvalue * args)哎!这个我知道可以说两句LOL~~这里的jvalue通过查代码发现就是JNI中各个数据类型的union,所以可以使用任何类型复制!所以参数的传入方式是通过一个jvalue的数组,数组内的元素可以是任何jni类型。

然后问题又来了:(挖掘机技术到底哪家强?!o(*≧▽≦)ツ┏━┓) 如果传进来的参数和java声明的参数的不一致会怎么样!(即不符合方法签名)这里文档中没用明确解释,但是说道: > Exceptions raised during the execution of the Java method.

typedef union jvalue {
    jboolean z;
    jbyte    b;
    jchar    c;
    jshort   s;
    jint     i;
    jlong    j;
    jfloat   f;
    jdouble  d;
    jobject  l;
} jvalue;
  1. 调用实例方法(instance method):
    1. CallMethod(jobject obj, jmethodID methodID, ...);调用一个具有类型返回值的方法。
    2. CallMethodV(jobject obj, jmethodID methodID, va_list args);
    3. CallMethodA(jobject obj, jmethodID methodID, const jvalue * args)
  2. 调用静态方法(static method):
    1. CallStaticMethod(jobject obj, jmethodID methodID, ...);
    2. CallStaticMethodV(jobject obj, jmethodID methodID, va_list args);
    3. CallStaticMethodA(jobject obj, jmethodID methodID, const jvalue * args)
  3. 调用父类方法(super.method),这个就有点不一样了。多了一个jclass参数,jclass可以使obj的父类,也可以是obj自己的class,但是methodID必须是从jclass获取到的,这样就可以调用到父类的方法。
    1. CallNonvirtualMethod(jobject obj, jclass clazz, jmethodID methodID, ...)
    2. CallNonvirtualMethodV(JNIEnv *env, jobject obj, jclass clazz, jmethodID methodID, va_list args);
    3. CallNonvirtualMethodA(JNIEnv *env, jobject obj, jclass clazz, jmethodID methodID, const jvalue *args);

#### 数组的操作

数组是一个很常用的数据类型,在但是在JNI中并不能直接操作jni数组(比如jshortArray,jfloatArray)。使用方法是:

  1. 获取数组长度:jsize GetArrayLength(jarray array)
  2. 创建新数组: ArrayType NewArray(jsize length);
  3. 通过JNI数组获取一个C/C++数组:* GetArrayElements(jshortArray array, jboolean *isCopy)
  4. 指定原数组的范围获取一个C/C++数组(该方法只针对于原始数据数组,不包括Object数组):void GetArrayRegion(JNIEnv *env, ArrayType array, jsize start, jsize len, NativeType *buf);
  5. 设置数组元素:void SetArrayRegion(jshortArray array, jsize start, jsize len,const *buf)。again,如果是Object数组需要使用:void SetObjectArrayElement(JNIEnv *env, jobjectArray array, jsize index, jobject value);
  6. 使用完之后,释放数组:void ReleaseArrayElements(jshortArray array, jshort *elems, jint mode)

有点要说明的:

  • 上面的 3中的 isCopy:当你调用getArrayElements时JVM(Runtime)可以直接返回数组的原始指针,或者是copy一份,返回给你,这是由JVM决定的。所以isCopy就是用来记录这个的。他的值是JNI_TURE或者JNI_FALSE

  • 上面 6 释放数组。一定要释放你所获得数组。其中有一个mode参数,其有三个可选值,分别表示:
    0
        原始数组:允许原数组被垃圾回收。
        copy: 数据会从get返回的buffer copy回去,同时buffer也会被释放。
    JNI_COMMIT
        原始数组:什么也不做
        copy: 数据会从get返回的buffer copy回去,同时buffer不会被释放。
    JNI_ABORT
        原始数组:允许原数组被垃圾回收。之前由JNI_COMMIT提交的对数组的修改将得以保留。
        copy: buffer会被释放,同时buffer中的修改将不会copy回数组!

####关于引用与垃圾回收 比如上面有个方法传了一个jobject进来,然后我把她保存下来,方便以后使用。这样做是不行哒!因为他是一个LocalReference,所以不能保证jobject指向的真正的实例不被回收。也就是说有可能你用的时候那个指针已经是个野指针的。然后你的程序就直接Segment Fault了,呵呵。。。

在 JNI 中提供了三种类型的引用:

  1. Local Reference:即本地引用。在JNI层的函数,所有非全局引用对象都是Local Reference, 它包括函数调用是传入的jobject和JNI成函数创建的jobject。Local Reference的特点是一旦JNI层的函数返回,这些jobject就可能被垃圾回收。
  2. Glocal Reference:全局引用,这些对象不会主动释放,永远不会被垃圾回收。
  3. Weak Glocal Reference:弱全局引用,一种特殊的Global Reference,在运行过程中有可能被垃圾回收。所以使用之前需要使用jboolean IsSameObject(jobject obj1, jobject obj2)判断它是否已被回收。

Glocal Reference:
1. 创建:jobject NewGlobalRef(jobject lobj);
2. 释放:void DeleteGlobalRef(jobject gref);

Local Reference:
LocalReference也有一个释放的函数:void DeleteLocalRef(jobject obj),他会立即释放Local Reference。 这个方法可能略显多余,其实也是有它的用处的。刚才说Local Reference会再函数返回后释放掉,但是假如函数返回前就有很多引用占了很多内存,最好函数内就尽早释放不必要的内存。

####关于JNI_OnLoad 开头提到JNI_OnLoad是java1.2中新增加的方法,对应的还有一个JNI_OnUnload,分别是动态库被JVM加载、卸载的时候调用的函数。有点类似于WIndows里的DllMain。
前面提到的实现对应native的方法是实现javah生成的头文件中定义的方法,这样有几个弊端:

  1. 函数名太长。很长。。相当长。。。
  2. 函数会被导出,也就谁说可以在动态库的导出函数表里面找到这些函数。这将有利于别人对动态库的逆向工程,因此带来安全问题。

现在有了JNI_OnLoad,情况好多了。你不光能在其中完成动态注册native函数的工作还可以完成一些初始化工作。java对应的有了jint RegisterNatives(jclass clazz, const JNINativeMethod *methods,jint nMethods)函数。参数分别是:

  1. jclass clazz,于native层对应的java class

  2. const JNINativeMethod *methods这是一个数组,数组的元素是JNI定义的一个结构体JNINativeMethod

  3. 上面的数组的长度

JNINativeMethod:代码中的定义如下:

/*
 * used in RegisterNatives to describe native method name, signature,
 * and function pointer.
 */

typedef struct {
    char *name;
    char *signature;
    void *fnPtr;
} JNINativeMethod;

所以他有三个字段,分别是

字段 含义
char *name java class中的native方法名,只需要方法名即可
char *signature 方法签名
void *fnPtr 对应native方法的函数指针

于是现在你可以不用导出native函数了,而且可以随意给函数命名,唯一要保证的是参数及返回值的统一。然后需要一个const JNINativeMethod *methods数组来完成映射工作。

看起来大概是这样的:

//只需导出JNI_OnLoad和JNI_OnUnload(这个函数不实现也行)
/**
 * These are the exported function in this library.
*/
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved);
JNIEXPORT void JNICALL JNI_OnUnload(JavaVM *vm, void *reserved);

//为了在动态库中不用导出函数,全部声明为static
//native methods registered by JNI_OnLoad
static jint native_newInstance (JNIEnv *env, jclass);

//实现native方法
/*
* Class:     com_young_soundtouch_SoundTouch
* Method:    native_newInstance
* Signature: ()I
*/
static jint native_newInstance
(JNIEnv *env, jclass ) {
	int instanceID = ++sInstanceIdentifer;
	SoundTouchWrapper *instance = new SoundTouchWrapper();
	if (instance != NULL) {
		sInstancePool[instanceID] = instance;
		++sInstanceCount;
	}
	LOGDBG("create new SouncTouch instance:%d", instanceID);
	return instanceID;
}

//构造JNINativeMethod数组
static JNINativeMethod gsNativeMethods[] = {
		{
			"native_newInstance",
			"()I",
			reinterpret_cast (native_newInstance)
		}
};
//计算数组大小
static const int gsMethodCount = sizeof(gsNativeMethods) / sizeof(JNINativeMethod);

//JNI_OnLoad,注册native方法。
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
	JNIEnv* env;
	jclass clazz;
	LOGD("JNI_OnLoad called");
	if (vm->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6) != JNI_OK) {
		return -1;
	}
	//FULL_CLASS_NAME是个宏定义,定义了对应java类的全名(要把包名中的点(.)_替换成斜杠(/))
	clazz = env->FindClass(FULL_CLASS_NAME);
	LOGDBG("register method, method count:%d", gsMethodCount);
	//注册JNI函数
	env->RegisterNatives(clazz, gsNativeMethods,
		gsMethodCount);
	//必须返回一个JNI_VERSION_1_1以上(不含)的版本号,否则直接加载失败
	return JNI_VERSION_1_6;
}

###实战技巧篇

这里主要是巧用C中的宏来减少重复工作:

####迅速生成全名

//修改包名时只需要改以下的宏定义即可
#define FULL_CLASS_NAME "com/young/soundtouch/SoundTouch"
#define func(name) Java_ ## com_young_soundtouch_SoundTouch_ ## name
#define constance(cons) com_young_soundtouch_SoundTouch_ ## cons

比如func(native_1newInstance)展开成:Java_com_young_soundtouch_SoundTouch_native_1newInstance即JNI中需要导出的函数名(不过用动态注册方式没太大用了)

constance(AUDIO_FORMAT_PCM16)展开成com_young_soundtouch_SoundTouch_AUDIO_FORMAT_PCM16这个着实有用。

而且如果包名改了也可以很方便的适应之。

###安卓的log

//define __USE_ANDROID_LOG__ in makefile to enable android log
#if defined(__ANDROID__) && defined(__USE_ANDROID_LOG__)
#include 
#define LOGV(...)   __android_log_print((int)ANDROID_LOG_VERBOSE, "ST_jni", __VA_ARGS__)
#define LOGD(msg)  __android_log_print((int)ANDROID_LOG_DEBUG, "ST_jni_dbg", "line:%3d %s", __LINE__, msg)
#define LOGDBG(fmt, ...) __android_log_print((int)ANDROID_LOG_DEBUG, "ST_jni_dbg", "line:%3d " fmt, __LINE__, __VA_ARGS__)
#else
#define LOGV(...) 
#define LOGD(fmt) 
#define LOGDBG(fmt, ...) 
#endif

通过这样的宏定义在打LOGD或者LOGDBG的时候还能自动加上行号!调试起来爽多了!

####C++中清理内存的方式

由于C++里面需要手动清楚内存,因此我的解决方案是定义一个map,给每个实例一个id,用id把java中的对象和native中的对象绑定起来。在java层定义一个release方法,用来释放本地的对象。 本地的 KEY-对象 映射 static std::map sInstancePool;

####关于NDK 因为安卓的约定是把本地代码放到jni目录下面,但是假如有多个jni lib的时候会比较混乱,所以方案是每一个lib都在jni里面建一个子目录,然后jni里面的Android.mk就可以去构建子目录中的lib了。

jni/Android.mk如下(超级简单):

LOCAL_PATH := $(call my-dir)
include $(call all-subdir-makefiles)

然后在子目录soundtouch_module中的Android.mk就可以像一般的Android.mk一样书写规则了。

同时记录一下在Andoroid.mk中使用makefile内建函数wildcard的方法。 有时候源文件是一个目录下的所有.cpp/.c文件,这时候wildcard来统配会很方便。但是Android.mk与普通的Makefile的不同在于:

  1. 调用Android.mkmingling的${CWD}并不是Android.ml所在的目录。所以Android.mk中有一个变量LOCAL_PATH := $(call my-dir)来记录当前 Android.mk所在的目录。
  2. 同时还会把所有的LOCAL_SRC_FILES 前面加上$(LOCAL_PATH)这样写makefile的时候就可以用相对路径了,提供了方便。但是这也导致了坑!

因为1,直接使用相对路径会导致wildcard匹配不到源文件。所以最好这么写FILE_LIST := $(wildcard $(LOCAL_PATH)/soundtouch_source/source/SoundTouch/*.cpp)。然而又因为2,这样还是不行的。所以还需要匹配之后把$(LOCAL_PATH)的部分去掉,因此还得这样$(FILE_LIST:$(LOCAL_PATH)/%=%).

还有个小tip:LOCAL_CFLAGS中最好加上这个定义-fvisibility=hidden这样就不会在动态库中导出不必要的函数了。

###附录签名

JAVA中的函数签名包括了函数的参数类型,返回值类型。因此即使是重载了的函数,其函数签名也不一样。java编译器就会根据函数签名来判断你调用的到地址哪个方法。 签名中表示类型是这样的

1.基本类型都对应一个大写字母,如下:

JAVA类型 类型签名
boolean Z
byte B
char C
short S
int I
long J
float F
double D
void V

2.如果是类则是: L + 类全名(报名中的点(.)用(/)代替)+ ; 比如java.lang.String 对应的是 Ljava/lang/String;

3.如果是数组,则在前面加[然后加类型签名,几位数组就加几个[ 比如int[]对应[I,boolean[][] 对应 [[Z,java.lang.Class[]对应[Ljava/lang/Class;

可以通过javap命令来获取签名(javah生成的头文件注释中也有签名):javap -x -p <类全名> 坑爹的是java中并不能通过反射来获取方法签名,需要自己写一个帮助类。 (其实我还写了个小程序可以自动生成签名,和JNI_OnLoad中注册要用到的JNINativeMethod数组,从此再也不用糟心的去写那该死的数组了。LOL~~~)

3. 在 VS 中创建解决方案

接下来打开 Visual studio 2019,新建动态链接库: JniProject

安卓逆向_6 --- JNI、NDK开发、jni静态注册、jni_onload动态注册_第17张图片

填写 项目名,项目所在目录:

安卓逆向_6 --- JNI、NDK开发、jni静态注册、jni_onload动态注册_第18张图片

创建完成后再添加类:

安卓逆向_6 --- JNI、NDK开发、jni静态注册、jni_onload动态注册_第19张图片

设置项目包含目录

  • 本来我是按照这篇文章复制jni.h等文件的,但是一直报错“找不到 源 文件 jni.h”。搞来搞去总是不成,后来才发现,我在vs2017直接复制,jni.h并没有到C++项目目录下,而是仍然在原来的目录里,这与java的ide很不同啊。虽然被这个问题搞到差点摔桌子,但我转念一想,在原来的目录下就还不错啊,省得我复制来复制去。于是刷刷刷设置了包含路径

  • 点击项目,我的项目叫 jniCppDemo,在菜单栏选择:项目 -> 属性 -> 配置属性 -> VC++目录 -> 包含目录
  • 设置包含路径:          
              设置 jni.h 所在路径: C:\Program Files\Java\jdk1.8.0_181\include
              设置 jni_md.h 所在路径: C:\Program Files\Java\jdk1.8.0_181\include\win32
              设置刚刚生成头文件所在路径: D:\javaWorkspace\jniJavaDemo\jni

如果不想设置 包含目录,可以直接把文件( jni.h、com_jni_test_JNIDemo.h、jni_md.h )复制到工程目录下.

JDK 安装目录的 include 目录下有一个 jni.h 的文件,include 的 win32 目录下有个 jni_md.h 文件,还有 java 工程的 src 目录下的C 头文件,一起拷贝到 C工程的 JniProject 目录下:( JniProject ---> jni.h   com_jni_test_JNIdemo.h    jni_md.h )如下图:

安卓逆向_6 --- JNI、NDK开发、jni静态注册、jni_onload动态注册_第20张图片

在 C项目的头文件文件夹上面:右键 --- > 添加 ---> 现有项

安卓逆向_6 --- JNI、NDK开发、jni静态注册、jni_onload动态注册_第21张图片

选择 jni.h、com_jni_test_JNIDemo.h、jni_md.h

安卓逆向_6 --- JNI、NDK开发、jni静态注册、jni_onload动态注册_第22张图片

添加完可以在 头文件 目录中看到

安卓逆向_6 --- JNI、NDK开发、jni静态注册、jni_onload动态注册_第23张图片

打开 com_jni_test_JNIDemo.h 文件

安卓逆向_6 --- JNI、NDK开发、jni静态注册、jni_onload动态注册_第24张图片

#include 修改为 #include "jni.h" 错误提示消失。

安卓逆向_6 --- JNI、NDK开发、jni静态注册、jni_onload动态注册_第25张图片

4. 编写 cpp 文件

然后在 TestJNI.cpp 文件中写入如下代码:

#include "pch.h"
#include "TestJNI.h"
#include "com_jni_test_JNIDemo.h"


JNIEXPORT void JNICALL Java_com_jni_test_JNIDemo_testHello_11
(JNIEnv*, jobject) {
	printf("this is C++ print : Java_com_jni_test_JNIDemo_testHello_11\n");
}


JNIEXPORT jint JNICALL Java_com_jni_test_JNIDemo_testHello_12
(JNIEnv*, jclass) {
	printf("this is C++ print : Java_com_jni_test_JNIDemo_testHello_12\n");
	return 100;
}

5. 生成 dll 文件

使用 C/C++ 实现本地方法生成动态库文件(windows下扩展名为 DDL,linux 下扩展名为 so):

写好了 cpp,就可以生成 dll。右击项目生成/重新生成,就生成了 dll 文件。从控制台输出可看到 dll 的地址

注意:设置为 64位

安卓逆向_6 --- JNI、NDK开发、jni静态注册、jni_onload动态注册_第26张图片

保存,运行,编译生成 DLL 文件,在工程项目的 release 目录中可以找到。

安卓逆向_6 --- JNI、NDK开发、jni静态注册、jni_onload动态注册_第27张图片

6. 运行 Java

示例代码 1:

package com.jni.test;

public class JNIDemo {
    public native void testHello_1();
    public static native int testHello_2();

    public static void main(String[] args) {
        try {
            // System.loadLibrary("JNIPROJECT.dll");
            System.load("D:\\jni_demo\\src\\com\\jni\\test\\JNIPROJECT.dll");

            JNIDemo jniDemo =new JNIDemo();
            jniDemo.testHello_1();

            int retVal = testHello_2();
            System.out.println("retVal : " + retVal);
        }
        catch (Exception ex) {
            ex.printStackTrace();
        }
    }
}

运行截图:

安卓逆向_6 --- JNI、NDK开发、jni静态注册、jni_onload动态注册_第28张图片

示例代码 2:

package com.jni.test;

public class JNIDemo {
    public native void testHello_1();
    public static native int testHello_2();

    static {
        // System.loadLibrary("JNIPROJECT.dll");
        System.load("D:\\jni_demo\\src\\com\\jni\\test\\JNIPROJECT.dll");
    }

    public static void main(String[] args) {
        try {
            JNIDemo jniDemo =new JNIDemo();
            jniDemo.testHello_1();

            int retVal = testHello_2();
            System.out.println("retVal : " + retVal);
        }
        catch (Exception ex) {
            ex.printStackTrace();
        }
    }
}

运行截图:

安卓逆向_6 --- JNI、NDK开发、jni静态注册、jni_onload动态注册_第29张图片

注意:

  • 1、一般在 static 代码块中加载动态链接库
  • 2、如果将 DLL 路径加入 PATH 环境变量的时候,eclipse是开着的,那么要关闭 eclipse 再开,让 eclipse 重新读取环境变量
  • 3、必须在本类中使用native方法

4、Android Studio 的 JNI 开发

JNI 技术的整体步骤和原理:

  • 1、新建一个Android studio工程(注意  把这个勾选上,不然后面还需要配置,勾选上就无须自己配置Cmake,Gradle啦)
  • 2、Android 指定位置新建一个类,如JavaJNI.java类(一般位置为、src/main/java/"你的包名"/),在该类里面声明一个方法,该方法有本地端实现,即如 public native void open();
  • 3、写好之后本地native方法之后,配置Javah, 操作步骤:File-Tools-External Tools-"点击加号"    name随便起一个,方便统一叫javah, 下面的Description可以和上面一致;

Program: $JDKPath$\bin\javah.exe
Auguments: -classpath . -jni -o $ModuleFileDir$/src/main/jni/$Prompt$ $FileClass$
Working directory: $ModuleFileDir$\src\main\java

以上为配置Javah过程,到这就配置好了,注意上面几个配置你可以理解为固定配置,其实是一些路径定义,可以不用管的,

  • 4、鼠标选中刚刚新建的含有本地实现方法的类,右击选择External Tools的Javah, 随机在弹出的窗口输入名字(这个名字就是马上生成的C或C++头文件的名字,文件会保存在/src/main/jni/下面),这样我们的C或C++头文件就生成好了,在JNI文件下,
  • 5、将生成的.h头文件 放到、src/main/cpp文件中
  • 6、在cpp文件下在新建一个对应的.cpp文件,开始编写需要调用的本地函数方法(具体做法把刚生成.h文件中的方法名复制过来,“;”改为方法体“{}”,然后在方法体中用C++实现你需要的功能)
  • 7、在MainActivity.java测试类中调用JavaJNI.java类中本地声明的方法
  • 8、编译即可成功调用实现你写的C++方法

NDK 安装配置

在 File ---> Settings ---> appearance ---> system settings ---> Android SDK,下查看 NDK 安装配置情况,如果没有下载配置 NDK ,以及相关的包,对应下载相关的安装包。

打开 sdkManager下载 CMake 和 LLDB

下载安装好后,可以在 File - Project Structure 的 SDK Location 下查看对应的安装配置路径情况,

Android Studio JNI开发

:https://blog.csdn.net/fengruoying93/article/details/124222174

打开 Android Studio,新建一个 Native C++ 项目。示例:JNIDemo

安卓逆向_6 --- JNI、NDK开发、jni静态注册、jni_onload动态注册_第30张图片

项目创建成功后,开始创建 jni 文件夹:src 右键 ---> New ---> Folder ---> JNI Fold

创建 JNI 类

public class JNITest { 
    static {
        System.loadLibrary("JniLib");
    } 
    public native String getString(); 
}

生成 .h 文件

方法 1:

配置 Anroid Studio 外部工具,一劳永逸,往后无需命令行,File ---> Setting ---> Tools ---> External Tools ---> “+” 进入页面

安卓逆向_6 --- JNI、NDK开发、jni静态注册、jni_onload动态注册_第31张图片

Program:$JDKPath$\bin\javah.exe 
Parameters:-classpath . -jni -d $ModuleFileDir$\src\main\jni $FileClass$
Working directory:$ModuleFileDir$\src\main\Java
  
注释: 
-classpath classes 指明类所在的位置
-jni com.jni.jnitest.JNITest 类的绝对路径 
-d 产生的.h文件放到指定目录下;

配置成功如图:

安卓逆向_6 --- JNI、NDK开发、jni静态注册、jni_onload动态注册_第32张图片

开始生成 .h文件,选中 JNI类 右键 ---> New ---> External Tools ---> javah,如图:

安卓逆向_6 --- JNI、NDK开发、jni静态注册、jni_onload动态注册_第33张图片

成功后如图:

安卓逆向_6 --- JNI、NDK开发、jni静态注册、jni_onload动态注册_第34张图片

方法 二

右键拖动JNI类所在的包的路径到Terminal,自动切换到该目录下

javac 编译生成 class 文件( 生成class文件的方法有很多,这里提供一种):java JNIText.java

右键拖动 java 文件夹到 Terminal,自动切换到该目录下

必须在包名外使用 javah 命令,编译生成.h文件,把.h文件移动到jni文件夹(生成.h文件后可以删除class文件)如图:

安卓逆向_6 --- JNI、NDK开发、jni静态注册、jni_onload动态注册_第35张图片

示例命令:javah -d jni -classpath ./java com.example.myapplication.hello

创建文件 JniLib.cpp 、Android.mk、Application.mk

在 jni 目录下分别创建并编写 JniLib.cpp、Android.mk、Application.mk 这三个文件

复制.h文件内容到 JniLib.cpp 并修改,如下(此文件为JNI内容文件):

/* DO NOT EDIT THIS FILE - it is machine generated */
#include
/* Header for class com_jni_jnitest_JNITest */
 
/*
 * Class:     com_jni_jnitest_JNITest
 * Method:    getString
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_com_jni_jnitest_JNITest_getString
  (JNIEnv * env, jobject jobject){
 
  return (*env).NewStringUTF("成功调用JNI内容");
 
  }

Android.mk

LOCAL_PATH := $(call my-dir)
 
include $(CLEAR_VARS)
 
LOCAL_MODULE := JniLib
LOCAL_SRC_FILES =: JniLib.cpp
include $(BUILD_SHARED_LIBRARY)

Application.mk

APP_MODULES := JniLib
APP_ABI := all

修改 app下的 build.gradle 文件

ndk{
            moduleName "JniLib"
//            abiFilters "armeabi", "armeabi-v7a", "x86" //输出指定的三种abi体系下的so库
        }
        sourceSets.main{
            jni.srcDirs = []
            jniLibs.srcDir "src/main/libs"
        }

项目下的gradle.properties文件(如果没有此文件,自己新建一个)添加代码:

android.useDeprecatedNdk=true

执行 ndk-build

此处我用的是配置好的工具来执行,和  javah  外部工具 一样的步骤

安卓逆向_6 --- JNI、NDK开发、jni静态注册、jni_onload动态注册_第36张图片

选中JNI类右键->New->External Tools->ndk-build,结果如图:

安卓逆向_6 --- JNI、NDK开发、jni静态注册、jni_onload动态注册_第37张图片

调用 so。示例代码:

package com.jni.jnitest;
 
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
 
public class MainActivity extends AppCompatActivity {
    Button button;
    TextView tv;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        button = findViewById(R.id.button);
        tv = findViewById(R.id.tv);
 
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                tv.setText("结果:"+ new JNITest().getString());
            }
        });
 
    }
}

5、 jni 静态注册、JNI_OnLoad 动态注册

错误:  编码GBK的不可映射字符 ( https://blog.csdn.net/talenter111/article/details/53418999 )
解决方法: 应该使用-encoding参数指明编码方式,如:
javah -jni -encoding UTF-8 com.example.XXXX.XXXX.MainActivity

安卓逆向_6 --- JNI、NDK开发、jni静态注册、jni_onload动态注册_第38张图片

静态注册、动态注册 示例代码

/**************静态方法**********************/
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_calc_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}
/*************************************************************/
 
JNIEXPORT void JNICALL Java_com_example_jni_1demo_MainActivity_javaToC(JNIEnv *env, jobject obj)
{
    // 获取 类
    jclass fdClass = env->FindClass("com/example/jni_demo/MainActivity");
 
    // 获取 普通方法id
    jmethodID _jmethodID = env->GetMethodID(fdClass, "_method", "()V");
 
    // 获取 静态方法id
    jmethodID _staticjmethodID = env->GetStaticMethodID(fdClass, "_staticMethod", "()V");
 
    // 调用 java中 的 普通方法
    env->CallVoidMethod(obj, _jmethodID);
 
    // 调用 java中 的 静态方法
    env->CallStaticVoidMethod(fdClass, _staticjmethodID);
}
 
 
/************************* 动态注册 nativate 方法 ********************************/ 
JNINativeMethod nativeMethod[] = {  // 方法数组映射
        // 定义数组,用于绑定 java方法 和 C方法的 关系
        {"addMethod", "(FF)F", (void*)my_add},          // java中方法名,方法签名,C++中方法名
        {"subMethod", "(FF)F", (void*)my_sub},
        {"mulMethod", "(FF)F", (void*)my_mul},
        {"divMethod", "(FF)F", (void*)my_div}
};
 
 
/************************* 实现 JNI_OnLoad 动态注册方法 *******************************/
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved)
{
    JNIEnv* env;
    if(vm->GetEnv((void**)&env, JNI_VERSION_1_6) != JNI_OK)
    {
        return JNI_ERR;
    }
 
    // 获取 java native 方法对应的 类
    jclass fdClass = env->FindClass("com/example/calc/MainActivity");
 
    // 注册 java 层 native 方法
    jint retVal = env->RegisterNatives(fdClass, nativeMethod, sizeof(nativeMethod)/sizeof(nativeMethod[0]));
    if(retVal != JNI_OK)
    {
        // 注册失败返回 -1
        return JNI_ERR;
    }
    return JNI_VERSION_1_6; //必须返回一个版本号
}

NDK 开发之 jni 静态注册

  • Java 层 调用 C/C++ 层 示例
  • Java 层调用 C/C++ 层,然后从 C/C++ 层调用 Java 层的 普通 字段
  • Java 层调用 C/C++ 层,然后从 C/C++ 层调用 Java 层的 静态 字段
  • Java 层调用 C/C++ 层,然后从 C/C++ 层调用 Java 层的 普通 方法
  • Java 层调用 C/C++ 层,然后从 C/C++ 层调用 Java 层的 静态 方法

NDK 开发之 动态注册 JNI_OnLoad

安卓逆向_6 --- JNI、NDK开发、jni静态注册、jni_onload动态注册_第39张图片

安卓逆向_6 --- JNI、NDK开发、jni静态注册、jni_onload动态注册_第40张图片

安卓逆向_6 --- JNI、NDK开发、jni静态注册、jni_onload动态注册_第41张图片

安卓逆向_6 --- JNI、NDK开发、jni静态注册、jni_onload动态注册_第42张图片

JNI 动态注册和静态注册的详解:https://blog.csdn.net/bill_xiao/article/details/89095020

Android:JNI 动态注册和静态注册的详解(附android studio实例):https://blog.csdn.net/qq_37858386/article/details/103765111

Android Studio3.0开发JNI流程------JNI静态注册和动态注册(多个类的native动态注册-经典篇):https://blog.csdn.net/cloverjf/article/details/78878814

Android JNI 函数注册的两种方式(静态注册/动态注册):https://www.jianshu.com/p/1d6ec5068d05

NDK 开发总结

JNI_动态注册_静态注册.zip : https://pan.baidu.com/s/1wpTYA9euSdPqE1Z2bA_BHA 提取码: 7h97

  • 静态注册、动态注册、使用 IDA 反编译简单 so 文件
  • jni.h 文件介绍说明

安装完jdk后就可以在安装目录的 include 目录中找到 jni.h 头文件(示例:C:\Program Files (x86)\Java\jdk1.8.0_261\include)

jni.h 头文件,其实就是 API 文档,里面有一些方法声明、结构体、等图示:

安卓逆向_6 --- JNI、NDK开发、jni静态注册、jni_onload动态注册_第43张图片

静态注册

安卓逆向_6 --- JNI、NDK开发、jni静态注册、jni_onload动态注册_第44张图片

如果是普通函数,第二个参数是 jobject

安卓逆向_6 --- JNI、NDK开发、jni静态注册、jni_onload动态注册_第45张图片

如果是静态函数,第二个参数是 jclass

安卓逆向_6 --- JNI、NDK开发、jni静态注册、jni_onload动态注册_第46张图片

动态注册

安卓逆向_6 --- JNI、NDK开发、jni静态注册、jni_onload动态注册_第47张图片

你可能感兴趣的:(Android,逆向,android,android,studio,java)