Android集成React Native启动白屏问题优化

一、关于预加载方案预研

有一个方案是使用内存换取读取时间的一种折中的方案,网上通篇也说的这个方案。关于这个给大家一个链接,大家可以参考。

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

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

我们可以看到耗时最长的是JsBundle离线包的加载与解析。使用上面的那种全局Map存放RootView的方案,只是优化的是从Bundle解析页面的时间。追踪React Native源码这种方案优化的只是这一部分的时间,本质上并不能解决启动白屏的现像。
  /**
   * Schedule rendering of the react component rendered by the JS application from the given JS
   * module (@{param moduleName}) using provided {@param reactInstanceManager} to attach to the
   * JS context of that manager. Extra parameter {@param launchOptions} can be used to pass initial
   * properties for the react component.
   */
  public void startReactApplication(
      ReactInstanceManager reactInstanceManager,
      String moduleName,
      @Nullable Bundle initialProperties) {
    Systrace.beginSection(TRACE_TAG_REACT_JAVA_BRIDGE, "startReactApplication");
    try {
      UiThreadUtil.assertOnUiThread();

      // TODO(6788889): Use POJO instead of bundle here, apparently we can't just use WritableMap
      // here as it may be deallocated in native after passing via JNI bridge, but we want to reuse
      // it in the case of re-creating the catalyst instance
      Assertions.assertCondition(
        mReactInstanceManager == null,
        "This root view has already been attached to a catalyst instance manager");

      mReactInstanceManager = reactInstanceManager;
      mJSModuleName = moduleName;
      mAppProperties = initialProperties;

      if (!mReactInstanceManager.hasStartedCreatingInitialContext()) {
        mReactInstanceManager.createReactContextInBackground();
      }

      attachToReactInstanceManager();

    } finally {
      Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE);
    }
  }
复制代码

二、彻底解决应用白屏的方案

其实说起来也很简单,只是我们在使用时不会去注意这么细节。上面也说了要优化白屏,那就着重2个方面入手一个是JsBundle的加载一个是JsBundle的解析。前一种方案只是优化了JsBundle的解析的时间,那么加载JsBundle是在哪一个函数里加载的呢?
分析源码首先我们来看getReactNativeHost(),这是一个接口函数,我们的实现是传递一些初始化的ReactPackage和JSBundle文件名。
     private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
        @Override
        public boolean getUseDeveloperSupport() {
            return BuildConfig.DEBUG;
        }

        @Override
        protected List<ReactPackage> getPackages() {
            return Arrays.<ReactPackage>asList(
                    new MainReactPackage(),
                    new RNFSPackage(),
                    new LottiePackage(),
                    new AutoHeightWebViewPackage(),
                    new MReactPackage(),
                    new LinearGradientPackage(),
                    new CodePush(BuildConfig.CODEPUSH_KEY, SysApplication.this, BuildConfig.DEBUG),
                    new SvgPackage(),
                    new RNViewShotPackage()
            );
        }

        @Override
        protected String getJSBundleFile() {
            return CodePush.getJSBundleFile();
        }
    };

    @Override
    public ReactNativeHost getReactNativeHost() {
        return mReactNativeHost;
    }
复制代码
好像这里getJSBundleFile有点像,但只是返回文件名,并不能执行真正的加载,看来加载JSBundle不在这里,继续追踪getReactInstanceManager()方法。追踪到createReactInstanceManager()方法里可以看到ReactNativeHost中声明的getJSBundleFile()在这里调用。使用ReactInstanceManagerBuilder构造ReactInstanceManager
  protected ReactInstanceManager createReactInstanceManager() {
    ReactMarker.logMarker(ReactMarkerConstants.BUILD_REACT_INSTANCE_MANAGER_START);
    ReactInstanceManagerBuilder builder = ReactInstanceManager.builder()
      .setApplication(mApplication)
      .setJSMainModulePath(getJSMainModuleName())
      .setUseDeveloperSupport(getUseDeveloperSupport())
      .setRedBoxHandler(getRedBoxHandler())
      .setJavaScriptExecutorFactory(getJavaScriptExecutorFactory())
      .setUIImplementationProvider(getUIImplementationProvider())
      .setJSIModulesProvider(getJSIModulesProvider())
      .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()));
    }
    ReactInstanceManager reactInstanceManager = builder.build();
    ReactMarker.logMarker(ReactMarkerConstants.BUILD_REACT_INSTANCE_MANAGER_END);
    return reactInstanceManager;
  }
