1.问题描述
本地版支持64位应用包供主流应用商城要求上架,在测试过程中发现 RN 界面大概率出现白屏、卡死。
2. 基础排查
# | case |
---|---|
1 | 同城 64位包,切换到县域/找工作,无此问题 |
2 | 对比同城、本地版 64位包 RN 相关 so,md5 一致 |
3 | 排查与 soloader 版本无关 |
4 | 排查与 common bundle 无关 |
5 | 经测试排除本地版自定义 RN 载体页原因 |
6 | ReactNative 框架、JS 业务无明显错误日志 |
3. 解决了?module 调用太频繁?抓错人了~
排除以上原因,再看下业务 module 调用,使用命令行:
adb logcat | grep ‘react’
(1) 本地版“找工作”页面,触发 native module 调用如下:
进入后调用了几十次 NativeDataModule.fetchHeader || NativeDataModule.fetchAction,并且列表滑动过程中也在一直触发调用
(2) 本地版“部落”,触发 native module 调用如下:
进入后频繁调用 WBInitialParams.obtainInitialParams
(3) 同城->县域“找工作”,触发 native module 调用如下:
同城同样的页面仅有少量的 WBInitialParams.obtainInitialParams 触发调用
最后和 FE 沟通、联调,发现在政府整改需求中,FE 业务侧更改了相关 module 的触发,一进入页面所有业务请求、埋点请求均会触发相关 module 调用。
恢复到之前的调用频次进行 debug 调试,本地版 64位包“找工作”页面连续进入 20 次未出现白屏、卡死现象。而本地版 32位包同样的调用频次,为何不出现白屏、卡死现象(但是也会影响性能,造成卡顿),难道和 arm64 内存占用变大有关?
“64位”的优点与缺点:
-
尽管64位与硬件可支持的最大内存无关,但便于单一程序使用更大内存。在32位CPU中,单一程序仅有4GB地址空间,减去被操作系统和标准库所占用的部分,只剩1~3GB可用。如果一个32位系统的RAM超过4GB,单一程序很难充分利用全部空间。
-
即使对于物理内存较小的系统,更大的地址空间也有帮助。内存映射文件是种有用的结构,在32位系统中,程序不能映射大文件(通常是指超过几百MB的文件),而64位系统的可用地址空间更大,不必有这方面的担心。
不过,增加指针宽度有个严重的缺点:
在所有其他条件都相同的情况下,单一程序在64位CPU系统中更占内存。因为指针本身也需要存储于内存中,在64位系统 上,这个空间增加了一倍。而大多数程序运用指针很频繁,所以额外占用的空间往往不少。这给缓存带来了压力,从而导致性能降低。
RN 0.57.8 中对于 arm64 的少部分处理 (内存相关):
###很遗憾,经过苦战排查,不是这个原因
# | case |
---|---|
1 | 64 位包中,其他 RN 页面 debug 连接本地模式下都没有问题,所以之前和 FE 联调的结果被推翻 |
2 | debug 断点模式,64位包也没有问题 |
3 | FE 提供一个简单的倒计时 bundle,release 方式加载也会出现卡死问题 |
4. 柳暗花明 - 初始化预创建 WubaRN 对象
那就很纳闷了,感觉冷启动越慢反而越没有问题,而 arm64 的执行速度毫无疑问是比 v7a 快的。那冷启动时做了什么呢?
adb logcat *:S ReactNative:V ReactNativeJS:V
输出下 ReactNative 框架日志:
10-29 16:14:24.115 11757 11757 D ReactNative: ReactInstanceManager.ctor()
10-29 16:14:24.116 11757 11757 D ReactNative: ReactInstanceManager.createReactContextInBackground()
10-29 16:14:24.116 11757 11757 D ReactNative: ReactInstanceManager.recreateReactContextInBackgroundInner()
10-29 16:14:24.116 11757 11757 D ReactNative: ReactInstanceManager.recreateReactContextInBackgroundFromBundleLoader()
10-29 16:14:24.116 11757 11757 D ReactNative: ReactInstanceManager.recreateReactContextInBackground()
10-29 16:14:24.116 11757 11757 D ReactNative: ReactInstanceManager.runCreateReactContextOnNewThread()
10-29 16:14:24.117 11757 14386 D ReactNative: ReactInstanceManager.createReactContext()
10-29 16:14:24.129 11757 11757 D ReactNative: ReactInstanceManager.attachRootViewToInstance()
10-29 16:14:24.130 11757 14386 D ReactNative: Initializing React Xplat Bridge.
10-29 16:14:24.132 11757 14386 D ReactNative: Initializing React Xplat Bridge before initializeBridge
10-29 16:14:24.136 11757 14386 D ReactNative: Initializing React Xplat Bridge after initializeBridge
10-29 16:14:24.136 11757 14386 D ReactNative: CatalystInstanceImpl.runJSBundle()
10-29 16:14:24.137 11757 14388 D ReactNative: ReactInstanceManager.setupReactContext()
10-29 16:14:24.137 11757 14388 D ReactNative: CatalystInstanceImpl.initialize()
这是初始化 RN 环境的流程,回看下 WubaRN SDK,确实有这个逻辑:
初始化 -> 提前创建 WubaRN 对象 -> 打开载体页 -> 复用上一个 WubaRN 对象 -> 提前创建 WubaRN 对象
主要目的是加快 RN 载体页启动速度。大胆猜测下,是不是由于 arm64 环境下,初始化速度变快,第一个预创建的 WubaRN 对象依赖的其他组件还未初始化完成。这个出错的 WubaRN 对象引起了整个 React Native 环境的全局异常。经过测试,去掉初始化时预创建 WubaRN 对象的逻辑后,64位包下 RN 运行正常。
看看 WubaRN 对象初始化中做了什么:
public ReactInstanceManagerBuilder buildReactInstanceManager(String mainModuleName, Context context) {
ReactInstanceManagerBuilder builder = ReactInstanceManager.builder()
.setApplication((Application) context.getApplicationContext())
.addPackage(new MainReactPackage()) //RN内置的Package,里面有相机、存储、图片、网络等功能
.addPackage(new WubaRCTPackage()) // 自定义Package
.setRedBoxHandler(mExceptionHandler)
.setUseDeveloperSupport(RNDebugSwitcher.getInstance().isDebug())
.setInitialLifecycleState(LifecycleState.BEFORE_CREATE);
// 如果是release包,则我们自己处理Exception,防止崩溃
builder.setNativeModuleCallExceptionHandler(mExceptionHandler);
if (!TextUtils.isEmpty(mainModuleName)) {
builder.setJSMainModulePath(mainModuleName);
} else {
builder.setJSMainModulePath("index.android");
}
List<WubaBaseReactPackage> packages = RNPackageContainer.getInstance().getPackageExport(WubaBaseReactPackage.class);
for (WubaBaseReactPackage reactPackage : packages) {
builder.addPackage(reactPackage);
}
builder.addPackage(new ReanimatedPackage());
String coreBundleUri = BundleFileManager.getInstance().prepare(context).getCoreBundleFile().getAbsolutePath();
WubaRNLogger.i("ReactInstance prepareReactRootViewAndLoad load bundle " + coreBundleUri);
builder.setJSBundleLoader(JSBundleLoader.createFileLoader(coreBundleUri));
return builder;
}
ReactInstanceManager 是 RN 的核心类,管理着 ReactRootView、Native ReactPackage、JSIModulePackage、ViewManagers、ReactContext 等。而 ReactContext 又管理着 JSBridge 相关的逻辑,如 CatalystInstance.
至于 ReactInstanceManager 依赖的哪些库未初始化完成还需要再深度剖析,期待下章。
5.吐槽
RN 涉及 Native 域、JS 域、Bridge 域,框架本身的日志太少(当然它也无法提供完整的调用链日志,因为本身的设计就导致日志注定断开)