在日常的安卓逆向中,可能会遇到代码在
libxxx.so
中的情况,而这种在so
中的代码的编写就涉及到了安卓的 JNI (Java Native Interface) 开发,俗话说要想会逆向,那么首先得要学会正向,假如都没见过某种语法,那还怎么逆向下去嘞?所以就玩玩 JNI 咯~
# 在 Android studio 中开发 JNI
首先我们需要创建一个 jni 文件夹,我们只需要右键 app, 然后点击 New->Folder->JNI Folder
就可以创建了
接下来从 Android
布局转到 Project
布局,就可以看到新创建的 jni
文件夹
随后创建一个 oacia.c
和 Android.mk
之后我们需要编写 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
然后选中我们之前编写的 Android.mk
文件即可完成链接
随后我们来到声明 jni 层函数的位置,鼠标悬浮在爆红的函数,点击 Create JNI function for
Android studio 便自动为我们完成了函数的定义工作
# 数据类型
# 基本数据类型
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++ 代码。
# 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
, -
nOptions
为options
的数量.-
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 中的数据结构.
# 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 给我们的代码提示
这个 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 的值
# 动态注册
动态注册都是调用 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 层的函数只需要四步就可以完成
- 获取 class
- 获取函数签名
- 获取 class 的实例
- 调用函数
jclass loadso = env->FindClass("com/oacia/loadso/MainActivity"); | |
jmethodID MethodID = env->GetMethodID(loadso, "logA", "()V"); | |
jobject javaObject = env->AllocObject(loadso); | |
env->CallVoidMethod(javaObject, MethodID); |