前段時(shí)間在項(xiàng)目中遇到了一個(gè)問(wèn)題:從原生模塊跳轉(zhuǎn)到RN模塊時(shí)會(huì)有一段短暫的白屏?xí)r間,特別是在低端手機(jī)更加明顯。在網(wǎng)上搜了一圈,發(fā)現(xiàn)這個(gè)問(wèn)題非常常見(jiàn)。
ReactRootView mReactRootView = createRootView();
mReactRootView.startReactApplication(mReactInstanceManager, getMainComponentName(), getLaunchOptions());
這兩行代碼就是白屏的主要原因。因?yàn)檫@兩行代碼把jsbundle文件讀入到內(nèi)存中,這個(gè)過(guò)程肯定是需要耗費(fèi)一些時(shí)間的,當(dāng)jsbundle文件越大,可以預(yù)見(jiàn)加載到內(nèi)存中需要的時(shí)間就越長(zhǎng)。
解決辦法就是以空間換時(shí)間,在app啟動(dòng)時(shí)候,就將ReactRootView初始化出來(lái),并緩存起來(lái),在用的時(shí)候從緩存獲取ReactRootView使用,達(dá)到秒開(kāi)。
目前的React Native版本更新到了0.45.1,而網(wǎng)上大部分的解決方案都偏舊,但是解決思路還是一樣的,不過(guò)具體的解決方法會(huì)做些修改(因?yàn)镽N源碼的變動(dòng))。
下面會(huì)詳細(xì)說(shuō)明。
1. 創(chuàng)建ReactRootView緩存管理器
public class RNCacheViewManager {
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";
private static ReactRootView mRootView = null;
public static ReactRootView getRootView() {
if (mRootView == null) {
throw new RuntimeException("緩存view管理器尚未初始化!");
}
return mRootView;
}
public static ReactNativeHost getReactNativeHost(Activity activity) {
return ((ReactApplication) activity.getApplication()).getReactNativeHost();
}
public static void init(Activity act, String moduleName, Bundle lauchOptions) {
boolean needsOverlayPermission = false;
if (mRootView != null) {
return;
}
if (BuildConfig.DEBUG && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !Settings.canDrawOverlays(act)) {
needsOverlayPermission = true;
Intent serviceIntent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + act.getPackageName()));
FLog.w(ReactConstants.TAG, REDBOX_PERMISSION_MESSAGE);
Toast.makeText(act, REDBOX_PERMISSION_MESSAGE, Toast.LENGTH_LONG).show();
act.startActivityForResult(serviceIntent, REQUEST_OVERLAY_PERMISSION_CODE);
}
if (!needsOverlayPermission) {
mRootView = new ReactRootView(act);
mRootView.startReactApplication(
getReactNativeHost(act).getReactInstanceManager(),
moduleName,
lauchOptions);
}
}
/**
* 不能再調(diào)用原有的銷毀方法,否則rn初始化出來(lái)的對(duì)象會(huì)被銷毀,同時(shí)
* 在ReactActivity銷毀后,我們需要把 view從父視圖中移除。
*/
public static void onDestroy() {
try {
ViewParent parent = getRootView().getParent();
if (parent != null)
((android.view.ViewGroup) parent).removeView(getRootView());
} catch (Throwable e) {
e.printStackTrace();
}
}
}
2. 繼承ReactActivity和ReactActivityDelegate做相應(yīng)修改
第二步就是與之前不太一樣的地方,因?yàn)楝F(xiàn)在ReactActivity的主要邏輯基本都由ReactActivityDelegate代理實(shí)現(xiàn),所以所做的修改就有所不同,只需要實(shí)現(xiàn)自己的代理并在自己的ReactActivity覆蓋createReactActivityDelegate即可。
繼承ReactActivityDelegate
這里直接繼承ReactActivityDelegate并復(fù)寫(xiě)onCreate方法,loadApp方法,以及onDestroy方法。
public class CCCReactActivityDelegate extends ReactActivityDelegate {
private Activity mActivity;
public CCCReactActivityDelegate(Activity activity, @Nullable String mainComponentName) {
super(activity, mainComponentName);
mActivity = activity;
}
@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();
}
if (BuildConfig.DEBUG && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M&&!Settings.canDrawOverlays(mActivity)) {
TextView textView=new TextView(mActivity);
textView.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT,LinearLayout.LayoutParams.WRAP_CONTENT));
textView.setText(RNCacheViewManager.REDBOX_PERMISSION_MESSAGE+"\nPlease exit the app and grant again");
textView.setTextColor(Color.rgb(255, 0, 0));
mActivity.setContentView(textView);
}else{
loadApp(null);
}
}
@Override
protected void onDestroy() {
RNCacheViewManager.onDestroy();
super.onDestroy();
}
@Override
protected void loadApp(String appKey) {
ReactRootView mReactRootView = RNCacheViewManager.getRootView();
ViewParent viewParent = mReactRootView.getParent();
if (viewParent != null) {
ViewGroup vp = (ViewGroup) viewParent;
vp.removeView(mReactRootView);
}
mActivity.setContentView(mReactRootView);
}
}
重點(diǎn)關(guān)注onCreate方法與loadApp以及onDestroy方法。onCreate方法中調(diào)用了loadApp方法,因?yàn)榇藭r(shí)不需要appkey(appkey在緩存管理器初始化的時(shí)候就使用了)所以直接傳空即可.onDestroy使用RNCacheViewManager.onDestroy()來(lái)移除ReactRootView。
繼承ReactActivity并覆蓋代理創(chuàng)建方法
public class CCCReactActivity extends ReactActivity {
@Override
protected ReactActivityDelegate createReactActivityDelegate() {
return new CCCReactActivityDelegate(this,null);
}
}
重寫(xiě)ReactActivity的地方很少,即是替換掉原來(lái)的ReactActivityDelegate。
3. 創(chuàng)建React Native對(duì)應(yīng)的Activity
在這里可以像之前繼承ReactActivit那樣創(chuàng)建自己的Activity(繼承CCCReactActivity),覆蓋自己需要的方法,也可以直接使用CCCReactActivity。
4. 在App第一個(gè)Activity啟動(dòng)時(shí)候初始化ReactRootView緩存管理器
RNCacheViewManager.init(this, "這里填寫(xiě)模塊名", null);
需要注意的是:
第三個(gè)參數(shù)可以設(shè)置傳遞給RN的屬性(Bundle封裝類型),如有需要才傳值,否則傳空即可。
如果應(yīng)用只有一個(gè)Activity,加載RN還是比較難避免白屏(因?yàn)榭赡軙?huì)打開(kāi)這個(gè)Activity立即跳轉(zhuǎn)RN頁(yè)面,此時(shí)JsBundle還未完全加載到內(nèi)存)。因此最好有個(gè)啟動(dòng)界面。
5.對(duì)比測(cè)試
在三星SM-G3609手機(jī)(運(yùn)存768M)上做了幾次測(cè)試,打包后的jsBundle大?。?22KB
無(wú)預(yù)加載的情況下,從原生模塊打開(kāi)RN頁(yè)面平均耗時(shí)1769 ms
有預(yù)加載的情況下,從原生模塊打開(kāi)RN頁(yè)面平均耗時(shí)160ms
效果非常明顯!
從用戶體驗(yàn)來(lái)說(shuō),打開(kāi)頁(yè)面如果有1 2秒白屏這簡(jiǎn)直不能忍,而通過(guò)預(yù)加載可以達(dá)到幾乎是秒開(kāi)的體驗(yàn),所以為什么不用呢?
6.關(guān)于targetSdkVersion 23以上的SYSTEM_ALERT_WINDOW權(quán)限問(wèn)題(7.5更新)
如果工程是在調(diào)試模式并且targetSdkVersion在23以上,打開(kāi)應(yīng)用就會(huì)報(bào)如下錯(cuò)誤:
android.view.WindowManager$BadTokenException: Unable to add window android.view.ViewRootImpl$W@dac42 -- permission denied for this window type
在調(diào)試模式并且targetSdkVersion在23以上的工程中,如果未授權(quán)就去執(zhí)行startReactApplication就會(huì)報(bào)BadTokenException錯(cuò)誤。因此我們需要對(duì)啟動(dòng)React Native加一些必要的條件判斷。
boolean needsOverlayPermission = false;
if (mRootView != null) {
return;
}
if (BuildConfig.DEBUG && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !Settings.canDrawOverlays(act)) {
needsOverlayPermission = true;
Intent serviceIntent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + act.getPackageName()));
FLog.w(ReactConstants.TAG, REDBOX_PERMISSION_MESSAGE);
Toast.makeText(act, REDBOX_PERMISSION_MESSAGE, Toast.LENGTH_LONG).show();
act.startActivityForResult(serviceIntent, REQUEST_OVERLAY_PERMISSION_CODE);
}
if (!needsOverlayPermission) {
mRootView = new ReactRootView(act);
mRootView.startReactApplication(
getReactNativeHost(act).getReactInstanceManager(),
moduleName,
lauchOptions);
}
官方做法是打開(kāi)React Native承載的Activity才去申請(qǐng)權(quán)限以及接收權(quán)限是否授予都在同一個(gè)Activity中處理,而預(yù)加載方法則是在應(yīng)用可能會(huì)在啟動(dòng)時(shí)就開(kāi)始申請(qǐng)權(quán)限,因此也建議在申請(qǐng)所在的Activity接收權(quán)限是否授予回調(diào),否則即使授予了權(quán)限也不會(huì)去執(zhí)行startReactApplication方法了,此時(shí)只能殺死應(yīng)用重新打開(kāi)才能加載RN了。
如果條件允許則在申請(qǐng)所在的Activity接收權(quán)限是否授予回調(diào),即覆蓋onActivityResult方法,如果被授權(quán)則會(huì)開(kāi)始加載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) {
if (Settings.canDrawOverlays(this)) {
RNCacheViewManager.getRootView().startReactApplication(
RNCacheViewManager.getReactNativeHost(this).getReactInstanceManager(),
MODULE_NAME,
null);
}
}
}
而如果是在SplashActivtiy中加載RN,因?yàn)镾plashActivtiy會(huì)自動(dòng)關(guān)閉,等到授權(quán)完成了可能Activity已經(jīng)不在了,此時(shí)
onActivityResult也不會(huì)起作用了,這時(shí)候只能授權(quán)并殺死應(yīng)用重新啟動(dòng)了。
7.局限性
如果在init時(shí)沒(méi)有需要傳遞的屬性(lauchOptions),init方法當(dāng)然是早一點(diǎn)調(diào)用比較好,例如App啟動(dòng)時(shí)就調(diào)用該方法,
如果需要傳遞屬性(lauchOptions),而lauchOptions需要比較遲才能獲取到,就只能在RN頁(yè)面打開(kāi)加載之前去init了(強(qiáng)烈不建議這種做法,預(yù)加載幾乎無(wú)作用)。因此建議lauchOptions最好只傳遞盡可能早明確的屬性,例如一些appkey配置等。而如果需要通過(guò)傳遞屬性來(lái)動(dòng)態(tài)選擇RN加載的頁(yè)面,這種多入口的方式就不合適選擇預(yù)加載,此時(shí)更推薦選擇多注冊(cè)的方式來(lái)實(shí)現(xiàn)多入口。
此外,以上實(shí)現(xiàn)方式只針對(duì)Activity中加載RN,如果需要在Fragment中加載則需要選擇另外一種實(shí)現(xiàn),下回分解。
項(xiàng)目源碼:RNPreloadBundle
運(yùn)行方法:
- 在項(xiàng)目根目錄下使用
npm i安裝node_modules依賴 - 在項(xiàng)目根目錄下使用react-native start 命令啟動(dòng)react-native服務(wù)器
- 運(yùn)行項(xiàng)目安裝應(yīng)用
參考文章:
ReactNative安卓首屏白屏優(yōu)化