Bottom principle of RN communication -- Summary

Facebook announced the plan and roadmap for large-scale reconstruction of Rn in June 2018. The purpose of the whole reconstruction is to make RN lighter, more suitable for mixed development, close to or even achieve the original experience. The technical core of the new architecture is JSI, and Turbomodule is implemented based on it.

In the hope that we can have a deeper understanding of the highlights of the new RN architecture together, the next sharing will start with the underlying implementation of the old architecture and the new architecture, and understand the principle of the RN framework one by one.

1, Java calls C + +: JNI

As we all know, JNI is a means to realize the communication between different languages of C + + and Java. Through it, we can easily realize java calling C + + and C + + calling Java. In Android system, JNI method is implemented in C + + language, and then compiled into a so file. JNI methods need to be loaded into the address space of the current application process before they can be called, which means that JNI needs the local system to execute directly.

The process of JNI realizing the communication between C + + and Java can be divided into two steps: first, register, that is, load the JNI method into the address space of the current application; Furthermore, call the native method.

1. Registration method of JNI

Static registration and dynamic registration. Next, let's take a look at these two registration methods.

2.1 static registration

This method is generally applicable to the development of NDK, and is applicable to demand scenarios where the logic is not very complex and the communication is not very frequent.

The implementation steps are divided into six steps, including:

  1. to write. java and declare the native method

  2. Compile using javac commands java generation class file

  3. Use the javah command to generate the corresponding to the declared native method h header file

  4. It is implemented in C + + h header file

  5. Compile and generate so file

  6. Compile and run to complete loading, registration and method call

Step 1: write a Java class with a method decorated with the native keyword

public class Sample {
    // Declare four types of native methods
    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) {
        // Add sample So dynamic class library, loaded into the current process
        System.loadLibrary("Sample");
        Sample sample = new Sample();
        //Call the native method
        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});
        //Printed value
        System.out.println("intMethod: " + square);
        System.out.println("booleanMethod: " + bool);
        System.out.println("stringMethod: " + text);
        System.out.println("intArrayMethod: " + sum);
    }
}

Step 2: compile the java file to generate a class file

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

Step 3: generate the corresponding header file

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

The. h header file code is as follows:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
#ifndef _Included_Sample
#define _Included_Sample
#ifdef __cplusplus

extern "C" {
#endif

/*
 * Class: Sample
 * Method: intMethod
 * Signature: (I)I
 * Java_Full class name_ Method name. The complete class name includes the package name.
 */
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

The method modified by JNIEXPORT is the implementation of the Native method declared in Java in C + +. The function name is defined by Java_ Full class name_ Method name. Each function will have a parameter JNIEnv *, which is a JNI environment object generated by Dalvik virtual machine. It is similar to reflection in Java.

The Signature type of method Signature is as follows:

Header java typeSignatureremarks
booleanZ-
byteB
charC
shortS
intI
longL
floatF
doubleD
voidV
objectComplete class name split by / LFor example, Ljava/lang/String represents String type
Array[signature]For example: [I represents int array, [Ljava/lang/String represents String array
Method(parameter signature) return type signatureFor example: ([I)I means that the parameter type is an int array and returns a method of type int

Step 4: C + + implements the functions in the header file

#include "Sample.h"
#include <string.h>

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;
}

In the above C + + code, GetStringUTFChars() is used to convert a Java string into a C string. Because Java itself uses double byte characters and C language itself is a single byte character, it needs to be converted. NewString utf() converts a C + + string to a UTF8 string. ReleaseStringUTFChars() is used to release objects. There is a garbage collection mechanism in Dalvik virtual machine, but in C + + language, these objects must be recycled manually, otherwise memory leakage may occur

Step 5: compile and generate so file

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

Step 6: compile and run to complete loading, registration and method call

This is done through the System's static method loadLibrary() so, the loading process is the JNI registration process. Method calls are initiated in Java and executed by the local System.

2.2 dynamic registration

Dynamic registration, also known as active registration, that is, it will provide a function mapping table to make one-to-one correspondence between C + + method names and native method names in Java to form a mapping relationship. When modifying or adding native methods later, you only need to modify or add the required associated methods in C + +, complete the mapping relationship in the getMethods array, and finally regenerate the so library through NDK build.

The dynamic registration method is applicable to the complex business scenarios and frequent communication requirements such as RN, which makes the development process more flexible. In addition, the way of dynamic registration requires us to implement a JNI in C + +_ Onload method, which is the entry to perform dynamic registration.

The implementation steps are divided into five steps, including:

1. Preparation java and declare the native method

2. Write C + + files and implement JNI_OnLoad method

3. Corresponding to the Native method declared in Java, implement a function mapping table and the specific implementation functions on the C + + side, and complete the dynamic registration code in the C + + file

4. Compile and generate so file

5. Compile and run to complete loading, registration and method call

Step 1: write java file and declare the required Native method

public class Hello {

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

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

    public native String stringFromJNI();
}

Step 2: write C + + code and implement JNI_OnLoad method

#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <jni.h>
#include <assert.h>

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);

    //Execute registration and return to registration status
    if (!registerNatives(env)) {
        return -1;
    }

    //success
    result = JNI_VERSION_1_4;
    return result;
}

Step 3: corresponding to the native method declared in Java, implement a function mapping table and the specific implementation functions on the C + + side, and complete the dynamic registration code writing in the C + + file

#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <jni.h>
#include <assert.h>

/*
 * Use the Native method to return a new VM String.
 */
jstring native_hello(JNIEnv* env, jobject thiz) {
    return (*env)->NewStringUTF(env, "Dynamic registration JNI");
}

/**
 * Method mapping table
 * struct:  Java The name of the Native method declared on the side, the signature of the Native method, and the C + + function to be pointed to
 */
static JNINativeMethod gMethods[] = {
    {"stringFromJNI", "()Ljava/lang/String;", (void*)native_hello},
};

static int registerNativeMethods(JNIEnv* env, const char* className, JNINativeMethod* gMethods, int numMethods) {
    jclass clazz;
    //Get the class according to the complete name of the class provided
    clazz = (*env)->FindClass(env, className);
    if (clazz == NULL) {
        return JNI_FALSE;
    }

    //Check whether the registration is successful
    if ((*env)->RegisterNatives(env, clazz, gMethods, numMethods) < 0) {
        return JNI_FALSE;
    }
    return JNI_TRUE;
}

static int registerNatives(JNIEnv* env) {

    //Specify the class to register
    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);

    //Execute registration and return to registration status
    if (!registerNatives(env)) {
        return -1;
    }

    //success
    result = JNI_VERSION_1_4;
    return result;
}

