使用 React Native 開發(fā)混合應(yīng)用的過程中烦磁,我們?cè)诖蛲?bundle 進(jìn) release 包后滩愁,會(huì)發(fā)現(xiàn)第一次進(jìn)入頁面(React 的 Activity)會(huì)有一個(gè)短暫的白屏過程(在真機(jī)上近 1秒励翼,在模擬器上比較快镐侯,在 200毫秒 左右)技矮,而且在完全退出后再進(jìn)入泳炉,仍然會(huì)有這個(gè)白屏。
仔細(xì)查看加載過程(其實(shí)猜猜都能知道)后可以發(fā)現(xiàn)任洞,這個(gè)過程就是在加載我們的 js bundle蓄喇,通常即便是一個(gè)小的 RN 應(yīng)用(混合應(yīng)用中的子業(yè)務(wù)),也會(huì)動(dòng)輒到 1MB 的大小交掏,除非是完整的 RN 應(yīng)用妆偏,可以把這個(gè)當(dāng)做是啟動(dòng)速度,否則這樣的加載速度都是對(duì)用戶體驗(yàn)的很大傷害盅弛。
于是我們決定進(jìn)行 Bundle 預(yù)加載的優(yōu)化钱骂。
項(xiàng)目源碼上傳在:markzhai/react-native-preloader,稍后會(huì)上傳到 maven挪鹏,版本號(hào)會(huì)和 rn 保持一致见秽。
耗時(shí)操作
見 ReactActivity
的 onCreate
方法:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getUseDeveloperSupport() && Build.VERSION.SDK_INT >= 23) {
// Get permission to show redbox in dev builds.
if (!Settings.canDrawOverlays(this)) {
Intent serviceIntent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
startActivity(serviceIntent);
FLog.w(ReactConstants.TAG, REDBOX_PERMISSION_MESSAGE);
Toast.makeText(this, REDBOX_PERMISSION_MESSAGE, Toast.LENGTH_LONG).show();
}
}
mReactRootView = createRootView();
mReactRootView.startReactApplication(
getReactNativeHost().getReactInstanceManager(),
getMainComponentName(),
getLaunchOptions());
setContentView(mReactRootView);
mDoubleTapReloadRecognizer = new DoubleTapReloadRecognizer();
}
打點(diǎn)后可以發(fā)現(xiàn)耗時(shí)的其實(shí)是
- createRootView();
- startReactApplication();
這兩個(gè)操作,所以考慮只需要提前創(chuàng)建 ReactRootView
進(jìn)行 render讨盒,之后直接掛載該 view 上去即可解取。
預(yù)加載
創(chuàng)建預(yù)加載類 ReactPreLoader
:
/**
* React Native Bundle Pre-loader.
*
* @author markzhai on 16/8/20
* @version 1.3.0
*/
public class ReactPreLoader {
private static final String TAG = "ReactPreLoader";
private static final Map<String, ReactRootView> CACHE_VIEW_MAP =
new ArrayMap<>();
/**
* Get {@link ReactRootView} with corresponding {@link ReactInfo}.
*/
public static ReactRootView getRootView(ReactInfo reactInfo) {
return CACHE_VIEW_MAP.get(reactInfo.getMainComponentName());
}
/**
* Pre-load {@link ReactRootView} to local {@link Map}, you may want to
* load it in previous activity.
*/
public static void init(Activity activity, ReactInfo reactInfo) {
if (CACHE_VIEW_MAP.get(reactInfo.getMainComponentName()) != null) {
return;
}
ReactRootView rootView = new ReactRootView(activity);
rootView.startReactApplication(
((ReactApplication) activity.getApplication()).getReactNativeHost().getReactInstanceManager(),
reactInfo.getMainComponentName(),
reactInfo.getLaunchOptions());
CACHE_VIEW_MAP.put(reactInfo.getMainComponentName(), rootView);
}
/**
* Remove {@link ReactRootView} from parent.
*/
public static void onDestroy(ReactInfo reactInfo) {
try {
ReactRootView rootView = getRootView(reactInfo);
ViewGroup parent = (ViewGroup) rootView.getParent();
if (parent != null) {
parent.removeView(rootView);
}
} catch (Throwable e) {
Logger.e(TAG, e);
}
}
}
在 init 操作中,我們通過 ReactInfo 緩存把 view 緩存在本地的 ArrayMap
催植。
值得注意的是 onDestroy
肮蛹,在 ReactActivity 銷毀后,我們需要把 view 從 parent 上卸載下來创南。
使用預(yù)加載的 view
使用預(yù)加載的 View伦忠,就需要侵入 activity 的創(chuàng)建過程,我們無法再使用 RN 庫提供的 ReactActivity
稿辙,只能建立自己的昆码,以下列出修改的方法,其他方法照抄 ReactActivity
:
/**
* Base Activity for React Native applications.
*
* @author markzhai on 16/7/28
* @version 1.3.0
*/
public abstract class MrReactActivity extends Activity
implements DefaultHardwareBackBtnHandler, PermissionAwareActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getUseDeveloperSupport() && Build.VERSION.SDK_INT >= 23) {
// Get permission to show redbox in dev builds.
if (!Settings.canDrawOverlays(this)) {
Intent serviceIntent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
startActivity(serviceIntent);
FLog.w(ReactConstants.TAG, REDBOX_PERMISSION_MESSAGE);
Toast.makeText(this, REDBOX_PERMISSION_MESSAGE, Toast.LENGTH_LONG).show();
}
}
mReactRootView = ReactPreLoader.getRootView(getReactInfo());
if (mReactRootView != null) {
Logger.i(TAG, "use pre-load view");
} else {
Logger.i(TAG, "createRootView");
mReactRootView = createRootView();
if (mReactRootView != null) {
mReactRootView.startReactApplication(
getReactNativeHost().getReactInstanceManager(),
getMainComponentName(),
getLaunchOptions());
}
}
setContentView(mReactRootView);
mDoubleTapReloadRecognizer = new DoubleTapReloadRecognizer();
}
@Override
protected void onDestroy() {
super.onDestroy();
if (mReactRootView != null) {
mReactRootView.unmountReactApplication();
mReactRootView = null;
ReactPreLoader.onDestroy(getReactInfo());
}
// getReactNativeHost().clear();
}
public abstract ReactInfo getReactInfo();
}
使用
在進(jìn)入該 RN activity 的上一個(gè) activity 調(diào)用:
ReactPreLoader.init(this, ReactCardActivity.reactInfo);
ReactCardActivity 繼承我們自己的 ReactActivity:
public class ReactCardActivity extends MrReactActivity {
public static final ReactInfo reactInfo = new ReactInfo("card", null);
@Override
protected String getMainComponentName() {
return reactInfo.getMainComponentName();
}
@Override
public ReactInfo getReactInfo() {
return reactInfo;
}
}
優(yōu)化后可以達(dá)到瞬間加載邻储。
已知的坑
由于進(jìn)行了預(yù)加載赋咽,目前已知的問題是 Modal
無法顯示 —— 因?yàn)?Modal
在 Android 的實(shí)現(xiàn)使用了 Dialog
,而該 View 將創(chuàng)建 ReactRootView
的 context 作為參數(shù)傳給了 Dialog
吨娜,而不是實(shí)際運(yùn)行時(shí)所在的 Activity
context脓匿。查看源碼可以驗(yàn)證(com.facebook.react.views.modal)。
TimePickerAndroid
這類 picker 則沒有問題宦赠。
作為規(guī)避方案陪毡,目前使用 MutableContextWrapper
進(jìn)行 context 替換。見 GitHub 上的具體實(shí)現(xiàn)勾扭。