前段时间在项目中遇到了一个问题:从原生模块跳转到RN模块时会有一段短暂的白屏时间,特别是在低端手机更加明显。在网上搜了一圈,发现这个问题非常常见。
ReactRootView mReactRootView = createRootView();
mReactRootView.startReactApplication(mReactInstanceManager, getMainComponentName(), getLaunchOptions());
这两行代码就是白屏的主要原因。因为这两行代码把jsbundle文件读入到内存中,这个过程肯定是需要耗费一些时间的,当jsbundle文件越大,可以预见加载到内存中需要的时间就越长。
解决办法就是以空间换时间,在app启动时候,就将ReactRootView初始化出来,并缓存起来,在用的时候从缓存获取ReactRootView使用,达到秒开。
目前的React Native版本更新到了0.45.1,而网上大部分的解决方案都偏旧,但是解决思路还是一样的,不过具体的解决方法会做些修改(因为RN源码的变动)。
下面开始详细说明。
1. 创建ReactRootView缓存管理器
View缓存管理器先提前将ReactRootView初始化并用一个WeakHashMap保存。在这里需要十分小心内存泄露的问题。
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();
}
}
}
2. Activity中预加载ReactNative
第二步就是与旧的实现方式不太一样的地方,因为现在ReactActivity的主要逻辑基本都由ReactActivityDelegate代理实现,所以所做的修改就有所不同,只需要实现自己的代理并在自己的ReactActivity覆盖createReactActivityDelegate即可。
2.1创建自己的ReactActivityDelegate
这里直接继承ReactActivityDelegate并重写需要的方法。
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置空,防止可能出现的内存泄漏。
2.2继承ReactActivity并修改代理创建方法
/**
* 将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。
2.3 创建React Native对应的Activity
在这里可以像之前继承ReactActivit那样创建自己的Activity,bu tong继承CCCReactActivity)。
例如:
public class PreLoadRNActivity extends C3ReactAppCompatActivity {
public static final String COMPONENT_NAME=PreLoadRNActivity.class.getSimpleName();
@Override
public String getMainComponentName() {
return COMPONENT_NAME;
}
}
3. Fragment中预加载ReactNative
在Fragment中预加载ReactNative其实比Activity中加载更简单。众所周知在Fragment的onCreateView方法中需要返回显示在界面的View,而这时候我们就可以返回RNCacheViewManager中缓存的ReactRootView。
具体代码如下:
3.1 支持预加载的React Native Fragment
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;
}
}
3.2 Fragment相关的Activity和Delegate
与第二节中Activity和Delegate相差无几,其中最大的区别是FragmentDelegate中不需要调用loadApp方法。完整的代码见文末传送门。
4.初始化ReactRootView缓存管理器
初始化方法
RNCacheViewManager.init(this, "这里填写模块名", null);
需要注意的是:
第三个参数可以设置传递给RN的属性(Bundle封装类型),如有需要才传值,否则传空即可。
初始化时机
现在主流的应用大部分都是这种结构:启动Activity+主Activity(可能包含几个Fragment)+其他Activity
而预加载时机个人任务最好就是在主Activity,因为主Activity有几乎整个应用相同的生命周期,可以保证预加载RN视图的成功,并且在主Activity销毁的时候同时销毁RNCacheViewManager可以避免内存泄露
5.对比测试
在三星SM-G3609手机(运存768M)上做了几次测试,打包后的jsBundle大小:522KB
无预加载的情况下,从原生模块打开RN页面平均耗时1769 ms
有预加载的情况下,从原生模块打开RN页面平均耗时160ms
效果非常明显!
从用户体验来说,打开页面如果有1 2秒白屏这简直不能忍,而通过预加载可以达到几乎是秒开的体验,所以为什么不用呢?
6.关于RN的SYSTEM_ALERT_WINDOW权限问题
在调试模式需要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);
}
7.局限性
局限性一:lauchOptions传递受限
通过预加载可以很顺滑地打开RN页面,但是lauchOptions这个传递RN的数据就比较受限了,因为在预加载的时候需要传递给RN的lauchOptions其实不多,因此建议lauchOptions最好只传递尽可能早明确的属性,例如一些appkey配置等
。而如果需要通过传递lauchOptions来动态选择RN加载的页面,这种多入口的方式就不合适选择预加载,此时更推荐选择多注册的方式来实现多入口。关于RN多入口方式实现详情可以看这里:传送门
局限性二:组件componentDidMount方法会在预加载完成后提前调用
组件的生命周期componentDidMount方法是非常重要的方法,比如会在这里发起网络请求,注册事件等等,而预加载完成componentDidMount就被调用了其实很多时候并不是我们想要的,但是为了使用预加载而不得不做的一个妥协。
局限性三:预加载不能过多
使用预加载方式肯定会占用一定内存,因此强烈不建议每个页面都用预加载,个人觉得1到2个RN页面使用预加载方式还是可以接受的
项目相关代码:react-native-android-preload
参考文章:
ReactNative安卓首屏白屏优化
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。