Jan 17

小记一个ReactNative Android上的设计实现缺陷导致界面卡死的问题

Lrdcq , 2020/01/17 02:51 , 程序 , 閱讀(2087) , Via 本站原創
在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。
//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的版本上修复了。
关键词:rn
logo