前些时间和大家分享了一系列关于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源码入手:
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方法:
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方法: @H_404_72@ 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); }
protected ReactRootView createRootView() { return new ReactRootView(getContext()); }
loadApp方法中调用了createRootView创建了ReactRootView,即React Native界面,并且将界面设置到Activity中。那么问题很可能出现在这了。插个断点,调试看看执行时间。
一切恍然大悟,在createRootView和startReactApplication时,消耗了较长时间。
既然是createRootView和startReactApplication执行了耗时操作的问题,那么我们只需要将其提前执行,创建出ReactRootView并缓存下来。当跳转到React Native界面时,直接设置到ContentView即可。有了解决思路,又该到我们甩起袖子撸码了。
三、功能实现
/** * 预加载工具类 * 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创建部分,其他照搬源码即可。
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(),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(); } }
if (mMainComponentName != null && !needsOverlayPermission) { // 1.从缓存中获取RootView mReactRootView = ReactNativePreLoader.getReactRootView(mMainComponentName); if(mReactRootView == null) { // 2.缓存中不存在RootView,null); } // 3.将RootView设置到Activity布局 mActivity.setContentView(mReactRootView); }(1)首先从缓存中取ReactRootView
(2)缓存中不存在ReactRootView,直接创建。此时和系统帮我们创建ReactRootView没有区别
(3)将ReactRootView设置到Activity布局
很明显,我们让加载流程先经过缓存,如果缓存中已经存在了RootView,那么就可以直接设置到Activity布局,如果缓存中不存在,再去执行创建过程。
ReactNativePreLoader.preLoad(this,"HotRN");
我们在启动React Native前一个界面,执行preLoad方法优先加载出ReactRootView,此时就完成了视图预加载,让React Native界面达到秒显的效果。
四、效果对比
优化前: 优化后:
Ok,到此想必大家都想撸起袖子体验一下了,那就开始吧~~ 源码已分享到Github,别忘了给颗star哦~