【安卓framework实战】Android JNI 分析与实践

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函数,然后继续往下看源码即可。

但如果有时间的话,最好还是知其然并知其所以然,有这么些问题:

  1. Java世界中的“getNativeHeapSize”方法是如何通过gMethods[]这样一个数组就找到“android_os_Debug_getNativeHeapSize”方法的;
  2. 代码中“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/

  1. 基本数据类型:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-B5Vg0AHL-1631245822973)(/home/zxs/.config/Typora/typora-user-images/image-20210908145603405.png)]

  2. 引用数据类型:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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代码使用的对象引用分为两类:

  1. local ,局部引用:本地引用在本地方法调用期间有效,并在本地方法返回后自动释放。
  2. 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对象的成员变量与方法

  1. 调用方法:

JNI 允许本地代码访问变量并调用 Java 对象的方法。JNI 通过符号名称和类型签名来标识方法和变量。一个两步过程从名称和签名中计算出定位字段或方法的成本。比如调用fcls 中的方法,原生代码首先获取一个方法ID,如下:

jmethodID mid = env->GetMethodID(cls, "f", "(ILjava/lang/String;)D");

然后,本机代码可以重复使用方法 ID,而无需进行方法查找,如下所示:

jdouble result = env->CallDoubleMethod(obj, mid, 10, str);
  1. 调用变量:

调用变量直接复制了邓凡平博客里的介绍。

//获得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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 197,099评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,846评论 2 374
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 144,146评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,789评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,656评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,467评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,870评论 3 389
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,500评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,790评论 1 293
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,828评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,628评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,449评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,881评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,077评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,376评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,879评论 2 343
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,089评论 2 338

推荐阅读更多精彩内容