Image from: https://unsplash.com
Author of this article: wyl
background
By 2022, refined operation has become a strong demand of major app manufacturers. You should be familiar with Alibaba's DinamicX and Tangram. Many app manufacturers have also developed some similar frameworks. Although DSL-based dynamic solutions have performance However, it is not Turing-complete after all. The implementation cost of some requirements that require dynamic distribution of logic is too high, or it cannot be realized due to the limitations of DSL itself. To solve this problem, we used RN to make some exploration attempts, and we have been relatively perfect. The RN infrastructure, combined with the client list capability, realizes a set of dynamic capabilities at low cost, while taking into account a certain performance experience.
The dynamic list scheme based on ReactNative is simply to embed the ReactNative container in the ViewHolder of RecyclerView
Since the main frame of the page is still developed and rendered by Native, the loading speed of the first screen is guaranteed, and the partial RN implementation also enables the page to obtain dynamic capabilities, so as to achieve the dynamic capabilities between performance and "complete logic execution". A balance has been achieved. According to our experience, several dynamization schemes are ranked as follows:
- Overall performance experience ranking:
Pure Native > DSL-based dynamic solution >= ReactNative dynamic list solution > Pure ReactNative page > H5 - Dynamic ability sorting:
H5 = pure ReactNative page > ReactNative dynamic list scheme > DSL based dynamic scheme > pure Native - To achieve the ability to sort:
Pure Native >= RN dynamic list scheme = pure ReactNative page > H5 > DSL-based dynamic scheme
It can be seen from the above ranking that the ReactNative dynamic list scheme is in a middle or upper middle position as a whole, and its implementation ability is far better than that of the DSL-based dynamic scheme, which is basically equivalent to the Native ability, and can achieve some complex UI interaction effects. And compared to the first screen speed of the page realized by pure RN, it will have great advantages. In addition, it can be easily embedded without changing the overall framework of the page. In terms of development and maintenance costs, the RN dynamic list scheme is compared with various DSL-based solutions. The dynamic solution of RN will have obvious advantages. It does not require an additional development component management platform, and does not need to read difficult DSLs when troubleshooting problems. The most important thing is that RN has Turing-complete capabilities, so in general, RN is used. Embedding it into the Native RecyclerView to realize the partial dynamic of the Native page is a relatively cost-effective way, which is worth a try.
Introduction of technical solutions
Here we share some technical details, principles and problems encountered by our solution from the perspective of Android. First, some common terms we use:
-
moduleName
is the unique key of the RN offline package, which is equivalent to the name of the offline package; -
componentName
is the component of registerComponent in RN, corresponding to the execution entry of a service implemented by RN; - Cards refer to the display content inside each viewholder on the homepage of Cloud Music, and the displayed UI style is the card style;
- RN engine refers to the entire JS offline package runtime environment dominated by RN Bridge.
The overall program structure is as follows:
It can be seen from the figure that the overall solution adopts a data-driven approach. The server uses fields such as type, component, moduleName and other fields carried in the data to uniquely specify whether to use RN to render and execute which component logic in the RN offline package.
There are several details on the overall plan:
- In a data-driven way, the access page does not need to pay attention to the specific display data, but only needs to transparently transmit the data to the JS side of the RN.
- Since RN needs to load the offline package before executing JS to generate the client view, loading the offline package of RN when the RecyclerView binds data will inevitably slow down the display of the entire module, so here we do the preloading of the entire offline package
- The display element of each ViewHolder in the homepage list is called a card. The current strategy is to put multiple cards in an RN offline package and display them separately through the same RN container to avoid excessive resource consumption by multiple containers.
The following disassembles the entire solution from the perspective of data flow. The overall solution can be divided into three main steps: server-side data definition and delivery, container data transparent transmission, and JS-side data parsing:
- Server-side data definition and delivery
Since it is the server-side interface that drives the content display in RecyclerView, the data sent by the interface needs to have a type field to identify whether to use RN or Native display, and the native display style tag field can be used. Since the specific style displayed in RN is directly related to which JS code to run, Therefore, the data sent by the server needs to carry the corresponding moduleName and componentName. The overall data structure is defined as follows:
[
{
"type":"rn",
"rnInfo":{
"moduleName":"bizDiscovery",
"component":"hotSong",
"otherInfo":{
}
},
"data":{
"songInfo":{
}
}
},
{
"type":"dragonball",
"data":{
"showInfo":{
}
}
}
]
After getting the data, you only need to bind the data to different ViewHolders according to the normal usage of RecyclerView.
- Container data transparent transmission
The RN container is directly embedded in the ViewHolder. In the viewHolder, only the ViewGroup container that carries the RN JS rendering view needs to be defined. After the RN Bridge creates the ReactRootView, it can call the add method of the created ReactRootView and add it to the container. The data transfer is transparent. The transmission method is passed to the JS side through the initialProperty of RN, and parsed and used on the JS side. The data transmission code is as follows:
mReactRootView?.startReactApplication(reactInstanceManager, componentName, initialProperties)
The point to note here is that since all cards displayed using RN correspond to the same RecyclerView type, that is, the same ViewHolder, two situations may occur when RecyclerView is reused: 1. There is only one RN card, sliding up and down When RecyclerView is reused, it basically does not need to be processed. 2. There are two different types of RN cards, and completely different offline package codes will be run during reuse. This situation will cause the JS side to re-execute the rendering logic to generate a new view , if the JS side is re-rendered every time when scrolling up and down, it will greatly affect the performance when sliding, causing the sliding to freeze and drop frames. In response to this problem, we also cache the ReactRootView of RN. The overall structure is as follows:
It can be seen from the figure that the container in the ViewHolder and the ReactRootView of the RN have a one-to-many relationship. After the first initialization is completed, the ReactRootView of the RN is still hanging in the virtual view tree managed by the RN, and the RecyclerView slides to switch different display types. You only need to remove the ReactRootView that is not displayed from the container of the ViewHolder, and then re-add the ReactRootView that needs to be displayed. There is no need to re-execute the JS side. After re-adding the ReactRootView, you also need to pass the current data to the JS side to adapt to the same style. Cards show the needs of different data. The principle here is that under normal circumstances, an RN Bridge will only create one ReactRootView, but looking at the RN source code, RN actually supports the ability of one RN Bridge to bind multiple RootViews. The code is as follows:
public void addRootNode(ReactShadowNode node) {
mThreadAsserter.assertNow();
int tag = node.getReactTag();
mTagsToCSSNodes.put(tag, node);
mRootTags.put(tag, true);
}
A ReactRootView is a view tree. RN will traverse all ReactRootViews when updating the client view. The code is as follows:
protected void updateViewHierarchy() {
....
try {
for (int i = 0; i < mShadowNodeRegistry.getRootNodeCount(); i++) {
int tag = mShadowNodeRegistry.getRootTag(i);
ReactShadowNode cssRoot = mShadowNodeRegistry.getNode(tag);
if (cssRoot.getWidthMeasureSpec() != null && cssRoot.getHeightMeasureSpec() != null) {
...
try {
notifyOnBeforeLayoutRecursive(cssRoot);
} finally {
Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE);
}
calculateRootLayout(cssRoot);
...
try {
applyUpdatesRecursive(cssRoot, 0f, 0f);
} finally {
}
...
}
}
} finally {
Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE);
}
}
Therefore, the rendering logic of multiple ReactRootView RNs can be executed normally. Here, a ReactRootView corresponds to a Component in the JS implementation. When we run the RN business code, we will see that the implementation of startApplication is in ReactRootView, and the parameter passed in by startApplication is Component , the corresponding code is as follows:
public class ReactRootView extends FrameLayout implements RootView, ReactRoot {
public void startReactApplication(
ReactInstanceManager reactInstanceManager,
String moduleName,
@Nullable Bundle initialProperties,
@Nullable String initialUITemplate) {
...
}
}
At this point, the key implementation of the client side is basically completed, and the next step is the JS side.
- JS profile changes
The writing method of card development on the JS side is basically the same as that of normal RN development. The only difference is that multiple components need to be registered at the same time. When each business card is started on the client side, it only needs to start the corresponding Component. The code example is as follows:
AppRegistry.registerComponent('hotTopic', () => EStyleTheme(HotTopic));
AppRegistry.registerComponent('musicCalendar', () => EStyleTheme(MusicCalendar));
AppRegistry.registerComponent('newSong', () => EStyleTheme(NewSong));
- JS and Native communication
So far, the entire rendering process has been introduced, and the card can be displayed normally. However, since RN has Turing-complete capabilities, there are bound to be some UI changes caused by user interaction, such as clicking the "fork" on the card. The uninteresting operation, click After that, you need to notify the client to pop up the uninteresting components of the client. Multiple cards correspond to the same JS engine, and the communication channels between JS and Native are also multiplexed. How to decide which card to pop up? Our approach is to use the card for the first time. When rendering, the hash value of the timestamp is used to generate a unique key, and this key is used as the unique identifier to distinguish different services between the native side and the JS side, and is associated with the specific displayed business card and stored on both sides, so that each subsequent time During communication, both sides can confirm the communication object through the key to ensure that the communication will not be confused.
- RN engine warm up
Offline package loading generally consumes a lot of time during the entire RN execution cycle, so in order to improve performance as much as possible, we also preheat the entire offline package corresponding to the page card, that is, load the offline package to the memory in advance The runtime environment of the business logic is ready in the preheating process. To warm up, you only need to create a ReactInstanceManager and call createReactContextInBackground(). After the call, the entire offline package will be handed over to the JS engine for preprocessing. The code is as follows:
ReactInstanceManager.builder()
.setApplication(ApplicationWrapper.getInstance())
.setJSMainModulePath("index.android")
.addPackage(MainReactPackage())
...
.build()
.createReactContextInBackground()
Another point to note here is the code debugging ability. If the original page already has a shake gesture, the RN native debugging menu will not be able to be called out. Additional interaction methods need to be added to solve this problem. A floating button has been added to the card.
At this point, the overall framework has been introduced, and memory usage and reasonable exception handling outside the framework are also the key points to be considered.
Memory
In addition to the overall technical implementation, another focus of our attention is the memory usage. We have counted the memory usage of the RN container with the RN Bridge as the core, and used the Profiler tool to obtain the data as follows:
No RN container (native/java) | 1 RN container (native/java) | 2 RN container (native/java) | 3 RN container (native/java) | 5 RN container (native/java) | |
---|---|---|---|---|---|
Redmi k30pro 6G | 148/54.6 | 154/56 | 157/55.7 | 153/56.7 | 208/59.8 |
Google Pixel 2XL 4G | 137.8/60 | 163/73 | 176/83 | 186/91 | 196/101 |
Redmi k30 8G | 118/52 | 143/56 | 136/55 | 138/56 | 142/60 |
Overall, the overall memory does not increase much when there are less than 5 RN containers, and the overall memory usage is in a controllable state. Since this solution adopts the method of one RN Bridge corresponding to multiple cards, it is equivalent to adding only one Bridge. The impact on memory is small, and there is no new OOM problem in actual online operation.
exception handling
- How to handle exceptions
Whether it is the reason of JS writing or the stability of ReactNative itself, there is always a certain probability that there will be exceptions. At this time, reasonable logic processing is required to ensure that the function and user experience will not be greatly affected. Our current processing strategy is exception monitoring. Or use NativeExceptionHandler to monitor SoftException and FatalException, notify the upper business (recyclerView layer) in a unified callback when an exception occurs, and then according to the specific business situation, the business layer will uniformly eliminate or rebuild the RN container to ensure that the experience is not affected or has a small impact , Take the usage scenario of the cloud music homepage as an example. At present, the total PV of the card is about 100 million, and the error rate is less than 1/10,000. The overall operation is stable, and there is no relevant user feedback.
- How to deal with data incompatibility caused by RN version upgrade
RN uses an offline package strategy. In order to ensure that users can obtain offline packages normally and ensure that offline packages can be updated quickly and efficiently, we have adopted strategies such as pocket package integration, update information server interface ride-hailing, etc., but it is limited by the user's model area. , network status and other reasons, there is still a certain probability that the update is unsuccessful. In this case, we save the card information supported by the current RN offline package in the configuration file of the offline package, and expose the interface obtained through the offline package to the business side. Before running the offline package, you can filter the network request results according to the configuration information to ensure that no exception occurs when the new version of the data matches the old version of the offline package.
future plan
In the short term, we hope to combine the RN dynamic list solution with our existing RN low-code capabilities to achieve dynamic construction and release of homepage operations. On the other hand, the main performance improvement is that we are still using RN 0.60.5 version. The execution efficiency of JS and The current version of the multi-threaded framework is our biggest bottleneck, and we will try more on the new architecture in the future.
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!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。