在Android中預(yù)加載React Native jsBundle

前段時(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)行方法:

  1. 在項(xiàng)目根目錄下使用npm i安裝node_modules依賴
  2. 在項(xiàng)目根目錄下使用react-native start 命令啟動(dòng)react-native服務(wù)器
  3. 運(yùn)行項(xiàng)目安裝應(yīng)用

參考文章:
ReactNative安卓首屏白屏優(yōu)化

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容