Step 4: compile and generate so file

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

Step 5: compile and run to complete loading, registration and method call

Through system Loadlibrary() completed so, the loading process is the JNI registration process. Method calls are initiated in Java and executed by the local system.

2.3 differences between the two registration methods

Let's use a diagram to show the difference between the two methods in creating the process:

Static registration:

1. After so is loaded, the JVM virtual machine needs to find relevant functions through the function name. When calling the JIN method for the first time, it needs to establish an association, which affects the efficiency.

2. Static registration is mostly used for NDK development.

Dynamic registration:

1. Due to the existence of the mapping table, the JVM no longer needs to find relevant functions through the function name, but executes through the corresponding relationship of the existing function mapping table, so the execution efficiency is higher.

2. Dynamic registration is mostly used for Framework development.

Whether static registration or dynamic registration, you need to compile the c file into the library required by the platform.

2.4 essence of JNI registration in JVM

As mentioned earlier, the JNI method is When the so file is loaded, it is registered to the virtual machine, so the registration of JNI in the JVM is from The loading of so file starts, that is, from system Loadlibrary() starts.

Next, we will talk about an implementation mechanism of the JVM to register JNI in the case of dynamic registration (active registration). Finished executing JNI in C + + file_ After onload, it will execute the RegisterNatives(env, clazz, gMethods, numMethods) function, then execute a series of judgments and calls, and finally execute the dvmsettivefunc (method * method, dalvikbridgefunc, const U2 * insns) function.

Let's take a look at its specific implementation:

void dvmSetNativeFunc(Method* method, DalvikBridgeFunc func, const u2* insns){
    ......
    if (insns != NULL) {
        //Update insns and nativeFunc
        method->insns = insns;
        //Setting operation of Android atomic variables
        android_atomic_release_store((int32_t) func,
        (void*) &method->nativeFunc);
    } else {
        //Update only nativeFunc
        method->nativeFunc = func;
    }
    ......
}

This function is where JNI finally implements registration. It has three parameters: method, func and insns.

  • method:

Represents a Method object, that is, the Java class member function to register the JNI Method. When it is detected that it is a JNI Method, its member variable Method Native func saves the function address of the JNI Method, that is, when it is found to be a native function, it saves the C + + function address to Method In nativefunc, subsequent calls to the native Method are equivalent to passing the Method Nativefunc to call a C + + function.

  • func:

