在日常的安卓逆向中,可能会遇到代码在 libxxx.so 中的情况,而这种在 so 中的代码的编写就涉及到了安卓的 JNI (Java Native Interface) 开发,俗话说要想会逆向,那么首先得要学会正向,假如都没见过某种语法,那还怎么逆向下去嘞?所以就玩玩 JNI 咯~

# 在 Android studio 中开发 JNI

首先我们需要创建一个 jni 文件夹,我们只需要右键 app, 然后点击 New->Folder->JNI Folder 就可以创建了

image-20230921182326835

接下来从 Android 布局转到 Project 布局,就可以看到新创建的 jni 文件夹

image-20230921182611380

image-20230921182644581

随后创建一个 oacia.cAndroid.mk

image-20230921192049257

之后我们需要编写 Android.mk , 关于语法可以参考下面的链接

Android.mk 语法

于是 Android.mk 的内容如下

LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := oacia
LOCAL_SRC_FILES := oacia.c
include $(BUILD_SHARED_LIBRARY)
#BUILD_SHARED_LIBRARY 生成共享链接库
#如果想要生成单独的可执行文件,可以使用 BUILD_EXECUTABLE

Android.mk 编写完成之后,我们需要将这个配置文件和我们的目标源文件 oacia.c 链接在一起,具体操作如下

右键 app 文件夹,选择 Add C++ to Module

image-20230921193649062

然后选中我们之前编写的 Android.mk 文件即可完成链接

image-20230921193805084

随后我们来到声明 jni 层函数的位置,鼠标悬浮在爆红的函数,点击 Create JNI function for

image-20230922173927187

Android studio 便自动为我们完成了函数的定义工作

image-20230921194130893

# 数据类型

# 基本数据类型

Java 类型 JNI 数据类型 位数
boolean jboolean unsigned 8 bits
byte jbyte signed 8 bits
char jchar unsigned 16 bits
short jshort signed 16 bits
int jint signed 32 bits
long jlong signed 64 bits
float jfloat 32 bits
double jdouble 64 bits

# 引用数据类型

Java 类型 JNI 数据类型
void void
java.lang.Object jobject
java.lang.Class jclass
java.lang.String jstring
java.lang.Throwable jthrowable
Object[] jobjectArray
boolean[] jbooleanArray
byte[] jbyteArray
char[] jcharArray
short[] jshortArray
int[] jintArray
long[] jlongArray
float[] jfloatArray
double[] jdoubleArray

# jvalue 类型

typedef union jvalue {
    jboolean    z;
    jbyte       b;
    jchar       c;
    jshort      s;
    jint        i;
    jlong       j;
    jfloat      f;
    jdouble     d;
    jobject     l;
} jvalue;

# 类型描述符

看起来和 smali 的类型声明一模一样

