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

来源:互联网 发布:开启linux主机snmp 编辑:程序博客网 时间:2024/06/14 16:32

前些时间和大家分享了一系列关于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
  1. public abstract class ReactActivity extends Activity  
  2.     implements DefaultHardwareBackBtnHandler, PermissionAwareActivity {  
  3.   
  4.   private final ReactActivityDelegate mDelegate;  
  5.   
  6.   protected ReactActivity() {  
  7.     mDelegate = createReactActivityDelegate();  
  8.   }  
  9.   
  10.   /** 
  11.    * Returns the name of the main component registered from JavaScript. 
  12.    * This is used to schedule rendering of the component. 
  13.    * e.g. "MoviesApp" 
  14.    */  
  15.   protected @Nullable String getMainComponentName() {  
  16.     return null;  
  17.   }  
  18.   
  19.   /** 
  20.    * Called at construction time, override if you have a custom delegate implementation. 
  21.    */  
  22.   protected ReactActivityDelegate createReactActivityDelegate() {  
  23.     return new ReactActivityDelegate(this, getMainComponentName());  
  24.   }  
  25.   
  26.   @Override  
  27.   protected void onCreate(Bundle savedInstanceState) {  
  28.     super.onCreate(savedInstanceState);  
  29.     mDelegate.onCreate(savedInstanceState);  
  30.   }  
  31.   
  32.   @Override  
  33.   protected void onPause() {  
  34.     super.onPause();  
  35.     mDelegate.onPause();  
  36.   }  
  37.   
  38.   @Override  
  39.   protected void onResume() {  
  40.     super.onResume();  
  41.     mDelegate.onResume();  
  42.   }  
  43.   
  44.   @Override  
  45.   protected void onDestroy() {  
  46.     super.onDestroy();  
  47.     mDelegate.onDestroy();  
  48.   }  
  49.   // 其余代码略......  
  50. }  
不难发现,ReactActivity中的行为都交给了ReactActivityDelegate类来处理。很明显是委托模式。至于白屏原因是因为第一次创建时,那么我们直接看onCreate即可。找到ReactActivityDelegate的onCreate方法:

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

[java] view plain copy
  1. protected void loadApp(String appKey) {  
  2.   if (mReactRootView != null) {  
  3.     throw new IllegalStateException("Cannot loadApp while app is already running.");  
  4.   }  
  5.   mReactRootView = createRootView();  
  6.   mReactRootView.startReactApplication(  
  7.     getReactNativeHost().getReactInstanceManager(),  
  8.     appKey,  
  9.     getLaunchOptions());  
  10.   getPlainActivity().setContentView(mReactRootView);  
  11. }  
[java] view plain copy
  1. protected ReactRootView createRootView() {  
  2.    return new ReactRootView(getContext());  
  3.  }  

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

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

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


三、功能实现


