Image source: unsplash.com/photos/gy08…

Author: fifty-two

Across the communication

In the mobile development scenario, the solution that can run the APP on Both Android and iOS systems using a single piece of code is known as cross-terminal solution. Webview and React Native are cross-end solutions used by large front-end teams of cloud music. Although these solutions can improve development efficiency, they cannot directly call the system like Native languages. As a result, developers often have to invoke Native capabilities when doing HTML5 (H5) or React Native (RN) requirements. Native capabilities are written in Native languages and have their own runtime environment. RN pages are written in JS and have an independent runtime environment. Such calls across runtime environments are called cross-end communication.

Cross-end communication in H5 is called JSBridge. When a JSBridge call is made, it carries the call parameters. By default, there are four parameters:

ModuleId: ModuleId MethodId: MethodId params: parameter CallbackId: JS callback nameCopy the code

ModuleId and MethodId can locate the specific native method called, and params parameter is used as the parameter of the native method called. Finally, H5 can get the call result from the JS callback function by CallbackId. This process is mainly use the Webview container’s ability to intercept the request and the client calls JS function, such as android WebChromeClient. Is usually used in onJsPrompt method to intercept the request of the H5, The evaluateJavascript method is used to perform the callback. But React Native doesn’t have the ability to implement these calls with a Webview; it handles them in a completely different way. In addition, in the APP of cloud music team, there will be H5 and RN pages at the same time, that is, two cross-end communication modes coexist in the same APP, but the native method they finally call comes from the same native module. This paper mainly introduces RN’s communication mechanism and Bridge capability (hereinafter referred to as Bridge) from the RN implementation of Android system, and explains how to realize a Bridge available in business based on the problems encountered in the above communication scenarios. It consists of three parts. First, it introduces the different constituent modules and their roles in RN. The second part is between each module call way and concrete example; The final section explores the implementation of Bridges in the business.

An RN

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

  • The platform layer is responsible for rendering native components and providing a variety of native capabilities, implemented in native languages;
  • Bridge module is responsible for parsing JS code, JS and Java/OC code intermodulation, implemented by C++ language;
  • The JS layer is responsible for the specific business logic of the cross-page.

RN has a bridge layer, a C++ layer, compared to Webview. This article first introduces the function of this module, and why there is such a module.

Bridge layer (C++ layer)

React Native, like H5, uses JS as the cross-page development language, so it must have a JS execution engine. In the case of H5, Webview is the JS execution engine, and the Webview is also the page rendering engine. RN is different in that it already has its own rendering layer, which is handed over to the Java layer because RN’s JS component code ends up being rendered as native components. RN therefore only needs a JS execution engine to run the React code. The RN team chose JSCore as the JS execution engine, and JSCore’s external interface is written in C and C++. Therefore, the platform layer Java code/OC code to get JS module and callback function through JSCore, can only be obtained through the interface provided by C++, plus C++ in iOS and android system also has a good cross-terminal running function, choose it as a bridge layer is a good choice.

JSCore

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

JSContextGetGlobalObject: Gets the Global object of the JavaScript runtime environment. JSObjectSetProperty/JSObjectGetProperty: JavaScript object attribute of the operation: set and get. JSEvaluateScript: Executes a JavaScript script in a JavaScript environment. JSObjectCallAsFunction: Calls a JavaScript function in a JavaScript environmentCopy the code

JSContextGetGlobalObject (JSContextGetGlobalObject, JSContextGetGlobalObject, JSContextGetGlobalObject, JSContextGetGlobalObject, JSContextGetGlobalObject) Then convert it into a data structure that C++ can use and manipulate it, injecting the API. JSObjectSetProperty and JSContextGetGlobalObject are two more important apis that will come into play later in the communication process.

Native modules and JavaScript modules

Speaking of communication, there must be source and home, namely sender and receiver of messages, in the whole process. In RN communication, they are Native and JS modules, and they provide capabilities to each other in modules, similar to the concept of ModuleID in JSBridge protocol.

  • Native module is Java module in Android system, which is implemented by platform code. JS is called by moduleID (moduleID) and methodID (methodID), generally in RN source engineeringjava/com/facebook/react/modules/RN pages can be exposed to native system capabilities, such as timer implementation modulesTimingThe ability to provide timers for JS code.
  • JavaScript modules are implemented by JS, code in/Libraries/ReactNative/Directory, such as App startup moduleAppRegisteryFor the Java environment, it provides apis for manipulating the JS environment, such as callbacks, broadcasts, and so on. Java’s calling methods are exposed through JScallFunctionReturnFlushedQueueAPI.