Bridge function representing the current JNI method; It is an appropriate bridge function selected for the JNI to be registered according to the startup options of Dalvik virtual machine. The so-called bridge function is actually an initialization process selected according to the startup parameters of the virtual machine before JNI is actually called. Different initialization processes have different preparations before calling. When the JVM does not enable JNI checking, the bridge will return a dvmCallJNIMethod() function.

  • insnc:

Represents the actual function address of the JNI method to be registered;

From here, we can know that the registration process is to point the Method member nativeFunc to DalvikBridgeFunc and the member insnc to the actual nativefunction. In this way, when the Java layer starts calling the native function, it will first enter a function called dvmCallJNIMethod(), and the real native function pointer is stored in Method - > insns. The dvmCallJNIMethod() function will first prepare the startup parameters, and then call the function dvmPlatformInvoke() to execute the corresponding native Method (that is, the Method pointed to by Method - > insns), so as to complete the call of the native function.

2, C + + actively invokes Java: Reflection

When talking about JNI earlier, I talked about an implementation process of Java calling C + +, so how does C + + call Java? In a word, it is actually a way similar to java reflection to complete the call of C + + to Java. Then let's start to understand.

The functions required for the implementation of this method are as follows:

  1. Create virtual machine
  2. Find the class object and create the object
  3. Calling static and member methods
  4. Get member properties and modify member properties

Steps of this method:

Step 1: write Java code

public class Sample {

    public String name;

    public static String sayHello(String name) {
        return "Hello, " + name + "!";
    }

    public String sayHello() {
        return "Hello, " + name + "!";
    }
}

Step 2: compile and generate class files

>javac Sample.java

Step 3: write C + + code and complete the call to Java functions

#include <jni.h>
#include <string.h>
#include <stdio.h>
int main(void){
    //Relevant parameters required for virtual machine creation
    JavaVMOption options[1]; //Equivalent to the parameters passed in the command line
    JNIEnv *env; //JNI environment variable
    JavaVM *jvm; //Virtual machine instance
    JavaVMInitArgs vm_args; //The initialization parameter created by the virtual machine. This parameter will contain JavaVMOption
    long status; //Status of whether the virtual machine started successfully
 
 
    jclass cls; //Class object to look for
    jmethodID mid; //Method Id in Class object
    jfieldID fid; //Attribute Id in Class object
    jobject obj; //New object created
 
 
    //Create virtual machine
    // "-Djava.class.path=." This is what the JVM will look for and load Class file path
    options[0].optionString = "-Djava.class.path=."; 
    memset(&vm_args, 0, sizeof(vm_args));
    vm_args.version = JNI_VERSION_1_4; //vm_args.version is the version of Java
    vm_args.nOptions = 1; //vm_args.nOptions is the length of the parameter passed in
    vm_args.options = options; //Pass JavaVMOption to JavaVMInitArgs
 
 
    //Start the virtual machine
    status = JNI_CreateJavaVM(&jvm, (void**)&env, &vm_args);
    if (status != JNI_ERR){
        // First, get the class object (the JVM is started by itself in Java, but it can only be started manually in C + +. After starting, it will be the same as in Java, but use the syntax of C + +).
        cls = (*env)->FindClass(env, "Sample2");
        if (cls != 0){
            // Get the method ID and call the static method through the method name and signature
            mid = (*env)->GetStaticMethodID(env, cls, "sayHello", "(Ljava/lang/String;)Ljava/lang/String;");
            if (mid != 0){
                const char* name = "World";
                jstring arg = (*env)->NewStringUTF(env, name);
                jstring result = (jstring)(*env)->CallStaticObjectMethod(env, cls, mid, arg);
                const char* str = (*env)->GetStringUTFChars(env, result, 0);
                printf("Result of sayHello: %s\n", str);
                (*env)->ReleaseStringUTFChars(env, result, 0);
            }
 
 
            /*** Create a new object start***
            // Call default constructor
            //obj = (*env)->AllocObjdect(env, cls);
            // Call the specified constructor called < init >
            mid = (*env)->GetMethodID(env, cls, "<init>", "()V");
            obj = (*env)->NewObject(env, cls, mid);
            if (obj == 0){
                printf("Create object failed!\n");
            }
            /*** Create a new object***/
 
            // Get the attribute ID by attribute name and signature
            fid = (*env)->GetFieldID(env, cls, "name", "Ljava/lang/String;");
            if (fid != 0){
                const char* name = "icejoywoo";
                jstring arg = (*env)->NewStringUTF(env, name);
                (*env)->SetObjectField(env, obj, fid, arg); // modify attribute
            }
        
            // Call member method
            mid = (*env)->GetMethodID(env, cls, "sayHello", "()Ljava/lang/String;");
            if (mid != 0){
                jstring result = (jstring)(*env)->CallObjectMethod(env, obj, mid);
                const char* str = (*env)->GetStringUTFChars(env, result, 0);
                printf("Result of sayHello: %s\n", str);
                (*env)->ReleaseStringUTFChars(env, result, 0);
            }
 
            //We can see that static methods only need class objects and no instances, while non static methods need to use the objects we instantiated before
        }
 
        //Destroy the virtual machine after the operation
        (*jvm)->DestroyJavaVM(jvm);
        return 0;
    } else{
        printf("JVM Created failed!\n");
        return -1;
    }
}

