React Native基于最新版本实现JsBundle预加载,界面秒开优化

前端之家收集整理的这篇文章主要介绍了React Native基于最新版本实现JsBundle预加载,界面秒开优化前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。

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


Android原生项目集成React Native

@H_404_11@

React Native与Android通信交互

@H_404_11@

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

@H_404_11@

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源码入手:

  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方法
  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方法
  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(),appKey,getLaunchOptions());
  8. getPlainActivity().setContentView(mReactRootView);
  9. }
  1. protected ReactRootView createRootView() {
  2. return new ReactRootView(getContext());
  3. }

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

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

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


三、功能实现


  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(),componentName,null);
  23.  
  24. // 2.添加到缓存
  25. CACHE.put(componentName,rootView);
  26. }
  27.  
  28. /**
  29. * 获取ReactRootView
  30. * @param componentName
  31. * @return
  32. */
  33. public static ReactRootView getReactRootView(String componentName) {
  34. return CACHE.get(componentName);
  35. }
  36.  
  37. /**
  38. * 从当前界面移除 ReactRootView
  39. * @param component
  40. */
  41. public static void deatchView(String component) {
  42. try {
  43. ReactRootView rootView = getReactRootView(component);
  44. ViewGroup parent = (ViewGroup) rootView.getParent();
  45. if (parent != null) {
  46. parent.removeView(rootView);
  47. }
  48. } catch (Throwable e) {
  49. Log.e("ReactNativePreLoader",e.getMessage());
  50. }
  51. }

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

(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创建部分,其他照搬源码即可。

  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(),mMainComponentName,null);
  37. }
  38. // 3.将RootView设置到Activity布局
  39. mActivity.setContentView(mReactRootView);
  40. }
  41.  
  42. mDoubleTapReloadRecognizer = new DoubleTapReloadRecognizer();
  43. }
  44.  
  45. public void onResume() {
  46. if (getReactNativeHost().hasInstance()) {
  47. getReactInstanceManager().onHostResume(mActivity,(DefaultHardwareBackBtnHandler)mActivity);
  48. }
  49. if (mPermissionsCallback != null) {
  50. mPermissionsCallback.invoke();
  51. mPermissionsCallback = null;
  52. }
  53. }
  54.  
  55. public void onPause() {
  56. if (getReactNativeHost().hasInstance()) {
  57. getReactInstanceManager().onHostPause(mActivity);
  58. }
  59. }
  60.  
  61. public void onDestroy() {
  62.  
  63. if (mReactRootView != null) {
  64. mReactRootView.unmountReactApplication();
  65. mReactRootView = null;
  66. }
  67. if (getReactNativeHost().hasInstance()) {
  68. getReactInstanceManager().onHostDestroy(mActivity);
  69. }
  70.  
  71. // 清除View
  72. ReactNativePreLoader.deatchView(mMainComponentName);
  73. }
  74.  
  75. public boolean onNewIntent(Intent intent) {
  76. if (getReactNativeHost().hasInstance()) {
  77. getReactInstanceManager().onNewIntent(intent);
  78. return true;
  79. }
  80. return false;
  81. }
  82.  
  83. public void onActivityResult(int requestCode,int resultCode,Intent data) {
  84. if (getReactNativeHost().hasInstance()) {
  85. getReactInstanceManager().onActivityResult(mActivity,requestCode,resultCode,data);
  86. } else {
  87. // Did we request overlay permissions?
  88. if (requestCode == REQUEST_OVERLAY_PERMISSION_CODE && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
  89. if (Settings.canDrawOverlays(mActivity)) {
  90. if (mMainComponentName != null) {
  91. if (mReactRootView != null) {
  92. throw new IllegalStateException("Cannot loadApp while app is already running.");
  93. }
  94. mReactRootView = new ReactRootView(mActivity);
  95. mReactRootView.startReactApplication(
  96. getReactInstanceManager(),null);
  97. mActivity.setContentView(mReactRootView);
  98. }
  99. }
  100. }
  101. }
  102. }
  103.  
  104. public boolean onBackPressed() {
  105. if (getReactNativeHost().hasInstance()) {
  106. getReactInstanceManager().onBackPressed();
  107. return true;
  108. }
  109. return false;
  110. }
  111.  
  112. public boolean onRNKeyUp(int keyCode) {
  113. if (getReactNativeHost().hasInstance() && getReactNativeHost().getUseDeveloperSupport()) {
  114. if (keyCode == KeyEvent.KEYCODE_MENU) {
  115. getReactInstanceManager().showDevOptionsDialog();
  116. return true;
  117. }
  118. boolean didDoubleTapR = Assertions.assertNotNull(mDoubleTapReloadRecognizer)
  119. .didDoubleTapR(keyCode,mActivity.getCurrentFocus());
  120. if (didDoubleTapR) {
  121. getReactInstanceManager().getDevSupportManager().handleReloadJS();
  122. return true;
  123. }
  124. }
  125. return false;
  126. }
  127.  
  128. public void requestPermissions(String[] permissions,int requestCode,PermissionListener listener) {
  129. mPermissionListener = listener;
  130. mActivity.requestPermissions(permissions,requestCode);
  131. }
  132.  
  133. public void onRequestPermissionsResult(final int requestCode,final String[] permissions,final int[] grantResults) {
  134. mPermissionsCallback = new Callback() {
  135. @Override
  136. public void invoke(Object... args) {
  137. if (mPermissionListener != null && mPermissionListener.onRequestPermissionsResult(requestCode,permissions,grantResults)) {
  138. mPermissionListener = null;
  139. }
  140. }
  141. };
  142. }
  143.  
  144. /**
  145. * 获取 Application中 ReactNativeHost
  146. * @return
  147. */
  148. private ReactNativeHost getReactNativeHost() {
  149. return MainApplication.getInstance().getReactNativeHost();
  150. }
  151.  
  152. /**
  153. * 获取 ReactInstanceManager
  154. * @return
  155. */
  156. private ReactInstanceManager getReactInstanceManager() {
  157. return getReactNativeHost().getReactInstanceManager();
  158. }
  159. }

代码很长,重点在onCreate方法

  1. if (mMainComponentName != null && !needsOverlayPermission) {
  2. // 1.从缓存中获取RootView
  3. mReactRootView = ReactNativePreLoader.getReactRootView(mMainComponentName);
  4.  
  5. if(mReactRootView == null) {
  6.  
  7. // 2.缓存中不存在RootView,null);
  8. }
  9. // 3.将RootView设置到Activity布局
  10. mActivity.setContentView(mReactRootView);
  11. }
(1)首先从缓存中取ReactRootView

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

(3)将ReactRootView设置到Activity布局

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

  1. ReactNativePreLoader.preLoad(this,"HotRN");

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


四、效果对比


优化前: 优化后:




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


点击查看源码

猜你在找的React相关文章