A mapping of moduleIDS and methodID of all Native modules will be maintained in the JS environment, which is used to find the corresponding ids when calling Native modules. The Java environment also maintains a mapping of JavaScript modules, JSModuleRegistry, which calls JS code. However, in actual code, the communication between Native module and JS module needs to pass the transition of the middle layer, that is, the C++ layer. In other words, Native module and JS module are actually only communicating with C++ module.

C++ and JS communication

As mentioned above, JSCore allows C++ to take the JS runtime’s global object and manipulate its properties. JS code will inject some API required by native modules into the global object. This is the main way that JS provides the manipulation API to C++.

  • JS in an RN environment is set in a global object__fbBatchedBridgeVariable, and in the variable into the 4 API, as the entry to be called JS, the main API includes:
CallFunctionReturnFlushedQueue / / make call JS module invokeCallbackAndReturnFlushedQueue / / c + + c + + call JS callback flushedQueue / / Empty the JS task queue callFunctionReturnResultAndFlushedQueue / / c + + call JS module and returns the resultCopy the code
  • JS is also set in global__fbGenNativeModuleMethod used to generate a Java module mapping object in the JS environment after C++ calls, that isNativeModulesThe module. Its data structure is similar to (deviated from) the actual data structure:
{
    "Timing": {
        "moduleID": "1001",
        "method": {
            "createTimer": {
                "methodID": "10001"
            }
        }
    }
}
Copy the code
  • throughNativeModulesDevelopers can get access to calling modules and methodsmoduleIDmethodID, will be mapped to a specific Native method in the call process.

Similarly, C++ uses JSCore’s JSObjectSetProperty method to stuff several Native apis into the global object so that JS can call C++ modules through them. The main apis are:

NativeFlushQueueImmediate / / immediately to empty JS task queue nativeCallSyncHook synchronous call Native methods nativeRequire / / / / loading Native modulesCopy the code
  • When the API is introduced above, there are several apis with similar functions, namely, emptying JS task queue. That is because JS calls Native modules asynchronously, and it will wrap the call parameters into a call task and put them into JS task queueMessageQueue, and wait for Native to call. The call time is generally when the event is triggered, which will trigger the callback function of Native callback JS, and the Native module needs to pass__fbBatchedBridgeFour API callbacks to JS code, and all four apis have themflushedQueueConsume Native call tasks in the queue by emptying the task queue and executing all tasks. But if one call is a distance from the last oneflushedQueueIf the behavior is too long (typically greater than 5 ms), it triggers an immediate invocation of logic, a JS callnativeFlushQueueImmediateAPI, which actively triggers task consumption.

Platform (Java) and C++ communication

Java and C++ call each other through JNI (Java Native Interface), through JNI, C++ layer will expose some API to the Java layer to call, so that Java can communicate with JS layer. Here are some of the ways C++ exposes Java via JNI:

InitializeBridge // Initialize: C++ gets Native modules from Java, Generate NativeModules jniLoadScriptFromFile // load JS file jniCallJSFunction // call JS module jniCallJSCallback// call JS callback SetGlobalVariable // Edit the global variable getJavaScriptContext // to get the JS runtime environmentCopy the code
  • C++ is responsible for the role of some middle layer, JS loading, parsing work, as well as provide API operation JS running environment;
  • All of the apis that operate on JS here are going to go back to the previous section__fbBatchedBridgeFour apis, such asjniCallJSFunctionWill be calledcallFunctionReturnFlushedQueue.jniCallJSCallbackWill be calledinvokeCallbackAndReturnFlushedQueue. Thus, the call links of the three modules are connected.