Additional supplement: java String uses unicode, which is a double byte character, while C + + uses a single byte character

  1. For characters converted from C to java, use the newString UTF method:

jstring arg = (*env)->NewStringUTF(env, name);

  1. Characters converted from java to C, using GetStringUTFChars

const char* str = (*env)->GetStringUTFChars(env, result, 0);

Step 4: compile and run

Compile and run to complete the call of C + + to Java.

3, JS and C + + inter call: JS engine

First, we need to explain why we need to understand the injection principle of JS engine. Everyone should be familiar with JSBridge. Those who have done webview hybrid development will have a deeper understanding. It is a means and an abstract concept for realizing two-way communication between JS language and non JS language. The implementation methods of JSBridge include: Javascript interface, rewriting the original object of the browser, and Url scheme (i.e. Url interception). Either way, its performance is very unsatisfactory. In order for you to understand more clearly, these three methods will be explained below.

3.1 why not use Js to call Java: JSBridge directly

3.1.1 implementation of Js calling Java

  1. JavaScriptInterface

Before Android API 4.2, addjavascript interface was used, but there are serious security risks. Therefore, after Android 4.2, Google provided @ JavascriptInterface object annotation to establish the binding between JS objects and Java objects. The methods provided to JavaScript calls must have @ JavascriptInterface. The principle is to inject a namespace into the browser global object window through the addJavascriptInterface method provided by WebView, and then add some reflections that can operate java to the Web.

Principle implementation (source code analysis: https://www.cnblogs.com/aimqqroad-13/p/13893588.html )

Implementation principle of addjavascript interface: from WebView Addjavascript interface () starts with a series of calls

With and based on JNI, and finally through Chromium's IPC mechanism (interprocess communication IPC mechanism is based on

Unix Socket communication protocol), sent a message. The communication efficiency of this method is very low.

  1. Overwrite browser objects

This is mainly to modify some methods on the global object window in the browser, then intercept the parameters of fixed rules, and finally distribute them to the corresponding Java methods for processing. The following four methods are commonly used:

  • alert -- can be monitored by onJsAlert of webview
  • confirm -- can be monitored by onJsConfirm of webview
  • console.log -- can be monitored by onConsoleMessage of webview
  • prompt -- can be monitored by onJsPrompt of webview

This method has strong limitations and is not suitable for complex business development scenarios.

  1. URL scheme

It can intercept the URL request when jumping to the page, parse the scheme protocol, capture the behavior according to certain rules and submit it to Native(Android/Ios) for solution. The methods used by Android and iOS to intercept URL requests are:

  • shouldOverrideUrlLoading
  • delegate of UIWebView

To put it bluntly, it is similar to interception redirection, which is far from flexible enough.

3.1.2 implementation method of Java calling JS

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

3.2 communication between JS and C + + based on JS engine

Above, we analyze the implementation of JSBridge and get the running performance. Obviously, it can not meet the high-performance requirements of ReactNative for hybrid development. Therefore, ReactNative goes beyond Webview and makes a drastic use of its execution engine. Both the old RN architecture and the new RN architecture adopt the injection method to realize the communication between JS and Native(C + +). When it comes to the method of injecting into the engine, I believe everyone has used console Log (), setTimeout(), setInterval(), etc. these methods are injected into the JS engine through polyfill. The JS engine itself does not have these methods. Along this line of thinking, we can actually invoke the injected Native(C++) method in JS by injecting methods and variables into the engine.

Here, let's take JavaScript core as an example to learn how to inject methods and variables like JS engine. It provides rich API s for upper layer calls( See here for details ).

1. JavaScript core API data structure:

data typedescribe
JSGlobalContextRefJavaScript global context. That is, the execution environment of JavaScript.
JSValueRef: A value of JavaScript, which can be a variable, an object, or a function.
JSObjectRef: An object or function of JavaScript.
JSStringRef: A string for JavaScript.
JSClassRef: JavaScript class.
JSClassDefinition: JavaScript class definition. Using this structure, C and C + + can define and inject JavaScript classes.

2. Main functions of JavaScript core API:

APIdescribe
JSGlobalContextCreate,JSGlobalContextReleaseCreate and destroy JavaScript Global Context
JSContextGetGlobalObject: Get the Global object of JavaScript
JSObjectSetProperty,JSObjectGetPropertyProperty operations for JavaScript objects
JSEvaluateScriptExecute a JS script
JSClassCreateCreate a JavaScript class
JSObjectMakeCreate a JavaScript object
JSObjectCallAsFunctionCall a JavaScript function
JSStringCreateWithUTF8Cstring,JSStringReleaseCreate and destroy a JavaScript string
JSValueToBoolean,JSValueToNumber,JSValueToStringCopy,JSValueToObjectConvert JSValueRef to C + + type
JSValueMakeBoolean,JSValueMakeNumber,JSValueMakeStringConvert C + + type to JSValueRef

3. Calling JS with C + +

1. Create JS execution environment

//Create JS global context (JS execution environment)
JSGlobalContextRef context = JSGlobalContextCreate(NULL);

2. Get Global object

//Get Global object
JSObjectRef global = JSContextGetGlobalObject(context);

3. Get the global variables, global functions and global complex objects of JS

/**
 * Get the global variable of JS
 */

// Get the variable name to be called and convert it to JS string
JSStringRef varName = JSStringCreateWithUTF8CString("JS Variable name");

// Find and get the JS global variable varName from the global object global
JSValueRef var = JSObjectGetProperty(context, global, varName, NULL); 

// Manually destroy the variable name string to free up memory
JSStringRelease(varName);

// Convert JS variables to C + + types
int n = JSValueToNumber(context, var, NULL);



/**
 * Get the global function of JS
 */

//Get the function name to be called and convert it to JS string
JSStringRef funcName = JSStringCreateWithUTF8CString("JS Function name");

// Find and get the JS global function funcName from the global object Global
JSValueRef func = JSObjectGetProperty(context, global, funcName, NULL);

// Manually destroy the string to free up memory
JSStringRelease(funcName);

// Convert JS function to an object
JSObjectRef funcObject = JSValueToObject(context,func, NULL);

// Prepare the parameters with two values 1 and 2 as two parameters
JSValueRef args[2];
args[0] = JSValueMakeNumber(context, 1);
args[1] = JSValueMakeNumber(context, 2);

// Call JS function and receive the return value
JSValueRef returnValue = JSObjectCallAsFunction(context, funcObject, NULL, 2, args, NULL);

// Convert the return value of JS to C + + type
int ret = JSValueToNumber(context, returnValue, NULL);



/**
 * Get complex objects
 */

//Get the name of the JS object to be called and convert it to a JS string
JSStringRef objName = JSStringCreateWithUTF8CString("JS Object name");

// Find and generate an object from under global
JSValueRef obj = JSObjectGetProperty(context, global, objName, NULL); 

// Free memory
JSStringRelease(objName);

// Convert obj to object type
JSObjectRef object = JSValueToObject(context, obj, NULL);

// Get method name in JS complex object
JSStringRef funcObjName = JSStringCreateWithUTF8CString("JS Method name in object");

// Get functions in complex objects
JSValueRef objFunc = JSObjectGetProperty(context, object, funcObjName, NULL); 

//Free memory
JSStringRelease(funcObjName);

//Call the method of complex object, and the parameters and return values are omitted here
JSObjectCallAsFunction(context, objFunc, NULL, 0, 0, NULL);

From the above code for C + + calling JS, we can see that when calling, we first need to create a JS context environment, and then obtain a global object. All methods and variables that will be called by C + + will be mounted on the global object. We do a series of queries and type conversion through the API provided by JavaScript core, and finally complete the call to JS.

4. JS calling C + +: Engine Injection

To call C + +, JS must first inject variables, functions and classes on the C + + side into JS after type conversion (that is, it needs to be set as an attribute on the global object). In this way, there is a cross reference table on the JS side, so the call process can be initiated on the JS side.

Let's take an example to see how JS completes the call to C + + in the JSC layer. First, we define a C + + class and define a set of global functions from it, then encapsulate the call of JavaScript core to C + + class, and finally provide JSC with CallBack.

