RN旧架构实现通信的基础原理 --- 01 -- Java&C++通信实现机制

提到RN通信,大家并不会陌生,即JS、C++之间的通信与C++、Native之间的通信。对于JS与C++的互调会在后续的文章中讲解,本篇文章主要带大家一起了解下 C++与Native 的通信实现机制。

我们知道 C++与Java 的通信是借助于JNI来完成的,那么什么是JNI呢?

说到JNI,这里有两个比较重要的概念需要先提一下:静态注册(被动注册) 动态注册(主动注册)因为像RN这样以JNI为基础实现不同语言之间通信的框架,应用到了很多且频繁的JNI方法调用。如果以静态注册的方式来注册JNI,那么将是一件极为繁琐的事情,而且在对native方法的初次调用的时候,效率也较低,相比之下动态注册的方式就很好的解决了此类问题。

谈到JNI的注册方式,那么再往底层走是怎样实现的呢?为此这篇分享里我们还会继续深入一点,了解一下JNI在Dalvik虚拟机中是如何完成注册的。

本篇分享内容如下:

  1. 什么是JNI?
  2. JNI的两种注册方式
  3. JNI在JVM中注册的本质
  4. 浅谈 JNI、JSBridge、JSI

一、JNI的概念

JNI ( 全称是Java Native Interface ),即 Java 与 C++ 通信接口。C++是系统级的编程语言, 可以用来开发任何和系统相关的程序和类库, 但是Java本身编写底层的应用比较难实现,通过JNI接口,Java可以方便的调用现有的本地库,极大地灵活了Java的开发。

在Android系统中,JNI方法是以 C/C++ 语言来实现的,然后编译成一个so文件。JNI方法需要被加载到当前应用程序进程的地址空间,才能够被调用,意思也就是JNI是需要本地系统来直接执行的。

二、JNI的两种注册方式

JNI的注册方式分为两种:静态注册动态注册。那么接下来我们分别介绍两种注册方式的实现以及优缺点。

2.1 静态注册

2.1.1 实现步骤如下:

  1. 编写.java并声明native方法

  2. 使用javac命令编译.java生成.class文件

  3. 使用javah命令生成 与 声明的native方法对应的 .h 头文件

  4. 使用 C++ 实现 .h 头文件

  5. 编译生成 .so 文件

  6. 编译运行完成加载、注册、方法调用

步骤 1 : 编写带有native关键字修饰的方法的Java类

public class Sample {

    // 声明四种类型的native方法

    public native int intMethod(int n);

    public native boolean booleanMethod(boolean bool);

    public native String stringMethod(String text);

    public native int intArrayMethod(int[] intArray);

    public static void main(String[] args) {

        // 将Sample.so动态类库,加载到当前进程中

        System.loadLibrary("Sample");

        Sample sample = new Sample();

        //调用native方法

        int square = sample.intMethod(5);

        boolean bool = sample.booleanMethod(true);

        String text = sample.stringMethod("Java");

        int sum = sample.intArrayMethod(new int[]{1,2,3,4,5,8,13});

        //打印得到的值

        System.out.println("intMethod: " + square);

        System.out.println("booleanMethod: " + bool);

        System.out.println("stringMethod: " + text);

        System.out.println("intArrayMethod: " + sum);

    }

}

步骤 2: 将 java文件编译生成 class文件

>jimmy@58deMacBook-Pro-9 ~> javac Sample.java

步骤 3 : 生成对应的头文件

>jimmy@58deMacBook-Pro-9 ~> javah Sample

.h 头文件代码如下:

/* DO NOT EDIT THIS FILE - it is machine generated */

#include 

#ifndef _Included_Sample

#define _Included_Sample

#ifdef __cplusplus

extern "C" {

#endif

/*
 * Class: Sample
 * Method: intMethod
 * Signature: (I)I
 * Java_完整类名_方法名, 完整类名包括了包名。
 */
JNIEXPORT jint JNICALL Java_Sample_intMethod(JNIEnv *, jobject, jint);

/*
 * Class: Sample
 * Method: booleanMethod
 * Signature: (Z)Z
 */
JNIEXPORT jboolean JNICALL Java_Sample_booleanMethod(JNIEnv *, jobject, jboolean);

/*
 * Class: Sample
 * Method: stringMethod
 * Signature: (Ljava/lang/String;)Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_Sample_stringMethod(JNIEnv *, jobject, jstring);

/*
 * Class: Sample
 * Method: intArrayMethod
 * Signature: ([I)I
 */
JNIEXPORT jint JNICALL Java_Sample_intArrayMethod(JNIEnv *, jobject, jintArray);

#ifdef __cplusplus

}

