美文网首页
Android集成React Native启动白屏问题优化

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

作者: RmondJone | 来源:发表于2018-08-09 20:54 被阅读0次

    一、关于预加载方案预研

    有一个方案是使用内存换取读取时间的一种折中的方案,网上通篇也说的这个方案。关于这个给大家一个链接,大家可以参考。
    React-Native 安卓预加载优化方案
    对比IOS端与Android端的首屏时间数据,我们发现安卓端占有一定的劣势,我们在启动React-Native安卓应用时,会发现第一次启动React-Native安卓页面会有一个短暂的白屏过程,而且在完全退出后再进入,仍然会有这个白屏,为什么Android端的白屏时间较IOS较长呢?我们首先分析React-Native页面加载各个阶段的时间响应图

    image

    我们可以看到耗时最长的是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();
            }
        }
    

    相关文章

      网友评论

          本文标题:Android集成React Native启动白屏问题优化

          本文链接:https://www.haomeiwen.com/subject/tqthbftx.html