3
头图

Image credit: https://unsplash.com/photos/gy08FXeM2L4

Author: Five Twenty

Cross-end communication

In the mobile terminal development scenario, the solution that can run APP on Android and iOS systems at the same time with one code is known as the cross-end solution. Webview and React Native are both cross-end solutions used by the cloud music front-end team. Although these solutions can improve development efficiency, they cannot directly call the system like native languages, so they are doing HTML5 (hereinafter referred to as H5). ) or React Native (hereinafter referred to as RN), developers often encounter situations where they want to call the Native capabilities. Native capabilities are written in native language and have their own running environment. RN pages are written in JS and have independent running environments. This call across the running environment is called cross-end communication.

webView jsb

The cross-end communication in H5 is called JSBridge . When making a JSBridge call, it will carry the call parameters. There are 4 parameters by default:

ModuleId: 模块 ID
MethodId: 方法 ID
params: 参数
CallbackId: JS 回调名

Among them, ModuleId and MethodId can locate the native method of the specific call, and the params parameter is used as the parameter of the native method call. Finally, the callback function of JS is called back through CallbackId , and H5 can get the call result from the callback function. This process mainly uses the ability of in the Webview container to intercept requests and clients to call JS functions. For example, in Android, the WebChromeClient.onJsPrompt method is usually used to intercept H5 requests, and the evaluateJavascript method is used to execute callbacks. But React Native didn't introduce the ability for Webview to implement these calls, it handled it in a completely different way. In addition, in the cloud music team's APP, there will be H5 and RN pages at the same time, that is, two cross-end communication methods coexist in the same APP, but the native method they call last comes from the same native module. This article mainly introduces the RN communication mechanism and bridging capability (hereinafter referred to as Bridge) from the RN implementation of the Android system, and combines the problems encountered in the above communication scenarios to explain how to implement a bridge available in a business. It consists of three parts. The first part introduces the different components in RN and their respective roles; the second part is the calling mode and specific examples between each module; the last part discusses the realization of Bridge in business.

RN composition

In RN, there are three important components: platform layer (Android or OC environment), bridge layer (C++) and JS layer.

  • The platform layer is responsible for rendering native components and providing various native capabilities, which are implemented by native languages;
  • The bridge module is responsible for parsing JS code, intermodulation between JS and Java/OC code, implemented by C++ language;
  • The JS layer is responsible for the specific business logic of cross-end pages.

rn 通信模块

Compared with the structure of Webview, the structure of RN has an additional layer of bridging layer, that is, the C++ layer. The article first introduces the function of this module and why there is such a module.

Bridge layer (C++ layer)

Like H5, React Native uses JS as the development language for cross-end pages, so it must have a JS execution engine. In the case of H5, Webview is the execution engine of JS, and Webview is also the rendering engine of the page. The difference between RN is that it already has its own rendering layer, and this function is handed over to the Java layer, because RN's JS component code will eventually be rendered into native components. So RN only needs a JS execution engine to run React code. The RN team chose JSCore as the execution engine of JS, while the external interface of JSCore was written in C and C++. Therefore, if the Java code/OC code of the platform layer wants to obtain the JS module and callback function through JSCore , it can only be obtained through the interface provided by C++, and C++ also has a good cross-end running function on iOS and Android systems. It is a good choice to choose it as the bridge layer.

JSCore

JSCore is the main module in the bridging layer. It is the JS engine in the RN architecture and is responsible for loading and parsing JS code. Let's take a look at its main API first:

JSContextGetGlobalObject:获取JavaScript运行环境的Global对象。
JSObjectSetProperty/JSObjectGetProperty:JavaScript对象的属性操作:set和get。
JSEvaluateScript:在JavaScript环境中执行一段JS脚本。
JSObjectCallAsFunction:在JavaScript环境中调用一个JavaScript函数

It can be seen from the API that developers can use JSEvaluateScript to execute a piece of JS code in the JSCore environment, or they can get the Global variable of the JS context through JSContextGetGlobalObject , and then convert it into a data structure that can be used by C++, manipulate it, and inject it into the API. And JSObjectSetProperty and JSContextGetGlobalObject are also two more important APIs, which will play a role in the communication process later.

Native modules and JavaScript modules

Speaking of communication, there must be a source and a sink in the whole process, that is, the sender and receiver of the message. In the communication of RN, they are the modules of Native and JS, and they provide the ability to each other with modules as the functional unit. , similar to the concept of ModuleID in the JSBridge protocol.

  • The Native module is a Java module under the Android system, which is implemented by the platform code. JS is called through the module ID (moduleID) and method ID (methodID), which are generally in the java/com/facebook/react/modules/ directory of the RN source code project, which can open the native system to the RN page. Capabilities, such as timer implementation module Timing , provide JS code with timer capabilities.
  • The JavaScript module is implemented by JS, and the code is in the /Libraries/ReactNative/ directory, such as the App startup module AppRegistery . For the Java environment, its function is to provide APIs for operating the JS environment, such as callbacks and broadcasts. The calling method of Java is the callFunctionReturnFlushedQueue API exposed by JS.