#endif

#endif

        以 JNIEXPORT 修饰的方法,就是Java中声明的Native方法在C++中的实现方式。函数名由 Java_完整类名_方法名 组成,每个函数中都会有一个参数JNIEnv * ,它是由Dalvik虚拟机生成的一个JNI环境对象,使用起来类似Java中的反射。

方法签名Signature类型详表如下:

表头java类型 Signature 备注
boolean Z -
byte B
char C
short S
int I
long L
float F
double D
void V
object L用/分割的完整类名 例如: Ljava/lang/String表示String类型
Array [签名 例如: [I表示int数组, [Ljava/lang/String表示String数组
Method (参数签名)返回类型签名 例如: ([I)I表示参数类型为int数组, 返回int类型的方法

步骤 4 : C++ 实现头文件中的函数

#include "Sample.h"
#include 

JNIEXPORT jint JNICALL Java_Sample_intMethod(JNIEnv *env, jobject obj, jint num){
    return num * num;
}

JNIEXPORT jboolean JNICALL Java_Sample_booleanMethod(JNIEnv *env, jobject obj, jboolean boolean){
    return !boolean;
}

JNIEXPORT jstring JNICALL Java_Sample_stringMethod(JNIEnv *env, jobject obj, jstring string){
    const char* str = env->GetStringUTFChars(string, 0);
    char cap[128];
    strcpy(cap, str);
    env->ReleaseStringUTFChars(string, 0);
    return env->NewStringUTF(strupr(cap));
}

JNIEXPORT jint JNICALL Java_Sample_intArrayMethod(JNIEnv *env, jobject obj, jintArray array){
    int i, sum = 0;
    jsize len = env->GetArrayLength(array);
    jint *body = env->GetIntArrayElements(array, 0);
    for (i = 0; i < len; ++i){
        sum += body[i];
    }
    env->ReleaseIntArrayElements(array, body, 0);
    return sum;
}

在上面的 C++代码中,GetStringUTFChars() 是用来将一个Java字符串转换为C字符串的,因为Java本身都使用了双字节的字符,而C语言本身都是单字节的字符, 所以需要进行转换。NewStringUTF() 是将一个 C++ 字符串转换为一个UTF8字符串。ReleaseStringUTFChars() 是用来释放对象的,在Dalvik虚拟机中是有一个垃圾回收机制的, 但是在C++语言中,这些对象必须手动回收,否则可能造成内存泄漏.

步骤 5 : 编译生成 .so 文件

>jimmy@58deMacBook-Pro-9 ~> ndk-build

步骤 6: 编译运行完成加载、注册、方法调用

通过 System.loadLibrary() 完成 .so 的加载,加载的过程即为JNI注册的过程。方法调用是在 Java中发起的并由本地系统完成JNI方法的调用执行。

2.2 动态注册

        所谓的动态注册 是指动态的对Java中声明的Native方法完成注册,使得 C++ 中方法名 和 Java 中 Native 方法名不同的情况下,进行一一对应,形成映射关系。以后再修改或新增 Native方法时,只需在C++ 中修改或新增所需关联的方法,并在 getMethods 数组中完成映射关系,最后再通过ndk-build 重新生成so库就可以运行了。

        动态注册是在JNI 层实现的,JAVA层不需要关心,因为在 System.loadLibrary() 执行后就会去调用JNI_OnLoad,有就注册,没有就不注册。

2.2.1 实现步骤如下:

  1. 编写.java并声明native方法

  2. 编写 C++ 文件并实现 JNI_OnLoad 方法

  3. 对应 Java中声明的Native方法,实现一个函数映射表以及在C++侧的具体实现函数,并在 C++ 文件中完成动态注册代码编写

  4. 编译生成 .so 文件

  5. 编译运行完成加载、注册、方法调用

步骤 1: 编写 .java 文件并声明所需的Native方法

public class Hello {

    static {
        System.loadLibrary("hello");
    }

    public static final main(String[] args){
        System.out.println(stringFromJNI());
    }

    public native String stringFromJNI();

}

步骤 2: 编写 C++ 代码 并实现 JNI_OnLoad 方法

#include 
#include 
#include 
#include 
#include 

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
    JNIEnv* env = NULL;
    jint result = -1;
    if ((*vm)->GetEnv(vm, (void**) &env, JNI_VERSION_1_4) != JNI_OK) {
        return -1;
    }

    assert(env != NULL);

    //执行注册并返回注册状态
    if (!registerNatives(env)) {
        return -1;
    }

    //成功 
    result = JNI_VERSION_1_4;
    return result;
}

步骤 3: 对应 Java中声明的Native方法,实现一个函数映射表以及在C++侧的具体实现函数,并在 C++ 文件中完成动态注册代码编写

#include 
#include 
#include 
#include 
#include 

/*
 * 使用Native方法返回一个新的 VM String。
 */
jstring native_hello(JNIEnv* env, jobject thiz) {
    return (*env)->NewStringUTF(env, "动态注册JNI");
}

/**
 * 方法映射表
 * struct: Java侧声明的Native方法名称、Native方法签名、需要指向的C++ 函数
 */
static JNINativeMethod gMethods[] = {
    {"stringFromJNI", "()Ljava/lang/String;", (void*)native_hello},
};

static int registerNativeMethods(JNIEnv* env, const char* className, JNINativeMethod* gMethods, int numMethods) {

    jclass clazz;

    //根据提供的class类完整名称,获取该class类
    clazz = (*env)->FindClass(env, className);
    if (clazz == NULL) {
        return JNI_FALSE;
    }

    //检查是否注册成功
    if ((*env)->RegisterNatives(env, clazz, gMethods, numMethods) < 0) {
        return JNI_FALSE;
    }
    return JNI_TRUE;
}

static int registerNatives(JNIEnv* env) {

    //指定要注册的类
    const char* kClassName = "com/example/hello/Hello";
    return registerNativeMethods(env, kClassName, gMethods,
sizeof(gMethods) / sizeof(gMethods[0]));
}

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {

    JNIEnv* env = NULL;
    jint result = -1;
    if ((*vm)->GetEnv(vm, (void**) &env, JNI_VERSION_1_4) != JNI_OK) {
        return -1;
    }

    assert(env != NULL);

    //执行注册并返回注册状态
    if (!registerNatives(env)) {
        return -1;
    }

    //成功
    result = JNI_VERSION_1_4;
    return result;
}

步骤 4 : 编译生成 .so 文件

>jimmy@58deMacBook-Pro-9 ~> ndk-build

步骤 5: 编译运行完成加载、注册、方法调用

通过 System.loadLibrary() 完成 .so 的加载,加载的过程即为JNI注册的过程。方法调用是在 Java中发起的并由本地系统完成JNI方法的调用执行。

2.3 两种注册方式的区别

我们用一张图来展现一下两种方式在创建流程上的区别:

RN旧架构实现通信的基础原理 --- 01 -- Java&C++通信实现机制_第1张图片

 静态注册:

  1. 当so被加载之后,JVM虚拟机需要通过函数名来查找相关函数,初次调用JIN方法时需要建立关联,影响效率。
  2. 静态注册多用于NDK开发。

动态注册:

  1. 由于映射表的存在,JVM不再需要通过函数名来查找相关函数,而是通过现成的函数映射表的对应关系来执行,因此执行效率更高。

  2. 动态注册多用于Framework开发。

不管是静态注册方式,还是动态注册方式,都需要将c文件编译成平台所需要的库。

三、JNI在JVM中注册的本质

        前面提到,JNI方法是 .so 文件被加载的时候,被注册到虚拟机的,因此JNI在JVM中的注册是从 .so 文件的加载开始的,也就是从 System.loadLibrary() 开始。

动态注册(主动注册)的情况下,会先执行到 RegisterNatives(env, clazz, gMethods, numMethods) 函数,进而执行一系列的判断与调用,最终执行到 dvmSetNativeFunc(Method* method, DalvikBridgeFunc func, const u2* insns) 函数。

我们看一下它的具体实现:

void dvmSetNativeFunc(Method* method, DalvikBridgeFunc func, const u2* insns){

    ......
    if (insns != NULL) {
        //更新 insns 和 nativeFunc
        method->insns = insns;
        //Android原子变量的设置操作
        android_atomic_release_store((int32_t) func,
        (void*) &method->nativeFunc);
    } else {
        //只更新 nativeFunc
        method->nativeFunc = func;
    }
    ......
}

该函数就是JNI最终实现注册的地方,它有三个参数:method、func、insns。

  • method

表示一个 Method 对象,即要注册JNI方法的Java类成员函数。当检测到它是一个 JNI 方法时,它的成员变量 method.nativeFunc 保存的就是该JNI方法的函数地址,即当发现是个native函数时,就将C++函数地址保存到 method.nativeFunc 中,后续调用native方法的时候,就相当于通过 method.nativeFunc 来调用一个C++函数。

  • func

表示当前JNI方法的 Bridge 函数;它就是根据Dalvik虚拟机的启动选项来为即将要注册的JNI选择的一个合适的Bridge函数。所谓Bridge函数,其实就是在JNI被真正调用之前,根据虚拟机的启动参数,来选择的一个初始化流程,不同的Bridge函数有不同的初始化流程,调用之前的准备工作不同。在JVM不开启JNI检查的时候,该Bridge会返回一个 dvmCallJNIMethod() 函数。

  • insnc

表示要注册的JNI方法的函数地址;

到这里我们可以知道,注册的过程也就是将 Method 的成员 nativeFunc 指向 DalvikBridgeFunc,将成员 insnc 指向实际的native函数。这样一来,当Java层开始调用native函数的时候就会首先进入一个名叫 dvmCallJNIMethod() 函数,而真正的native函数指针则存储在Method->insns中。dvmCallJNIMethod() 函数会先准备好启动参数,然后再调用函数 dvmPlatformInvoke() 执行对应的 native方法 (也就是method->insns所指向的方法),这样就完成了native 函数的调用。

四、浅谈 JNI、JSBridge、RN -- JsBridge、JSI

        JSBridge是一个宽泛的概念,它是JS与Native(Android/IOS)进行双向通信的一种手段,使得JS可以借助它实现调用Native(Android/Ios)的功能,以及Native(Android/Ios)可以方便的调用JavaScript中的功能函数。

4.1 JSBridge

4.1.1 实现Js调用Java

1. JavaScriptInterface

Android API 4.2 之前使用的是 addJavascriptInterface,但是存在严重的安全隐患,Android 4.2 之后,谷歌提供了@JavascriptInterface对象注解的方式建立JS对象和Java对象的绑定,提供给JavaScript调用的方法必须带有@JavascriptInterface。原理就是通过WebView提供的addJavascriptInterface方法给浏览器 全局对象 window 注入一个命名空间,然后给Web增加一些可以操作Java的反射。

原理实现(源码分析: https://www.cnblogs.com/aimqqroad-13/p/13893588.html )

addJavascriptInterface实现原理:从 webview.addJavaScriptInterface() 开始,通过一系列的调

用,并基于JNI 交由C++来实现,最终是通过Chromium的IPC机制( 进程间通信IPC机制则是基于

Unix Socket通信协议实现的 ),发出了一条消息。

2. 改写浏览器原有对象

这主要是修改浏览器中全局对象 window 上的某些方法,进而拦截固定规则的参数,最后分发给Java对应的方法去处理。这里常用的是以下四个方法:

  • alert -- 可以被webview的 onJsAlert 监听
  • confirm -- 可以被webview的 onJsConfirm 监听
  • console.log -- 可以被webview的 onConsoleMessage 监听
  • prompt -- 可以被webview的 onJsPrompt 监听

3. URL scheme

它可以通过拦截跳转页面时的 URL请求,并解析这个scheme协议,按照一定规则捕获行为并交由Native(Android/Ios)解决。安卓和iOS分别用到拦截URL请求的方法是:

  • shouldOverrideUrlLoading
  • UIWebView的delegate

4.1.2 Java调用JS的实现方式

webView.loadUrl("javascript:callFromJava('call from java')");

4.2 RN -- JSBridge

RN旧版本中所指的JSBridge,其实现原理为,对脚本引擎 JSCore 进行了封装,并以 JS 与 C++进行信息交换的方式,完成 JS 与 C++ 的通信。

4.3 JSI

新架构中的JSI是对 JavaScript 引擎底层API进行封装,而产生的 JS <=> C++ 的映射框架,以注入的方式实现JS与C++的通信。有了这一层映射,JS与C++可以相互感知、相互持有对方的实例,从而实现JS与C++的同步通讯,也摒弃了JSON数据的序列化和反序列化的过程,大大提升通信效率。

4.4 JNI、JSBridge、RN -- JSBridge、JSI 的区别

  • JNI解决的是Java与C++之间的通讯,同时是同一进程不同线程之间实现通信。
  • JSBridge 是JS与Java之间实现通讯的一种方式,该方式是借助于 socket 的方式实现进程间通信
  • RN -- JSBridge 是JS与C++之间实现通讯的一种方式,该方式为同一进程不同线程之间的通信,通过封装 JSCore 并借助于 JSON信息的序列化和反序列化实现JS与C++的异步通信。
  • JSI 是JS与C++之间实现通讯的一种方式,属于同一进程不同线程,通过对JavaScript引擎底层API的封装实现 JS <=> C++ 的映射层,以注入方法、变量的方式实现。此方式为同步,同时不需要 JSON的支持。

你可能感兴趣的:(ReactNative,react,native)