1. Define a C + + class

class test{         
public:
    test(){
        number = 0;
    };
    void func(){
        number++;
    }
    int number;
};

2. Define a variable g_test

test g_test;

3. Package to test Call to func()

JSValueRef testFunc(JSContextRef ctx, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef*){
    test* t = static_cast<test*>(JSObjectGetPrivate(thisObject));
    t->func();
    return JSValueMakeUndefined(ctx);
}

4. Package to test get operation of number

JSValueRef getTestNumber(JSContextRef ctx, JSObjectRefthisObject, JSStringRef, JSValueRef*){
    test* t = static_cast<test*>(JSObjectGetPrivate(thisObject));
    return JSValueMakeNumber(ctx, t->number);
}

5. Write a method to create JS class objects

JSClassRef createTestClass(){

    //There can be multiple member variable definitions of a class. The last one must be {0, 0, 0}. You can also specify a set operation
    static JSStaticValue testValues[] = {
        {"number", getTestNumber, 0, kJSPropertyAttributeNone },
        { 0, 0, 0, 0}
    };

    //There can be multiple member method definitions of a class. The last one must be {0, 0, 0}
    static JSStaticFunction testFunctions[] = {
        {"func", testFunc, kJSPropertyAttributeNone },
        { 0, 0, 0 }
    };

    //Define a class and set member variables and member methods
    static JSClassDefinition classDefinition = {
        0,kJSClassAttributeNone, "test", 0, testValues, testFunctions,
        0, 0, 0, 0,0, 0, 0, 0, 0, 0, 0
    };

    //Create a JS class object
    static JSClassRef t = JSClassCreate(&classDefinition);
    return t;
}

6. Inject JS into the global object global after C + + class conversion

// Create JS global context (JS execution environment)
JSGlobalContextRef ctx = JSGlobalContextCreate(NULL);

// Get global global object
JSObjectRef globalObj = JSContextGetGlobalObject(ctx);
 
// Create a new JS class object and bind it to g_test variable
JSObjectRef classObj = JSObjectMake(ctx, createTestClass(), &g_test);

// Get the name of the JS object to be called and convert it to a JS string
JSStringRef objName = JSStringCreateWithUTF8CString("g_test");

// Inject the newly created JS class object into JS (that is, mount classObj into the global object global as an attribute)
JSObjectSetProperty(ctx, globalObj, objName, classObj, kJSPropertyAttributeNone, NULL);

7. Complete calling in JS

g_test.func();
let n = g_test.number;
let t = new test;

4, New architecture JSI

JSI is the cornerstone of the new RN architecture to realize the communication between JS and Native, and Turbomodules is also implemented based on JSI. To understand the new architecture of RN, it is very important to understand JSI first. Let's talk about JSI.

1. What is JSI?

The full name of JSI is JavaScript Interface, that is, JS Interface. It encapsulates the mutual call between JS engine and Native (C + +), and realizes bilateral mapping through HostObject interface. Officially, it is also called mapping framework.

With this layer of encapsulation, ReactNative has two improvements:

  • You can switch engines freely, such as JavaScript core, V8, Hermes, etc.
  • In JS, C++ is injected into the JS engine. The data format is normalized through the HostObject interface. It abandons the asynchronous mechanism of using JSON as data in the old architecture, so that the call between JS and Native can be realized synchronously.

2. Relationship between JSI, JS, JS Runtime and Native(C + +)

Js runs in the Js Runtime. The so-called method injection is to inject the required methods into the Js Runtime. JSI is responsible for the specific injection and completes the injection of C + + methods through the API provided by the Js engine. The figure above is a simple architecture for Js and Native(C + +) to realize communication in the new JSI architecture.

Next, let's continue to understand how JSI implements intermodulation communication between JS and Native.

3. Practical application of JSI

Next, let's understand how JSI realizes the communication between JS and Native(C + +) through a practical example. First, let's take a look at the process of JS calling Native(C + +).

1. JS calls Native (C + +)

The steps are as follows:

1.1 preparation java file

package com.terrysahaidak.test.jsi;

public class TestJSIInstaller {

    // native method
    public native void installBinding(long javaScriptContextHolder);
    
    // stringField will be called by JS
    private String stringField = "Private field value";

    static {
        //Registration so dynamic library
        System.loadLibrary("test-jsi");
    }

    // runTest will be called by JS
    public static String runTest() {
        return "Static field value";
    }
}

