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

来源:互联网 发布:ue编辑器 mac版 编辑:程序博客网 时间:2024/05/22 00:13

本文来自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源码入手:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
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() { 
    returnnull
  
 
  /**
   * Called at construction time, override if you have a custom delegate implementation.
   */ 
  protected ReactActivityDelegate createReactActivityDelegate() { 
    returnnew 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方法:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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 = newIntent(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 = newDoubleTapReloadRecognizer(); 
  }

从源码可以看到,最终调用了loadApp方法,继续跟踪loadApp方法:

?
1
2
3
4
5
6
7
8
9
10
11
protected void loadApp(String appKey) { 
  if(mReactRootView != null) { 
    thrownew IllegalStateException("Cannot loadApp while app is already running."); 
  
  mReactRootView = createRootView(); 
  mReactRootView.startReactApplication( 
    getReactNativeHost().getReactInstanceManager(), 
    appKey, 
    getLaunchOptions()); 
  getPlainActivity().setContentView(mReactRootView); 
}
?
1
2
3
protected ReactRootView createRootView() { 
   returnnew ReactRootView(getContext()); 
 }

loadApp方法中调用了createRootView创建了ReactRootView,即React Native界面,并且将界面设置到Activity中。那么问题很可能出现在这了。插个断点,调试看看执行时间。

一切恍然大悟,在createRootView和startReactApplication时,消耗了较长时间。

既然是createRootView和startReactApplication执行了耗时操作的问题,那么我们只需要将其提前执行,创建出ReactRootView并缓存下来。当跳转到React Native界面时,直接设置到ContentView即可。有了解决思路,又该到我们甩起袖子撸码了。

三、功能实现
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
/**
 * 预加载工具类
 * Created by Song on 2017/5/10.
 */ 
public class ReactNativePreLoader { 
 
    private static final Map<String,ReactRootView> CACHE = newArrayMap<>(); 
 
    /**
     * 初始化ReactRootView,并添加到缓存
     * @param activity
     * @param componentName
     */ 
    public static void preLoad(Activity activity, String componentName) { 
 
        if(CACHE.get(componentName) != null) { 
            return
        
        // 1.创建ReactRootView 
        ReactRootView rootView = newReactRootView(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) { 
        returnCACHE.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创建部分,其他照搬源码即可。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
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 = newIntent(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 = newReactRootView(mActivity); 
                mReactRootView.startReactApplication( 
                        getReactInstanceManager(), 
                        mMainComponentName, 
                        null); 
            
            // 3.将RootView设置到Activity布局 
            mActivity.setContentView(mReactRootView); 
        
 
        mDoubleTapReloadRecognizer = newDoubleTapReloadRecognizer(); 
    
 
    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); 
            returntrue
        
        returnfalse
    
 
    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) { 
                            thrownew IllegalStateException("Cannot loadApp while app is already running."); 
                        
                        mReactRootView = newReactRootView(mActivity); 
                        mReactRootView.startReactApplication( 
                                getReactInstanceManager(), 
                                mMainComponentName, 
                                null); 
                        mActivity.setContentView(mReactRootView); 
                    
                
            
        
    
 
    public boolean onBackPressed() { 
        if(getReactNativeHost().hasInstance()) { 
            getReactInstanceManager().onBackPressed(); 
            returntrue
        
        returnfalse
    
 
    public boolean onRNKeyUp(int keyCode) { 
        if(getReactNativeHost().hasInstance() && getReactNativeHost().getUseDeveloperSupport()) { 
            if(keyCode == KeyEvent.KEYCODE_MENU) { 
                getReactInstanceManager().showDevOptionsDialog(); 
                returntrue
            
            boolean didDoubleTapR = Assertions.assertNotNull(mDoubleTapReloadRecognizer) 
                    .didDoubleTapR(keyCode, mActivity.getCurrentFocus()); 
            if(didDoubleTapR) { 
                getReactInstanceManager().getDevSupportManager().handleReloadJS(); 
                returntrue
            
        
        returnfalse
    
 
    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 = newCallback() { 
            @Override 
            public void invoke(Object... args) { 
                if(mPermissionListener != null&& mPermissionListener.onRequestPermissionsResult(requestCode, permissions, grantResults)) { 
                    mPermissionListener = null
                
            
        }; 
    
 
    /**
     * 获取 Application中 ReactNativeHost
     * @return
     */ 
    private ReactNativeHost getReactNativeHost() { 
        returnMainApplication.getInstance().getReactNativeHost(); 
    
 
    /**
     * 获取 ReactInstanceManager
     * @return
     */ 
    private ReactInstanceManager getReactInstanceManager() { 
        returngetReactNativeHost().getReactInstanceManager(); 
    
}

代码很长,重点在onCreate方法:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if(mMainComponentName != null&& !needsOverlayPermission) { 
           // 1.从缓存中获取RootView 
           mReactRootView = ReactNativePreLoader.getReactRootView(mMainComponentName); 
 
           if(mReactRootView == null) { 
 
               // 2.缓存中不存在RootView,直接创建 
               mReactRootView = newReactRootView(mActivity); 
               mReactRootView.startReactApplication( 
                       getReactInstanceManager(), 
                       mMainComponentName, 
                       null); 
           
           // 3.将RootView设置到Activity布局 
           mActivity.setContentView(mReactRootView);

(1)首先从缓存中取ReactRootView

(2)缓存中不存在ReactRootView,直接创建。此时和系统帮我们创建ReactRootView没有区别

(3)将ReactRootView设置到Activity布局

很明显,我们让加载流程先经过缓存,如果缓存中已经存在了RootView,那么就可以直接设置到Activity布局,如果缓存中不存在,再去执行创建过程。

?
1
ReactNativePreLoader.preLoad(this,"HotRN");

我们在启动React Native前一个界面,执行preLoad方法优先加载出ReactRootView,此时就完成了视图预加载,让React Native界面达到秒显的效果。

四、效果对比

优化前:                                                                                                     优化后:

                             

Ok,到此想必大家都想撸起袖子体验一下了,那就开始吧~~ 源码已分享到Github,别忘了给颗star哦吐舌头~

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

0 0
原创粉丝点击