Java 类型 类型描述符
boolean Z
short S
float F
byte B
int I
double D
char C
long J
void V
引用类型 L + 全限定名 + ;
数组 [+ 类型描述符
方法 (参数的类型描述符) 返回值的类型描述符
  • 表示一个 string 类

    L + 全限定名 ,其中 . 换成 / , 最后加上 ;

    java 类型 类型描述符
    java.lang.String Ljava/lang/String;
  • 表示数组

    java 类型 类型描述符
    String[] [Ljava/lang/String;
    int[][] [[I
  • 表示方法

    java 类型 类型描述符
    long f (int n,String s,int arr[]); (ILjava/lang/String;[I) J
    void f (); () V

# 其他常用类型

typedef jint            jsize;
struct _jfieldID;                       /* opaque structure */
typedef struct _jfieldID* jfieldID;     /* field IDs */
struct _jmethodID;                      /* opaque structure */
typedef struct _jmethodID* jmethodID;   /* method IDs */
#define JNI_FALSE   0
#define JNI_TRUE    1
#define JNI_VERSION_1_1 0x00010001
#define JNI_VERSION_1_2 0x00010002
#define JNI_VERSION_1_4 0x00010004
#define JNI_VERSION_1_6 0x00010006
#define JNI_OK          (0)         /* no error */
#define JNI_ERR         (-1)        /* generic error */
#define JNI_EDETACHED   (-2)        /* thread detached from the VM */
#define JNI_EVERSION    (-3)        /* JNI version error */
#define JNI_COMMIT      1           /* copy content, do not free buffer */
#define JNI_ABORT       2           /* free buffer w/o copying back */

# JavaVM

# 定义

javaVM 是 java 虚拟机在 jni 层的代表,在 Android 上, 一个进程只有一个 JavaVM,所有的线程共用一个 JavaVM, 也就是在 Android 进程中是通过有且只有一个虚拟机对象来服务所有 Java 和 C/C++ 代码。

image-20230923141210982

# Invocation API

Invocation API 允许软件提供商在原生程序中内嵌 Java 虚拟机。因此可以不需要链接任何 Java 虚拟机代码来提供 Java-enabled 的应用程序。

# DestoryJavaVM

卸载一个 Java 虚拟机,并收回它拥有的资源。

/*
@param vm: 需要被销毁的虚拟机。
@return: 成功返回 JNI_OK ,失败返回负数。
*/
jint DestroyJavaVM(JavaVM *vm);
  • JDK/JRE 1.1 还没有完全支持这个函数。在 JDK/JRE 1.1 只有主线程才允许调用该函数。
  • 从 JDK/JRE 1.2 开始,任何线程,不管是否已经 attached,都可以调用该函数,如果当前线程已经 attached,则虚拟机会等待当前线程作为唯一的非守护用户线程。
  • 如果当前线程没有 attached,则先 attached,再等待当前线程作为唯一的非守护用户线程。
  • JDK/JRE 1.1.2 不支持 unload 虚拟机。

# AttachCurrentThread

附加当前线程到 JavaVM , 并返回 JNIEnv

/*
@param vm: 需要被 attach 到的虚拟机。
@param p_env: 返回的当前线程的 JNI 接口指针。
@param thr_args: JavaVMAttachArgs 结构体来指定附加信息,或传入 NULL
@return: 成功返回 JNI_OK ,失败返回负数。
*/
jint AttachCurrentThread(JavaVM *vm, void **p_env, void *thr_args);
thr_args

thr_args 的结构体如下

typedef struct JavaVMAttachArgs {
    jint version;  /* must be at least JNI_VERSION_1_2 */
    char *name;    /* the name of the thread as a modified UTF-8 string, or NULL */
    jobject group; /* global ref of a ThreadGroup object, or NULL */
} JavaVMAttachArgs
  • 尝试 attach 已经 attached 过的线程不会执行任何操作(no-op)。
  • 一个本地线程不能同时 attach 到两个不同的 Java 虚拟机。
  • 当前一个线程 attach 到虚拟机,它的上下文 ClassLoader 是 Bootstrap ClassLoader。

# AttachCurrentThreadAsDaemon

和 AttachCurrentThread 类似,只是新创建的 java.lang.Thread 被设置为守护线程(daemon)

/*
@param vm: 需要被 attach 到的虚拟机。
@param p_env: 返回的当前线程的 JNI 接口指针。
@param thr_args: JavaVMAttachArgs 结构体来指定附加信息,或传入 NULL
@return: 成功返回 JNI_OK ,失败返回负数。
*/
jint AttachCurrentThreadAsDaemon(JavaVM* vm, void** p_env, void* args);

# DetachCurrentThread

从 java 虚拟机 detach 当前线程。所有这个线程持有的 Java 监视区 (monitor) 都会被释放。

jint DetachCurrentThread(JavaVM *vm);
  • 从 JDK/JRE 1.2 开始,主线程可以从虚拟机 detach。

# GetEnv

获取当前线程的 JNI 接口指针 JNIEnv

/*
@param vm: 当前的 JavaVM 虚拟机
@param env: 存储返回的当前线程的 JNI 接口指针。
@param version: JNI 版本。
@return: 
	如果当前线程还没有 attach 到虚拟机,则设置 *env 为 NULL ,并返回 JNI_EDETACHED。
	如果指定的 JNI 版本不被支持,则设置 *env 为 NULL ,并且返回 JNI_EVERSION。
	否则设置 *env 为正常的接口,并返回 JNI_OK 
*/
jint GetEnv(JavaVM *vm, void **env, jint version);

# JavaVM 虚拟机加载流程

# 创建虚拟机

JNI_CreateJavaVM() 函数载入和初始化一个 Java 虚拟机。调用该函数的线程被视为是主线程(main thread)。

#inlcude <jni.h>
/*
@param p_vm: 指向 JavaVM * 的指针,函数成功返回时会给 JavaVM * 指针赋值。
@param p_env: 指向 JNIEnv * 的指针,函数成功返回时会给 JNIEnv * 指针赋值。
@param vm_args: 指向 JavaVMInitArgs 的指针,是初始化虚拟机的参数。
@return: 如果函数执行成功,返回 JNI_OK (值为 0),如果失败返回负值。
*/
jint JNI_CreateJavaVM(JavaVM **p_vm, void **p_env, void *vm_args);
vm_args

第 3 个参数 vm_args 的结构体为:

typedef struct JavaVMInitArgs {
    jint version;
    jint nOptions;
    JavaVMOption *options;
    jboolean ignoreUnrecognized;
} JavaVMInitArgs;
  • version 必须大于等于 JNI_VERSION_1_2 ,

  • nOptionsoptions 的数量.

    • options 的结构体为:

      typedef struct JavaVMOption {
          char *optionString;  /* the option as a string in the default platform encoding */
          void *extraInfo;
      } JavaVMOption;
  • ignoreUnrecognized 设置为 JNI_TRUE ,则会忽视所有不被识别的以 -X_ 开头的参数字符串,如果设置为 JNI_FALSE ,则遇到不被识别的参数时 JNI_CreateJavaVM 函数会返回 JNI_ERR

    所有虚拟机的实现都支持它自己的非标准参数。非标准参数必须以 -X_ 开头。例如,JDK/JRE 支持 -Xms-Xmx 参数来允许开发者指定初始化和最大的 heap 大小。

  • 在 JDK/JRE 1.2,不允许在同一个进程创建多个 Java 虚拟机。

使用示例

JavaVMInitArgs vm_args;
JavaVMOption options[4];
options[0].optionString = "-Djava.compiler=NONE";           /* disable JIT */
options[1].optionString = "-Djava.class.path=c:\myclasses"; /* user classes */
options[2].optionString = "-Djava.library.path=c:\mylibs";  /* set native library path */
options[3].optionString = "-verbose:jni";                   /* print JNI-related messages */
vm_args.version = JNI_VERSION_1_2;
vm_args.options = options;
vm_args.nOptions = 4;
vm_args.ignoreUnrecognized = TRUE;
/* Note that in the JDK/JRE, there is no longer any need to call
 * JNI_GetDefaultJavaVMInitArgs.
 */
res = JNI_CreateJavaVM(&vm, (void **)&env, &vm_args);
if (res < 0) ...

# 线程附加到虚拟机

JNI 接口指针 (JNIEnv) 只在当前线程有效,如果需要在另一个线程访问 Java 虚拟机,必须先调用 AttachCurrentThread() 来将自己 attach 到虚拟机来获得 JNI 接口指针 JNIEnv

线程必须有足够的栈空间来执行一定的工作。每个线程分配多少栈空间根据系统而不同。

# 脱离虚拟机

一个 attach 到虚拟机的本地线程必须在退出前调用 DetachCurrentThread() 来和虚拟机脱离。如果还有 Java 方法在 call stack 中,则这个线程不能被 detach。

# 卸载虚拟机

使用 JNI_DestroyJavaVM() 函数来卸载一个 Java 虚拟机

虚拟机会等待(阻塞),直到当前线程成为唯一的非守护进程的用户进程,才真正执行卸载操作。

用户进程 (user thread) 包括:

  • Java 线程 (java threads)
  • attached 到虚拟机的本地线程 (attached native threads)

为什么要做这样的限制(强制等待),是因为 Java 线程和 native 线程可能会 hold 住系统资源,例如锁,窗口等资源,而虚拟机不能自动释放这些资源。通过限制当前线程是唯一的运行中的用户线程才 unload 虚拟机,则将释放这种系统资源的任务交给程序员自己来负责了。

# 获取 JavaVM 接口

  • 在 Java VM 虚拟机加载动态链接库时,可以在 JNI_OnLoad 的参数中获取到 JavaVM
JavaVM *global_jvm;
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
    global_jvm = vm;
}
  • 通过 JNIEnv 获取 JavaVM

    JavaVM *gJavaVM;
    jobject gJavaObj;
    JNIEXPORT void JNICALL Java_com_xxx_android2native_JniManager_openJni
        (JNIEnv * env, jobject object)
    {
        // 线程不允许共用 env 环境变量,但是 JavaVM 指针是整个 jvm 共用的,所以可以通过下面的方法保存 JavaVM 指针,在线程中使用
        env->GetJavaVM(&gJavaVM);
        // 同理,jobject 变量也不允许在线程中共用,因此需要创建全局的 jobject 对象在线程中访问该对象
        gJavaObj = env->NewGlobalRef(object);
    }

# JNIEnv

# 定义

JNIEnv 是提供 JNI Native 函数的基础环境,线程相关,不同线程的 JNIEnv 相互独立,并且 JNIEnv 是一个 JNI 接口指针,指向了本地方法的一个函数表,该函数表中的每一个成员指向了一个 JNI 函数,本地方法通过 JNI 函数来访问 JVM 中的数据结构.

image-20230923222945135

# JNI 函数

# 版本信息

# GetVersion

获取 JNI 版本号

/*
@param env: JNI interface 指针
@return: 返回一个十六进制整数,其中高 16 位表示主版本号,低 16 位标识表示次版本号,如:1.2, GetVersion () 返回 0x00010002, 1.4, GetVersion () returns 0x00010004.
*/
jint GetVersion(JNIEnv *env);

后面再出现 JNIEnv *env 这样的参数不再注释

# 类操作

# DefineClass

从原始类数据的缓冲区中加载类。

/*
@param loader: 分派给所定义的类的类加载器
@param buf: 包含 .class 文件数据的缓冲区 
@param buflen: 缓冲区长度
@return: 返回 Java 类对象。如果出错则返回 NULL。
@throw:
		ClassFormatError  如果类数据指定的类无效
		ClassCircularityError  如果类或接口是自身的超类或超接口
		OutOfMemoryError  如果系统内存不足
*/
jclass DefineClass (JNIEnv *env, jobject loader, const jbyte *buf , jsize bufLen);

# FindClass

该函数用于加载 Java 类。它将在 CLASSPATH 环境变量所指定的目录和 zip 文件里搜索指定的类名。

/*
@param name: 类全名 = (包名 +‘/’+ 类名).replace ('.', '/');
@return: 类对象全名;如果找不到该类,则返回 NULL。
@throw:  
		ClassFormatError      如果类数据指定的类无效
		ClassCircularityError   如果类或接口是自身的超类或超接口
		NoClassDefFoundError    如果找不到所请求的类或接口的定义
		OutOfMemoryError        如果系统内存不足
*/
jclass FindClass(JNIEnv *env, const char *name);

# GetObjectClass

通过对象获取这个类。该函数比较简单,唯一注意的是对象不能为 NULL,否则获取的 class 肯定返回也为 NULL。

/*
@param obj: Java 类对象实例
*/    
jclass GetObjectClass (JNIEnv *env, jobject obj);

# GetSuperclass

获取父类或者说超类

/*
@param clazz: Java 类对象
@return: 如果 clazz 代表一般类而非 Object 类,则该函数返回由 clazz 所指定的类的超类。 如果 clazz 指向 Object 类或代表某个接口,则该函数返回 NULL。
*/  
jclass GetSuperclass (JNIEnv *env, jclass clazz);

# IsAssignableFrom

确定 clazz1 的对象是否可安全地强制转换为 clazz2

/*
@param clazz1: 源类对象
@param clazz2: 目标类对象
@return: 以下三种情况返回 JNI_TRUE, 否则返回 JNI_FALSE
	1. 第一及第二个类参数引用同一个 Java 类
	2. 第一个类是第二个类的子类
	3. 第二个类是第一个类的某个接口
*/   
jboolean IsAssignableFrom (JNIEnv *env, jclass clazz1,  jclass clazz2);

# 异常操作

# Throw

抛出 java.lang.Throwable 对象

/*
 @param obj: java.lang.Throwable 对象
 @return: 成功时返回 0,失败时返回负数
 @throw: java.lang.Throwable 对象 obj
*/
jint  Throw(JNIEnv *env, jthrowable obj);

# ThrowNew

利用指定类的消息(由 message 指定)构造异常对象并抛出该异常

/*
@param clazz: java.lang.Throwable 的子类
@param message: 用于构造 java.lang.Throwable 对象的消息
@return: 成功时返回 0,失败时返回负数
@throw: 新构造的 java.lang.Throwable 对象
*/    
jint ThrowNew (JNIEnv *env ,  jclass clazz,  const char *message);

# ExceptionOccurred

确定某个异常是否正被抛出。在本地代码调用 ExceptionClear () 或 Java 代码处理该异常前,异常将始终保持抛出状态。

/*
@return: 返回正被抛出的异常对象,如果当前无异常被抛出,则返回 NULL
*/    
jthrowable ExceptionOccurred (JNIEnv *env);

# ExceptionDescribe

将异常及堆栈的回溯输出到标准输出(例如 stderr)。该例程可便利调试操作。

void ExceptionDescribe (JNIEnv *env);

# ExceptionClear

清除当前抛出的任何异常。如果当前无异常,则不产生任何效果。

void ExceptionClear (JNIEnv *env);

# FatalError

抛出致命错误并且不希望虚拟机进行修复。该函数无返回值

/*
@param msg: 错误消息
*/    
void FatalError (JNIEnv *env, const char *msg);

# 全局及局部引用

# DeleteWeakGlobalRef

删除弱全局引用

void DeleteWeakGlobalRef(JNIEnv *env, jweak obj);

# NewWeakGlobalRef

用 obj 创建新的弱全局引用

/*
@param obj: 全局或局部引用
@return: 返回弱全局引用,弱 obj 指向 null,或者内存不足时返回 NULL,同时抛出异常
*/    
jweak NewWeakGlobalRef(JNIEnv *env, jobject obj);

# DeleteLocalRef

删除 localRef 所指向的局部引用

/*
@param localRef: 局部引用
*/    
void  DeleteLocalRef (JNIEnv *env, jobject localRef);

# NewLocalRef

创建 obj 参数所引用对象的局部引用,创建的引用要通过调用 DeleteLocalRef () 来显式删除

/*
@param obj: 全局或局部引用
@return: 返回局部引用,如果系统内存不足则返回 NULL
*/    
jobject NewLocalRef(JNIEnv *env, jobject ref);

# DeleteGlobalRef

删除 globalRef 所指向的全局引用

/*
@param globalRef: 全局引用
*/    
void DeleteGlobalRef (JNIEnv *env, jobject globalRef);

# NewGlobalRef

创建 obj 参数所引用对象的新全局引用,创建的引用要通过调用 DeleteGlobalRef () 来显式撤消

/*
@param obj: 全局或局部引用
@return: 返回全局引用,如果系统内存不足则返回 NULL
*/
object NewGlobalRef (JNIEnv *env, jobject obj);

# 对象操作

# IsSameObject

测试两个引用是否引用同一 Java 对象

/*
@param ref1: java 对象
@param ref2: java 对象
@return: 如果 ref1 和 ref2 引用同一 Java 对象或均为 NULL,则返回 JNI_TRUE。否则返回 JNI_FALSE
*/    
jboolean IsSameObject (JNIEnv *env, jobject ref1, jobject ref2);

# IsInstanceOf

测试对象是否为某个类的实例

/*
@param obj: Java 对象
@param clazz: Java 类对象
@return: 如果可将 obj 强制转换为 clazz,则返回 JNI_TRUE。否则返回 JNI_FALSE。NULL 对象可强制转换为任何类
*/    
jboolean IsInstanceOf (JNIEnv *env, jobject obj, jclass clazz);

# GetObjectClass

返回对象的类

/*
@param obj: Java 对象(不能为 NULL)
@return: Java 类对象
*/    
jclass GetObjectClass (JNIEnv *env, jobject obj);

# NewObject

构造新 Java 对象。方法 methodId 指向应调用的构造函数方法。注意:该 ID 特指该类 class 的构造函数 ID,必须通过调用 GetMethodID () 获得,且调用时的方法名必须为 <init> ,而返回类型必须为 void (V),clazz 参数务必不要引用数组类。

/*
@return: 返回 Java 对象,如果无法构造该对象,则返回 NULL
@throw: InstantiationException  如果该类为接口或抽象类
        OutOfMemoryError   如果系统内存不足
*/
jobject NewObject (JNIEnv *env ,  jclass clazz,  jmethodID methodID, ...);   // 参数附加在函数后面
jobject NewObjectA (JNIEnv *env , jclassclazz,  jmethodID methodID, jvalue *args);    // 参数以指针形式附加 
jobjec tNewObjectV (JNIEnv *env , jclassclazz,  jmethodID methodID, va_list args);    // 参数以 "链表" 形式附加

# AllocObject

分配新 Java 对象而不调用该对象的任何构造函数,返回该对象的引用;clazz 参数务必不要引用数组类。

/*
@param clazz: Java 类对象
@return: 返回 Java 对象;如果无法构造该对象,则返回 NULL
@throw: InstantiationException:如果该类为一个接口或抽象类
        OutOfMemoryError:如果系统内存不足
*/
jobject AllocObject (JNIEnv *env, jclass clazz);

# 字符串操作

# Get/ReleaseStringCritical

这两个函数的语义与 Get/ReleaseStringChars 函数类似,但 VM 会尽量返回一个指针。但是使用这一对函数时必须有严格限制:在这对函数调用之间绝对不能调用其他 JNI 方法,否则将导致当前线程阻塞。

const jchar * GetStringCritical(JNIEnv *env, jstring string, jboolean *isCopy);
void ReleaseStringCritical(JNIEnv *env, jstring string, const jchar *carray);

# GetStringUTFRegion

将 str 偏移位置 start 开始的 len 长度 unicode 字符转换为 C char 字符,并放在 buf 中

void GetStringUTFRegion(JNIEnv *env, jstring str, jsize start, jsize len, char *buf);

# GetStringRegion

从 str 的偏移位置 start 开始,复制 len 长度的 unicode 字符到 buf 中

void GetStringRegion(JNIEnv *env, jstring str, jsize start, jsize len, jchar *buf);

# ReleaseStringUTFChars

通知本地代码不要再访问 utf。utf 参数是一个指针,可利用 GetStringUTFChars () 获得

/*
@param string: Java 字符串对象
@param utf: 指向 UTF-8 字符串的指针
*/    
void  ReleaseStringUTFChars (JNIEnv *env, jstring string,  const char *utf);

# GetStringUTFChars

返回指向字符串的 UTF-8 字符数组的指针。该数组在被 ReleaseStringUTFChars () 释放前将一直有效。 如果 isCopy 不是 NULL,*isCopy 在复制完成后即被设为 JNI_TRUE。如果未复制,则设为 JNI_FALSE。

/*
@param string: Java 字符串对象
@param isCopy: 指向布尔值的指针
@return: 指向 UTF-8 字符串的指针。如果操作失败,则为 NULL
*/    
const char* GetStringUTFChars (JNIEnv*env, jstring string, jboolean *isCopy);

# GetStringUTFLength

以字节为单位返回字符串的 UTF-8 长度

/*
@param string: Java 字符串对象
@return: 返回字符串的长度
*/    
jsize  GetStringUTFLength (JNIEnv *env, jstring string);

# NewStringUTF

利用 UTF-8 字符数组构造新 java.lang.String 对象

/*
@param bytes: 指向 UTF-8 字符串的指针
@return: Java 字符串对象。如果无法构造该字符串,则为 NULL
@throw: OutOfMemoryError 如果系统内存不足
*/    
jstring  NewStringUTF (JNIEnv *env, const char *bytes);

# ReleaseStringChars

通知本地代码不要再访问 chars。参数 chars 是一个指针,可通过 GetStringChars () 从 string 获得

/*
@param chars: 指向 Unicode 字符串的指针
*/    
void ReleaseStringChars(JNIEnv *env, jstring string, const jchar *chars);

# GetStringChars

返回指向字符串的 Unicode 字符数组的指针。该指针在调用 ReleaseStringchars () 前一直有效。如果 isCopy 非空,则在复制完成后将 *isCopy 设为 JNI_TRUE。如果没有复制,则设为 JNI_FALSE

/*
@param string: Java 字符串对象
@param isCopy: 指向布尔值的指针
@return: 指向 Unicode 字符串的指针,如果操作失败,则返回 NULL
*/
const jchar * GetStringChars(JNIEnv*env, jstring string, jboolean *isCopy);

# GetStringLength

返回 Java 字符串的长度(Unicode 字符数)

/*
@param string: Java 字符串对象
@return: Java 字符串的长度
*/
jsize  GetStringLength (JNIEnv *env, jstring string);

# NewString

利用 Unicode 字符数组构造新的 java.lang.String 对象

/*
@param unicodeChars: 指向 Unicode 字符串的指针
@param len: Unicode 字符串的长度
@return Java 字符串对象。如果无法构造该字符串,则为 NULL.
@throw OutOfMemoryError:如果系统内存不足
*/
jstring  NewString (JNIEnv *env, const jchar *unicodeChars, jsize len);

# 数组操作

# SetObjectArrayElement

设置 Object 数组的元素

/*
@param array: Java 数组
@param index: 元素索引
@param value: 新的对象
@throw: ArrayIndexOutOfBoundsException 如果 index 不是数组中的有效下标
       ArrayStoreException 如果 value 的类不是数组元素类的子类
*/   
void  SetObjectArrayElement (JNIEnv *env, jobjectArray array,  jsize index, jobject value);

# GetObjectArrayElement

返回 Object 数组的元素

/*
@param array: Java 数组
@param index: 元素索引
@return: Java 对象
@throw: ArrayIndexOutOfBoundsException 如果 index 不是数组中的有效下标
*/    
jobject GetObjectArrayElement (JNIEnv *env, jobjectArray array, jsize index);

# NewObjectArray

构造新的数组,它将保存类 elementClass 中的对象。所有元素初始值均设为 initialElement

/*
@param length: 数组大小
@param elementClass: 数组元素类对象
@return: initialElement 初始值,可以为 NULL
@throw: OutOfMemoryError 如果系统内存不足
*/
jarray NewObjectArray(JNIEnv *env, jsize length, jclass elementClass, jobject initialElement);

# GetArrayLength

返回数组中的元素数

/*
@param array: Java 数组对象
@return: 数组的长度
*/
jsize GetArrayLength (JNIEnv *env, jarray array);

# New[PrimitiveType]Array Routines

用于构造基本类型数组对象

/*
@param length: 要构造的数组的长度
@return Java 数组。如果无法构造该数组,则为 NULL
*/
ArrayType New[PrimitiveType]Array(JNIEnv *env, jsize length);

下表说明了特定的基本类型数组构造函数。用户应把 New [PrimitiveType] Array 替换为某个实际的基本类型数组构造函数例程名,然后将 ArrayType 替换为该例程相应的数组类型:

New[PrimitiveType]Array ArrayType
NewBooleanArray() jbooleanarray
NewByteArray() jbytearray
NewCharArray() jchararray
NewShortArray() jshortarray
NewIntArray() jintarray
NewLongArray() jlongarray
NewFloatArray() jfloatarray
NewDoubleArray() jdoublearray

# Get[PrimitiveType]ArrayElements

一组返回基本类型数组体的函数。结果在调用相应的 Release [PrimitiveType] ArrayElements () 函数前将一直有效。由于返回的数组可能是 Java 数组的副本,因此对返回数组的更改不必在基本类型数组中反映出来,直到调用了 Release [PrimitiveType] ArrayElements ()。

/*
@param array: Java 对象数组
@param isCopy: 如果 isCopy 不是 NULL,*isCopy 在复制完成后即被设为 JNI_TRUE; 如果未复制,则设为 JNI_FALSE
@return: 返回指向数组的指针,如果操作失败,则为 NULL
*/
NativeType * Get[PrimitiveType]ArrayElements (JNIEnv *env, ArrayType array, jboolean *isCopy);
Get[PrimitiveType]ArrayElements NativeType ArrayType
GetBooleanArrayElements() jboolean jbooleanArray
GetByteArrayElements() jbyte jbyteArray
GetCharArrayElements() jchar jcharArray
GetShortArrayElements() jshort jshortArray
GetIntArrayElements() jint jintArray
GetLongArrayElements() jlong jlongArray
GetFloatArrayElements() jfloat jfloatArray
GetDoubleArrayElements() jdouble jdoubleArray

# Release[PrimitiveType]ArrayElements

释放 elems,通知本地代码不要再访问 elems

/*
@param array: Java 数组对象
@param elems: 参数是一个通过使用对应的 Get [PrimitiveType] ArrayElements () 函数由 array 导出的指针。
@param mode: 释放模式,mode 参数将提供有关如何释放数组缓冲区的信息。
如果 elems 不是 array 中数组元素的副本,mode 将无效;否则,mode 将具有下表所述的功能:
	0               复制回内容并释放 elems 缓冲区
	JNI_COMMIT      复制回内容但不释放 elems 缓冲区
	JNI_ABORT       释放缓冲区但不复制回变化
*/    
void  Release[PrimitiveType]ArrayElements (JNIEnv *env, ArrayType array, NativeType *elems, jint mode);

Release [PrimitiveType] ArrayElements 惯用法里的类型参数与 Get [PrimitiveType] ArrayElements 对应,不再列出

# Get[PrimitiveType]ArrayRegion

将基本类型数组某一区域复制到缓冲区中的一组函数,使用时替换 PrimitiveType, ArrayType,和 NativeType,如 GetBooleanArrayRegion () ,jbooleanArray 和 jboolean

/*
@param array: Java 数组 
@param start: 起始位置
@param len:   要复制的长度
@param buf:   目标缓冲区
@throw: ArrayIndexOutOfBoundsException 如果区域中的某个下标无效
*/
void  Get[PrimitiveType]ArrayRegion (JNIEnv *env, ArrayType array, jsize start, jsize len, NativeType *buf);

# Set[PrimitiveType]ArrayRegion

将基本类型数组的某一区域从缓冲区中复制回来的一组函数,使用时替换 PrimitiveType, ArrayType,和 NativeType,如 SetBooleanArrayRegion () ,jbooleanArray 和 jboolean

/*
@param array: Java 数组
@param start: 起始位置
@param len:   写回的长度
@param buf:   源缓冲区
@throw: ArrayIndexOutOfBoundsException:如果区域中的某个下标无效
*/    
void  Set[PrimitiveType]ArrayRegion (JNIEnv *env, ArrayType array, jsize start, jsize len, NativeType *buf);

# GetPrimitiveArrayCritical 与 ReleasePrimitiveArrayCritical

作用同 Get/Release [PrimitiveType] ArrayElements 相同,但是 VM 尽可能返回原 java 数组的指针,否则返回一份拷贝。

这两组调用之间不能调用其他 JNI 函数或进行其他系统调用,否则会导致线程阻塞。

void * GetPrimitiveArrayCritical(JNIEnv *env, jarray array, jboolean *isCopy);
void ReleasePrimitiveArrayCritical(JNIEnv *env, jarray array, void *carray, jint mode);

# 访问对象的属性和方法

# GetStaticMethodID

获取类对象的静态方法 ID

jfieldID  GetStaticMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig);

# GetFieldID

返回 Java 类(非静态)域的属性 ID。该域由其名称及签名指定。访问器函数的 Get [type] Field 及 Set [type] Field 系列使用域 ID 检索对象域。GetFieldID () 不能用于获取数组的长度域。应使用 GetArrayLength ()。

/*
@param clazz: Java 类对象
@param name:  该属性的 Name 名称
@param sig:   该属性的域签名
@return: 属性 ID 对象。如果操作失败,则返回 NULL
@throw: NoSuchFieldError  如果找不到指定的域
        ExceptionInInitializerError 如果由于异常而导致类初始化程序失败
        OutOfMemoryError 如果系统内存不足
*/
jfieldID  GetFieldID (JNIEnv *env, jclass clazz, const char *name, const char *sig);

# GetMethodID

返回类或接口实例(非静态)方法的方法 ID。方法可在某个 clazz 的超类中定义,也可从 clazz 继承。该方法由其名称和签名决定。 GetMethodID () 可使未初始化的类初始化。要获得构造函数的方法 ID,应将 <init> 作为方法名,同时将 void (V) 作为返回类型

/*
@param clazz: Java 类对象
@param name:  该方法的 Name 名称
@param sig:   该方法参数和返回值域签名     *
@return: 方法 ID,如果找不到指定的方法,则为 NULL
@throw: NoSuchMethodError 如果找不到指定方法
        ExceptionInInitializerError 如果由于异常而导致类初始化程序失败
        OutOfMemoryError 如果系统内存不足
*/
jmethodID GetMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig);

# GetStaticFieldID

获取类的静态域 ID 方法

jfieldID  GetStaticFieldID (JNIEnv *env,jclass clazz, const char *name, const char *sig);

# Get[type]Field

该例程系列返回对象的实例(非静态)域的值。要访问的域由通过调用 GetFieldID () 而得到的域 ID 指定。

/*
@param obj: Java 对象(不能为 NULL)
@param fieldID: 有效的域 ID
@return:  属性的内容
*/
NativeType Get[type]Field (JNIEnv*env, jobject obj, jfieldID fieldID);  
// 获取类对象静态域的值
NativeType GetStatic[type]Field (JNIEnv*env, jclass classzz, jfieldID fieldID);
Get[type]Field NativeType
GetObjectField() jobject
GetBooleanField() jboolean
GetByteField() jbyte
GetCharField() jchar
GetShortField() jshort
GetIntField() jint
GetLongField() jlong
GetFloatField() jfloat
GetDoubleField() jdouble

# Set[type]Field

该惯用法设置对象的实例(非静态)属性的值。要访问的属性由通过调用 SetFieldID () 而得到的属性 ID 指定。

/*
@param obj: Java 对象(不能为 NULL)
@param fieldId: 有效的域 ID
@param value: 域的新值
*/
void  Set[type]Field (JNIEnv *env, jobject obj, jfieldID fieldID, NativeType value);
// 设置类的静态域的值
void  SetStatic[type]Field (JNIEnv *env, jclass classzz, jfieldID fieldID, NativeType value);
Set[type]Field NativeType
SetObjectField() jobject
SetBooleanField() jboolean
SetByteField() jbyte
SetCharField() jchar
SetShortField() jshort
SetIntField() jint
SetLongField() jlong
SetFloatField() jfloat
SetDoubleField() jdouble

# Call[type]Method

这三个操作的方法用于从本地方法调用 Java 实例方法。它们的差别仅在于向其所调用的方法传递参数时所用的机制。

这三个操作将根据所指定的 methodID 调用 Java 对象的实例(非静态)方法。参数 methodID 必须通过调用 GetMethodID () 来获得。当这些函数用于调用私有方法和构造函数时,methodID 必须从 obj 的真实类派生而来,而不应从其某个超类派生。当然,附加参数可以为空 。

/*
@param obj Java 对象
@param methodId 方法 ID
*/
NativeType Call[type]Method (JNIEnv *env, jobject obj, jmethodID methodID, ...);     // 参数附加在函数后面,
NativeType Call[type]MethodA (JNIEnv *env, jobject obj, jmethodID methodID, jvalue *args);  // 参数以指针形式附加
NativeType Call[type]MethodV (JNIEnv *env, jobject obj,jmethodID methodID, va_list args); // 参数以 "链表" 形式附加
Call[type]Method<A/V> NativeType
CallVoidMethod()
CallObjectMethod() jobect
CallBooleanMethod () jboolean
CallByteMethod() jbyte
CallCharMethod() jchar
CallShortMethod() jshort
CallIntMethod() jint
CallLongMethod() jlong
CallFloatMethod() jfloat
CallDoubleMethod() jdouble

# 注册本地方法

# RegisterNatives

向 clazz 参数指定的类注册本地方法。

/**
@param clazz: 目标类对象
@param methods: JNINativeMethod 结构数组,其中包含本地方法的名称、签名和函数指针
@param nMethods: methods 参数的长度
@return: 成功时返回 0;失败时返回负数
@throw:  NoSuchMethodError 如果找不到指定的方法或方法不是本地方法
*/
jint RegisterNatives(JNIEnv *env, jclass clazz, const JNINativeMethod *methods, jint nMethods);
methods

JNINativeMethod 定义如下:

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

# UnregisterNatives

反注册类的本地方法。类将返回到链接或注册了本地方法函数前的状态。该函数不应在本地代码中使用。相反,它可以为某些程序提供一种重新加载和重新链接本地库的途径。

/*
@param clazz: Java 类对象
@throw: 成功时返回 0;失败时返回负数
*/
jint UnregisterNatives (JNIEnv *env, jclass clazz);

# jobject thiz

当我们在 native 中注册一个 java 函数后,一般可以看到函数的声明都像下面这个样子

Java_com_oacia_loadso_MainActivity_hello(JNIEnv *env, jobject thiz) {
    // TODO: implement hello()
}

那么函数的第二个参数 jobject thiz 有什么作用呢?

我们不妨看一看 android studio 给我们的代码提示

image-20240710102719540

这个 thiz 指向的是 native 函数声明所在的类

demo 如下,在 MainActivity 中的 onCreate 会对变量 s 赋值

package com.oacia.loadso;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
public class MainActivity extends AppCompatActivity {
    static
    {
        try
        {
            System.loadLibrary("oacia");
        }
        catch (Exception ignored)
        {
        }
    }
    String TAG = "oacia_tag";
    public native void hello();
    public String s = "1234";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        s="change in create";
        hello();
    }
}

在 JNI 中会通过 jobject thiz 读取变量 s

#include <jni.h>
#include "stdio.h"
extern "C"{
#include <android/log.h>
#define TAG "oacia_tag" // 这个是自定义的 LOG 的标识
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,TAG ,__VA_ARGS__) // 定义 LOGD 类型
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,TAG ,__VA_ARGS__) // 定义 LOGI 类型
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN,TAG ,__VA_ARGS__) // 定义 LOGW 类型
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,TAG ,__VA_ARGS__) // 定义 LOGE 类型
#define LOGF(...) __android_log_print(ANDROID_LOG_FATAL,TAG ,__VA_ARGS__) // 定义 LOGF 类型
jint JNI_OnLoad(JavaVM* vm, void* reserved __unused)
{
    return JNI_VERSION_1_6;
}
JNIEXPORT void JNICALL
Java_com_oacia_loadso_MainActivity_hello(JNIEnv *env, jobject thiz) {
    // TODO: implement hello()
    jclass clazz = env->GetObjectClass(thiz);
    jfieldID s_field = env->GetFieldID(clazz,"s","Ljava/lang/String;");
    jstring str = (jstring)env->GetObjectField(thiz,s_field);
    LOGD("%s",env->GetStringUTFChars(str, nullptr));
}
}