The JS environment will maintain a mapping of moduleID and methodID of all Native modules NativeModules , which is used to find the corresponding ID when calling the Native module; the Java environment will also maintain a mapping of JavaScript modules JSModuleRegistry , which is used to call JS code. In the actual code, the communication between the Native module and the JS module needs to pass through the transition of the middle layer, that is, the C++ layer, that is to say, the Native module and the JS module are actually only communicating with the C++ module.

C++ and JS communication

As mentioned above, JSCore allows C++ to get the global object of the JS runtime environment and manipulate its properties, and the JS code will inject some APIs needed by native modules into the global object, which is the main way JS provides C++ with operation APIs .

  • In the RN environment, JS will set the __fbBatchedBridge variable in the global object, and insert 4 APIs into the variable as the entry point for JS to be called. The main APIs include:

<!---->

callFunctionReturnFlushedQueue // 让 C++ 调用 JS 模块
invokeCallbackAndReturnFlushedQueue // 让 C++ 调用 JS 回调
flushedQueue // 清空 JS 任务队列
callFunctionReturnResultAndFlushedQueue // 让 C++ 调用 JS 模块并返回结果
  • JS also sets the __fbGenNativeModule method in global, which is used to generate the mapping object of the Java module in the JS environment after calling C++, that is, the NativeModules module. Its data structure is similar to (deviates from the actual data structure):

<!---->

{
    "Timing": {
        "moduleID": "1001",
        "method": {
            "createTimer": {
                "methodID": "10001"
            }
        }
    }
}
  • Through the mapping of NativeModules , developers can get moduleID and methodID of calling modules and methods, which will be mapped to specific Native methods during the calling process.

Similarly, C++ inserts several Native APIs into the global object through the JSObjectSetProperty method of JSCore, so that JS can call C++ modules through them. The main APIs are:

nativeFlushQueueImmediate // 立即清空 JS 任务队列
nativeCallSyncHook // 同步调用 Native 方法
nativeRequire  // 加载 Native 模块
  • When the API was introduced above, there are several APIs with similar functions, which is to clear the task queue of JS. That is because JS calls the Native module asynchronously, and it wraps the call parameters into a call task and puts it into the JS task MessageQueue , and then wait for the call from Native. The calling time is generally when the event is triggered, and the event will trigger the callback function of the Native callback JS. The Native module needs to call back the JS code through the four APIs of __fbBatchedBridge , and these four APIs have the function of flushedQueue : clear the task queue and execute all The task to consume the Native call task in the queue. However, if a certain call is a little long from the last flushedQueue behavior (usually greater than 5 ms), the logic of the immediate call will be triggered, and JS will call the nativeFlushQueueImmediate API to actively trigger task consumption.

Platform (Java) and C++ Communication

The mutual call between Java and C++ is through JNI (Java Native Interface). Through JNI, the C++ layer will expose some APIs to call the Java layer, so that Java can communicate with the JS layer. Here are some of the methods that C++ exposes to Java via JNI:

initializeBridge // 初始化:C++ 从 Java 拿到 Native 模块,作为参数传给 JS 生成 NativeModules
jniLoadScriptFromFile // 加载 JS 文件
jniCallJSFunction // 调用 JS 模块
jniCallJSCallback// 调用 JS 回调
setGlobalVariable // 编辑 global 变量
getJavaScriptContext // 获取 JS 运行环境
  • It can be basically judged from the above API that C++ is responsible for some middle-tier roles, including JS loading, parsing, and providing APIs for operating the JS operating environment;
  • The APIs that operate JS here will go to the four APIs of __fbBatchedBridge in the previous section. For example, jniCallJSFunction will call callFunctionReturnFlushedQueue . jniCallJSCallback will call invokeCallbackAndReturnFlushedQueue . As a result, the call links of the three modules are connected.

call example

Take the setTimeout method in RN as an example to walk through the calling process.

  • initialization process

