JNI,即Java Native Interface的缩写,中文为Java本地调用,它连接了Java与Native之间的世界。
鉴于功力尚浅,本文从基本概念、原理与实战三个方面对JNI在安卓系统中的使用进行了粗浅的介绍,如果有兴趣可以自行阅读Jdk文档或者其他大神的资料。
通常我们在JDK源码中看到的方法,如果带有native,则表示这个方法是一个本地方法。Thread类中的几个方法,如java程序员经常使用的Thread.sleep(),就是一个native方法。
package java.lang;
class Thread implements Runnable {
@FastNative
public static native Thread currentThread();
public static native void yield();
@FastNative
private static native void sleep(Object lock, long millis, int nanos)
throws InterruptedException;
}
一、安卓中的JNI
先找到一个安卓系统中的native方法如下,该方法返回Native堆空间的大小:
frameworks/base/core/java/android/os/Debug.java
/**
* Returns the size of the native heap.
* @return The size of the native heap in bytes.
*/
public static native long getNativeHeapSize();
@UnsupportedAppUsage
public static native boolean getMemoryInfo(int pid, MemoryInfo memoryInfo);
想要找到该方法对应的native方法,需要在目录“frameworks/base/core/jni”下找到对应的jni.cpp文件,这里找到“frameworks/base/core/jni/android_os_Debug.cpp”文件即可。
在此文件中搜索“getNativeHeapSize”,可以找到以下代码:
frameworks/base/core/jni/android_os_Debug.cpp
static jlong android_os_Debug_getNativeHeapSize(JNIEnv *env, jobject clazz)
{
struct mallinfo info = mallinfo();
return (jlong) info.usmblks;
}
static const JNINativeMethod gMethods[] = {
{ "getNativeHeapSize", "()J",
(void*) android_os_Debug_getNativeHeapSize },
{ "getNativeHeapAllocatedSize", "()J",
(void*) android_os_Debug_getNativeHeapAllocatedSize },
{ "getNativeHeapFreeSize", "()J",
(void*) android_os_Debug_getNativeHeapFreeSize },
... ...
};
可以看到,“getNativeHeapSize”对应着“android_os_Debug_getNativeHeapSize”,那么在java调用“getNativeHeapSize()”方法,实际上调用到了native中的“android_os_Debug_getNativeHeapSize()”方法。
到这里,对仅仅想要看java层在native源码实现的同学来说,找到对应native函数,然后继续往下看源码即可。
但如果有时间的话,最好还是知其然并知其所以然,有这么些问题:
- Java世界中的“getNativeHeapSize”方法是如何通过gMethods[]这样一个数组就找到“android_os_Debug_getNativeHeapSize”方法的;
- 代码中“jlong”, “jobject”,“"()J"”具体是什么意思。
二、动态注册JNI函数的流程
通过注册jni函数,就能实现在Java层调用到对应的native函数了。
在上面的例子中,“getNativeHeapSize”函数被注册之后,在Java层调用此函数时就能在native层调用到“android_os_Debug_getNativeHeapSize”方法了。
JNI函数的注册分为“静态注册”与“动态注册”,这里介绍例子中函数使用的动态注册。
在“frameworks/base/core/jni/android_os_Debug.cpp”中,有一个动态注册JNI函数的方法“register_android_os_Debug”如下:
frameworks/base/core/jni/android_os_Debug.cpp
//android_os_Debug.cpp中定义了该函数完成JNI的函数注册。
//此函数调用的时机是zygote启动期间,打开Java虚拟机时。
//这里顺便说明一下,新的Java进程创建时由于是复制了zygote进程,会复用此虚拟机,不需要重新打开Java虚拟机。包括在zygote进程启动是预加载的资源以及load的library库都是仅在zygote进程启动时加载一次,为的是加快新进程启动的效率,与减少占用的物理内存空间。“读时共享,写时复制”。
int register_android_os_Debug(JNIEnv *env)
{
jclass clazz = env->FindClass("android/os/Debug$MemoryInfo");
... ...
return jniRegisterNativeMethods(env, "android/os/Debug", gMethods, NELEM(gMethods));
}
frameworks/base/core/jni/AndroidRuntime.cpp
extern int register_android_os_Debug(JNIEnv* env);
static const RegJNIRec gRegJNI[] = {
...
REG_JNI(register_android_os_Debug),
...
}
/*
* Register android native functions with the VM.
* 通过VM虚拟机注册安卓native函数
*/
/*static*/ int AndroidRuntime::startReg(JNIEnv* env)
{
ATRACE_NAME("RegisterAndroidNatives");
androidSetCreateThreadFunc((android_create_thread_fn) javaCreateThreadEtc);
ALOGV("--- registering native functions ---\n");
env->PushLocalFrame(200);
//最终是在这里注册JNI函数
if (register_jni_procs(gRegJNI, NELEM(gRegJNI), env) < 0) {
env->PopLocalFrame(NULL);
return -1;
}
env->PopLocalFrame(NULL);
return 0;
}
//zygote进程启动时,会启动虚拟机。正是启动虚拟机时,调用startReg()注册的JNI函数
void AndroidRuntime::start(const char* className, const Vector<String8>& options, bool zygote)
{
ALOGD(">>>>>> START %s uid %d <<<<<<\n",
className != NULL ? className : "(unknown)", getuid());
... ...
/* 开启虚拟机*/
JniInvocation jni_invocation;
jni_invocation.Init(NULL);
JNIEnv* env;
if (startVm(&mJavaVM, &env, zygote, primary_zygote) != 0) {
ret
/*
* 注册安卓函数
*/
if (startReg(env) < 0) {
ALOGE("Unable to register all android natives\n");
return;
}
... ...
}
我们想要实现自己的JNI函数注册只需要调用“jniRegisterNativeMethods()”,并传入相关参数即可完成注册。
关于注册的时机,只需要在JNI函数被调用之前即可。可以在zygote启动时注册,也可以不依赖安卓系统,自己写一个可执行文件,然后在main函数中完成注册。
虽然我们知道了Java函数如何与JNI函数关联了,但是真正上手实践时还是会有许多问题。比如调用“jniRegisterNativeMethods()”的参数是什么、“JNIEnv”结构体是用来做什么的、“jclass,jstring、jobject”是什么类型、“()J”代表什么意思等诸多问题。
下面我们就以记录,备忘的方式一起简单的看一看。
三、进一步 了解JNI的规则
以下代码中有两个Java的同名重载函数,分别在native中对应不同的函数。
根据Java1号函数的参数列表对应native1号函数的参数列表就可以猜出他们之间的对应关系,但是我们最终需要实现JNI,所以不能光靠猜。
下面先解释一下,“int”与“jint”、“MemoryInfo”与“jobject”的关系,因为它们是一一对应的。
Java层:<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
frameworks/base/core/java/android/os/Debug.java
//1号函数
public static native void getMemoryInfo(MemoryInfo memoryInfo);
//2号函数
@UnsupportedAppUsage
public static native boolean getMemoryInfo(int pid, MemoryInfo memoryInfo);
Native层:<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
frameworks/base/core/jni/android_os_Debug.cpp
static const JNINativeMethod gMethods[] = {
{ "getMemoryInfo", "(Landroid/os/Debug$MemoryInfo;)V",
(void*) android_os_Debug_getDirtyPages },
{ "getMemoryInfo", "(ILandroid/os/Debug$MemoryInfo;)Z",
(void*) android_os_Debug_getDirtyPagesPid },
}
//1号函数,native
static void android_os_Debug_getDirtyPages(JNIEnv *env, jobject clazz, jobject object)
{
android_os_Debug_getDirtyPagesPid(env, clazz, getpid(), object);
}
//2号函数,native
static jboolean android_os_Debug_getDirtyPagesPid(JNIEnv *env, jobject clazz,
jint pid, jobject object)
{
... ...
}
3.1 Java数据类型与NativeJNI数据类型的转换
分为“基本数据类型”与“引用数据类型”两种。Java中对应的类型来到JNI时,都需要使用对应类型代替。
以下截图来自JDK文档https://docs.oracle.com/javase/10/docs/specs/jni/
基本数据类型:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-B5Vg0AHL-1631245822973)(/home/zxs/.config/Typora/typora-user-images/image-20210908145603405.png)]
引用数据类型:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NcwKqYT7-1631245822975)(/home/zxs/.config/Typora/typora-user-images/image-20210908145759655.png)]
3.2 类型签名
"(Landroid/os/Debug$MemoryInfo;)V" 就是一个类型签名,它的作用是在Native的JNI中标识Java函数的参数列表与返回值类型,其中V标识返回值void。
此签名括弧内的部分标识Java参数列表的类型,括弧后的部分标识的是返回值类型。
根据下表可以看出此签名标识的Java函数的参数列表为(int, Memoryinfo),返回值为void。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4YLpG4wF-1631245822976)(/home/zxs/.config/Typora/typora-user-images/image-20210908154926069.png)]
3.3 native方法的参数列表
//1号函数,java
public static native void getMemoryInfo(MemoryInfo memoryInfo);
//1号函数,native
static void android_os_Debug_getDirtyPages(JNIEnv *env, jobject clazz, jobject object)
{
android_os_Debug_getDirtyPagesPid(env, clazz, getpid(), object);
}
1号函数的native代码如上所示,看到其参数列表为“(JNIEnv *env, jobject clazz, jobject object)”。
其中,第一个参数是“JNI 接口指针”,JNI 接口指针的类型为JNIEnv。
JNIEnv是一个和线程相关的,代表JNI环境的结构体。
“JNI 接口指针”是指向指针的指针,这个指针指向一个指针数组,每个指针指向一个接口函数。每个接口函数都位于数组内的预定义偏移量处。下图表示了“JNI 接口指针”的结构:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CaQIWCFG-1631245822977)(/home/zxs/.config/Typora/typora-user-images/image-20210908165957571.png)]
其结构体的定义代码可以在“libnativehelper/include_jni/jni.h”搜索“struct _JNIEnv {”找到。
第二个参数根据native方法是静态还是非静态而有所不同,非静态本机方法的第二个参数是对对象的引用,静态本地方法则是对其 Java 类的引用。
这么理解,如果某个native方法位于Class1.java名为fun,且此方法为静态方法,则用户在java层调用时使用以下代码:
Class1.fun();
这时,第二个参数就在native层标识对该类Class1的引用。
如果某个native方法位于Class1.java名为fun,且此方法为非静态方法,则用户在java层调用时使用以下代码:
Class1 clz = new Class1();
clz.fun();
这时,第二个参数就在native层标识对clz对象的引用。
3.4 JNIEnv 和 JavaVM
此小结直接使用了邓凡平的文档原文,原文链接:https://blog.csdn.net/Innost/article/details/47204557。
上面提到说JNIEnv,是一个和线程有关的变量。也就是说,线程A有一个JNIEnv,线程B有一个JNIEnv。由于线程相关,所以不能在线程B中使用线程A的JNIEnv结构体。读者可能会问,JNIEnv不都是native函数转换成JNI层函数后由虚拟机传进来的吗?使用传进来的这个JNIEnv总不会错吧?是的,在这种情况下使用当然不会出错。不过当后台线程收到一个网络消息,而又需要由Native层函数主动回调Java层函数时,JNIEnv是从何而来呢?根据前面的介绍可知,我们不能保存另外一个线程的JNIEnv结构体,然后把它放到后台线程中来用。这该如何是好?
还记得前面介绍的那个JNI_OnLoad函数吗?它的第一个参数是JavaVM,它是虚拟机在JNI层的代表,代码如下所示:
//全进程只有一个JavaVM对象,所以可以保存,任何地方使用都没有问题。 jint JNI_OnLoad(JavaVM* vm, void* reserved)
正如上面代码所说,不论进程中有多少个线程,JavaVM却是独此一份,所以在任何地方都可以使用它。那么,JavaVM和JNIEnv又有什么关系呢?答案如下:
· 调用JavaVM的AttachCurrentThread函数,就可得到这个线程的JNIEnv结构体。这样就可以在后台线程中回调Java函数了。
· 另外,后台线程退出前,需要调用JavaVM的DetachCurrentThread函数来释放对应的资源。
四、 在native使用Java对象
Java的基本类型传入JNI时,会直接复制过去,而Java的引用类型则会通过引用传递。。
Java VM 必须跟踪已传递给本机代码的所有对象,以便垃圾收集器不会释放这些对象。反过来,本机代码必须有一种方法来通知 VM 它不再需要这些对象以防止内存泄露。
此外,垃圾收集器必须能够移动native代码引用的对象。
4.1 本地方法中的引用类型
JNI 将native代码使用的对象引用分为两类:
- local ,局部引用:本地引用在本地方法调用期间有效,并在本地方法返回后自动释放。
- global ,全局引用:全局引用在被显式释放之前一直有效。
为了实现本地引用,Java VM 为每次从 Java 到本地方法的控制转换创建一个注册表。注册表将不可移动的本地引用映射到 Java 对象,并防止对象被垃圾收集。传递给本机方法的所有 Java 对象(包括作为 JNI 函数调用结果返回的对象)都会自动添加到注册表中。本地方法返回后,注册表将被删除,从而允许对其所有条目进行垃圾收集。
4.2 本地方法中报告编程错误
JNI 不检查编程错误,例如传入 NULL 指针或非法参数类型。非法参数类型包括使用普通 Java 对象而不是 Java 类对象等。由于以下原因,JNI 不会检查这些编程错误:
- 大多数 C 库函数不能防止编程错误
- 强制 JNI 函数检查所有可能的错误条件会降低正常(正确)本机方法的性能
- 在许多情况下,没有足够的运行时类型信息来执行此类检查。
Java异常的处理:JDK文档https://docs.oracle.com/javase/10/docs/specs/jni/design.html#accessing-java-objects
4.3 访问Java对象的成员变量与方法
- 调用方法:
JNI 允许本地代码访问变量并调用 Java 对象的方法。JNI 通过符号名称和类型签名来标识方法和变量。一个两步过程从名称和签名中计算出定位字段或方法的成本。比如调用f
类cls 中的方法,原生代码首先获取一个方法ID,如下:
jmethodID mid = env->GetMethodID(cls, "f", "(ILjava/lang/String;)D");
然后,本机代码可以重复使用方法 ID,而无需进行方法查找,如下所示:
jdouble result = env->CallDoubleMethod(obj, mid, 10, str);
- 调用变量:
调用变量直接复制了邓凡平博客里的介绍。
//获得fieldID后,可调用Get<type>Field系列函数获取jobject对应成员变量的值。
NativeType Get<type>Field(JNIEnv *env,jobject obj,jfieldID fieldID)
//或者调用Set<type>Field系列函数来设置jobject对应成员变量的值。
void Set<type>Field(JNIEnv *env,jobject obj,jfieldID fieldID,NativeType value)
//下面我们列出一些参加的Get/Set函数。
GetObjectField() SetObjectField()
GetBooleanField() SetBooleanField()
GetByteField() SetByteField()
GetCharField() SetCharField()
GetShortField() SetShortField()
GetIntField() SetIntField()
GetLongField() SetLongField()
GetFloatField() SetFloatField()
GetDoubleField() SetDoubleField()
五、JNI实践
使用JNI完成一个求“两数字之和的”的简单实践:在Java层调用“twoIntSum(int a, int b)”函数然后在native求和并输出日志。此功能没有实际意义,读者仅可以通过示例的形式走通JNI,完成JNI功能。
1:在Java层编写类的静态函数,调用Java native方法“twoIntSumNative”。
frameworks/base/core/java/android/app/MyJNITest.java
package android.app;
import android.util.Slog;
public class MyJNITest {
static final String TAG = "Zou: MyJNITest";
public static int twoIntSum(int a, int b) {
Slog.e(TAG, "MyJNITest, twoIntSum() start");
twoIntSumNative(a, b);
Slog.e(TAG, "MyJNITest, twoIntSum() end");
return 1;
}
private static native int twoIntSumNative(int a, int b);
}
2:编写对应的JNI文件
frameworks/base/core/jni/android_app_MyJNITest.cpp
//
// Created by zxs on 21-9-9.
//
#include "core_jni_helpers.h"
#include "jni.h"
#include <android-base/logging.h>
namespace android
{
//native函数的核心逻辑,输出“a+b”的和
static jint android_app_Debug_twoIntSumNative(JNIEnv *env, jobject clazz,
jint a, jint b)
{
jint result = a + b;
LOG(ERROR) << "Zou: android_app_Debug_twoIntSumNative " << a + b;
return result;
}
/*
* JNI registration.
*/
// JNI函数签名,其中“(II)I”表示Java函数的参数列表为两个int,返回值为int
static const JNINativeMethod gMethods[] = {
{ "twoIntSumNative", "(II)I",
(void*) android_app_Debug_twoIntSumNative },
};
//JNI函数的注册函数
int register_android_app_MyJNITest(JNIEnv *env)
{
return jniRegisterNativeMethods(env, "android/app/MyJNITest", gMethods, 1);
}
};// namespace android
3:在AndroidRuntime.cpp中调用JNI函数的注册函数,在这里调用的话虚拟机启动时就会注册,也可以不在这里调用。
完成这一步之后,JNI的主要逻辑已经完成。
frameworks/base/core/jni/AndroidRuntime.cpp
... ...
namespace android {
/*
* JNI-based registration functions. Note these are properly contained in
* namespace android.
*/
... ...
//导入android::register_android_app_MyJNITest函数
extern int register_android_app_MyJNITest(JNIEnv *env);
//把register_android_app_MyJNITest函数放在gRegJNI[]数组中
static const RegJNIRec gRegJNI[] = {
REG_JNI(register_android_app_MyJNITest),
... ...
}
};// namespace android
... ...
4:在framework/base/core的bp文件中编入此android_app_MyJNITest.cpp文件,这样libandroid_runtime.so库中就会有包含此文件了。
frameworks/base/core/jni/Android.bp
cc_library_shared {
name: "libandroid_runtime",
target: {
android: {
srcs: [
... ...
"android_app_MyJNITest.cpp",
... ...
],
},
},
}
5:现在就是选择一个时机调用Java方法了,越方便验证越好。
由于JNI的动态注册我选择了在zygote启动时打开Java虚拟机期间,调用Java 方法应该在其之后,所以我选择放在启动AMS(ActivityManagerService)期间调用。代码如下:
frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java
/**
* Ready. Set. Go!
*/
public void systemReady(final Runnable goingCallback, @NonNull TimingsTraceAndSlog t) {
.. ...
MyJNITest.twoIntSum(1, 88);
... ...
}
6:代码已经写完了,现在lunch机器之后,编译模块“framework-minus-apex”与“services”。
shell命令如下:
source build/envsetup.sh
lunch 对应机型,对应版本
make framework-minus-apex services -j10
编译完成之后,将机器diable-verity然后remount。
使用以下命令将编译出来的“/system/framework”目录,“/system/lib目录”,“/system/lib64目录”全部push进手机的"目录下"
cd ~/MyWork/SourceCode/android-XXXX-dev/out/target/product/XXXXsystem
adb push framework/ /system/
adb push lib/ system
adb push lib64/ system
完成之后重启手机。
7: 手机开机时,若出现以下日志,且和为“88+1”则表示功能正常。
08-31 16:53:50.592 864 864 E Zou: MyJNITest: MyJNITest, twoIntSum() start
08-31 16:53:50.592 864 864 E system_server: Zou: android_app_Debug_twoIntSumNative 89
08-31 16:53:50.592 864 864 E Zou: MyJNITest: MyJNITest, twoIntSum() end
至此,一次JNI的调用已经完成
参考文档
【1】https://docs.oracle.com/javase/10/docs/specs/jni/design.html#accessing-java-objects
【2】https://blog.csdn.net/Innost/article/details/47204557?spm=1001.2014.3001.5501