1.2 preparation h file, implementation java, and declare a SampleModule object here, which is the implementation of TurboModule. SampleModule needs to inherit the I in JSI (i.e. HostObject interface, which defines the details of injection operation and the logic of bilateral data exchange), and implement the install method and get method.

#pragma once
#include <jni.h>
#include "../../../../../../node_modules/react-native/ReactCommon/jsi/jsi/jsi.h"

using namespace facebook;

extern "C" {
    JNIEXPORT void JNICALL
    Java_com_terrysahaidak_test_jsi_TestJSIInstaller_installBinding(JNIEnv* env, jobject thiz, jlong runtimePtr);
}

// Declare that SampleModule inherits HostObject and implements the install method
class SampleModule : public jsi::HostObject {
public:
    static void install(
            jsi::Runtime &runtime,
            const std::shared_ptr<SampleModule> sampleModule
    );
    // Every turbomodule -- all methods and properties in samplemodule need to be declared in get through declarative registration
    jsi::Value get(jsi::Runtime &runtime, const jsi::PropNameID &name) override;
private:
    JNIEnv jniEnv_;
};

1.3 write C + + files to realize the relevant logic of SampleModule

#include <jsi/jsi.h>
#include <jni.h>
#include "TestJSIInstaller.h"

// Virtual machine instance to obtain JNIenv environment
JavaVM *jvm;
// class instance
static jobject globalObjectRef;
// class object
static jclass globalClassRef;

// The specific implementation of the native method installBinding
extern "C" JNIEXPORT void JNICALL
Java_com_terrysahaidak_test_jsi_TestJSIInstaller_installBinding(JNIEnv *env, jobject thiz, jlong runtimePtr){
  // runtimePtr is a value of long type, which is strongly converted to Runtime type, that is, the JS engine represented
  auto &runtime = *(jsi::Runtime *)runtimePtr;
  
  // Instantiate SampleModule through smart pointer
  auto testBinding = std::make_shared<SampleModule>();

  // Call the install method of SampleModule,
  SampleModule::install(runtime, testBinding);

  // Get and store the virtual machine instance and store it to the & JVM
  env->GetJavaVM(&jvm);

  // Create an instance reference of a global object
  globalObjectRef = env->NewGlobalRef(thiz);
  
  //Create a global class object reference through the class path
  auto clazz = env->FindClass("com/terrysahaidak/test/jsi/TestJSIInstaller");
  globalClassRef = (jclass)env->NewGlobalRef(clazz);
}

// Specific implementation of install method
void SampleModule::install(jsi::Runtime &runtime, const std::shared_ptr<SampleModule> sampleModule){
  // Define the name of TurboModule, that is, the name used when calling on the JS side.
  auto testModuleName = "NativeSampleModule";
  
  // Create a HostObject instance, that is, a SampleModule instance
  auto object = jsi::Object::createFromHostObject(runtime, sampleModule);
  
  // Get the global object of the JS world through the global() method in runtime,
  // runtime is an instance of the JS engine Global() gets the global object of the JS world,
  // Then call setProperty() to inject "NativeSampleModule" into global,
  // This completes the export of "NativeSampleModule".
  // ***Note: runtime global(). Setproperty is a method implemented in JSI***
  runtime.global().setProperty(runtime, testModuleName, std::move(object));
}

// The get method of TurboModule starts to use "." on the JS side To call a method, it will execute here.
jsi::Value SampleModule::get(
    jsi::Runtime &runtime,
    const jsi::PropNameID &name){
  auto methodName = name.utf8(runtime);
  // Get the name of the member to be called and judge 
  if (methodName == "getStaticField"){
    // Creating HostFunction objects dynamically
    return jsi::Function::createFromHostFunction(
        runtime,
        name,
        0,
        [](
            jsi::Runtime &runtime,
            const jsi::Value &thisValue,
            const jsi::Value *arguments,
            size_t count) -> jsi::Value {
          // Here, the Java side method is called through reflection
          auto runTest = env->GetStaticMethodID(globalClassRef, "runTest", "()Ljava/lang/String;");
          auto str = (jstring)env->CallStaticObjectMethod(globalClassRef, runTest);
          const char *cStr = env->GetStringUTFChars(str, nullptr);
          return jsi::String::createFromAscii(runtime, cStr);
        });
  }
  if (methodName == "getStringPrivateField"){
    return jsi::Function::createFromHostFunction(
        runtime,
        name,
        0,
        [](
            jsi::Runtime &runtime,
            const jsi::Value &thisValue,
            const jsi::Value *arguments,
            size_t count) -> jsi::Value {
          auto valId = env->GetFieldID(globalClassRef, "stringField", "Ljava/lang/String;");
          auto str = (jstring)env->GetObjectField(globalObjectRef, valId);
          const char *cStr = env->GetStringUTFChars(str, nullptr);
          return jsi::String::createFromAscii(runtime, cStr);
        });
  }
  return jsi::Value::undefined();
}