[java] view plain copy
  1. /** 
  2.  * 预加载工具类 
  3.  * Created by Song on 2017/5/10. 
  4.  */  
  5. public class ReactNativePreLoader {  
  6.   
  7.     private static final Map<String,ReactRootView> CACHE = new ArrayMap<>();  
  8.   
  9.     /** 
  10.      * 初始化ReactRootView,并添加到缓存 
  11.      * @param activity 
  12.      * @param componentName 
  13.      */  
  14.     public static void preLoad(Activity activity, String componentName) {  
  15.   
  16.         if (CACHE.get(componentName) != null) {  
  17.             return;  
  18.         }  
  19.         // 1.创建ReactRootView  
  20.         ReactRootView rootView = new ReactRootView(activity);  
  21.         rootView.startReactApplication(  
  22.                 ((ReactApplication) activity.getApplication()).getReactNativeHost().getReactInstanceManager(),  
  23.                 componentName,  
  24.                 null);  
  25.   
  26.         // 2.添加到缓存  
  27.         CACHE.put(componentName, rootView);  
  28.     }  
  29.   
  30.     /** 
  31.      * 获取ReactRootView 
  32.      * @param componentName 
  33.      * @return 
  34.      */  
  35.     public static ReactRootView getReactRootView(String componentName) {  
  36.         return CACHE.get(componentName);  
  37.     }  
  38.   
  39.     /** 
  40.      * 从当前界面移除 ReactRootView 
  41.      * @param component 
  42.      */  
  43.     public static void deatchView(String component) {  
  44.         try {  
  45.             ReactRootView rootView = getReactRootView(component);  
  46.             ViewGroup parent = (ViewGroup) rootView.getParent();  
  47.             if (parent != null) {  
  48.                 parent.removeView(rootView);  
  49.             }  
  50.         } catch (Throwable e) {  
  51.             Log.e("ReactNativePreLoader",e.getMessage());  
  52.         }  
  53.     }  

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

(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
  1. public class PreLoadReactDelegate {  
  2.   
  3.     private final Activity mActivity;  
  4.     private ReactRootView mReactRootView;  
  5.     private Callback mPermissionsCallback;  
  6.     private final String mMainComponentName;  
  7.     private PermissionListener mPermissionListener;  
  8.     private final int REQUEST_OVERLAY_PERMISSION_CODE = 1111;  
  9.     private DoubleTapReloadRecognizer mDoubleTapReloadRecognizer;  
  10.   
  11.     public PreLoadReactDelegate(Activity activity, @Nullable String mainComponentName) {  
  12.         this.mActivity = activity;  
  13.         this.mMainComponentName = mainComponentName;  
  14.     }  
  15.   
  16.     public void onCreate() {  
  17.         boolean needsOverlayPermission = false;  
  18.         if (getReactNativeHost().getUseDeveloperSupport() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {  
  19.             // Get permission to show redbox in dev builds.  
  20.             if (!Settings.canDrawOverlays(mActivity)) {  
  21.                 needsOverlayPermission = true;  
  22.                 Intent serviceIntent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + mActivity.getPackageName()));  
  23.                 mActivity.startActivityForResult(serviceIntent, REQUEST_OVERLAY_PERMISSION_CODE);  
  24.             }  
  25.         }  
  26.   
  27.         if (mMainComponentName != null && !needsOverlayPermission) {  
  28.             // 1.从缓存中获取RootView  
  29.             mReactRootView = ReactNativePreLoader.getReactRootView(mMainComponentName);  
  30.   
  31.             if(mReactRootView == null) {  
  32.   
  33.                 // 2.缓存中不存在RootView,直接创建  
  34.                 mReactRootView = new ReactRootView(mActivity);  
  35.                 mReactRootView.startReactApplication(  
  36.                         getReactInstanceManager(),  
  37.                         mMainComponentName,  
  38.                         null);  
  39.             }  
  40.             // 3.将RootView设置到Activity布局  
  41.             mActivity.setContentView(mReactRootView);  
  42.         }  
  43.   
  44.         mDoubleTapReloadRecognizer = new DoubleTapReloadRecognizer();  
  45.     }  
  46.   
  47.     public void onResume() {  
  48.         if (getReactNativeHost().hasInstance()) {  
  49.             getReactInstanceManager().onHostResume(mActivity, (DefaultHardwareBackBtnHandler)mActivity);  
  50.         }  
  51.         if (mPermissionsCallback != null) {  
  52.             mPermissionsCallback.invoke();  
  53.             mPermissionsCallback = null;  
  54.         }  
  55.     }  
  56.   
  57.     public void onPause() {  
  58.         if (getReactNativeHost().hasInstance()) {  
  59.             getReactInstanceManager().onHostPause(mActivity);  
  60.         }  
  61.     }  
  62.   
  63.     public void onDestroy() {  
  64.   
  65.         if (mReactRootView != null) {  
  66.             mReactRootView.unmountReactApplication();  
  67.             mReactRootView = null;  
  68.         }  
  69.         if (getReactNativeHost().hasInstance()) {  
  70.             getReactInstanceManager().onHostDestroy(mActivity);  
  71.         }  
  72.   
  73.         // 清除View  
  74.         ReactNativePreLoader.deatchView(mMainComponentName);  
  75.     }  
  76.   
  77.     public boolean onNewIntent(Intent intent) {  
  78.         if (getReactNativeHost().hasInstance()) {  
  79.             getReactInstanceManager().onNewIntent(intent);  
  80.             return true;  
  81.         }  
  82.         return false;  
  83.     }  
  84.   
  85.     public void onActivityResult(int requestCode, int resultCode, Intent data) {  
  86.         if (getReactNativeHost().hasInstance()) {  
  87.             getReactInstanceManager().onActivityResult(mActivity, requestCode, resultCode, data);  
  88.         } else {  
  89.             // Did we request overlay permissions?  
  90.             if (requestCode == REQUEST_OVERLAY_PERMISSION_CODE && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {  
  91.                 if (Settings.canDrawOverlays(mActivity)) {  
  92.                     if (mMainComponentName != null) {  
  93.                         if (mReactRootView != null) {  
  94.                             throw new IllegalStateException("Cannot loadApp while app is already running.");  
  95.                         }  
  96.                         mReactRootView = new ReactRootView(mActivity);  
  97.                         mReactRootView.startReactApplication(  
  98.                                 getReactInstanceManager(),  
  99.                                 mMainComponentName,  
  100.                                 null);  
  101.                         mActivity.setContentView(mReactRootView);  
  102.                     }  
  103.                 }  
  104.             }  
  105.         }  
  106.     }  
  107.   
  108.     public boolean onBackPressed() {  
  109.         if (getReactNativeHost().hasInstance()) {  
  110.             getReactInstanceManager().onBackPressed();  
  111.             return true;  
  112.         }  
  113.         return false;  
  114.     }  
  115.   
  116.     public boolean onRNKeyUp(int keyCode) {  
  117.         if (getReactNativeHost().hasInstance() && getReactNativeHost().getUseDeveloperSupport()) {  
  118.             if (keyCode == KeyEvent.KEYCODE_MENU) {  
  119.                 getReactInstanceManager().showDevOptionsDialog();  
  120.                 return true;  
  121.             }  
  122.             boolean didDoubleTapR = Assertions.assertNotNull(mDoubleTapReloadRecognizer)  
  123.                     .didDoubleTapR(keyCode, mActivity.getCurrentFocus());  
  124.             if (didDoubleTapR) {  
  125.                 getReactInstanceManager().getDevSupportManager().handleReloadJS();  
  126.                 return true;  
  127.             }  
  128.         }  
  129.         return false;  
  130.     }  
  131.   
  132.     public void requestPermissions(String[] permissions, int requestCode, PermissionListener listener) {  
  133.         mPermissionListener = listener;  
  134.         mActivity.requestPermissions(permissions, requestCode);  
  135.     }  
  136.   
  137.     public void onRequestPermissionsResult(final int requestCode, final String[] permissions, final int[] grantResults) {  
  138.         mPermissionsCallback = new Callback() {  
  139.             @Override  
  140.             public void invoke(Object... args) {  
  141.                 if (mPermissionListener != null && mPermissionListener.onRequestPermissionsResult(requestCode, permissions, grantResults)) {  
  142.                     mPermissionListener = null;  
  143.                 }  
  144.             }  
  145.         };  
  146.     }  
  147.   
  148.     /** 
  149.      * 获取 Application中 ReactNativeHost 
  150.      * @return 
  151.      */  
  152.     private ReactNativeHost getReactNativeHost() {  
  153.         return MainApplication.getInstance().getReactNativeHost();  
  154.     }  
  155.   
  156.     /** 
  157.      * 获取 ReactInstanceManager 
  158.      * @return 
  159.      */  
  160.     private ReactInstanceManager getReactInstanceManager() {  
  161.         return getReactNativeHost().getReactInstanceManager();  
  162.     }  
  163. }  

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

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

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

(3)将ReactRootView设置到Activity布局

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

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

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


四、效果对比


优化前:                                                                                                     优化后:


                               



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


点击查看源码