复制代码
继续追踪ReactInstanceManagerBuilder的build()方法
  /**
   * Instantiates a new {@link ReactInstanceManager}.
   * Before calling {@code build}, the following must be called:
   * <ul>
   * <li> {@link #setApplication}
   * <li> {@link #setCurrentActivity} if the activity has already resumed
   * <li> {@link #setDefaultHardwareBackBtnHandler} if the activity has already resumed
   * <li> {@link #setJSBundleFile} or {@link #setJSMainModulePath}
   * </ul>
   */
  public ReactInstanceManager build() {
    Assertions.assertNotNull(
      mApplication,
      "Application property has not been set with this builder");

    Assertions.assertCondition(
      mUseDeveloperSupport || mJSBundleAssetUrl != null || mJSBundleLoader != null,
      "JS Bundle File or Asset URL has to be provided when dev support is disabled");

    Assertions.assertCondition(
      mJSMainModulePath != null || mJSBundleAssetUrl != null || mJSBundleLoader != null,
      "Either MainModulePath or JS Bundle File needs to be provided");

    if (mUIImplementationProvider == null) {
      // create default UIImplementationProvider if the provided one is null.
      mUIImplementationProvider = new UIImplementationProvider();
    }

    // We use the name of the device and the app for debugging & metrics
    String appName = mApplication.getPackageName();
    String deviceName = getFriendlyDeviceName();

    return new ReactInstanceManager(
        mApplication,
        mCurrentActivity,
        mDefaultHardwareBackBtnHandler,
        mJavaScriptExecutorFactory == null
            ? new JSCJavaScriptExecutorFactory(appName, deviceName)
            : mJavaScriptExecutorFactory,
        (mJSBundleLoader == null && mJSBundleAssetUrl != null)
            ? JSBundleLoader.createAssetLoader(
                mApplication, mJSBundleAssetUrl, false /*Asynchronous*/)
            : mJSBundleLoader,
        mJSMainModulePath,
        mPackages,
        mUseDeveloperSupport,
        mBridgeIdleDebugListener,
        Assertions.assertNotNull(mInitialLifecycleState, "Initial lifecycle state was not set"),
        mUIImplementationProvider,
        mNativeModuleCallExceptionHandler,
        mRedBoxHandler,
        mLazyNativeModulesEnabled,
        mLazyViewManagersEnabled,
        mDelayViewManagerClassLoadsEnabled,
        mDevBundleDownloadListener,
        mMinNumShakes,
        mMinTimeLeftInFrameForNonBatchedOperationMs,
      mJSIModulesProvider);
  }
}
复制代码
可以发现有这么一段代码
JSBundleLoader.createAssetLoader(
                mApplication, mJSBundleAssetUrl, false /*Asynchronous*/)
复制代码
紧接着看JSBundleLoader的源码
  /**
   * This loader is recommended one for release version of your app. In that case local JS executor
   * should be used. JS bundle will be read from assets in native code to save on passing large
   * strings from java to native memory.
   */
  public static JSBundleLoader createAssetLoader(
      final Context context,
      final String assetUrl,
      final boolean loadSynchronously) {
    return new JSBundleLoader() {
      @Override
      public String loadScript(CatalystInstanceImpl instance) {
        instance.loadScriptFromAssets(context.getAssets(), assetUrl, loadSynchronously);
        return assetUrl;
      }
    };
  }
复制代码
近loadScriptFromAssets()可以发现这里调用了JNI方法从Assets文件夹里读取打包完的JsBundle文件
  /* package */ void loadScriptFromAssets(AssetManager assetManager, String assetURL, boolean loadSynchronously) {
    mSourceURL = assetURL;
    jniLoadScriptFromAssets(assetManager, assetURL, loadSynchronously);
  }
复制代码
那么是哪个函数调用了JSBundleLoader的loadScript(CatalystInstanceImpl instance)了呢?好像明明之中我们也知道了答案,除了createReactContextInBackground()这个方法没有进去看,其他的基本都看了。那么我们来看看createReactContextInBackground()这个方法的内部实现。
我们一步步追踪到这个函数,可以看到这里有一个mBundleLoader对象,通过全局搜索可以得知这个是ReactInstanceManager的一个内部变量,而这个变量就是在上面的ReactInstanceManagerBuilder的build()方法里初始化的。
  @ThreadConfined(UI)
  private void recreateReactContextInBackgroundFromBundleLoader() {
    Log.d(
      ReactConstants.TAG,
      "ReactInstanceManager.recreateReactContextInBackgroundFromBundleLoader()");
    PrinterHolder.getPrinter()
        .logMessage(ReactDebugOverlayTags.RN_CORE, "RNCore: load from BundleLoader");
    recreateReactContextInBackground(mJavaScriptExecutorFactory, mBundleLoader);
  }
