前段时间在项目中遇到了一个问题:从原生模块跳转到RN模块时会有一段短暂的白屏时间,特别是在低端手机更加明显。在网上搜了一圈,发现这个问题很是常见。java
ReactRootView mReactRootView = createRootView(); mReactRootView.startReactApplication(mReactInstanceManager, getMainComponentName(), getLaunchOptions());
这两行代码就是白屏的主要缘由。由于这两行代码把jsbundle文件读入到内存中,这个过程确定是须要耗费一些时间的,当jsbundle文件越大,能够预见加载到内存中须要的时间就越长。
解决办法就是以空间换时间,在app启动时候,就将ReactRootView初始化出来,并缓存起来,在用的时候从缓存获取ReactRootView使用,达到秒开。
目前的React Native版本更新到了0.45.1,而网上大部分的解决方案都偏旧,可是解决思路仍是同样的,不过具体的解决方法会作些修改(由于RN源码的变更)。
下面开始详细说明。react
View缓存管理器先提早将ReactRootView初始化并用一个WeakHashMap保存。在这里须要十分当心内存泄露的问题。android
public class RNCacheViewManager { public static Map<String, ReactRootView> CACHE; public static final int REQUEST_OVERLAY_PERMISSION_CODE = 1111; public static final String REDBOX_PERMISSION_MESSAGE = "Overlay permissions needs to be granted in order for react native apps to run in dev mode"; public static ReactRootView getRootView(String moduleName) { if (CACHE == null) return null; return CACHE.get(moduleName); } public static ReactNativeHost getReactNativeHost(Activity activity) { return ((ReactApplication) activity.getApplication()).getReactNativeHost(); } /** * 预加载所需的RN模块 * @param activity 预加载时所在的Activity * @param launchOptions 启动参数 * @param moduleNames 预加载模块名 * 建议在主界面onCreate方法调用,最好的状况是主界面在应用运行期间一直存在不被关闭 */ public static void init(Activity activity, Bundle launchOptions, String... moduleNames) { if (CACHE == null) CACHE = new WeakHashMap<>(); boolean needsOverlayPermission = false; if (BuildConfig.DEBUG && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !Settings.canDrawOverlays(activity)) { needsOverlayPermission = true; Intent serviceIntent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + activity.getPackageName())); FLog.w(ReactConstants.TAG, REDBOX_PERMISSION_MESSAGE); Toast.makeText(activity, REDBOX_PERMISSION_MESSAGE, Toast.LENGTH_LONG).show(); activity.startActivityForResult(serviceIntent, REQUEST_OVERLAY_PERMISSION_CODE); } if (!needsOverlayPermission) { for (String moduleName : moduleNames) { ReactRootView rootView = new ReactRootView(activity); rootView.startReactApplication( getReactNativeHost(activity).getReactInstanceManager(), moduleName, launchOptions); CACHE.put(moduleName, rootView); FLog.i(ReactConstants.TAG, moduleName+" has preload"); } } } /** * 销毁指定的预加载RN模块 * * @param componentName */ public static void onDestroyOne(String componentName) { try { ReactRootView reactRootView = CACHE.get(componentName); if (reactRootView != null) { ViewParent parent = reactRootView.getParent(); if (parent != null) { ((android.view.ViewGroup) parent).removeView(reactRootView); } reactRootView.unmountReactApplication(); } } catch (Throwable e) { e.printStackTrace(); } } /** * 销毁所有RN模块 * 建议在主界面onDestroy方法调用 */ public static void onDestroy() { try { for (Map.Entry<String, ReactRootView> entry : CACHE.entrySet()) { ReactRootView reactRootView = entry.getValue(); ViewParent parent = reactRootView.getParent(); if (parent != null) { ((android.view.ViewGroup) parent).removeView(reactRootView); } reactRootView.unmountReactApplication(); reactRootView=null; } CACHE.clear(); CACHE = null; } catch (Throwable e) { e.printStackTrace(); } } }
第二步就是与旧的实现方式不太同样的地方,由于如今ReactActivity的主要逻辑基本都由ReactActivityDelegate代理实现,因此所作的修改就有所不一样,只须要实现本身的代理并在本身的ReactActivity覆盖createReactActivityDelegate便可。git
这里直接继承ReactActivityDelegate并重写须要的方法。github
public class C3ReactActivityDelegate extends ReactActivityDelegate { private Activity mActivity; private String mainComponentName; private ReactRootView reactRootView; public C3ReactActivityDelegate(Activity activity, @Nullable String mainComponentName) { super(activity, mainComponentName); this.mActivity = activity; this.mainComponentName = mainComponentName; } @Override protected void onCreate(Bundle savedInstanceState) { Class<ReactActivityDelegate> clazz = ReactActivityDelegate.class; try { Field field = clazz.getDeclaredField("mDoubleTapReloadRecognizer"); field.setAccessible(true); field.set(this, new DoubleTapReloadRecognizer()); } catch (Exception e) { e.printStackTrace(); } loadApp(null); } @Override protected void onPause() { super.onPause(); } @Override protected void onResume() { super.onResume(); } @Override protected void onDestroy() { super.onDestroy(); reactRootView.unmountReactApplication(); reactRootView=null; } @Override protected ReactNativeHost getReactNativeHost() { return super.getReactNativeHost(); } @Override protected void loadApp(String appKey) { if (mainComponentName == null) { FLog.e(ReactConstants.TAG, "mainComponentName must not be null!"); return; } reactRootView = RNCacheViewManager.getInstance().getRootView(mainComponentName); try { if (reactRootView == null) { // 2.缓存中不存在RootView,直接建立 reactRootView = new ReactRootView(mActivity); reactRootView.startReactApplication( getReactInstanceManager(), mainComponentName, null); } ViewParent viewParent = reactRootView.getParent(); if (viewParent != null) { ViewGroup vp = (ViewGroup) viewParent; vp.removeView(reactRootView); } mActivity.setContentView(reactRootView); } catch (Exception e) { e.printStackTrace(); } } }
重点关注onCreate
方法与loadApp
以及onDestroy
方法。onCreate
方法没有调用父类的方法,而不是彻底重写,其中重点是调用了loadApp方法,由于此时是经过预加载方式先把ReactRootView渲染了,所以此时appkey是什么都不重要了.onDestroy
方法调用ReactActivityDelegate的onDestroy方法,同时须要手动调用reactRootView与Activity分离方法并将reactRootView置空,防止可能出现的内存泄漏。react-native
/** * 将Activity继承本类将会预加载RN模块 * Created by lizhj on 2017/8/23. */ public abstract class C3ReactAppCompatActivity extends AppCompatActivity implements DefaultHardwareBackBtnHandler, PermissionAwareActivity { private final C3ReactActivityDelegate mDelegate; protected C3ReactAppCompatActivity() { mDelegate = createReactActivityDelegate(); } /** * Returns the name of the main component registered from JavaScript. * This is used to schedule rendering of the component. * e.g. "MoviesApp" */ public abstract String getMainComponentName(); /** * Called at construction time, override if you have a custom delegate implementation. */ protected C3ReactActivityDelegate createReactActivityDelegate() { return new C3ReactActivityDelegate(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(); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { mDelegate.onActivityResult(requestCode, resultCode, data); } @Override public boolean onKeyUp(int keyCode, KeyEvent event) { return mDelegate.onKeyUp(keyCode, event) || super.onKeyUp(keyCode, event); } @Override public void onBackPressed() { if (!mDelegate.onBackPressed()) { super.onBackPressed(); } } @Override public void invokeDefaultOnBackPressed() { super.onBackPressed(); } @Override protected void onNewIntent(Intent intent) { if (!mDelegate.onNewIntent(intent)) { super.onNewIntent(intent); } } @Override public void requestPermissions( String[] permissions, int requestCode, PermissionListener listener) { mDelegate.requestPermissions(permissions, requestCode, listener); } @Override public void onRequestPermissionsResult( int requestCode, String[] permissions, int[] grantResults) { mDelegate.onRequestPermissionsResult(requestCode, permissions, grantResults); } protected final ReactNativeHost getReactNativeHost() { return mDelegate.getReactNativeHost(); } protected final ReactInstanceManager getReactInstanceManager() { return mDelegate.getReactInstanceManager(); } }
代码不少,其实最关键的只是这两处,即修改原有的代理为本身写的代理。缓存
private final C3ReactActivityDelegate mDelegate; protected C3ReactAppCompatActivity() { mDelegate = createReactActivityDelegate(); } protected C3ReactActivityDelegate createReactActivityDelegate() { return new C3ReactActivityDelegate(this, getMainComponentName()); }
其实这里面彻底能够直接继承ReactActivity,这样上面的大部分方法其实都不需复写了,这里继承只是为了代表这个支持预加载的Activity彻底能够继承你须要的Activity,像我以前在的项目中就是将C3ReactAppCompatActivity 继承自项目的基类Activity。网络
在这里能够像以前继承ReactActivit那样建立本身的Activity,bu tong继承CCCReactActivity)。
例如:app
public class PreLoadRNActivity extends C3ReactAppCompatActivity { public static final String COMPONENT_NAME=PreLoadRNActivity.class.getSimpleName(); @Override public String getMainComponentName() { return COMPONENT_NAME; } }
在Fragment中预加载ReactNative其实比Activity中加载更简单。众所周知在Fragment的onCreateView方法中须要返回显示在界面的View,而这时候咱们就能够返回RNCacheViewManager中缓存的ReactRootView。
具体代码以下:ide
public abstract class C3ReactFragment extends Fragment { private ReactRootView mReactRootView; public abstract String getMainComponentName(); @Override public void onAttach(Context context) { super.onAttach(context); mReactRootView = RNCacheViewManager.getInstance().getRootView(getMainComponentName()); } @Nullable @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { super.onCreateView(inflater, container, savedInstanceState); return mReactRootView; } }
与第二节中Activity和Delegate相差无几,其中最大的区别是FragmentDelegate中不须要调用loadApp方法。完整的代码见文末传送门。
初始化方法
RNCacheViewManager.init(this, "这里填写模块名", null);
须要注意的是:
第三个参数能够设置传递给RN的属性(Bundle封装类型),若有须要才传值,不然传空便可。
初始化时机
如今主流的应用大部分都是这种结构:启动Activity+主Activity(可能包含几个Fragment)+其余Activity
而预加载时机我的任务最好就是在主Activity,由于主Activity有几乎整个应用相同的生命周期,能够保证预加载RN视图的成功,而且在主Activity销毁的时候同时销毁RNCacheViewManager能够避免内存泄露
在三星SM-G3609手机(运存768M)上作了几回测试,打包后的jsBundle大小:522KB
无预加载的状况下,从原生模块打开RN页面平均耗时1769 ms
有预加载的状况下,从原生模块打开RN页面平均耗时160ms
效果很是明显!
从用户体验来讲,打开页面若是有1 2秒白屏这简直不能忍,而经过预加载能够达到几乎是秒开的体验,因此为何不用呢?
在调试模式须要SYSTEM_ALERT_WINDOW权限,用来打开调试信息窗口。官方作法是打开React Native承载的Activity才去申请权限以及接收权限是否授予都在同一个Activity中处理,而预加载方法则是在应用可能会在启动时就开始申请权限,所以也建议在主Activity接收权限是否授予回调,即覆盖onActivityResult
方法,若是被受权则会开始加载React Native,具体代码以下:
@Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); //处理调试模式下悬浮窗权限被授予回调 if (requestCode == REQUEST_OVERLAY_PERMISSION_CODE && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && Settings.canDrawOverlays(this)) { restartApp(); } } /** * 重启应用以使预加载生效 */ private void restartApp() { Intent mStartActivity = new Intent(this, MainActivity.class); int mPendingIntentId = 123456; PendingIntent mPendingIntent = PendingIntent.getActivity(this, mPendingIntentId, mStartActivity, PendingIntent.FLAG_CANCEL_CURRENT); AlarmManager mgr = (AlarmManager)this.getSystemService(Context.ALARM_SERVICE); mgr.set(AlarmManager.RTC, System.currentTimeMillis() + 10, mPendingIntent); System.exit(0); }
经过预加载能够很顺滑地打开RN页面,可是lauchOptions这个传递RN的数据就比较受限了,由于在预加载的时候须要传递给RN的lauchOptions其实很少,所以建议lauchOptions最好只传递尽量早明确的属性,例如一些appkey配置等
。而若是须要经过传递lauchOptions来动态选择RN加载的页面,这种多入口的方式就不合适选择预加载,此时更推荐选择多注册的方式来实现多入口。关于RN多入口方式实现详情能够看这里:传送门
组件的生命周期componentDidMount方法是很是重要的方法,好比会在这里发起网络请求,注册事件等等,而预加载完成componentDidMount就被调用了其实不少时候并非咱们想要的,可是为了使用预加载而不得不作的一个妥协。
使用预加载方式确定会占用必定内存,所以强烈不建议每一个页面都用预加载,我的以为1到2个RN页面使用预加载方式仍是能够接受的
项目相关代码:react-native-android-preload
参考文章:
ReactNative安卓首屏白屏优化