1.4 calling the injection method in JS

<Text style={styles.sectionTitle}>
    {
      global.NativeSampleModule.getStaticField()
    }
</Text>
<Text style={styles.sectionTitle}>
    {/* this is from C++ JSI bindings */}
    {
      global.NativeSampleModule.getStringPrivateField()
    }
</Text>

Summary and analysis: TurboModule needs to be registered (injected) into the JS engine before it can be called by JS. After executing the static method install, it finally passes the runtime Global () injects it into the JS engine. The method of exporting JSI HostObject to JS is not exported in advance, but created in time by lazy loading. After entering the get function from JSI, first judge by methodName and dynamically create a HostFunction as the return result of get. In the HostFunction method, the Java method is called through reflection, which completes the communication process of JS calling Java through JSI.

Now let's take a look at the communication mode of Native (C + +) calling JS.

2. Native (C + +) calls JS

Native calls JS mainly through runtime in JSI global(). getPropertyAsFunction(jsiRuntime, "jsMethod"). Call (jsiruntime) method implementation. Then let's take a look at the whole process.

The implementation steps are as follows:

2.1 add a JS method to be called by Native in JS module (jsMethod())

import React from "react";
import type {Node} from "react";
import {Text, View, Button} from "react-native";

const App: () => Node = () => {
  
  // Waiting to be called by Native
  global.jsMethod = (message) => {
    alert("hello jsMethod");
  };

  const press = () => {
    setResult(global.multiply(2, 2));
  };
  return (
    <View style={{}}>
    </View>
  );
};

export default App;

2.2 Native calls JS global method

runtime
  .global()
  .getPropertyAsFunction(*runtime, "jsMethod")
  .call(*runtime, "message content!");

Note: we need to get the methods in JS through getPropertyAsFunction() in JSI, but we need to note that getPropertyAsFunction() gets a property or method under the global variable. Therefore, when we declare a method to be called by Native in JS, we need to explicitly specify its scope.

4. Comparison between JSI and JSC

First of all, we need to declare that JSC here refers to the encapsulation layer of JavaScript core engine in RN.

Similarities:

First of all, in terms of the underlying implementation, JSI and JSC both realize the communication between JS and Native by injecting methods into the JS engine. At the same time, the injected methods are also mounted on the JS global global object.

difference:

The injection objects handled by JSC in the old architecture are JSON objects and C + + objects, which involve complex and frequent type conversion. And in

In the design of asynchronous transmission of JSBridge, there are three threads of communication: UI thread, Layout thread and JS thread. In the typical example of blank pages when the list slides quickly, the low efficiency is obviously reflected.

For JSI, the asynchronous bridge is abandoned, and the transmitted data no longer depends on the JSON data format. Instead, the HostObject interface is used as a bilateral communication protocol to realize efficient information transmission under bilateral synchronous communication.

In addition, the way of writing NativeModule has changed compared with the old architecture. In addition to the functions, the logic needs to be completed in a C + + class. Therefore, the implementation of a TurboModule is divided into two parts: C + + & Java (OC).

epilogue

Here, we have explained the underlying principles of communication in the new and old RN architectures. If we summarize the embodiment of JSI efficiency improvement in one sentence, It can be said as follows: "JSI realizes the customization of the communication bridge and replaces the JSON data structure based on asynchronous bridge in the old architecture through the HostObjec interface protocol, so as to realize synchronous communication, avoid the cumbersome operation of JSON serialization and deserialization, and greatly improve the communication efficiency between JS and Native."

In the future, we will continue to share the relevant knowledge of communication process and communication module architecture.

discuss

1. JNI, JSBridge, RN -- what are JSBridge, JSI, JSC and JavaScript core, and what are their relationships and differences?

2. The relationship between global in Rn and global in JS engine, and the relationship between global, window and globalThis in RN project

3. The bridge in the old version is closed to the outside world, and we cannot participate in it. In the new architecture, we can freely define our own communication bridge through JSI?

Keywords: Android React Native ReactNative

Added by fallenangel1983 on Sun, 23 Jan 2022 08:29:22 +0200