React Native基于最新版本实现JsBundle预加载,解决白屏等待,界面秒开优化

xiaoxiao2021-02-27  143

前些时间和大家分享了一系列关于React Native For Android的文章。这两天又对react Native增量热更新的博客进行了填充,增加了图片增量更新的实现方案和过程。有兴趣的朋友可以去浏览详细内容。为了方便,我将前几篇的博客链接贴出来供大家参考:

Android原生项目集成React Native

React Native与Android通信交互

React Native 实现热部署、差异化增量热更新

React Native开源项目 「漫画书」

一、问题分析

本篇博客同样和大家分享关于React Native的内容。想必大家在撸码中都发现了一个问题:从Android原生界面第一次跳转到React Native界面时,会有短暂的白屏过程,然后才会加载出界面。下次再跳转就不会出现类似问题。并且当我们杀死应用,重新启动App从Android Activity跳转到RN界面,依然会出现短暂白屏。

为什么第一次加载React Native界面会出现短暂白屏呢?大家别忘了,react native的渲染机制是对于JsBundle的加载。项目中所有的js文件最终会被打包成一个JsBundle文件,android环境下Bundle文件为:‘index.android.bundle’。系统在第一次渲染界面时,会首先加载JsBundle文件。那么问题肯定出现在加载JsBundle这个过程,即出现白屏可能是因为JsBundle正在加载。发现了原因,我们继续查看源码,看看是否能从源码中得知一二。

二、源码分析

Android集成的RN界面,需要继承ReactActivity,那么直接从ReactActivity源码入手:

[java]  view plain  copy public abstract class ReactActivity extends Activity       implements DefaultHardwareBackBtnHandler, PermissionAwareActivity {        private final ReactActivityDelegate mDelegate;        protected ReactActivity() {       mDelegate = createReactActivityDelegate();     }        /**     * Returns the name of the main component registered from JavaScript.     * This is used to schedule rendering of the component.     * e.g. "MoviesApp"     */     protected @Nullable String getMainComponentName() {       return null;     }        /**     * Called at construction time, override if you have a custom delegate implementation.     */     protected ReactActivityDelegate createReactActivityDelegate() {       return new ReactActivityDelegate(this, getMainComponentName());     }        @Override     protected void onCreate(Bundle savedInstanceState) {       super.onCreate(savedInstanceState);       mDelegate.onCreate(savedInstanceState);     }        @Override     protected void onPause() {       super.onPause();       mDelegate.onPause();     }        @Override     protected void onResume() {       super.onResume();       mDelegate.onResume();     }        @Override     protected void onDestroy() {       super.onDestroy();       mDelegate.onDestroy();     }     // 其余代码略......   }   不难发现,ReactActivity中的行为都交给了ReactActivityDelegate类来处理。很明显是委托模式。至于白屏原因是因为第一次创建时,那么我们直接看onCreate即可。找到ReactActivityDelegate的onCreate方法:

[java]  view plain  copy protected void onCreate(Bundle savedInstanceState) {       boolean needsOverlayPermission = false;       if (getReactNativeHost().getUseDeveloperSupport() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {         // Get permission to show redbox in dev builds.         if (!Settings.canDrawOverlays(getContext())) {           needsOverlayPermission = true;           Intent serviceIntent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + getContext().getPackageName()));           FLog.w(ReactConstants.TAG, REDBOX_PERMISSION_MESSAGE);           Toast.makeText(getContext(), REDBOX_PERMISSION_MESSAGE, Toast.LENGTH_LONG).show();           ((Activity) getContext()).startActivityForResult(serviceIntent, REQUEST_OVERLAY_PERMISSION_CODE);         }       }          if (mMainComponentName != null && !needsOverlayPermission) {         loadApp(mMainComponentName);       }       mDoubleTapReloadRecognizer = new DoubleTapReloadRecognizer();     }   从源码可以看到,最终调用了loadApp方法,继续跟踪loadApp方法:

[java]  view plain  copy protected void loadApp(String appKey) {     if (mReactRootView != null) {       throw new IllegalStateException("Cannot loadApp while app is already running.");     }     mReactRootView = createRootView();     mReactRootView.startReactApplication(       getReactNativeHost().getReactInstanceManager(),       appKey,       getLaunchOptions());     getPlainActivity().setContentView(mReactRootView);   }   [java]  view plain  copy protected ReactRootView createRootView() {      return new ReactRootView(getContext());    }  

loadApp方法中调用了createRootView创建了ReactRootView,即React Native界面,并且将界面设置到Activity中。那么问题很可能出现在这了。插个断点,调试看看执行时间。

一切恍然大悟,在createRootView和startReactApplication时,消耗了较长时间。

既然是createRootView和startReactApplication执行了耗时操作的问题,那么我们只需要将其提前执行,创建出ReactRootView并缓存下来。当跳转到React Native界面时,直接设置到ContentView即可。有了解决思路,又该到我们甩起袖子撸码了。

三、功能实现

[java]  view plain  copy /**   * 预加载工具类   * Created by Song on 2017/5/10.   */   public class ReactNativePreLoader {          private static final Map<String,ReactRootView> CACHE = new ArrayMap<>();          /**       * 初始化ReactRootView,并添加到缓存       * @param activity       * @param componentName       */       public static void preLoad(Activity activity, String componentName) {              if (CACHE.get(componentName) != null) {               return;           }           // 1.创建ReactRootView           ReactRootView rootView = new ReactRootView(activity);           rootView.startReactApplication(                   ((ReactApplication) activity.getApplication()).getReactNativeHost().getReactInstanceManager(),                   componentName,                   null);              // 2.添加到缓存           CACHE.put(componentName, rootView);       }          /**       * 获取ReactRootView       * @param componentName       * @return       */       public static ReactRootView getReactRootView(String componentName) {           return CACHE.get(componentName);       }          /**       * 从当前界面移除 ReactRootView       * @param component       */       public static void deatchView(String component) {           try {               ReactRootView rootView = getReactRootView(component);               ViewGroup parent = (ViewGroup) rootView.getParent();               if (parent != null) {                   parent.removeView(rootView);               }           } catch (Throwable e) {               Log.e("ReactNativePreLoader",e.getMessage());           }       }  

上述代码很简单,包含了三个方法:

(1)preLoad

         负责创建ReactRootView,并添加到缓存。

(2)getReactRootView

        获取创建的RootView

(3)deatchView

        将添加的RootView从布局根容器中移除,在 ReactActivity 销毁后,我们需要把 view 从 parent 上卸载下来,避免出现重复添加View的异常。

从源码分析部分我们知道,集成React Native界面时,只需要继承ReactActivity,并实现getMainComponentName方法即可。加载创建视图的流程系统都在ReactActivity帮我们完成。现在因为自定义了ReactRootView的加载方式,要使用预加载方式,就不能直接继承ReactActivity。所以接下来需要我们自定义ReactActivity。

从源码中我们已经发现,ReactActivity的处理都交给了ReactActivityDelegate。所以我们可以自定义一个新的ReactActivityDelegate,只需要修改onCreate创建部分,其他照搬源码即可。

[java]  view plain  copy public class PreLoadReactDelegate {          private final Activity mActivity;       private ReactRootView mReactRootView;       private Callback mPermissionsCallback;       private final String mMainComponentName;       private PermissionListener mPermissionListener;       private final int REQUEST_OVERLAY_PERMISSION_CODE = 1111;       private DoubleTapReloadRecognizer mDoubleTapReloadRecognizer;          public PreLoadReactDelegate(Activity activity, @Nullable String mainComponentName) {           this.mActivity = activity;           this.mMainComponentName = mainComponentName;       }          public void onCreate() {           boolean needsOverlayPermission = false;           if (getReactNativeHost().getUseDeveloperSupport() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {               // Get permission to show redbox in dev builds.               if (!Settings.canDrawOverlays(mActivity)) {                   needsOverlayPermission = true;                   Intent serviceIntent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + mActivity.getPackageName()));                   mActivity.startActivityForResult(serviceIntent, REQUEST_OVERLAY_PERMISSION_CODE);               }           }              if (mMainComponentName != null && !needsOverlayPermission) {               // 1.从缓存中获取RootView               mReactRootView = ReactNativePreLoader.getReactRootView(mMainComponentName);                  if(mReactRootView == null) {                      // 2.缓存中不存在RootView,直接创建                   mReactRootView = new ReactRootView(mActivity);                   mReactRootView.startReactApplication(                           getReactInstanceManager(),                           mMainComponentName,                           null);               }               // 3.将RootView设置到Activity布局               mActivity.setContentView(mReactRootView);           }              mDoubleTapReloadRecognizer = new DoubleTapReloadRecognizer();       }          public void onResume() {           if (getReactNativeHost().hasInstance()) {               getReactInstanceManager().onHostResume(mActivity, (DefaultHardwareBackBtnHandler)mActivity);           }           if (mPermissionsCallback != null) {               mPermissionsCallback.invoke();               mPermissionsCallback = null;           }       }          public void onPause() {           if (getReactNativeHost().hasInstance()) {               getReactInstanceManager().onHostPause(mActivity);           }       }          public void onDestroy() {              if (mReactRootView != null) {               mReactRootView.unmountReactApplication();               mReactRootView = null;           }           if (getReactNativeHost().hasInstance()) {               getReactInstanceManager().onHostDestroy(mActivity);           }              // 清除View           ReactNativePreLoader.deatchView(mMainComponentName);       }          public boolean onNewIntent(Intent intent) {           if (getReactNativeHost().hasInstance()) {               getReactInstanceManager().onNewIntent(intent);               return true;           }           return false;       }          public void onActivityResult(int requestCode, int resultCode, Intent data) {           if (getReactNativeHost().hasInstance()) {               getReactInstanceManager().onActivityResult(mActivity, requestCode, resultCode, data);           } else {               // Did we request overlay permissions?               if (requestCode == REQUEST_OVERLAY_PERMISSION_CODE && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {                   if (Settings.canDrawOverlays(mActivity)) {                       if (mMainComponentName != null) {                           if (mReactRootView != null) {                               throw new IllegalStateException("Cannot loadApp while app is already running.");                           }                           mReactRootView = new ReactRootView(mActivity);                           mReactRootView.startReactApplication(                                   getReactInstanceManager(),                                   mMainComponentName,                                   null);                           mActivity.setContentView(mReactRootView);                       }                   }               }           }       }          public boolean onBackPressed() {           if (getReactNativeHost().hasInstance()) {               getReactInstanceManager().onBackPressed();               return true;           }           return false;       }          public boolean onRNKeyUp(int keyCode) {           if (getReactNativeHost().hasInstance() && getReactNativeHost().getUseDeveloperSupport()) {               if (keyCode == KeyEvent.KEYCODE_MENU) {                   getReactInstanceManager().showDevOptionsDialog();                   return true;               }               boolean didDoubleTapR = Assertions.assertNotNull(mDoubleTapReloadRecognizer)                       .didDoubleTapR(keyCode, mActivity.getCurrentFocus());               if (didDoubleTapR) {                   getReactInstanceManager().getDevSupportManager().handleReloadJS();                   return true;               }           }           return false;       }          public void requestPermissions(String[] permissions, int requestCode, PermissionListener listener) {           mPermissionListener = listener;           mActivity.requestPermissions(permissions, requestCode);       }          public void onRequestPermissionsResult(final int requestCode, final String[] permissions, final int[] grantResults) {           mPermissionsCallback = new Callback() {               @Override               public void invoke(Object... args) {                   if (mPermissionListener != null && mPermissionListener.onRequestPermissionsResult(requestCode, permissions, grantResults)) {                       mPermissionListener = null;                   }               }           };       }          /**       * 获取 Application中 ReactNativeHost       * @return       */       private ReactNativeHost getReactNativeHost() {           return MainApplication.getInstance().getReactNativeHost();       }          /**       * 获取 ReactInstanceManager       * @return       */       private ReactInstanceManager getReactInstanceManager() {           return getReactNativeHost().getReactInstanceManager();       }   }  

代码很长,重点在onCreate方法:

[java]  view plain  copy if (mMainComponentName != null && !needsOverlayPermission) {              // 1.从缓存中获取RootView              mReactRootView = ReactNativePreLoader.getReactRootView(mMainComponentName);                 if(mReactRootView == null) {                     // 2.缓存中不存在RootView,直接创建                  mReactRootView = new ReactRootView(mActivity);                  mReactRootView.startReactApplication(                          getReactInstanceManager(),                          mMainComponentName,                          null);              }              // 3.将RootView设置到Activity布局              mActivity.setContentView(mReactRootView);          }   (1)首先从缓存中取ReactRootView

(2)缓存中不存在ReactRootView,直接创建。此时和系统帮我们创建ReactRootView没有区别

(3)将ReactRootView设置到Activity布局

很明显,我们让加载流程先经过缓存,如果缓存中已经存在了RootView,那么就可以直接设置到Activity布局,如果缓存中不存在,再去执行创建过程。

[java]  view plain  copy ReactNativePreLoader.preLoad(this,"HotRN");  

我们在启动React Native前一个界面,执行preLoad方法优先加载出ReactRootView,此时就完成了视图预加载,让React Native界面达到秒显的效果。

四、效果对比

优化前:                                                                                                     优化后:

                               

Ok,到此想必大家都想撸起袖子体验一下了,那就开始吧~~ 源码已分享到Github,别忘了给颗star哦~

点击查看源码

转载请注明原文地址: https://www.6miu.com/read-13840.html

最新回复(0)