ReactNative Android 白屏問題

使用 React Native 開發混合應用的過程中,我們在打完 bundle 進 release 包後,會發現第一次進入頁面(React 的 Activity)會有一個短暫的白屏過程(在真機上近 1秒,在模擬器上比較快,在 200毫秒 左右),而且在完全退出後再進入,仍然會有這個白屏。

仔細查看加載過程(其實猜猜都能知道)後可以發現,這個過程就是在加載我們的 js bundle,通常即便是一個小的 RN 應用(混合應用中的子業務),也會動輒到 1MB 的大小,除非是完整的 RN 應用,可以把這個當做是啓動速度,否則這樣的加載速度都是對用戶體驗的很大傷害。

於是我們決定進行 Bundle 預加載的優化。

項目源碼上傳在:markzhai/react-native-preloader,稍後會上傳到 maven,版本號會和 rn 保持一致。

耗時操作

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

打點後可以發現耗時的其實是

  • createRootView();
  • startReactApplication();

這兩個操作,所以考慮只需要提前創建 ReactRootView 進行 render,之後直接掛載該 view 上去即可。

預加載

創建預加載類 ReactPreLoader:

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) {
            Log.e(TAG, e+"");
        }
    }
}

在 init 操作中,我們通過 ReactInfo 緩存把 view 緩存在本地的 ArrayMap。值得注意的是 onDestroy,在 ReactActivity 銷燬後,我們需要把 view 從 parent 上卸載下來。

使用預加載的 view

使用預加載的 View,就需要侵入 activity 的創建過程,我們無法再使用 RN 庫提供的 ReactActivity,只能建立自己的,以下列出修改的方法,其他方法照抄 ReactActivity:

public abstract class MrReactActivity extends ReactActivity
        implements DefaultHardwareBackBtnHandler, PermissionAwareActivity {
    public ReactRootView mReactRootView;
    public DoubleTapReloadRecognizer mDoubleTapReloadRecognizer;
    public String TAG = "MrReactActivity";
    public String REDBOX_PERMISSION_MESSAGE = "REDBOX_PERMISSION_MESSAGE";
    @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) {
            Log.i(TAG, "use pre-load view");
        } else {
            Log.i(TAG, "createRootView");
            mReactRootView = createRootView(getApplicationContext());
            if (mReactRootView != null) {
                mReactRootView.startReactApplication(
                        getReactNativeHost().getReactInstanceManager(),
                        getReactInfo().getMainComponentName(),
                        getReactInfo().getLaunchOptions());
            }
        }

        setContentView(mReactRootView);

        mDoubleTapReloadRecognizer = new DoubleTapReloadRecognizer();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();

        if (mReactRootView != null) {
            mReactRootView.unmountReactApplication();
            mReactRootView = null;
            ReactPreLoader.onDestroy(getReactInfo());
        }
//        getReactNativeHost().clear();
    }
    protected ReactRootView createRootView(Context context/*Application application*/) {
        return new ReactRootView(context);
    }
    protected boolean getUseDeveloperSupport() {
        return BuildConfig.DEBUG;
    }
    public abstract ReactInfo getReactInfo();
}

ReactInfo類

/**
 * Created by songyu on 2017/1/18.
 */

public class ReactInfo {

    private String mMainComponentName;
    private Bundle mLaunchOptions;

    public ReactInfo(String mainComponentName) {
        mMainComponentName = mainComponentName;
    }

    public ReactInfo(String mainComponentName, Bundle launchOptions) {
        mMainComponentName = mainComponentName;
        mLaunchOptions = launchOptions;
    }

    public Bundle getLaunchOptions() {
        return mLaunchOptions;
    }

    public String getMainComponentName() {
        return mMainComponentName;
    }
}

優化後可以達到瞬間加載。

已知的坑

由於進行了預加載,目前已知的問題是 Modal 無法顯示 —— 因爲 Modal 在 Android 的實現使用了 Dialog,而該 View 將創建 ReactRootView 的 context 作爲參數傳給了 Dialog,而不是實際運行時所在的 Activity context。查看源碼可以驗證(com.facebook.react.views.modal)。

TimePickerAndroid 這類 picker 則沒有問題。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章