Invoke the sample

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

  • Initialization process

  • Timing Class: the implementation Class for delayed calls in Native, described by the @reactModule decorator as a Native module, is put into the ModuleRegistry mapping table when RN initializes for subsequent call mappings.

  • After the ModuleRegistry mapping table is constructed, call the C++ initializeBridge to register the ModuleRegistry module into the JS environment through the __fbGenNativeModule function.

  • The JSTimer class references the createTimer of the Timing module to implement setTimeout and delay the execution of the function.

    / / the source location: / Libraries/Core/Timers/JSTimers js const {Timing} = the require (".. /.. /BatchedBridge/NativeModules'); function setTimeout(func: Function, duration: number, ... Args: any): number {// Create callback const id = _allocateCallback(() => func.apply(undefined, args), 'setTimeout',); Timing.createTimer(id, duration || 0, Date.now(), /* recurring */ false); return id; },Copy the code
  • Call procedure of setTimeout

  • When setTimeout is called in jstimer. js, find Timing Class moduleID and methodID via NativeModules and place them in task queue MessageQueue.

  • Native emptying the MessageQueue queue via events or active triggers. The C++ layer gives ModuleRegistry the moduleID, methodID, and other call parameters to find the Native module code.

  • Timing Call createTimer to delay the call.

  • When the Timing ends, the Timing class needs to call back the JS function

    / / timerToCall callback function is the ID of the array getReactApplicationContext () getJSModule (JSTimers. Class). CallTimers (timerToCall);Copy the code
  • The getJSModule method finds the JS module to call through JSModuleRegistry and invokes the corresponding method. In this process, the callTimers method of the JSTimers module is called.

  • The Java code calls the JS module via the JNI interface jniCallJSFunction through C++, passing in module: JSTimers and method: callTimers;

  • C + + call JS exposed callFunctionReturnFlushedQueue API, take the module and method, return to JS calls the environment;

  • JS callFunctionReturnFlushedQueue execution method to find an RN initialization phase registered good JSTimer callTimers function module, the calling. After the call, clear the task queue MessageQueue.

The RN JSBridge

The above has gone through the communication flow of Java code and JS code in RN through setTimeout function of RN. In simple terms, Java modules and JS modules can make a cross-end call via NativeModules and JS callbacks to each other. But Bridges in business need to include some additional scenarios, such as concurrent calls, event listening, and so on.

  • Concurrent invocation: Similar to making multiple requests at the same time on the Web side, in order to call back the result of the request to the correct callback function, a map of the request-to-callback function needs to be saved. The same is true for Bridge invocation. This mapping can be maintained in JS code or Native code. In cross-end schemes, if both are feasible, JS code schemes are generally selected to maintain flexibility, and Native is only responsible for processing results and callback.

  • Event listening: The same callback function should be called multiple times when the page is switched to the background. However, RN’s JSCallback allows only one callback (each callback instance is marked with whether it was called or not), so callbacks are obviously not suitable for this scenario. Instead of callbacks, the Bridge for cloud music uses RN’s event notification: RCTDeviceEventEmitter. RCTDeviceEventEmitter is a pure JS implementation of event subscription distribution module. Native module can get its method through getJSModule, so it can emit a JS event with callback parameters and mapping ID, etc. Instead of going to JSCallback.

Back to the question of how to implement a Bridge to RN so that a Bridge API supports both H5 and RN calls. Most service scenarios of H5 and RN are the same, such as obtaining user information user.info and device information device.info interfaces are used in BOTH H5 and RN. In addition to the cross-call protocol to keep consistent, the specific implementation module, protocol parsing module can be reused. The difference is the call link. The main modules in an RN link include:

  • The NativeModule for the JS code calls, which act as the call entry. The JS code calls the exposed method passing in the call parameters and starting the call process, but the module does not resolve the protocol and parametersRNRPCNativeModule;
  • After the Native module is processed,RNRPCNativeModuleuseRCTDeviceEventEmitterGenerates an event callback to the JS code with the result of execution.

In addition to the above two different modules, other modules can be reused, such as the protocol parsing and task distribution module, the call module of the parsing protocol, methods and parameters, etc., which can be distributed to specific Native modules. There are also specific function implementation modules of Native, which can be kept consistent.

With the invocation process described above, developers can callUser.infoThis JSBridge is invoked to get user information as follows:

This process ensures that H5 and RN can use the same moduleID and methodID to call Native functions, and that processing is performed in the same module. From a developer’s point of view, this is a Bridge API that supports both H5 and RN calls.

The above.

The relevant data

  • React Native

  • The React Native module interacts with the JS module

  • Handler relationship with Looper and MessageQueue

  • React Native communication mechanism

  • React Native

  • How React Native constructs app layouts

This article is published by NetEase Cloud Music Technology team. Any unauthorized reprinting of this article is prohibited. We recruit technical positions all year round. If you are ready to change your job and you like cloud music, join us!