复制代码
继续追踪recreateReactContextInBackground()方法,最后可以看到BundleLoader对象被用于createReactContext()方法中
           try {
                  Process.setThreadPriority(Process.THREAD_PRIORITY_DISPLAY);
                  final ReactApplicationContext reactApplicationContext =
                      createReactContext(
                          initParams.getJsExecutorFactory().create(),
                          initParams.getJsBundleLoader());

                  mCreateReactContextThread = null;
                  ReactMarker.logMarker(PRE_SETUP_REACT_CONTEXT_START);
                  final Runnable maybeRecreateReactContextRunnable =
                      new Runnable() {
                        @Override
                        public void run() {
                          if (mPendingReactContextInitParams != null) {
                            runCreateReactContextOnNewThread(mPendingReactContextInitParams);
                            mPendingReactContextInitParams = null;
                          }
                        }
                      };
                  Runnable setupReactContextRunnable =
                      new Runnable() {
                        @Override
                        public void run() {
                          try {
                            setupReactContext(reactApplicationContext);
                          } catch (Exception e) {
                            mDevSupportManager.handleException(e);
                          }
                        }
                      };

                  reactApplicationContext.runOnNativeModulesQueueThread(setupReactContextRunnable);
                  UiThreadUtil.runOnUiThread(maybeRecreateReactContextRunnable);
                } catch (Exception e) {
                  mDevSupportManager.handleException(e);
                }
复制代码
进一步追踪createReactContext()最终发现,JSBundleLoader的loadScript(CatalystInstanceImpl instance)是在CatalystInstanceImpl中的runJSBundle()方法中调用。
  @Override
  public void runJSBundle() {
    Log.d(ReactConstants.TAG, "CatalystInstanceImpl.runJSBundle()");
    Assertions.assertCondition(!mJSBundleHasLoaded, "JS bundle was already loaded!");
    // incrementPendingJSCalls();
    mJSBundleLoader.loadScript(CatalystInstanceImpl.this);

    synchronized (mJSCallsPendingInitLock) {

      // Loading the bundle is queued on the JS thread, but may not have
      // run yet.  It's safe to set this here, though, since any work it
      // gates will be queued on the JS thread behind the load.
      mAcceptCalls = true;

      for (PendingJSCall function : mJSCallsPendingInit) {
        function.call(this);
      }
      mJSCallsPendingInit.clear();
      mJSBundleHasLoaded = true;
    }

    // This is registered after JS starts since it makes a JS call
    Systrace.registerListener(mTraceListener);
  }
复制代码
那么前面阐述的问题也便知道了答案,JsBundle是在createReactContextInBackground()中加载的
那么我们优化也是着重这一块优化,把这个函数放在Loading页面里去加载,把原来加载JsBundle的代码从Application挪到Loading页面(启动页:应用启动第一个页面)。
这样有2个好处,一个是Application不会因为加载JsBundle耗时,而迟迟Loading页显示不出来,如果没有做过Android冷启动优化的App可能就是白屏3S以上或者点击应用图标没有要过一会才能进Loading页面,这样就加快了应用的启动速度。
另一个好处就是可以在Loading页面预加载首页的React Native页面,加快首页的加载时间。还有一个就是今天的重点,怎么去解决首页白屏问题,那就是在Loading页也设置一个ReactRootView,并且给这个View设置setEventListener监听事件,待JsBundle加载完毕之后,就会走进这个监听方法里,在这个方法里跳转首页。这样就不会引起,JsBundle还未加载完成,就跳近了首页。导致首页白屏或者黑屏,需要等JsBundle加载完毕之后才能显示出来。如下面代码所示,initReactNative()在onCreate()中调用。
    /**
     * 作者:郭翰林
     * 时间:2018/8/9 0009 17:59
     * 注释:初始化RN,预加载JsBundle
     */
    private void initReactNative() {
        mReactInstanceManager = ((ReactApplication) GlobalServiceManager.getService(IAppService.class).getAppContext())
                .getReactNativeHost()
                .getReactInstanceManager();
        if (!mReactInstanceManager.hasStartedCreatingInitialContext()) {
            mReactInstanceManager.createReactContextInBackground();
            mReactRootView = findViewById(R.id.reactRootView);
            mReactRootView.startReactApplication(
                    mReactInstanceManager,
                    "NetWorkSettingPage",
                    null
            );
            //设置ReactRootView监听,如果JsBundle加载完成才允许跳转下个页面
            mReactRootView.setEventListener((rootView) -> {
                gotoHomeActivity();
            });
        } else {
            gotoHomeActivity();
        }
    }
复制代码
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值