ReactNative在Android上首屏加载白屏优化

  1. 背景:
    在项目集成了RN模块后,发现在Android从原生页面跳转到RN页面的时候,出现一段比较明显的白屏之后才加载出页面,这种情况在iOS上的体验就比较好,所以查看了下网上的解决方案,发现在官方的中文文档上有一篇文章给了具体的解决步骤,因此决定试试。
    在这里要吐槽下,百度出来的解决 方案基本都是一样的,估计copy的人自己都没有跑过,代码写的都很乱,也没有完整的Demo可以参考,不过思路基本可以看懂,代码还是要自己写一写的。
    文档地址: http://reactnative.cn/post/754

  2. 解决思路:
    用内存换时间,在App初始化的时候就把RN的View加载到缓存,跳转时直接取缓存,达到秒开的效果。
    实际开发之后,发现第一次加载还是有短暂的白屏现象,但是相对之前已经不明显了,而且第一次加载之后的跳转都没有了白屏现象,可以算是成功的优化方案了。

  3. 具体步骤:
    3.1 新建缓存RootView管理器RNCacheViewManager

public class RNCacheViewManager {

private static ReactRootView mRootView = null;
private static ReactInstanceManager mManager = null;
private static RnInfo mRnInfo = null;
//初始化方法,App启动时调用
public static void init(Activity act, RnInfo rnInfo) {
    if (mManager == null) {
        updateCache(act, rnInfo);
    }
}

//更新cache,适合于版本升级时候更新cache
public static void updateCache(Activity act, RnInfo rnInfo) {
    mRnInfo = rnInfo;
    mManager = createReactInstanceManager(act);
    mRootView = new ReactRootView(act);
    mRootView.startReactApplication(mManager, rnInfo.getMainComponentName(), null);
}

public static ReactRootView getReactRootView() {
    if(mRootView==null){
        throw new RuntimeException("缓存view管理器尚未初始化!");
    }
    return mRootView;
}
public static RnInfo getRnInfo() {
    if(mRnInfo==null){
        throw  new RuntimeException("缓存view管理器尚未初始化!");
    }
    return mRnInfo;
}
//这里要remove掉rootview的parent对象,否则下次再setContentView时候会报错
public static void onDestroy() {
    try {
        ViewParent parent = getReactRootView().getParent();
        if (parent != null)
            ((android.view.ViewGroup) parent).removeView(getReactRootView());
    } catch (Throwable e) {
        e.printStackTrace();
    }
}
//清理缓存数据
public static void clear() {
    try {
        if (mManager != null) {
            mManager.onHostDestroy();
            mManager = null;
        }
        if (mRootView != null) {
            onDestroy();
            mRootView = null;
        }
        mRnInfo = null;
    } catch (Throwable e) {
        e.printStackTrace();
    }
}
//创建 ReactInstanceManager
private static ReactInstanceManager createReactInstanceManager(Activity act) {

    ReactInstanceManager mReactInstanceManager = ReactInstanceManager.builder()
            .setApplication(act.getApplication())
            .setBundleAssetName(getRnInfo().getBundleAssetName())
            .setJSMainModuleName(getRnInfo().getJSMainModuleName())
            .addPackage(getRnInfo().getMainReactPackage())
            .setUseDeveloperSupport(getRnInfo().getUseDeveloperSupport())
            .setInitialLifecycleState(LifecycleState.BEFORE_RESUME).build();

    return mReactInstanceManager;
}

}
```
RnInfo只是自定义的一个类,里面只是ReactNative的一些配置信息,这里就不贴出来了,需要的自行到git上看。
3.2 创建一个Activity,将缓存的View加载到当前Activity

public class MyPreloadReactActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //加载缓存的mReactRootView
        ReactRootView mReactRootView = RNCacheViewManager.getReactRootView();
        setContentView(mReactRootView);
    }

    @Override
    protected void onDestroy() {
        //退出时要调用要RNCacheViewManager remove掉rootview的parent对象,否则下次再setContentView时候会报错
        RNCacheViewManager.onDestroy();
        super.onDestroy();
    }
}

3.3 App启动初始化加载RN View到缓存中
这一步虽然调用简单,但是也是很重要的一步。

//初始化ReactNative缓存处理器
        RNCacheViewManager.init(MainActivity.this, new RnInfo());
 到这里基本就完成了,后面附上效果图跟Demo的github地址。
 要注意的是这个处理针对的是原生项目加载RN页面,而且是加载离线的bundle文件。基友说iOS也是需要使用这种预加载策略的,之后再研究下iOS的预加载,研究成功的话再整理下实现的步骤。
  1. 效果图:
    4.1 未使用缓存加载:
ReactNative在Android上首屏加载白屏优化_第1张图片
cache.gif

4.2 使用缓存之后:

ReactNative在Android上首屏加载白屏优化_第2张图片
cached.gif

5 . GitHub地址:
Android:
https://git.oschina.net/GStick/androidiosfeatreactnative.git

6 . 后续补充:
以上都是在Demo上开发的,功能比较简单。之后正式在项目上使用的过程中发现两个比较大的坑,在此补充说下问题跟解决办法:

6.1 由于项目中使用原生与RN代码交互时是将自定义的ReactPackage与当前Activity绑定的,而预加载是在当前Activity初始化之前缓存的RootView,所以导致加入预加载的代码之后之前交互代码都无效了。

解决办法:需要修改自定义的ReactPackage,将其与Application绑定,然后用堆栈管理Activity,在自定义的ReactContextBaseJavaModule中获取当前的Activity作交互。

6.2 RN中有一个Modal控件比较特殊,在Android上是渲染成Dialog,渲染时把当前ReactRootView的context传给了Dialog,而不是当前Activity的context,导致RN上所有Modal控件都在初始化RootView管理器所在的Activity上弹出了。

这个问题困扰了我好久,最后是翻过重重外墙,在外网上找到了一个不是很明显的解决方法:

MutableContextWrapper

懂一点iOS的我看到Mutable这个前缀大概就知道这个API的大概作用了[嘿嘿],然后又去翻了一下API文档就隐约看到希望了。
大概的解决思路就是,初始化RootView管理器的时候使用MutableContextWrapper包住初始化使用的Activity context,在进入RN所在的Activity的onCreate()中用 setBaseContext(Activity act)替换MutableContextWrapper中的context对象。

mRootView = new ReactRootView(mutableContextWrapper);
RNCacheViewManager.getMutableContextWrapper().setBaseContext(PreloadReactActivity.this);

6.3 这里多补充一个问题,由于我们的应用存在多个Tab切换的场景,测试过程中发现ScrollViewscrollTo方法偶尔会出现滚动停止的现象,导致tab的切换卡在中间。经过很久的研究发现不是RN的问题,而是当Activity在缓存了RootView的情况下,对RootView的各种属性保存存在问题。
解决办法:在Activity OnCreate()之后更新缓存里面的RootView

你可能感兴趣的:(ReactNative在Android上首屏加载白屏优化)