1. 程式人生 > >React Native基於最新版本實現JsBundle預載入,解決白屏等待,介面秒開優化

React Native基於最新版本實現JsBundle預載入,解決白屏等待,介面秒開優化

剛建立的React Native 微信公眾號,歡迎微信掃描關注訂閱號,每天定期會分享react native 技術文章,移動技術乾貨,精彩文章技術推送。同時可以掃描我的微信加入react-native技術交流微信群。歡迎各位大牛,React Native技術愛好者加入交流!


前些時間和大家分享了一系列關於React Native For Android的文章。這兩天又對React Native增量熱更新的部落格進行了填充,增加了圖片增量更新的實現方案和過程。有興趣的朋友可以去瀏覽詳細內容。為了方便,我將前幾篇的部落格連結貼出來供大家參考:

一、問題分析


本篇部落格同樣和大家分享關於React Native的內容。想必大家在擼碼中都發現了一個問題:從Android原生介面第一次跳轉到React Native介面時,會有短暫的白屏過程,然後才會加載出介面。下次再跳轉就不會出現類似問題。並且當我們殺死應用,重新啟動App從Android Activity跳轉到RN介面,依然會出現短暫白屏。

為什麼第一次載入React Native介面會出現短暫白屏呢?大家別忘了,React Native的渲染機制是對於JsBundle的載入。專案中所有的js檔案最終會被打包成一個JsBundle檔案,Android環境下Bundle檔案為:‘index.android.bundle’。系統在第一次渲染介面時,會首先載入JsBundle檔案。那麼問題肯定出現在載入JsBundle這個過程,即出現白屏可能是因為JsBundle正在載入。發現了原因,我們繼續檢視原始碼,看看是否能從原始碼中得知一二。

二、原始碼分析


Android整合的RN介面,需要繼承ReactActivity,那麼直接從ReactActivity原始碼入手:

public abstract class ReactActivity extends Activity
    implements DefaultHardwareBackBtnHandler, PermissionAwareActivity {

  private final ReactActivityDelegate mDelegate;

  protected ReactActivity() {
    mDelegate = createReactActivityDelegate();
  }

  /**
   * Returns the name of the main component registered from JavaScript.
   * This is used to schedule rendering of the component.
   * e.g. "MoviesApp"
   */
  protected @Nullable String getMainComponentName() {
    return null;
  }

  /**
   * Called at construction time, override if you have a custom delegate implementation.
   */
  protected ReactActivityDelegate createReactActivityDelegate() {
    return new ReactActivityDelegate(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();
  }
  // 其餘程式碼略......
}
不難發現,ReactActivity中的行為都交給了ReactActivityDelegate類來處理。很明顯是委託模式。至於白屏原因是因為第一次建立時,那麼我們直接看onCreate即可。找到ReactActivityDelegate的onCreate方法:
protected void onCreate(Bundle savedInstanceState) {
    boolean needsOverlayPermission = false;
    if (getReactNativeHost().getUseDeveloperSupport() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
      // Get permission to show redbox in dev builds.
      if (!Settings.canDrawOverlays(getContext())) {
        needsOverlayPermission = true;
        Intent serviceIntent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + getContext().getPackageName()));
        FLog.w(ReactConstants.TAG, REDBOX_PERMISSION_MESSAGE);
        Toast.makeText(getContext(), REDBOX_PERMISSION_MESSAGE, Toast.LENGTH_LONG).show();
        ((Activity) getContext()).startActivityForResult(serviceIntent, REQUEST_OVERLAY_PERMISSION_CODE);
      }
    }

    if (mMainComponentName != null && !needsOverlayPermission) {
      loadApp(mMainComponentName);
    }
    mDoubleTapReloadRecognizer = new DoubleTapReloadRecognizer();
  }
從原始碼可以看到,最終呼叫了loadApp方法,繼續跟蹤loadApp方法:
  protected void loadApp(String appKey) {
    if (mReactRootView != null) {
      throw new IllegalStateException("Cannot loadApp while app is already running.");
    }
    mReactRootView = createRootView();
    mReactRootView.startReactApplication(
      getReactNativeHost().getReactInstanceManager(),
      appKey,
      getLaunchOptions());
    getPlainActivity().setContentView(mReactRootView);
  }
 protected ReactRootView createRootView() {
    return new ReactRootView(getContext());
  }

loadApp方法中呼叫了createRootView建立了ReactRootView,即React Native介面,並且將介面設定到Activity中。那麼問題很可能出現在這了。插個斷點,除錯看看執行時間。

一切恍然大悟,在createRootView和startReactApplication時,消耗了較長時間。

