首页 » React Native » React Native技术文章 » 正文

基于最新版本React Native实现JsBundle预加载,界面秒开优化

尊重版权,未经授权不得转载

本文来自Songlcy投稿:文章地址:http://blog.csdn.net/u013718120/article/details/71538263

一、问题分析

本篇博客同样和大家分享关于React Native的内容。想必大家在撸码中都发现了一个问题:从Android原生界面第一次跳转到React Native界面时,会有短暂的白屏过程,然后才会加载出界面。下次再跳转就不会出现类似问题。并且当我们杀死应用,重新启动App从Android Activity跳转到RN界面,依然会出现短暂白屏。

刚创建的React Native交流10群:157867561,欢迎各位大牛,React Native技术爱好者加入交流!同时博客右侧欢迎微信扫描关注订阅号,移动技术干货,精彩文章技术推送!

为什么第一次加载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哦吐舌头~

项目源码:https://github.com/songxiaoliang/ReactNativeApp

刚创建的React Native交流10群:157867561,欢迎各位大牛,React Native技术爱好者加入交流!同时博客右侧欢迎微信扫描关注订阅号,移动技术干货,精彩文章技术推送!

尊重原创,转载请注明:From Sky丶清(http://www.lcode.org) 侵权必究!

关注我的订阅号(codedev123),每天推送分享移动开发技术(Android/iOS),React Native技术文章,项目管理,程序猿日常点滴以及精品技术资讯文章(欢迎关注,精彩第一时间推送)。

关注我的微博,可以获得更多精彩内容