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原始碼入手:
不難發現,ReactActivity中的行為都交給了ReactActivityDelegate類來處理。很明顯是委託模式。至於白屏原因是因為第一次建立時,那麼我們直接看onCreate即可。找到ReactActivityDelegate的onCreate方法: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(); } // 其餘程式碼略...... }
從原始碼可以看到,最終呼叫了loadApp方法,繼續跟蹤loadApp方法: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(); }
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哦~