最终也是成功读取并打印出了经过修改后的变量 s 的值

image-20240710130611981

# 动态注册

动态注册都是调用 RegisterNatives 来实现的 JNINativeMethod 内数组的三个参数分别代表 函数名称函数签名主体函数

jint RegisterNatives(JNIEnv *env) {
    jclass clazz = env->FindClass("com/oacia/loadso/MainActivity");
    if (clazz == NULL) {
        LOGE("con't find class: com/oacia/loadso/MainActivity");
        return JNI_ERR;
    }
    JNINativeMethod methods_MainActivity[] = {
            {"stringFromJNI", "()Ljava/lang/String;", (void *) stringFromJNI},
            {"add",           "(II)I",                (void *) add}
    };
    // int len = sizeof(methods_MainActivity) / sizeof(methods_MainActivity[0]);
    return env->RegisterNatives(clazz, methods_MainActivity,
                                sizeof(methods_MainActivity) / sizeof(methods_MainActivity[0]));
}

# native 层调用 java 层函数

native 调用 java 层的函数只需要四步就可以完成

  1. 获取 class
  2. 获取函数签名
  3. 获取 class 的实例
  4. 调用函数
jclass loadso = env->FindClass("com/oacia/loadso/MainActivity");
jmethodID MethodID = env->GetMethodID(loadso, "logA", "()V");
jobject javaObject = env->AllocObject(loadso);
env->CallVoidMethod(javaObject, MethodID);
更新于 阅读次数