本文作者:ivweb 朱灵子

React-Native安卓预加载优化方案

本文针对使用React Native开发混合应用的过程中安卓端白屏时间较长的问题,提出了react-native安卓端RootView预加载优化方案,本文主要围绕以下几个方面展开分析:

  • 导致React-Native安卓端白屏时间较长的关键性因素
  • React-Native安卓预加载优化方案
  • React-Native安卓预加载方案实现细节

导致React-Native安卓端白屏时间较长的关键性因素

我们对不同网络状态下不同机型的React-Native线上项目进行了实时性能监控,下图所示为React Native IOS和安卓端线上性能数据对比分析图

对比IOS端与Android端的首屏时间数据,我们发现安卓端占有一定的劣势,我们在启动React-Native安卓应用时,会发现第一次启动React-Native安卓页面会有一个短暂的白屏过程,而且在完全退出后再进入,仍然会有这个白屏,为什么Android端的白屏时间较IOS较长呢?我们首先分析React-Native页面加载各个阶段的时间响应图

通过观察我们可以发现,React-Native页面加载时间占比最大的是React-Native bundle离线包加载与解析的时间,其次是首屏数据获取的时间。针对首屏获取时间较长的问题,项目已经采用React-Native前端异步数据缓存优化方案,而且在IOS和安卓端数据返回的平均值均在180ms左右,而页面加载的过程中界面渲染以及框架初始化的时间占比均只有9.3%,不为导致IOS和安卓端首屏时间差异较大的关键因素。综上可知,导致React-Native安卓端白屏时间较长的关键性因素是bundle离线包加载与解析的时间较长,因为React-Native安卓端bundle离线包加载与解析的过程是在java端完成的,而IOS bundle离线包加载与解析的过程是在Objective-C端完成的,java的执行效率较低,同时部分安卓低端机型性能较差。

因此,java执行效率较OC来讲相对较低,安卓端机型总体性能与IOS相比占有相对劣势都是导致React-Native安卓端bundle离线包加载与解析的时间较长的原因,也是造成React-Native安卓端白屏时间较长的关键性因素。

React-Native安卓预加载优化方案

为了优化React-Native安卓端线上业务的用户体验,我们提出了React-Native安卓Bundle预加载优化方案

首先展示的是React-Native安卓源码端ReactActivity中的onCreate方法

 @Override
protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  if (getUseDeveloperSupport() && Build.VERSION.SDK_INT >= 23) {
    // Get permission to show redbox in dev builds.
    if (!Settings.canDrawOverlays(this)) {
      Intent serviceIntent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
      startActivity(serviceIntent);
      FLog.w(ReactConstants.TAG, REDBOX_PERMISSION_MESSAGE);
      Toast.makeText(this, REDBOX_PERMISSION_MESSAGE, Toast.LENGTH_LONG).show();
    }
  }
  mReactRootView = createRootView();
  mReactRootView.startReactApplication(
    getReactNativeHost().getReactInstanceManager(),
    getMainComponentName(),
    getLaunchOptions());
  setContentView(mReactRootView);
  mDoubleTapReloadRecognizer = new DoubleTapReloadRecognizer();
}

ReactInstanceManager是用来创建及管理CatalyInstance的实例的上层接口、控制开发调试,生命周期与ReactRootView所在activity保持一致。ReactActivity onCreate方法中的getReactInstanceManager()步骤中执行了bundle离线包文件位置与bundle文件名的设置,如下代码所示

 ReactInstanceManager.Builder builder = ReactInstanceManager.builder()
  .setApplication(mApplication)
  .setJSMainModuleName(getJSMainModuleName())
  .setUseDeveloperSupport(getUseDeveloperSupport())
  .setRedBoxHandler(getRedBoxHandler())
  .setUIImplementationProvider(getUIImplementationProvider())
  .setInitialLifecycleState(LifecycleState.BEFORE_CREATE);

for (ReactPackage reactPackage : getPackages()) {
  builder.addPackage(reactPackage);
}

String jsBundleFile = getJSBundleFile();
if (jsBundleFile != null) {
  builder.setJSBundleFile(jsBundleFile);
} else {
  builder.setBundleAssetName(Assertions.assertNotNull(getBundleAssetName()));
}
builder.build();

为了优化 安卓端bundle离线包加载与解析的过程,我们需要将ReactActivity onCreate方法中的createRootView、startReactApplication以及getReactInstanceManager这些步骤提前,也就是实现react-native安卓端RootView预加载。

React-Native安卓预加载方案实现细节