RN setTimeout 初始化

  • Timing Class: The implementation class of delayed calls in Native, described as a Native module by the @reactModule decorator, and put into the ModuleRegistry mapping table when RN is initialized for subsequent call mapping.
  • After the ModuleRegistry mapping table is constructed, call the initializeBridge of C++ to register the ModuleRegistry module into the JS environment through the \__fbGenNativeModule function.
  • The JSTimer class in the JS code refers to the createTimer of the Timing module to implement setTimeout and delay the execution of the function.

     // 源代码位置:/Libraries/Core/Timers/JSTimers.js
     const {Timing} = require('../../BatchedBridge/NativeModules');
    
     function setTimeout(func: Function, duration: number, ...args: any): number {
        // 创建回调函数
        const id = _allocateCallback(
            () => func.apply(undefined, args),
            'setTimeout',
        );
        Timing.createTimer(id, duration || 0, Date.now(), /* recurring */ false);
        return id;
    },
    
  • The calling process of setTimeout

RN setTimeout 调用

  • When setTimeout is called in JSTimer.js, find the moduleID and methodID of Timing Class through NativeModules, and put it into the task queue MessageQueue ;
  • Native clears MessageQueue queue through events or active triggers, and the C++ layer passes the moduleID, methodID and other call parameters to ModuleRegistry , which finds the code of the Native module, the Timing class;
  • Timing calls createTimer method to call the system timing function to implement the delayed call;
  • When the timer ends, the Timing class needs to call back the JS function

    // timerToCall 是回调函数的 ID 数组
    getReactApplicationContext().getJSModule(JSTimers.class)
        .callTimers(timerToCall);
    
  • getJSModule method will find the JS module to be called through JSModuleRegistry and call the corresponding method. In this process, the callTimers method of the JSTimers module will be called.
  • The Java code calls the JS module through C++ through the JNI interface jniCallJSFunction , and passes in module: JSTimers and method: callTimers ;
  • C++ calls the callFunctionReturnFlushedQueue API exposed by JS, brings module and method, and returns to the calling environment of JS;
  • JS executes the callFunctionReturnFlushedQueue method to find the callTimers function of the JSTimer module registered in the RN initialization phase and calls it. After the call is complete, clear the task queue MessageQueue .

RN's JSBridge

The above has gone through the communication process between Java code and JS code in RN through the setTimeout function of RN. In short, Java modules and JS modules can call each other through NativeModules and JS callback functions to achieve a cross-end call. However, the bridge in the business needs to include some additional scenarios, such as concurrent calls, event monitoring, etc.

  • Concurrent call: Similar to sending multiple requests at the same time on the web side, in order to call back the request result to the correct callback function, it is necessary to save a request-to-callback function mapping, which is the same in the Bridge call. This mapping can be maintained in JS code or in Native code. In the cross-end solution, when both are feasible, the JS code solution is generally chosen to maintain flexibility. Native is only responsible for processing the result and calling back .
  • Event monitoring: For example, JS code monitors whether the page switches to the background, the same callback function should be called multiple times when the page switches to the background multiple times, but RN's JSCallback is only allowed to be called once (each callback instance will bring Callback is obviously not suitable for this scenario, Cloud Music's Bridge uses RN's event notification: RCTDeviceEventEmitter instead of callback. RCTDeviceEventEmitter is an event subscription distribution module implemented by pure JS. The Native module can get its method through getJSModule , so it can send a JS event on the Native side and bring the callback parameters and mapping ID, etc., without going through JSCallback.

Back to the previous question: how to implement RN's Bridge, so that a Bridge's API can support H5 and RN calls at the same time. Because most of the business scenarios of H5 and RN are the same, such as obtaining user information user.info and device information device.info similar interfaces will be used in both H5 and RN. In addition to the consistent cross-end calling protocol, the specific implementation modules and protocol parsing modules can be reused. The difference is the call link. The main modules in the RN link include:

  • The NativeModule called by the JS code, as the call entry, the JS code calls the method exposed by it to pass in the call parameters and start the call process, but the module does not parse the protocol and parameters, which can be called RNRPCNativeModule ;
  • After the Native module is processed, RNRPCNativeModule uses RCTDeviceEventEmitter to generate an event callback to the JS code with the execution result.

In addition to the above two different modules, other modules can be reused, such as protocol parsing and task distribution modules, parsing protocol calling modules, methods, parameters, etc., and distribute them to specific Native modules; and The specific function implementation modules of Native can be kept consistent.

Combined with the calling process described above, if the developer calls the User.info 0621c9d309262d to obtain user information, the calling process is as follows:
User.info 调用

This kind of processing can ensure that H5 and RN can use the same moduleID and methodID to call Native functions, and ensure that they are processed in the same module. From the developer's point of view, a Bridge API can support both H5 and RN calls.

above.

Relevant information

This article is published from the NetEase Cloud Music technical team, and any form of reprinting of the article is prohibited without authorization. We recruit various technical positions all year round. If you are ready to change jobs and happen to like cloud music, then join us at grp.music-fe(at)corp.netease.com!

云音乐技术团队
3.6k 声望3.5k 粉丝

网易云音乐技术团队