既然是createRootView和startReactApplication執行了耗時操作的問題,那麼我們只需要將其提前執行,創建出ReactRootView並快取下來。當跳轉到React Native介面時,直接設定到ContentView即可。有了解決思路,又該到我們甩起袖子擼碼了。

三、功能實現


/**
 * 預載入工具類
 * Created by Song on 2017/5/10.
 */
public class ReactNativePreLoader {

    private static final Map<String,ReactRootView> CACHE = new ArrayMap<>();

    /**
     * 初始化ReactRootView,並新增到快取
     * @param activity
     * @param componentName
     */
    public static void preLoad(Activity activity, String componentName) {

        if (CACHE.get(componentName) != null) {
            return;
        }
        // 1.建立ReactRootView
        ReactRootView rootView = new ReactRootView(activity);
        rootView.startReactApplication(
                ((ReactApplication) activity.getApplication()).getReactNativeHost().getReactInstanceManager(),
                componentName,
                null);

        // 2.新增到快取
        CACHE.put(componentName, rootView);
    }

    /**
     * 獲取ReactRootView
     * @param componentName
     * @return
     */
    public static ReactRootView getReactRootView(String componentName) {
        return CACHE.get(componentName);
    }

    /**
     * 從當前介面移除 ReactRootView
     * @param component
     */
    public static void deatchView(String component) {
        try {
            ReactRootView rootView = getReactRootView(component);
            ViewGroup parent = (ViewGroup) rootView.getParent();
            if (parent != null) {
                parent.removeView(rootView);
            }
        } catch (Throwable e) {
            Log.e("ReactNativePreLoader",e.getMessage());
        }
    }

上述程式碼很簡單,包含了三個方法:

(1)preLoad

         負責建立ReactRootView,並新增到快取。

(2)getReactRootView

        獲取建立的RootView

(3)deatchView

        將新增的RootView從佈局根容器中移除,在 ReactActivity 銷燬後,我們需要把 view 從 parent 上解除安裝下來,避免出現重複新增View的異常。

從原始碼分析部分我們知道,整合React Native介面時,只需要繼承ReactActivity,並實現getMainComponentName方法即可。載入建立檢視的流程系統都在ReactActivity幫我們完成。現在因為自定義了ReactRootView的載入方式,要使用預載入方式,就不能直接繼承ReactActivity。所以接下來需要我們自定義ReactActivity。

從原始碼中我們已經發現,ReactActivity的處理都交給了ReactActivityDelegate。所以我們可以自定義一個新的ReactActivityDelegate,只需要修改onCreate建立部分,其他照搬原始碼即可。

public class PreLoadReactDelegate {

    private final Activity mActivity;
    private ReactRootView mReactRootView;
    private Callback mPermissionsCallback;
    private final String mMainComponentName;
    private PermissionListener mPermissionListener;
    private final int REQUEST_OVERLAY_PERMISSION_CODE = 1111;
    private DoubleTapReloadRecognizer mDoubleTapReloadRecognizer;

    public PreLoadReactDelegate(Activity activity, @Nullable String mainComponentName) {
        this.mActivity = activity;
        this.mMainComponentName = mainComponentName;
    }

    public void onCreate() {
        boolean needsOverlayPermission = false;
        if (getReactNativeHost().getUseDeveloperSupport() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            // Get permission to show redbox in dev builds.
            if (!Settings.canDrawOverlays(mActivity)) {
                needsOverlayPermission = true;
                Intent serviceIntent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + mActivity.getPackageName()));
                mActivity.startActivityForResult(serviceIntent, REQUEST_OVERLAY_PERMISSION_CODE);
            }
        }

        if (mMainComponentName != null && !needsOverlayPermission) {
            // 1.從快取中獲取RootView
            mReactRootView = ReactNativePreLoader.getReactRootView(mMainComponentName);

            if(mReactRootView == null) {

                // 2.快取中不存在RootView,直接建立
                mReactRootView = new ReactRootView(mActivity);
                mReactRootView.startReactApplication(
                        getReactInstanceManager(),
                        mMainComponentName,
                        null);
            }
            // 3.將RootView設定到Activity佈局
            mActivity.setContentView(mReactRootView);
        }