创建预加载类ReactPreLoader

public class ReactPreLoader {
    private static final String TAG = "ReactPreLoader";
    private static final Map<String, ReactRootView> CACHE_VIEW_MAP =
            new ArrayMap<>();
    /**
     * Get {@link ReactRootView} with corresponding {@link ReactInfo}.
     */
    public static ReactRootView getRootView(ReactInfo reactInfo) {
        return CACHE_VIEW_MAP.get(reactInfo.getMainComponentName());
    }
    /**
     * Pre-load {@link ReactRootView} to local {@link Map}, you may want to
     * load it in previous activity.
     */
    public static void init(Activity activity, ReactInfo reactInfo) {
        if (CACHE_VIEW_MAP.get(reactInfo.getMainComponentName()) != null) {
            return;
        }
        ReactRootView rootView = new ReactRootView(activity);
        rootView.startReactApplication(
                ((ReactApplication) activity.getApplication()).getReactNativeHost().getReactInstanceManager(),
                reactInfo.getMainComponentName(),
                reactInfo.getLaunchOptions());
        CACHE_VIEW_MAP.put(reactInfo.getMainComponentName(), rootView);
    }
    /**
     * Remove {@link ReactRootView} from parent.
     */
    public static void onDestroy(ReactInfo reactInfo) {
        try {
            ReactRootView rootView = getRootView(reactInfo);
            ViewGroup parent = (ViewGroup) rootView.getParent();
            if (parent != null) {
                parent.removeView(rootView);
            }
        } catch (Throwable e) {
            Logger.e(TAG, e);
        }
    }
}

  • 在init操作中,我们通过ReactInfo缓存把view缓存在本地的ArrayMap
  • 同时为了优化React-Native线上项目内存方面的占用率,在ReactActivity销毁后,我们需要使用onDestroy()方法把view从 parent 上卸载下来

获取预加载之后缓存在本地ArrayMap中的rootView

为了获取并使用预加载之后缓存在本地ArrayMap中的rootView,我们需要侵入activity的创建过程,因此我们需要对React-Native原生库库提供的ReactActivity进行改造,以下列出修改方法:

public abstract class MrReactActivity extends Activity
        implements DefaultHardwareBackBtnHandler, PermissionAwareActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (getUseDeveloperSupport() && Build.VERSION.SDK_INT >= 23) {
            // Get permission to show redbox in dev builds.
            if (!Settings.canDrawOverlays(this)) {
                Intent serviceIntent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
                startActivity(serviceIntent);
                FLog.w(ReactConstants.TAG, REDBOX_PERMISSION_MESSAGE);
                Toast.makeText(this, REDBOX_PERMISSION_MESSAGE, Toast.LENGTH_LONG).show();
            }
        }
        mReactRootView = ReactPreLoader.getRootView(getReactInfo());
        if (mReactRootView != null) {
            Logger.i(TAG, "use pre-load view");
        } else {
            Logger.i(TAG, "createRootView");
            mReactRootView = createRootView();
            if (mReactRootView != null) {
                mReactRootView.startReactApplication(
                        getReactNativeHost().getReactInstanceManager(),
                        getMainComponentName(),
                        getLaunchOptions());
            }
        }
        setContentView(mReactRootView);
        mDoubleTapReloadRecognizer = new DoubleTapReloadRecognizer();
    }
    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (mReactRootView != null) {
            mReactRootView.unmountReactApplication();
            mReactRootView = null;
            ReactPreLoader.onDestroy(getReactInfo());
        }
    }
    public abstract ReactInfo getReactInfo();
}

使用预加载之后缓存在本地ArrayMap中的rootView

首先,在进入当前React-Native activity 的父级 activity调用ReactPreLoader中的init方法,如下图所示:

ReactPreLoader.init(this, ReactCardActivity.reactInfo);

其中ReactCardActivity继承上一个模块中对React-Native源码库进行简单改造后的ReactActivity:

public class ReactCardActivity extends MrReactActivity {
    public static final ReactInfo reactInfo = new ReactInfo("card", null);
    @Override
    protected String getMainComponentName() {
        return reactInfo.getMainComponentName();
    }
    @Override
    public ReactInfo getReactInfo() {
        return reactInfo;
    }
}

该React-Native安卓端预加载优化方案可以很大程度上减少安卓端React-Native线上项目的白屏时间,优化React-Native线上业务的业务体验!

原文链接:http://www.ivweb.io/topic/5958de8b1eb29e24e8a098a4

文章来源于腾讯云开发者社区,点击查看原文