Author: Xiong Wenyuan, Byte Game Client team
Client cross-end frameworks have been developed for many years. Recently popular aprons, Flutter and ReactNative are all successful and mature frameworks, which are targeted at different developers and widely used by many large apps. I was fortunate to participate in learning and using these excellent cross-end solutions early. In recent years of development and architecture design, in addition to supporting tens of millions of DAU in the App, ReactNative cross-terminal scheme was gradually applied to the game to improve the efficiency of development and iteration. In this article, we are going to introduce some of our explorations and practices in games in five chapters, which you can learn from:
- Part 1: Background on using ReactNative in games
- How to integrate ReactNative into a game
- ReactNative performance optimization in games
- ReactNative Hermes Engine Introduction
- Chapter 5: Introduction to ReactNative’s new architecture
(This is the last part of the series.)
The previous chapter introduced some practices of using ReactNative in the game. Through continuous iteration, we completed the construction of the game platform, and the overall performance and stability have reached the optimal, which can be regarded as a relatively mature platform. Of course, this platform is also suitable for the current client development, with low integration cost. However, the design flaws of the framework itself cannot be solved. In complex and highly interactive UI scenarios, rendering bottlenecks are obvious and can be deeply experienced in the game.
In June 2018, Facebook officially announced plans and roadmap for a massive refactoring of ReactNative. The goal is to make ReactNative more lightweight and adaptable to hybrid development, approaching or even matching the native experience. It has been a long time since the article was written, and the author has been busy with other things, so there are few updates on the new progress. Moreover, at the beginning, I only made a preliminary analysis of the design idea of Facebook. After such a long time of iteration, the new architecture has made a lot of progress, or it is infinitely close to the official release, which is worth sharing with everyone. This article will give you a deeper look at the current status and development process of the new architecture.
Here’s a quick overview of some of the changes in principle. Here’s a comparison of the old and new architectures:
I believe you can also find some differences. The communication between JS layer and Native layer of the original architecture relies too much on Bridge, and it is asynchronous communication, which makes it difficult to realize some interactions and design with high communication frequency, and also affects the rendering performance. However, it is from this point that the new architecture makes a lot of changes to bridge layer. The UI and API calls can be adjusted from the original asynchronous mode to synchronous or asynchronous communication with Native, which solves the bottleneck problem of frequent communication.
-
Old Architecture design
Before we get to the new architecture, let’s talk about how the current ReactNative framework works so you can understand the overall architecture design and why Facebook is redesigning the entire framework:
- ReactNative adopt the way of the front and the UI is rendering the native components, he provides apis and UI components at the same time, also facilitate developers design, expand their own API, provided ReactContextBaseJavaModule, ViewGroupManager, The UI of ReactNative is managed by UIManger. In fact, it is UIManagerModule on the Android side. In principle, it is also a BaseJavaModule and shares a native Module with the API.
- ReactNative page all API and UI components are managed by ReactPackageManger, engine initialization instanceManager process will read the injected package, and generate corresponding NativeModule and Views according to the name, This is just for the Java layer, and actually generates JNativeModule in the C++ layer
- Switching to the part of the above architecture diagram, the function of Native Module is to get through the API call from the front end to the Native end. The front end code runs in the JSC environment and is implemented in C++. In order to get through the Native call, it needs to be injected into the global environment before running. The front-end uses the Global object to manipulate the Proxy NativeModule, and then executes the JNativeModule
- After the front-end code Render generates the UI diff tree, the UIManager call to the native end is completed through ReactNativeRenderer. The specific API is as follows, which is mainly used to inform the native end to create, update View, batch management components, measure height, width, etc. :
- After the above series of API operations, the shadow Tree will be generated in the native end, which is used to manage the relationship of each node, which is one-to-one corresponding to the front end. Then, after the overall UI refresh, these UI components will be updated to ReactRootView
From the above analysis, it is not difficult to find that the current architecture is strongly dependent on Nativemodule, also known as bridge. For simple Native API calls, the performance is acceptable. However, for UI, every operation, including high calculation, update, etc., needs to use bridge. In addition, Bridge limits the call frequency and only allows asynchronous operations. As a result, it is difficult for some front-end updates to be timely reflected on the UI, especially for operations such as sliding and animation, which have high update frequency, so they often see a blank screen or lag.
-
New Architecture Design
The communication between JS layer and Native in the old architecture was too dependent on bridge, which made it difficult to realize some interaction and design with high communication frequency, and also affected the rendering performance. This is the main goal of This reconstruction of Facebook. In terms of the new design, ReactNative proposed several new concepts and designs:
- JSI(javascript Interface) : This is the core focus of this architecture reconstruction. It is precisely because of the adjustment of this layer that the original heavily dependent Native Bridge architecture is decoupled and free communication is realized.
- Fabric: relies on the design of JSI, and moves the shadow tree layer under the old architecture to the C++ layer. In this way, through JSI, the front-end component can achieve one-to-one control of UI components, and get rid of the asynchronous and batch operation of UI under the old architecture.
- TuborModule: a new native API architecture, replacing the original Java Module architecture, data structure in addition to support the basic types, began to support JSI objects, so that the front-end and client API form a one-to-one call
- Community: In the process of continuous iteration, the Facebook team found that the open source community provided more and more components and apis, and many of the components were better in design and architecture than ReactNative. Moreover, the official component was not invested enough due to resource problems, and the feedback, response and solution of some community problems were not timely. After the community, a large number of system components will be open to the community, for developers to maintain, such as the current WebView component
In fact, the above concepts have been reflected in the architecture diagram, which is mainly used to replace the original Bridge design. Now we will focus on analyzing the principle and function of these modules:
JSI :
JSI, which has been supported since version 0.60, is an adaptive architecture designed by Facebook on top of the JS engine that allows us to register Javascript interfaces to Javascript runtimes for methods that are available through global objects in the Javascript world, It can be written entirely in C++ or as a way to communicate with Objective C code on iOS and Java code on Android. Any native module that currently uses Bridge to communicate between Javascript and native can be converted into a JSI module by writing a simple layer in C++
- Standardized JS engine interface, ReactNative can replace v8, Hermes and other engines.
- It is a bridge between JS and native Java or Objc, similar to the role of the old JSBridge architecture, but the difference is that it adopts the way of memory sharing and proxy class. All the RUNNING environment of JS is under the JSRuntime environment. In order to achieve direct communication with the native end, We need to have a C++ layer implementation of JSI::HostObject, this data structure only get, set two interfaces, through prop to distinguish the call of different interfaces.
- Json and basic type data are more used in the original JS and Native data communication, but with JSI, the data types are richer and JSI Object is supported.
So the API call flow: JS->JSI->C++->JNI->JAVA, each API is more independent, no longer all rely on native module, but this also brings another problem, compared to the previous design is more complex, to design an API, developers need to package JS, C++, JNI, JAVA and other sets of interfaces. Of course, Facebook has this in mind, so when designing JSI, we provided a CodeGen module to help you with the basic code and environment setup. Here’s how to use these tools:
- Facebook provides a scaffolding project for creating Native Modules by adding NPX commands in advance
npx create-react-native-library react-native-simple-jsi
Copy the code
The previous steps are more in the configuration of some module information, it is worth noting that in the choice of module development language to pay attention to, here is to support a lot of types, for the native end development we use Java&OC more, you can also choose pure JS or C++ type, according to their own actual situation to choose, After completion, we need to choose UI Module or API Module. Here we choose API (Native Module) for testing:
The above is the directory structure after completion. You can see that this is a complete ReactNative App project, and the corresponding API needs to be developed by developers in the corresponding Android and iOS directories.
C++ Moulde has more CPP modules than Java and loads so in a Native lib in Moudle.
- In fact, we have not created the JSI module so far. After deleting the example directory, run the following command to import example/ Android in Android studio. After compiling the app project, we can package the C++ files in our CPP directory into so
npx react-native init example cd example yarn add .. /Copy the code
- Here we have the C++ library packaged, but it is not the JSI Module we want, we need to modify the Module Module, the code is as follows, we can see from the code, no longer reactmethod flag, but some direct install methods, Call the injection environment when the JSI Module is created
public class NewswiperJsiModule extends ReactContextBaseJavaModule {
public static final String NAME = "NewswiperJsi";
public NewswiperJsiModule(ReactApplicationContext reactContext) {
super(reactContext);
}
@Override
@NonNull
public String getName(a) {
return NAME;
}
static {
try {
// Used to load the 'native-lib' library on application startup.
System.loadLibrary("cpp");
} catch (Exception ignored) {
}
}
private native void nativeInstall(long jsi);
public void installLib(JavaScriptContextHolder reactContext) {
if(reactContext.get() ! =0) {
this.nativeInstall(
reactContext.get()
);
} else {
Log.e("SimpleJsiModule"."JSI Runtime is not available in debug mode"); }}}Copy the code
public class SimpleJsiModulePackage implements JSIModulePackage {
@Override
public List<JSIModuleSpec> getJSIModules(ReactApplicationContext reactApplicationContext, JavaScriptContextHolder jsContext) {
reactApplicationContext.getNativeModule(SimpleJsiModule.class).installLib(jsContext);
returnCollections.emptyList(); }}Copy the code
- CreateFromHostFunction creates a JSI proxy Object that communicates directly with JS. The JSI proxy Object is created using createFromHostFunction and injected into the JS runtime with global().setProperty
void install(Runtime &jsiRuntime) {
auto multiply = Function::createFromHostFunction(jsiRuntime,
PropNameID::forAscii(jsiRuntime,
"multiply"),
2,
[](Runtime &runtime,
const Value &thisValue,
const Value *arguments,
size_t count) -> Value {
int x = arguments[0].getNumber(a);int y = arguments[1].getNumber(a);return Value(x * y);
});
jsiRuntime.global().setProperty(jsiRuntime, "multiply".move(multiply));
Copy the code
global.multiply(2.4) / / 8
Copy the code
Now that you know how to build a JSIMoudle using JSI, this is the core underlying design of TurboModule and Fabric design.
Fabric :
Fabric is the UI framework of the new architecture, which is similar to the original UImanager framework. The previous chapter also explains some problems of the UImanager framework, especially the bottleneck in rendering performance. It seems that it is difficult to optimize based on the original architecture, and there is still a big gap between the rendering performance of the original components and animation. For example, under the state of Flatlist sliding rapidly, there will be a long white screen time, and interactive animations and gestures are difficult to support, which is also the focus of this architecture upgrade. Here we will briefly explain the features of the new architecture from the principle:
- FabricUIManager is designed to support Fabric render for component updates. It uses JSI to communicate with the CPP layer, corresponding to the C++ UIManagerBinding. Each operation and API call creates a different JSI. From here, the original problem of completely relying on a single Native bridge of UIManager is completely removed. Meanwhile, measure of component size can get rid of the dependence on Java and bridge and be directly shadow completed in C++ layer to improve rendering efficiency
export type Spec = {|
+createNode: (reactTag: number, viewName: string, rootTag: RootTag, props: NodeProps, instanceHandle: InstanceHandle,) = > Node,
+cloneNode: (node: Node) = > Node,
+cloneNodeWithNewChildren: (node: Node) = > Node,
+cloneNodeWithNewProps: (node: Node, newProps: NodeProps) = > Node,
+cloneNodeWithNewChildrenAndProps: (node: Node, newProps: NodeProps) = > Node,
+createChildSet: (rootTag: RootTag) = > NodeSet,
+appendChild: (parentNode: Node, child: Node) = > Node,
+appendChildToSet: (childSet: NodeSet, child: Node) = > void,
+completeRoot: (rootTag: RootTag, childSet: NodeSet) = > void,
+measure: (node: Node, callback: MeasureOnSuccessCallback) = > void,
+measureInWindow: (node: Node, callback: MeasureInWindowOnSuccessCallback,) = > void,
+measureLayout: (
node: Node,
relativeNode: Node,
onFail: () => void,
onSuccess: MeasureLayoutOnSuccessCallback,
) = > void,
+configureNextLayoutAnimation: (
config: LayoutAnimationConfig,
callback: () => void.// check what is returned here
// This error isn't currently called anywhere, so the `error` object is really not defined
// $FlowFixMe[unclear-type]
errorCallback: (error: Object) = >void.) = > void,
+sendAccessibilityEvent: (node: Node, eventType: string) = > void|};constFabricUIManager: ? Spec =global.nativeFabricUIManager;
module.exports = FabricUIManager;
Copy the code
if (methodName == "createNode") {
return jsi::Function::createFromHostFunction(
runtime,
name,
5,
[uiManager](
jsi::Runtime &runtime,
jsi::Value const &thisValue,
jsi::Value const *arguments,
size_t count) noexcept -> jsi::Value {
auto eventTarget =
eventTargetFromValue(runtime, arguments[4].arguments[0]);
if(! eventTarget) { react_native_assert(false);
return jsi::Value::undefined(a); }return valueFromShadowNode(
runtime,
uiManager->createNode(
tagFromValue(arguments[0]),
stringFromValue(runtime, arguments[1]),
surfaceIdFromValue(runtime, arguments[2]),
RawProps(runtime, arguments[3]),
eventTarget));
});
}
Copy the code
- With JSI, UI operations that previously relied on bridge in batches can be executed synchronously to the c++ layer. In the c++ layer, the new architecture completes the construction of a shadow layer, while the old architecture is implemented in the Java layer. The following are some important designs:
- FabricUIManager (JS, Java), JS side and native side UI management module.
- UIManager/UIManagerBinding (c + +), c + + is used to manage the UI module, and through the binding of JNI way through FabricUIManager native components (Java) management
- ComponentDescriptor (C++), a unique description of the native component and component property definition, registered in the CoreComponentsRegistry module
- Platform – the speci fi c
- Component Impl (Java, ObjC++), native Surface Component, managed via FabricUIManager
- In the new architecture, to develop a native component, you need to complete the Java layer native component and ComponentDescriptor (C++) development, which is more difficult than the original viewManager, but the ComponentDescriptor itself is a lot of shadow layer code, Facebook will also provide codeGen tools to automate code generation and simplify code generation
TurboModule:
TurboModule is already supported in version 0.64. Before we look at how TurboModule works, let’s explain why this module is designed to replace an important part of NativeModule.
- NativeModule contains many oF the apis that we need to register as part of the initialization process. As the development iteration progresses, the API and package that rely on NativeMoude become more and more numerous, and the time it takes to resolve and verify the Pakcages becomes longer, eventually affecting the TTI duration
- In addition, most Native Modules actually provide API services. In fact, they can be run in single-example mode instead of being created many times following the closing and opening of bridge
TurboModule is designed to address these issues. In principle, TurboModule uses the capabilities provided by JSI to make javascript calls directly to c++ host objects.
Code above is included in the current project as an example, through the realization of TurboModule NativeModule development, actually code process and original BaseJavaModule is roughly same, different is the underlying implementation:
- Current version can be ReactFeatureFlags. UseTurboModules to open the module function
- TurboModule components are managed using TurbomoduleManager.java. The modules that are injected can be initialized and uninitialized
- TurboModuleBinding provides a proxy moudle for Java /C++ modules that are registered in JNI/C++. __turboModuleProxy allows the c++ layer module to be called. The c++ layer implements Java code through jni. There are two types of moudles designed by facebook. LongLivedObject and non-stationary, that’s kind of the same design idea that we’re trying to solve here
void TurboModuleBinding::install(
jsi::Runtime &runtime,
const TurboModuleProviderFunctionType &&moduleProvider) {
runtime.global().setProperty(
runtime,
"__turboModuleProxy",
jsi::Function::createFromHostFunction(
runtime,
jsi::PropNameID::forAscii(runtime, "__turboModuleProxy"),
1.// Create a TurboModuleBinding that uses the global
// LongLivedObjectCollection
[binding =
std::make_shared<TurboModuleBinding>(std::move(moduleProvider))](
jsi::Runtime &rt,
const jsi::Value &thisVal,
const jsi::Value *args,
size_t count) {
return binding->jsProxy(rt, thisVal, args, count);
}));
}
Copy the code
const NativeModules = require('.. /BatchedBridge/NativeModules');
import type {TurboModule} from './RCTExport';
import invariant from 'invariant';
const turboModuleProxy = global.__turboModuleProxy;
function requireModule<T: TurboModule> (name: string): ?T {
// Bridgeless mode requires TurboModules
if (!global.RN$Bridgeless) {
// Backward compatibility layer during migration.
const legacyModule = NativeModules[name];
if(legacyModule ! =null) {
return((legacyModule: $FlowFixMe): T); }}if(turboModuleProxy ! =null) {
const module: ?T = turboModuleProxy(name);
return module;
}
return null;
}
Copy the code
CodeGen:
- In the new UI architecture, shadow and component layers of C++ layer are added, and most components are based on JSI, so the process of developing UI components and apis is more complicated, requiring developers to have C++ and JNI programming ability. In order to facilitate developers to develop quickly, Facebook also provides codegen tools. To help generate some automated code, see github.com/facebook/re…
- The following is an overview of the code generation process. Since CodeGen has not been officially released yet, there is little documentation on how to use it, but some developers have tried to generate some code, which can be found at github.com/karol-biszt…
-
Conclusion:
JSI and Turbormodule are available in the latest version of JSI, and the developer community has developed a large number of API components using JSI, such as the following modules that rely on C++ implementation:
- Github.com/mrousavy/re…
- Github.com/mrousavy/re…
- Github.com/mrousavy/re…
- Github.com/software-ma…
- Github.com/BabylonJS/B…
- Github.com/craftzdog/r…
- Github.com/craftzdog/r…
- Github.com/greentriang…
- Github.com/expo/expo/t…
- Github.com/ospfranco/r…
- Github.com/ammarahm-ed…
- Github.com/Nozbe/Water…
Judging from the latest code structure, the release of the new architecture seems to have entered the countdown. As a developer who has been devoting himself to learning and studying ReactNative, I believe that I must be looking forward to it as much as I am. From the official information of Facebook, Facebook App has adopted the new architecture. Should is expected this year will officially release, this time we can trust ReactNative should formally entered the city of version 1.0, ReactNative. Dev/blog / 2021/0…
The Bytedance team has tried to adopt ReactNative development scheme in games and apps for a long time, and the overall development, iteration efficiency and revenue have been greatly improved. Meanwhile, we continue to pay attention to the dynamics of ReactNative’s new architecture, believe that the overall solution and performance will be better and better, and look forward to moving to the new architecture soon.