在Android中預加載React Native jsBundle

前段時間在項目中遇到了一個問題:從原生模塊跳轉到RN模塊時會有一段短暫的白屏時間,特別是在低端手機更加明顯。在網上搜了一圈,發現這個問題很是常見。java

ReactRootView mReactRootView = createRootView();
mReactRootView.startReactApplication(mReactInstanceManager, getMainComponentName(), getLaunchOptions());

這兩行代碼就是白屏的主要緣由。由於這兩行代碼把jsbundle文件讀入到內存中,這個過程確定是須要耗費一些時間的,當jsbundle文件越大,能夠預見加載到內存中須要的時間就越長。
解決辦法就是以空間換時間,在app啓動時候,就將ReactRootView初始化出來,並緩存起來,在用的時候從緩存獲取ReactRootView使用,達到秒開。
目前的React Native版本更新到了0.45.1,而網上大部分的解決方案都偏舊,可是解決思路仍是同樣的,不過具體的解決方法會作些修改(由於RN源碼的變更)。
下面開始詳細說明。react

1. 建立ReactRootView緩存管理器

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();
        }
    }

}

2. Activity中預加載ReactNative

第二步就是與舊的實現方式不太同樣的地方,由於如今ReactActivity的主要邏輯基本都由ReactActivityDelegate代理實現,因此所作的修改就有所不一樣,只須要實現本身的代理並在本身的ReactActivity覆蓋createReactActivityDelegate便可。git

2.1建立本身的ReactActivityDelegate

這裏直接繼承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

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)。
例如:app

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。
具體代碼以下:ide

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安卓首屏白屏優化

相關文章
相關標籤/搜索