417 lines
24 KiB
Markdown
417 lines
24 KiB
Markdown
|
# 26|客户端优化:如何把性能提升到极致?
|
|||
|
|
|||
|
你好,我是众文,这一讲继续由我和惠姝来讲解。第22讲中我们讲解了如何用自定义组件满足业务的个性化需求,除了这一点之外,在 React Native 的应用中,还有一点是大家探讨得比较多的,就是性能优化这部分。
|
|||
|
|
|||
|
和原生开发相比,React Native 比较明显的不足在于页面加载速度,比如秒开率、页面加载的时长等。但在我们实际的落地过程中,React Native 页面达到了秒开的级别,我们是如何做到的呢?
|
|||
|
|
|||
|
其实,一个未经优化的、比较复杂的、动态更新的 React Native 应用,从大体上讲,可以分为 3 个瓶颈(以下数据来自我们的实际业务案例):
|
|||
|
|
|||
|
![图片](https://static001.geekbang.org/resource/image/0c/yy/0cd583fae8ef9fe0d4fcb1b939ac09yy.png?wh=1204x482)
|
|||
|
|
|||
|
![图片](https://static001.geekbang.org/resource/image/72/b7/72637b200c229ea2429636ffc2acecb7.png?wh=1920x749)
|
|||
|
|
|||
|
当然,其中还涉及 JavaScript 侧的优化。今天我们主要站从客户端角度,讲述React Native 如何在客户端侧将性能优化到极致,带你开启 React Native 的秒开世界。
|
|||
|
|
|||
|
## 环境预创建
|
|||
|
|
|||
|
在 React Native 最新架构中,Turbo Module 是按需加载,而不是像旧框架一般,一股脑初始化所有的 Native Modules,同时 Hermes 引擎放弃了 JIT,在启动速度方面也有明显提升。
|
|||
|
|
|||
|
那么,抛开这两个新版本的优化,在启动速度方面,客户端还能做些什么呢?有的,那就是 **React Native 环境预创建**。
|
|||
|
|
|||
|
在混合工程中,React Native 环境与加载页面的关系如下:
|
|||
|
|
|||
|
![图片](https://static001.geekbang.org/resource/image/cb/ce/cb6be36c212515883353714ff1ab83ce.png?wh=954x770)
|
|||
|
|
|||
|
从上图中可以看到,在混合应用中,独立的 React Native 载体页都拥有自己的执行环境。Native 域包括 React View、Native Modules;JavaScript 域包括 JavaScript 引擎、JS Modules、业务代码;中间通信使用 Bridge/JSI。
|
|||
|
|
|||
|
当然,业内也有多个页面复用一个引擎的优化。但是多页面复用一个引擎存在一些问题,比如JavaScript 上下文隔离、多页面渲染错乱、JavaScript 引擎不可逆异常,等等。而且复用的性能不稳定,考虑到投入产出比、维护成本等方面,通常在混合开发中,采用的是一个载体页一个引擎。
|
|||
|
|
|||
|
一个 React Native 页面加载渲染逻辑,可以大致分为以下几步:
|
|||
|
|
|||
|
```plain
|
|||
|
React Native 环境初始化 -> 下载/加载 bundle -> 执行 JavaScript 代码
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
环境初始化这一步包含创建 JavaScript 引擎、Bridge、加载 Native Modules(旧版)。根据我们的测试,初始化这一步,特别是在 Android 环境中,比较耗时。
|
|||
|
|
|||
|
那么,如何进行 React Native 环境初始化耗时优化呢?我们可以提前将 React Native 环境创建好,流程如下:
|
|||
|
|
|||
|
![图片](https://static001.geekbang.org/resource/image/87/1b/874951373fe72a395086ddcd6yyef21b.png?wh=728x752)
|
|||
|
|
|||
|
具体的代码如下 (Java):
|
|||
|
|
|||
|
```plain
|
|||
|
public class RNFactory {
|
|||
|
// 单例
|
|||
|
private static class Holder {
|
|||
|
private static RNFactory INSTANCE = new RNFactory();
|
|||
|
}
|
|||
|
|
|||
|
public static RNFactory getInstance() {
|
|||
|
return Holder.INSTANCE;
|
|||
|
}
|
|||
|
|
|||
|
private RNFactory() {
|
|||
|
}
|
|||
|
|
|||
|
private RNEnv mRNEnv;
|
|||
|
|
|||
|
// App 启动时调用 init 方法,提前创建一个 RN 环境
|
|||
|
public void init(Context context) {
|
|||
|
mRNEnv = new RNEnv(context);
|
|||
|
}
|
|||
|
|
|||
|
// 获取 RN 环境对象
|
|||
|
public RNEnv getRNEnv(Context context) {
|
|||
|
RNEnv rnEnv = mRNEnv;
|
|||
|
mRNEnv = createRNEnv(context);
|
|||
|
return rnEnv;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
RNEnv.java:
|
|||
|
|
|||
|
```plain
|
|||
|
public class RNEnv {
|
|||
|
private ReactInstanceManager mReactInstanceManager;
|
|||
|
private ReactContext mReactContext;
|
|||
|
|
|||
|
public RNEnv(Context context) {
|
|||
|
// 构建 ReactInstanceManager
|
|||
|
buildReactInstanceManager(context);
|
|||
|
// 其他初始化
|
|||
|
...
|
|||
|
}
|
|||
|
|
|||
|
private void buildReactInstanceManager(Context context) {
|
|||
|
// ...
|
|||
|
mReactInstanceManager = ...
|
|||
|
}
|
|||
|
|
|||
|
public void startLoadBundle(ReactRootView reactRootView, String moduleName, String bundleid) {
|
|||
|
// ...
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
在做预创建时,我们需要注意**线程同步**问题。在混合应用中,React Native 由应用级变成页面级使用,所以在线程安全这方面有不少的问题,预创建时会并发创建多个 React Native 环境,而 React Native 环境内部构建存在异步处理,一些全局的变量,如 ViewManagersPropertyCache:
|
|||
|
|
|||
|
```plain
|
|||
|
class ViewManagersPropertyCache {
|
|||
|
private static final Map<Class, Map<String, ViewManagersPropertyCache.PropSetter>> CLASS_PROPS_CACHE;
|
|||
|
private static final Map<String, ViewManagersPropertyCache.PropSetter> EMPTY_PROPS_MAP;
|
|||
|
|
|||
|
ViewManagersPropertyCache() {
|
|||
|
}
|
|||
|
...
|
|||
|
}
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
内部的 CLASS\_PROPS\_CACHE、EMPTY\_PROPS\_MAP 都是非线程安全的数据结构,并发时可能会存在 Map 扩容转换问题 (HashMap Node 转红黑树结构),又比如 DynmicFromMap也有此问题:
|
|||
|
![图片](https://static001.geekbang.org/resource/image/61/02/613fea41d575817922bdcb9ecc636c02.png?wh=1920x1005)
|
|||
|
|
|||
|
那么,这个问题如何解决呢?你可以参考[《混合应用:如何从零开始集成 React Native?》](https://time.geekbang.org/column/article/518965)框架 Bug 修复部分,对同步的地方进行处理。
|
|||
|
|
|||
|
## 异步更新
|
|||
|
|
|||
|
原先我们进入 React Native 载体页后需要先下载最新的 JavaScript 代码包版本,若有更新,就要下载最新的包并加载。在这个过程中,我们会经历两次网络请求。如果用户网络比较差,那他从进入页面到渲染页面内容需要等待较长时间。
|
|||
|
|
|||
|
所以我们针对部分特殊的页面,采取了异步更新的策略。这里所说的特殊页面可以由业务来指定,比如更新频率相对比较低的页面、页面进入路径较短的页面,等等。
|
|||
|
|
|||
|
异步更新策略的主要思路为在进入页面之前选择性地提前下载 JavaScript 代码包,进入载体页后再看 JavaScript 代码包是否有缓存,如果有,我们就优先加载缓存并渲染;然后再异步检测是否有最新版本的 JavaScript 代码包,如果有,下载到本地并进行缓存,再等下次进入载体页时生效。
|
|||
|
|
|||
|
我们先看一下从一个页面进入到一个 React Native 载体页后需要哪些流程:
|
|||
|
|
|||
|
![图片](https://static001.geekbang.org/resource/image/92/2b/92aac7a2015029fd091799a8fd0b7e2b.png?wh=531x361)
|
|||
|
|
|||
|
流程图中可以看出,我们从进入载体页到渲染页面,需要两次网络请求,不管网速快还是慢,这个流程算是比较漫长的,但在进行异步更新后,我们的流程就会变成下图这样:
|
|||
|
|
|||
|
![图片](https://static001.geekbang.org/resource/image/f8/8e/f8b52bd6babd2686e1004cf22dc1608e.png?wh=531x411)
|
|||
|
|
|||
|
在业务页面中,我们可以对 JavaScript 代码包进行提前下载并缓存,在用户跳转到 React Native 页面后,检测是否有缓存的 JavaScript 代码包,如果有我们就直接渲染页面。这样就不需要等待版本号检测网络接口以及下载最新包的网络接口,也不依赖于用户的网络情况,减少了用户等待时间。
|
|||
|
|
|||
|
在渲染页面的同时,我们通过异步检测 JavaScript 代码包的版本,若有新版本就进行更新并缓存,下次生效。当然,业务也可以选择更新完最新包之后,提示用户有新版本页面,以及是否选择刷新并加载最新页面。
|
|||
|
|
|||
|
## 接口预缓存
|
|||
|
|
|||
|
对 React Native 环境初始化、bundle 加载流程进行优化后,我们的React Native 页面就可以达到秒开级别了。不过,React Native 页面加载后,进入 JavaScript 业务执行区间,大部分业务都不可避免地会进行网络交互,请求服务器数据进行渲染,这部分其实也有很大的优化空间。我们现在就来分析分析。
|
|||
|
|
|||
|
我们先来看下具备热更新能力的 React Native 加载流程:
|
|||
|
|
|||
|
![图片](https://static001.geekbang.org/resource/image/44/46/440119a0899e5914f35e86efd25e9646.png?wh=1504x1034)
|
|||
|
|
|||
|
你可以看到,整个流程是从 React Native 环境初始化到热更新 ,再到JavaScript 业务代码执行,最后到业务界面展示。链路比较长,而且每一个步骤都依赖前一个步骤的结果。特别是热更新流程,最长可涉及两次网络调用,分别是检测是否需要更新与下载最新 bundle 文件。
|
|||
|
|
|||
|
那么这个时候,我们就想到,在等待网络返回的过程中,Native 能不能把闲置的 CPU 资源利用起来呢?
|
|||
|
|
|||
|
我们都知道,目前手机性能越来越强大,多核、4G/5G 使我们的“冲浪”体验越来越好。而且,Native 具备先天的多线程能力。在纯客户端开发中,我们经常使用接口数据缓存策略来提升用户体验,在最新数据返回前,先使用缓存数据进行页面渲染。那么在 React Native 中,我们也可以参考这一思路,对整个流程进行优化:
|
|||
|
|
|||
|
![图片](https://static001.geekbang.org/resource/image/f8/b9/f8fb64dc07f20ddf88e5708a53f42ab9.png?wh=918x1120)
|
|||
|
|
|||
|
具体代码,我也放在了下面 (Java)。
|
|||
|
|
|||
|
* 首先是预请求实体类:
|
|||
|
|
|||
|
```plain
|
|||
|
public class PrefetchBean {
|
|||
|
public String url; // 预加载的接口
|
|||
|
public String method; // 请求方式:GET/POST...
|
|||
|
public Map<String, String> headers; // 请求头
|
|||
|
public Map<String, String> params; // 请求参数
|
|||
|
}
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
* 打开载体页时,解析对应 bundle 缓存中的预请求接口配置数据,发起请求缓存数据:
|
|||
|
|
|||
|
```plain
|
|||
|
public class RNApiPreloadUtils {
|
|||
|
public static void preloadData(String bundleId) {
|
|||
|
// 根据 bundle id 解析对应的预请求接口配置,可存在多个接口
|
|||
|
List<PrefetchBean> prefetchBeans = parsePrefetchBeans(bundleId);
|
|||
|
// 请求接口,成功后缓存到本地存储
|
|||
|
requestDatas(prefetchBeans);
|
|||
|
}
|
|||
|
|
|||
|
public static String prefetchData(String url) {
|
|||
|
// 从本地缓存中,根据 url 获取对应的接口数据
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
* 获取接口缓存数据的 Module:
|
|||
|
|
|||
|
```plain
|
|||
|
public class PreFetchBusinessModule extends ReactContextBaseJavaModule
|
|||
|
implements ReactModuleWithSpec, TurboModule {
|
|||
|
public PreFetchBusinessModule(ReactApplicationContext reactContext) {
|
|||
|
super(reactContext.real());
|
|||
|
}
|
|||
|
|
|||
|
@ReactMethod
|
|||
|
public void prefetchData(String url, Callback callback) {
|
|||
|
String data = RNApiPreloadUtils.prefetchData(url);
|
|||
|
// 回传数据给 JS
|
|||
|
WritableMap resultMap = new WritableNativeMap();
|
|||
|
map.putInt("code", 1);
|
|||
|
map.putString("data", data);
|
|||
|
callback.invoke(resultMap);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
* JavaScript 调用:
|
|||
|
|
|||
|
```plain
|
|||
|
NativeModules.PreFetchBusinessModule.prefetchData(url, (result)=>{
|
|||
|
// 获取到结果后,判断是否为空,不为空解析数据,渲染页面
|
|||
|
console.info(result);
|
|||
|
}
|
|||
|
);
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
## 拆包
|
|||
|
|
|||
|
前面也提到了,React Native 页面的 JavaScript 代码包是热更新平台根据版本号进行下发的,每次有业务改动,我们都需要通过网络请求更新代码包。
|
|||
|
|
|||
|
不过,其实只要 React Native 官方版本没有发生变化,JavaScript 代码包中 React Native 源码相关的部分是不会发生变化的,所以我们不需要在每次业务包更新的时候都进行下发,在工程中内置一份就好了。
|
|||
|
|
|||
|
因此,我们可以将一个 JavaScript 代码包拆分成两个部分:一个是 Common 部分,也就是 React Native 源码部分,这一部分除非React Native官方版本进行升级,几乎不会发生变化;另一个是业务代码部分,也就是我们需要动态下载的部分。
|
|||
|
|
|||
|
![图片](https://static001.geekbang.org/resource/image/06/b7/06b66a591c76d37fb1c60e58fa0426b7.png?wh=688x486)
|
|||
|
|
|||
|
我们在打包时,对 React Native 代码包进行处理,拆分成 Common 包和业务代码包。Common包内置到工程中(至少为几百kb的大小),业务代码包进行动态下载。然后我们利用 JSContext 环境,在进入载体页后在环境中先加载 Common包,再加载业务代码包就可以完整的渲染出 React Native 页面:
|
|||
|
|
|||
|
```plain
|
|||
|
//载体页
|
|||
|
- (void)loadSourceForBridge:(RCTBridge *)bridge
|
|||
|
onProgress:(RCTSourceLoadProgressBlock)onProgress
|
|||
|
onComplete:(RCTSourceLoadBlock)loadCallback{
|
|||
|
if (!bridge.bundleURL) return;//加载新资源
|
|||
|
//开始加载bundle,先执行common bundle
|
|||
|
[RCTJavaScriptLoader loadCommonBundleOnComplete:^(NSError *error, RCTSource *source){
|
|||
|
loadCallback(error,newSource);
|
|||
|
}];
|
|||
|
}
|
|||
|
|
|||
|
//common执行完毕
|
|||
|
+ (void)commonBundleFinished{
|
|||
|
//开始执行buz bundle代码
|
|||
|
[RCTJavaScriptLoader loadBuzBundle:self.bridge.bundleURL onComplete:^(NSError *error, RCTSource *source){
|
|||
|
loadCallback(error,newSource);
|
|||
|
}];
|
|||
|
}
|
|||
|
|
|||
|
//RCTJavaScriptLoader.mm
|
|||
|
+ (void)loadBuzBundle:(NSURL *)buzURL
|
|||
|
onComplete:(WBSourceLoadBlock)onComplete{
|
|||
|
//执行buz包代码
|
|||
|
[self executeSource:buzURL onComplete:^(NSError *error){
|
|||
|
onComplete(error);//执行完毕
|
|||
|
}];
|
|||
|
}
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
在这里要注意,Common包和业务代码包必须要成对进行加载,否则页面无法展示。
|
|||
|
|
|||
|
## 按需加载
|
|||
|
|
|||
|
其实我们通过前面拆包的方案,已经减少了动态下载的业务代码包的大小。但是还会存在部分业务非常庞大,拆包后业务代码包的大小依然很大的情况,依然会导致下载速度较慢,并且还会受网络情况的影响。
|
|||
|
|
|||
|
因此,我们可以再次针对业务代码包进行拆分,**将一个业务代码包拆分为一个主包和多个子包的方式**。在进入页面后优先请求主包的 JavaScript 代码资源,能够快速地渲染首屏页面,紧接着用户点击某一个模块时,再继续下载对应模块的代码包并进行渲染,就能再进一步减少加载时间。示例如图:
|
|||
|
|
|||
|
![图片](https://static001.geekbang.org/resource/image/7c/3c/7c961a3a314ab265a78ede03a187233c.png?wh=1002x544)
|
|||
|
|
|||
|
那么,什么时候需要把业务代码包拆分成一个主包和多个子包呢?把什么模块作为主包,什么模块作为子包比较合适呢?我举一个简单的例子给你解释一下。
|
|||
|
|
|||
|
其实在简单的业务中,我们并不需要对业务代码包进行拆分,但是在交互较为复杂的页面中,可能需要进行拆包。下面我们看一下这个包含Tab的业务页面:
|
|||
|
|
|||
|
![图片](https://static001.geekbang.org/resource/image/70/4f/70ffeaccdcda8e40bf47dc2a6d955b4f.png?wh=301x541)
|
|||
|
|
|||
|
这个页面中包含三个Tab,也就是Tab1、Tab2和Tab3。如果这三个Tab中的内容相似,我们当然就不需要对业务代码包进行拆分了。但是如果这三个Tab中的内容差异化较大,页面模版完全不相同,我们就可以对业务代码包进行拆分。
|
|||
|
|
|||
|
比如三个Tab页面中,A页面是列表布局,B页面是瀑布流布局,C页面是视频页面,这几个页面之间的布局、样式、方案均无法统一管理。我们就对这三个不同的页面进行拆分,当用户选择某一个页面时,加载对应页面的样式以及布局。
|
|||
|
|
|||
|
我们可以将头部title、subtitle部分以及三个tab作为主包优先进行渲染,其次Tab1、Tab2、Tab3部分再分别打成子包,然后再根据用户选中的Tab,将对应的代码包下载下来并渲染。这样我们可以就减少每次下载的代码包的大小,加快渲染速度
|
|||
|
|
|||
|
不过,在 React Native 移动端的性能优化中,除了 React Native 环境创建、bundle 文件、接口数据等方面的优化外,还有一个大的优化点,就是**React Native 运行时优化**。
|
|||
|
|
|||
|
React Native 旧版本的运行效率有两大痛点:一是 JSC 引擎解释执行 JavaScript 代码效率低,引擎启动速度慢;二是 JavaScript 与 Native 通信效率低,特别是涉及批量地 UI 交互,如列表时更是如此。
|
|||
|
|
|||
|
针对于第二点,在[《自定义组件:如何满足业务的个性化需求?》](https://time.geekbang.org/column/article/519819)中,我们讲解了 React Native 新架构采用了 JSI 进行通信,替换了 JSBridge,无异步地序列化与反序列化操作、无内存拷贝,可以做到同步通信。
|
|||
|
|
|||
|
而除了 JSI 之外,React Native 0.60 以后的版本开始支持 [Hermes 引擎](https://www.react-native.cn/docs/hermes)。对比 JSC 引擎,Hermes 引擎在启动速度、代码执行效率上都有大幅提升,所以接下来我们就来重点讲解 Hermes 引擎的特点、它的优化手段以及如何在移动端启用。
|
|||
|
|
|||
|
## Hermes 引擎
|
|||
|
|
|||
|
Facebook 在 ChainReact 2019 大会上正式推出了新一代 JavaScript 执行引擎 Hermes。Hermes 是个轻量级的 JavaScript 引擎,专门对移动端上运行 ReactNative 进行了优化,Hermes 可执行字节码,也可以执行 JavaScript。
|
|||
|
|
|||
|
![图片](https://static001.geekbang.org/resource/image/36/cc/3699ac99c8cc113a17ab3271230843cc.png?wh=622x484)
|
|||
|
|
|||
|
在分析性能数据时,Facebook 团队发现 JavaScript 引擎是影响启动性能和应用包体积的重要因素。JavaScriptCore 最初是为桌面浏览器端设计,相较于桌面端,移动端能力有太多的限制。所以,为了能从底层对移动端进行性能优化,Facebook 团队选择自建 JavaScript 引擎,设计了Hermes。
|
|||
|
|
|||
|
那新设计的 Hermes 引擎能带来怎样的提升呢?Chain React 大会上官方给出了 Hermes 引擎一组数据:
|
|||
|
|
|||
|
* 从页面启动到用户可操作的时间长短(Time To Interact:TTI),从 4.3s 减少到 2.01s;
|
|||
|
* App 的下载大小,从 41MB 减少到 22MB;
|
|||
|
* 内存占用,从 185MB 减少到 136MB。
|
|||
|
|
|||
|
Hermes 的优化主要体现在**字节码预编译**和**放弃JIT**这两点上。
|
|||
|
|
|||
|
首先来看下字节码预编译。现代主流的JavaScript引执行一段JavaScript代码的大概流程是:
|
|||
|
|
|||
|
```plain
|
|||
|
读取源码文件 -> 解析转换成字节码 -> 执行
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
不过,在运行时解析源码转换字节码是一种时间浪费,所以 Hermes 选择预编译的方式在编译期间生成字节码。这样做,一方面避免了不必要的转换时间;另一方面,多出的时间可以用来优化字节码,从而提高执行效率。相关示意图如下:
|
|||
|
|
|||
|
![图片](https://static001.geekbang.org/resource/image/6d/9e/6d704ce81c62d6aa3dd3307e14008d9e.png?wh=1920x943)
|
|||
|
|
|||
|
第二点是放弃了JIT。为了加快执行效率,现在主流的 JavaScript 引擎都会使用一个 JIT 编译器,在运行时通过转换成机器码的方式优化 JavaScript 代码。Faceback 团队认为 JIT 编译器主要有两个问题:
|
|||
|
|
|||
|
* 要在启动时候预热,对启动时间有影响;
|
|||
|
* 会增加引擎 size 大小和运行时内存消耗。
|
|||
|
|
|||
|
但是这里需要注意,放弃了JIT,纯文本 JavaScript 代码执行效率会降低。放弃 JIT,是指放弃运行时 Hermes 引擎对纯文本 JavaScript 代码的编译优化。
|
|||
|
|
|||
|
当然了,Hermes 也会带来一些问题,首先就是Hermes 编译的字节码文件比纯文本 JavaScript 文件增大不少,第二点就是执行纯文本 JavaScript 耗时长。
|
|||
|
|
|||
|
那么,我们要如何开启 Hermes呢?除了可以参考[官方文档](https://reactnative.dev/docs/hermes)快速开启Hermes,下面我会也会给你介绍如何在混合工程中开启 Hermes引擎,我们以 Android 为例进行讲解。
|
|||
|
|
|||
|
第一步,获取 hermes.aar 文件 (node\_modules/hermes-engine):
|
|||
|
|
|||
|
![图片](https://static001.geekbang.org/resource/image/23/8b/23e27d4dc66ff7074290f0503da4078b.jpeg?wh=838x464)
|
|||
|
|
|||
|
第二步,将 hermes-cppruntime-release.aar 与 hermes-release.aar 放到工程的 libs 目录总,然后在模块的 build.gradle 中添加依赖,这两个 aar 中主要是 hermes 和 libc++\_shared so 文件:
|
|||
|
|
|||
|
```plain
|
|||
|
dependencies {
|
|||
|
implementation(name:'hermes-cppruntime-release', ext:'aar')
|
|||
|
implementation(name:'hermes-release', ext:'aar')
|
|||
|
}
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
第三步,设置 JavaScript 引擎:
|
|||
|
|
|||
|
```plain
|
|||
|
ReactInstanceManagerBuilder builder = ReactInstanceManager.builder()
|
|||
|
.setApplication((Application) context.getApplicationContext())
|
|||
|
.addPackage(new MainReactPackage())
|
|||
|
.setRedBoxHandler(mExceptionHandler)
|
|||
|
.setUseDeveloperSupport(RNDebugSwitcher.getInstance().isDebug())
|
|||
|
.setInitialLifecycleState(LifecycleState.BEFORE_CREATE)
|
|||
|
.setJavaScriptExecutorFactory(new HermesExecutorFactory()); // 设置为 hermes
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
最后,运行 hermes 编译出的字节码 bundle 文件就可以了。这一步又分为了几个小步骤,你参照下面步骤即可:
|
|||
|
|
|||
|
* 将 JavaScript 打包成 bundle 文件。
|
|||
|
|
|||
|
```plain
|
|||
|
react-native bundle --platform android --entry-file index.android.js
|
|||
|
--bundle-output ./bundles/index.android.bundle --assets-dest ./bundles
|
|||
|
--dev false
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
* 使用 hermes-engine 将 bundle 文件转换成字节码文件。下载 hermes-engine,使用 hermesc 命名进行转换。
|
|||
|
|
|||
|
```plain
|
|||
|
./hermesc -emit-binary -out index.android.bundle.hbc
|
|||
|
xxx/react-native/app/bundles/index.android.bundle
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
* 重命名 bundle 文件。
|
|||
|
这里要将之前 bundle 目录下的 index.android.bundle 删掉,将当前的 index.android.bundle.hbc 重命名为 index.android.bundle
|
|||
|
|
|||
|
讲完了 Hermes 引擎,我们最后再来了解下引擎的复用优化。Hermes 引擎是运行时执行效率的优化,而引擎复用是 React Native 创建引擎成本的优化。
|
|||
|
|
|||
|
## 引擎复用
|
|||
|
|
|||
|
在混合应用中,React Native 由应用级的使用变更为页面级,每一个页面都使用一个 React Native 引擎(包括 JSC/Hermes、Bridge/JSI),除了内存占用高以外,React Native 引擎的创建耗时也是比较严重的。前面我们讲了环境预创建,就是对于引擎创建成本的优化。在这一块儿,除了预创建外,我们还可以进行**引擎复用优化**。
|
|||
|
|
|||
|
以 Android 为例,React Native 引擎的直接表现就是 ReactInstanceManager,内部会初始化 React Native 相关的环境。而在混合应用中,一般会配合热更新策略进行页面加载,所以使用的是 JSC/Hermes 动态加载脚本的能力。从这个场景来看,似乎一个引擎可以运行不同的 bundle 文件,即可达到复用的目的。
|
|||
|
|
|||
|
但是引擎复用的坑也非常多,目前我们并未直接落地使用:
|
|||
|
|
|||
|
1. 创建和复用引擎的成本可能会导致不少页面,第一次进入和后续进入的速度,表现不一致,因此这类体验问题还需要专项排查并优化;
|
|||
|
2. 在多页面同时在前台的状态下,比如首页 TAB 不同页面使用的都是 React Native 页面,会存在莫名的同步问题;
|
|||
|
3. 复用 React Native 容器内容时,会保持上一次会话的全局变量,容易造成业务逻辑错误。同一个引擎加载不同 bundle,JavaScript上下文与新加载进去的代码能否实现 100% 隔离无污染可能是未知数。同时多页面 JavaScript 上下文隔离。目前引起复用的一大坑其实来源于 JavaScript 上下文多个页面混在一起,容易出错;
|
|||
|
4. JSC/Hermes 随时有可能发生不可逆转的异常,因此引擎维护的过程中异常状态识别也是一个问题。
|
|||
|
|
|||
|
如果你有什么好的解决思路和想法,也欢迎在评论区留言,我们一起讨论。
|
|||
|
|
|||
|
## 总结
|
|||
|
|
|||
|
今天我们学习了如何在客户端将 React Native 性能优化到极致,包括环境预创建、异步更新、接口预缓存、拆包、按需加载、Hermes 引擎、引擎复用等。这些手段在实际业务中非常实用,当然 React Native 框架也在从自身上不断优化、迭代,追求性能的更高水平。
|
|||
|
|
|||
|
接下来我们回顾下今天讲过的几个重点:
|
|||
|
|
|||
|
* 在优化 React Native 环境创建的耗时方面,我们可以使用环境预创建和引擎复用的方式进行优化。环境预创建更容易落地,而且坑更少。引擎复用在内存占用这块比环境预创建方式好,但是需要解决的问题更多;
|
|||
|
* 在热更新流程优化上,我们可以使用异步更新和预加载 bundle 的方式,优先使用 bundle 缓存进行加载,同时 JavaScript 业务可控制新版本更新策略;
|
|||
|
* 另外,如何在初始化->热更新->bundle下载->加载JavaScript->JavaScript业务接口请求的链路中,利用客户端多线程的优势,接口预缓存是不错的选择。
|
|||
|
* 如果bundle 过大,你可以拆分 common 包和业务包。为进一步提高加载速度,你还可以利用 JavaScript 引擎动态加载脚本的能力,按需加载子 bundle。
|
|||
|
* 最后,你也可以多关注最新的 Hermes 引擎,看看它的优缺点,以及它是如何实现优化的。
|
|||
|
|
|||
|
至此,Native 相关的三讲就告一段落了,后续我和惠姝还会参与 React Native 新框架原理篇的编写。
|
|||
|
|
|||
|
## 作业
|
|||
|
|
|||
|
1. 运行 Hermes 引擎 demo,并实现环境预创建功能。
|
|||
|
|
|||
|
如果有什么问题,欢迎在评论区留言,咱们下一讲见。
|
|||
|
|