Jan
17
在ReactNative使用过程中,我们遇到一个特殊动作下Android端RN页面直接卡死的场景,本文记录该卡死的排查过程与原理分析。
问题现象
假设我有一个RN页A,可以重复打开。我的操作是,root界面,打开A,再打开了其他界面X到Y到Z的业务链条。执行一定操作后,会触发回到root界面即clear top打开root界面,同时再次打开A,来实现快速回到A并刷A。这里不直接clear top到A的原因是,root界面和root界面打开A或者其他MN页面,都可以触发XYZ流程,并且在流程结束后,都期望回到root+A的页面状态,因此XYZ流程结束流程指定为“回到root界面即clear top打开root界面,同时再次打开A”。
假设一开始打开的A叫RN-A,第二次打开的A叫RN-B,那整个路径上页面栈的改变类似于:
背景如上,但是这个操作后打开的A,我们这里叫A一撇或者叫B吧,会卡死,页面事件可以响应但是很多桥触发不了,页面也无法返回包括物理返回,js异常也不会触发红屏了。
更离谱的是,一旦我home到桌面再回来,一切就恢复了。
关注到部分桥与调用行为在异常过程中报错“Activity is null!”,即可开始断点代码,发现当前页面ReactContext内的mCurrentActivity为空,取不到Activity。RN里大量内部桥/事件调用与第三方实现均依赖从ReactContext里拿Activity,拿不到当然就炸了啊。
ReactContext的问题
关注ReactContext中的mCurrentActivity设置与空置逻辑。
a. 当hostActivity生命周期onResume的时候(还有onNewIntent,本次讨论不涉及),会设置mCurrentActivity。
b. 当hostActivity生命周期onDestroy的时候,会将当前ReactContext置为null。
这里注意到ReactNative Android特意设计了hostActivity与ReactContext分离的设计,一个ReactContext运行中对应的Activity可以不断发生改变。类似于支持多Activity共用同一个Context的即现代RN框架常见的引擎缓存复用模式,同时mCurrentActivity预期储存的是当前最上展示的Activity。
因此发生异常的条件一定是,RN页面的最后一次生命周期调用是onDestroy。因此开始断点生命周期:
- 第一次进入 RN-A onCreate -> onResume
- 绑定动作 thisReactContext.mCurrentActivity => RN-A
- XYZ结束操作 RN-B onCreate -> onResume
- 绑定动作 thisReactContext.mCurrentActivity => RN-B
- RN-A 销毁 -> onDestroy
- 解绑动作 thisReactContext.mCurrentActivity => null
明显出现预期之外的事件,即RN-B创建早于RN-A销毁。
不过想想也很自然,Android端在同时发生多个页面关闭/打开,即页面替换时,页面生命周期并不是连续的,实际上onDestroy可能会稍晚于预期,容易发生异步生命周期问题,老问题了。因此根据记录的生命周期,实际上A的销毁晚于B的创建,因此最后一次生命周期调用为onDestroy,将复用的ReactContext持有Activity置为空。
同理可以解释当前后台切换时,onResume会恢复设置Activity,页面功能恢复。
用图描述ReactContext中持有的Activity状态如下:
咋修
短期方案
1. 避免引擎复用的RNActivity连续关闭打开。具体操作方式为,XYZ流程结束后直接其上打开RN-B,RN-B关闭时再执行清栈并回退到根页面。(实际采用了此方案)
2. 关闭引擎复用。(对性能影响较大,未使用)
长期方案
A销毁的时候将mCurrentActivity置为空,需判断mCurrentActivity中是否持有的自己,否则不应该置为空。
onHostDestroy对应方法调用方也需要修改,主要在Activity里,看起来影响面比较大,不过确实是最合理的改法了。
另外对于FB的RN仓库来说,这个改动对于标准RN使用者有严重的break级变更,估计短期内只能在自己fork的版本上修复了。
问题现象
假设我有一个RN页A,可以重复打开。我的操作是,root界面,打开A,再打开了其他界面X到Y到Z的业务链条。执行一定操作后,会触发回到root界面即clear top打开root界面,同时再次打开A,来实现快速回到A并刷A。这里不直接clear top到A的原因是,root界面和root界面打开A或者其他MN页面,都可以触发XYZ流程,并且在流程结束后,都期望回到root+A的页面状态,因此XYZ流程结束流程指定为“回到root界面即clear top打开root界面,同时再次打开A”。
假设一开始打开的A叫RN-A,第二次打开的A叫RN-B,那整个路径上页面栈的改变类似于:
背景如上,但是这个操作后打开的A,我们这里叫A一撇或者叫B吧,会卡死,页面事件可以响应但是很多桥触发不了,页面也无法返回包括物理返回,js异常也不会触发红屏了。
更离谱的是,一旦我home到桌面再回来,一切就恢复了。
关注到部分桥与调用行为在异常过程中报错“Activity is null!”,即可开始断点代码,发现当前页面ReactContext内的mCurrentActivity为空,取不到Activity。RN里大量内部桥/事件调用与第三方实现均依赖从ReactContext里拿Activity,拿不到当然就炸了啊。
ReactContext的问题
关注ReactContext中的mCurrentActivity设置与空置逻辑。
a. 当hostActivity生命周期onResume的时候(还有onNewIntent,本次讨论不涉及),会设置mCurrentActivity。
//ReactAndroid/src/main/java/com/facebook/react/bridge/ReactContext.java
public void onHostResume(@Nullable Activity activity) {
mLifecycleState = LifecycleState.RESUMED;
mCurrentActivity = new WeakReference(activity); // <---------- this
ReactMarker.logMarker(ReactMarkerConstants.ON_HOST_RESUME_START);
for (LifecycleEventListener listener : mLifecycleEventListeners) {
try {
listener.onHostResume();
} catch (RuntimeException e) {
handleException(e);
}
}
ReactMarker.logMarker(ReactMarkerConstants.ON_HOST_RESUME_END);
}
b. 当hostActivity生命周期onDestroy的时候,会将当前ReactContext置为null。
//ReactAndroid/src/main/java/com/facebook/react/bridge/ReactContext.java
public void onHostDestroy() {
UiThreadUtil.assertOnUiThread();
mLifecycleState = LifecycleState.BEFORE_CREATE;
for (LifecycleEventListener listener : mLifecycleEventListeners) {
try {
listener.onHostDestroy();
} catch (RuntimeException e) {
handleException(e);
}
}
mCurrentActivity = null; // <---------- this
}
这里注意到ReactNative Android特意设计了hostActivity与ReactContext分离的设计,一个ReactContext运行中对应的Activity可以不断发生改变。类似于支持多Activity共用同一个Context的即现代RN框架常见的引擎缓存复用模式,同时mCurrentActivity预期储存的是当前最上展示的Activity。
因此发生异常的条件一定是,RN页面的最后一次生命周期调用是onDestroy。因此开始断点生命周期:
- 第一次进入 RN-A onCreate -> onResume
- 绑定动作 thisReactContext.mCurrentActivity => RN-A
- XYZ结束操作 RN-B onCreate -> onResume
- 绑定动作 thisReactContext.mCurrentActivity => RN-B
- RN-A 销毁 -> onDestroy
- 解绑动作 thisReactContext.mCurrentActivity => null
明显出现预期之外的事件,即RN-B创建早于RN-A销毁。
不过想想也很自然,Android端在同时发生多个页面关闭/打开,即页面替换时,页面生命周期并不是连续的,实际上onDestroy可能会稍晚于预期,容易发生异步生命周期问题,老问题了。因此根据记录的生命周期,实际上A的销毁晚于B的创建,因此最后一次生命周期调用为onDestroy,将复用的ReactContext持有Activity置为空。
同理可以解释当前后台切换时,onResume会恢复设置Activity,页面功能恢复。
用图描述ReactContext中持有的Activity状态如下:
咋修
短期方案
1. 避免引擎复用的RNActivity连续关闭打开。具体操作方式为,XYZ流程结束后直接其上打开RN-B,RN-B关闭时再执行清栈并回退到根页面。(实际采用了此方案)
2. 关闭引擎复用。(对性能影响较大,未使用)
长期方案
A销毁的时候将mCurrentActivity置为空,需判断mCurrentActivity中是否持有的自己,否则不应该置为空。
//ReactAndroid/src/main/java/com/facebook/react/bridge/ReactContext.java
public void onHostDestroy() {
//code before
mCurrentActivity = null;
}
/******* 改为 ********/
public void onHostDestroy(Nullable Activity activity) {
//code before
if (mCurrentActivity.get() == activity) {
mCurrentActivity = null;
}
}
onHostDestroy对应方法调用方也需要修改,主要在Activity里,看起来影响面比较大,不过确实是最合理的改法了。
另外对于FB的RN仓库来说,这个改动对于标准RN使用者有严重的break级变更,估计短期内只能在自己fork的版本上修复了。