        mDoubleTapReloadRecognizer = new DoubleTapReloadRecognizer();
    }

    public void onResume() {
        if (getReactNativeHost().hasInstance()) {
            getReactInstanceManager().onHostResume(mActivity, (DefaultHardwareBackBtnHandler)mActivity);
        }
        if (mPermissionsCallback != null) {
            mPermissionsCallback.invoke();
            mPermissionsCallback = null;
        }
    }

    public void onPause() {
        if (getReactNativeHost().hasInstance()) {
            getReactInstanceManager().onHostPause(mActivity);
        }
    }

    public void onDestroy() {

        if (mReactRootView != null) {
            mReactRootView.unmountReactApplication();
            mReactRootView = null;
        }
        if (getReactNativeHost().hasInstance()) {
            getReactInstanceManager().onHostDestroy(mActivity);
        }

        // 清除View
        ReactNativePreLoader.deatchView(mMainComponentName);
    }

    public boolean onNewIntent(Intent intent) {
        if (getReactNativeHost().hasInstance()) {
            getReactInstanceManager().onNewIntent(intent);
            return true;
        }
        return false;
    }

    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (getReactNativeHost().hasInstance()) {
            getReactInstanceManager().onActivityResult(mActivity, requestCode, resultCode, data);
        } else {
            // Did we request overlay permissions?
            if (requestCode == REQUEST_OVERLAY_PERMISSION_CODE && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                if (Settings.canDrawOverlays(mActivity)) {
                    if (mMainComponentName != null) {
                        if (mReactRootView != null) {
                            throw new IllegalStateException("Cannot loadApp while app is already running.");
                        }
                        mReactRootView = new ReactRootView(mActivity);
                        mReactRootView.startReactApplication(
                                getReactInstanceManager(),
                                mMainComponentName,
                                null);
                        mActivity.setContentView(mReactRootView);
                    }
                }
            }
        }
    }

    public boolean onBackPressed() {
        if (getReactNativeHost().hasInstance()) {
            getReactInstanceManager().onBackPressed();
            return true;
        }
        return false;
    }

    public boolean onRNKeyUp(int keyCode) {
        if (getReactNativeHost().hasInstance() && getReactNativeHost().getUseDeveloperSupport()) {
            if (keyCode == KeyEvent.KEYCODE_MENU) {
                getReactInstanceManager().showDevOptionsDialog();
                return true;
            }
            boolean didDoubleTapR = Assertions.assertNotNull(mDoubleTapReloadRecognizer)
                    .didDoubleTapR(keyCode, mActivity.getCurrentFocus());
            if (didDoubleTapR) {
                getReactInstanceManager().getDevSupportManager().handleReloadJS();
                return true;
            }
        }
        return false;
    }

    public void requestPermissions(String[] permissions, int requestCode, PermissionListener listener) {
        mPermissionListener = listener;
        mActivity.requestPermissions(permissions, requestCode);
    }

    public void onRequestPermissionsResult(final int requestCode, final String[] permissions, final int[] grantResults) {
        mPermissionsCallback = new Callback() {
            @Override
            public void invoke(Object... args) {
                if (mPermissionListener != null && mPermissionListener.onRequestPermissionsResult(requestCode, permissions, grantResults)) {
                    mPermissionListener = null;
                }
            }
        };
    }

    /**
     * 獲取 Application中 ReactNativeHost
     * @return
     */
    private ReactNativeHost getReactNativeHost() {
        return MainApplication.getInstance().getReactNativeHost();
    }

    /**
     * 獲取 ReactInstanceManager
     * @return
     */
    private ReactInstanceManager getReactInstanceManager() {
        return getReactNativeHost().getReactInstanceManager();
    }
}

程式碼很長,重點在onCreate方法:

 if (mMainComponentName != null && !needsOverlayPermission) {
            // 1.從快取中獲取RootView
            mReactRootView = ReactNativePreLoader.getReactRootView(mMainComponentName);

            if(mReactRootView == null) {

                // 2.快取中不存在RootView,直接建立
                mReactRootView = new ReactRootView(mActivity);
                mReactRootView.startReactApplication(
                        getReactInstanceManager(),
                        mMainComponentName,
                        null);
            }
            // 3.將RootView設定到Activity佈局
            mActivity.setContentView(mReactRootView);
        }
(1)首先從快取中取ReactRootView

(2)快取中不存在ReactRootView,直接建立。此時和系統幫我們建立ReactRootView沒有區別

(3)將ReactRootView設定到Activity佈局

很明顯,我們讓載入流程先經過快取,如果快取中已經存在了RootView,那麼就可以直接設定到Activity佈局,如果快取中不存在,再去執行建立過程。

ReactNativePreLoader.preLoad(this,"HotRN");

我們在啟動React Native前一個介面,執行preLoad方法優先加載出ReactRootView,此時就完成了檢視預載入,讓React Native介面達到秒顯的效果。

四、效果對比

優化前:                                                                                                     優化後:

                               

Ok,到此想必大家都想擼起袖子體驗一下了,那就開始吧~~ 原始碼已分享到Github,別忘了給顆star哦吐舌頭~