很多人在技术选型的时候,会选择RN是因为它具有热更新,而且这是它的一个特性,所以实现起来会相对比较简单,不像原生那样,原生的热更新是一个大工程。那就目前来看,RN的热更新方案已有的,有微软的CodePush和reactnative中文网的pushy。实话说,这两个我还没有体验过。一来是当初选择RN是因为它不但拥有接近原生的体验感还具有热更新特性,那么就想自己来实现一下热更新,研究一下它的原理;二来,把自己的东西放在别人的服务器上总是觉得不是最好的办法,为什么不自己实现呢?因此,这篇文章便是记录自己的一些研究。
react native加载bundle过程
这篇文章是基于RNAndroid0.38.1
当我们创建完RN的基础项目后,打开android项目,项目只有MainActivity和MainApplication。
打开MainActivity,只有一个重写方法getMainComponentName,返回主组件名称,它继承于ReactActivity。
我们打开ReactActivity,它使用了代理模式,通过ReactActivityDelegate mDelegate对象将Activity需要处理的逻辑放在了代理对象内部,并通过getMainComponentName方法来设置(匹配)JS端AppRegistry.registerComponent端启动的入口组件。
Activity渲染出界面前,先是调用onCreate,所以我们进入代理对象的onCreate方法
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
protected void onCreate(Bundle savedInstanceState) {
if (getReactNativeHost().getUseDeveloperSupport() && Build.VERSION.SDK_INT >=
23) {
if (!Settings.canDrawOverlays(getContext())) {
Intent serviceIntent =
new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
getContext().startActivity(serviceIntent);
FLog.w(ReactConstants.TAG,RED
Box_PERMISSION_MESSAGE);
Toast.makeText(getContext(),RED
Box_PERMISSION_MESSAGE,Toast.LENGTH_LONG).show();
}
}
if (mMainComponentName !=
null) {
loadApp(mMainComponentName);
}
mDoubleTapReloadRecognizer =
new DoubleTapReloadRecognizer();
}
上面的代码并没什么实质的东西,主要是调用了loadApp,我们跟进看下
1
2
3
4
5
6
7
8
9
10
11
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);
}
生成了一个ReactRootView对象,然后调用它的startReactApplication方法,最后setContentView将它设置为内容视图。再跟进startReactApplication里
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
- 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
public void startReactApplication(
ReactInstanceManager reactInstanceManager,String moduleName,@Nullable Bundle launchOptions) {
UiThreadUtil.assertOnUiThread();
Assertions.assertCondition(
mReactInstanceManager ==
null,
"This root view has already been attached to a catalyst instance manager");
mReactInstanceManager = reactInstanceManager;
mJSModuleName = moduleName;
mLaunchOptions = launchOptions;
if (!mReactInstanceManager.hasStartedCreatingInitialContext()) {
mReactInstanceManager.createReactContextInBackground();
}
if (mWasMeasured) {
attachToReactInstanceManager();
}
}
startReactApplication传入三个参数,第一个ReactInstanceManager配置项管理类(非常重要);第二个是MainComponentName入口组件名称;第三个是Android Bundle类型,用于传递给JS端初始组件的props参数。首先,会根据ReactInstanceManager的配置去加载bundle过程,然后去渲染ReactRootView,将UI展示出来。现在我们不用去管attachToReactInstanceManager是如何去渲染ReactRootView,我们主要是研究如何加载bundle的,所以,我们跟进createReactContextInBackground,发现它是抽象类ReactInstanceManager的一个抽象方法。那它具体实现逻辑是什么呢?那我们就需要知道ReactInstanceManager的具体类的实例对象是谁了【1】。
好了,现在我们回到ReacActivityDelegate.Java的loadApp,在ReactRootView的startReactApplication传入的ReactInstanceManager对象是getReactNativeHost().getReactInstanceManager()
1
2
3
4
5
6
mReactRootView.startReactApplication(
getReactNativeHost().getReactInstanceManager(),getLaunchOptions());
getReactNativeHost(),又是什么呢?
1
2
3
4
5
protected ReactNativeHost
getReactNativeHost() {
return ((ReactApplication) getPlainActivity().getApplication()).getReactNativeHost();
}
所以我们在打开MainApplication类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
public class MainApplication extends Application implements ReactApplication {
private final ReactNativeHost mReactNativeHost =
new ReactNativeHost(
this) {
@Override
boolean getUseDeveloperSupport() {
return BuildConfig.DEBUG;
}
protected List<ReactPackage>
getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage()
);
}
};
@Override
public ReactNativeHost
getReactNativeHost() {
return mReactNativeHost;
}
}
MainApplication实现了ReactApplication接口,在getReactNativeHost()方法返回配置好的ReactNativeHost对象。由于我们把项目的Application配置成了MainApplication,所以ReacActivityDelegate的getReactNativeHost方法,返回的就是MainApplication mReactNativeHost对象。接着我们看下ReactNativeHost的getReactInstanceManager()方法,里面直接调用了createReactInstanceManager()方法,所以我们直接看createReactInstanceManager()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
protected ReactInstanceManager
createReactInstanceManager() {
ReactInstanceManager.Builder builder = ReactInstanceManager.builder()
.setApplication(mApplication)
.setJSMainModuleName(getJSMainModuleName())
.setUseDeveloperSupport(getUseDeveloperSupport())
.setRed
BoxHandler(getRed
BoxHandler())
.setUIImplementationProvider(getUIImplementationProvider())
.setInitialLifecycleState(LifecycleState.BEFORE_CREATE);
for (ReactPackage reactPackage : getPackages()) {
builder.addPackage(reactPackage);
}
String jsBundleFile = getJSBundleFile();
if (jsBundleFile !=
null) {
builder.setJSBundleFile(jsBundleFile);
}
else {
builder.setBundleAssetName(Assertions.assertNotNull(getBundleAssetName()));
}
return builder.build();
}
createReactInstanceManager()通过使用ReactInstanceManager.Builder构造器来设置一些配置并生成对象。从这里看,我们可以从MainApplication的mReactNativeHost对象来配置ReactInstanceManager,比如JSMainModuleName、UseDeveloperSupport、Packages、JSBundleFile、BundleAssetName等,也可以重写createReactInstanceManager方法,自己手动生成ReactInstanceManager对象。
这里看下jsBundleFile的设置,先判断了getJSBundleFile()是否为null,项目默认是没有重写的,所以默认就是null,那么走builder.setBundleAssetName分支,看下getBundleAssetName(),默认是返回”index.android.bundle”
1
2
3
4
5
6
7
public Builder
setBundleAssetName(String bundleAssetName) {
mJSBundleAssetUrl = (bundleAssetName ==
null ?
null :
"assets://" + bundleAssetName);
mJSBundleLoader =
null;
return this;
}
所以,默认情况下,mJSBundleAssetUrl=”assets://index.android.bundle”,mJSBundleLoader = null。
接着往下看,builder最后调用build()来生成ReactInstanceManager实例对象。我们进去build()方法看下。
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
- 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
public ReactInstanceManager
build() {
Assertions.assertNotNull(
mApplication,0);
Box-sizing: border-
Box;">"Application property has not been set with this builder");
Assertions.assertCondition(
mUseDeveloperSupport || mJSBundleAssetUrl !=
null || mJSBundleLoader !=
"JS Bundle File or Asset URL has to be provided when dev support is disabled");
Assertions.assertCondition(
mJSMainModuleName !=
null || mJSBundleAssetUrl !=
"Either MainModuleName or JS Bundle File needs to be provided");
if (mUIImplementationProvider ==
null) {
mUIImplementationProvider =
new UIImplementationProvider();
}
new XReactInstanceManagerImpl(
mApplication,mCurrentActivity,mDefaultHardwareBackBtnHandler,(mJSBundleLoader ==
null && mJSBundleAssetUrl !=
null) ?
JSBundleLoader.createAssetLoader(mApplication,mJSBundleAssetUrl) : mJSBundleLoader,mJSMainModuleName,mPackages,mUseDeveloperSupport,mBridgeIdleDebugListener,Assertions.assertNotNull(mInitialLifecycleState,0);
Box-sizing: border-
Box;">"Initial lifecycle state was not set"),mUIImplementationProvider,mNativeModuleCallExceptionHandler,mJSCConfig,mRed
BoxHandler,mLazyNativeModulesEnabled,mLazyViewManagersEnabled);
}
从上面看来,XReactInstanceManagerImpl的第四个参数,传入的是一个JSBundleLoader,并且默认是JSBundleLoader.createAssetLoader。
new的是XReactInstanceManagerImpl对象,也就是说,XReactInstanceManagerImpl是抽象类ReactInstanceManager的具体实现类。
好了,在【1】处留下的疑问,我们现在就解决了。也就是,说调用ReactInstanceManager的createReactContextInBackground方法,是去执行XReactInstanceManagerImpl的reateReactContextInBackground方法。
进去reateReactContextInBackground方法后,它调用了recreateReactContextInBackgroundInner()一个内部方法,直接看下recreateReactContextInBackgroundInner的实现代码
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
- 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
void recreateReactContextInBackgroundInner() {
UiThreadUtil.assertOnUiThread();
if (mUseDeveloperSupport && mJSMainModuleName !=
final DeveloperSettings devSettings = mDevSupportManager.getDevSettings();
if (mDevSupportManager.hasUpToDateJSBundleInCache() &&
!devSettings.isRemoteJSDebugEnabled()) {
onJSBundleLoadedFromServer();
}
else if (mBundleLoader ==
null) {
mDevSupportManager.handleReloadJS();
}
else {
mDevSupportManager.isPackagerRunning(
new DevServerHelper.PackagerStatusCallback() {
@Override
void onPackagerStatusFetched(
final boolean packagerIsRunning) {
UiThreadUtil.runOnUiThread(
new Runnable() {
@Override
void run() {
if (packagerIsRunning) {
mDevSupportManager.handleReloadJS();
}
else {
devSettings.setRemoteJSDebugEnabled(
false);
recreateReactContextInBackgroundFromBundleLoader();
}
}
});
}
});
}
return;
}
recreateReactContextInBackgroundFromBundleLoader();
}
由于我们发布出去的apk包,最后都是关闭了dev模式的,所以dev模式下的bundle加载流程我们先不需要太多的关注,那么mUseDeveloperSupport就是false,它就不会走进if里面,而是调用了recreateReactContextInBackgroundFromBundleLoader()方法。其实,你简单看下if里面的判断和方法调用也能知道,其实它就是去拉取通过React-native start启动起来的packages服务器窗口,再者如果打开了远程调试,那么它就走浏览器代理去拉取bundle。
recreateReactContextInBackgroundFromBundleLoader又调用了recreateReactContextInBackground
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
void recreateReactContextInBackground(
JavaScriptExecutor.Factory jsExecutorFactory,JSBundleLoader jsBundleLoader) {
UiThreadUtil.assertOnUiThread();
ReactContextInitParams initParams =
new ReactContextInitParams(jsExecutorFactory,jsBundleLoader);
if (mReactContextInitAsyncTask ==
// No background task to create react context is currently running,create and execute one.
mReactContextInitAsyncTask =
new ReactContextInitAsyncTask();
mReactContextInitAsyncTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,initParams);
}
else {
mPendingReactContextInitParams = initParams;
}
}
到这里,recreateReactContextInBackground使用了ReactContextInitAsyncTask(继承AsyncTask)开启线程去执行,并且将ReactContextInitParams当作参数,传递到了AsyncTask的doInBackground。ReactContextInitParams只是将jsExecutorFactory、jsBundleLoader两个参数封装成一个内部类,方便传递参数。
那么ReactContextInitAsyncTask开启线程去执行了什么?该类也是个内部类,我们直接看它的doInBackground方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
protected Result<ReactApplicationContext>
doInBackground(ReactContextInitParams... params) {
Process.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT);
Assertions.assertCondition(params !=
null && params.length >
0 && params[
0] !=
null);
try {
JavaScriptExecutor jsExecutor = params[
0].getJsExecutorFactory().create();
return Result.of(createReactContext(jsExecutor,params[
0].getJsBundleLoader()));
}
catch (Exception e) {
return Result.of(e);
}
}
好像也没处理什么,就是使用ReactContextInitParams传递进来的两个参数,去调用了createReactContext
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
- 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
private ReactApplicationContext
createReactContext(
JavaScriptExecutor jsExecutor,JSBundleLoader jsBundleLoader) {
FLog.i(ReactConstants.TAG,0);
Box-sizing: border-
Box;">"Creating react context.");
ReactMarker.logMarker(CREATE_REACT_CONTEXT_START);
mSourceUrl = jsBundleLoader.getSourceUrl();
List<ModuleSpec> moduleSpecs =
new ArrayList<>();
Map<Class,ReactModuleInfo> reactModuleInfoMap =
new HashMap<>();
JavaScriptModuleRegistry.Builder jsModulesBuilder =
new JavaScriptModuleRegistry.Builder();
final ReactApplicationContext reactContext =
new ReactApplicationContext(mApplicationContext);
if (mUseDeveloperSupport) {
reactContext.setNativeModuleCallExceptionHandler(mDevSupportManager);
}
ReactMarker.logMarker(PROCESS_PACKAGES_START);
Systrace.beginSection(
TRACE_TAG_REACT_JAVA_BRIDGE,0);
Box-sizing: border-
Box;">"createAndProces
scoreModulesPackage");
try {
CoreModulesPackage coreModulesPackage =
new CoreModulesPackage(
this,mBackBtnHandler,mUIImplementationProvider);
processPackage(
coreModulesPackage,reactContext,moduleSpecs,reactModuleInfoMap,jsModulesBuilder);
}
finally {
Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE);
}
for (ReactPackage reactPackage : mPackages) {
Systrace.beginSection(
TRACE_TAG_REACT_JAVA_BRIDGE,0);
Box-sizing: border-
Box;">"createAndProcessCustomReactPackage");
try {
processPackage(
reactPackage,jsModulesBuilder);
}
finally {
Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE);
}
}
ReactMarker.logMarker(PROCESS_PACKAGES_END);
ReactMarker.logMarker(BUILD_NATIVE_MODULE_REGISTRY_START);
Systrace.beginSection(TRACE_TAG_REACT_JAVA_BRIDGE,0);
Box-sizing: border-
Box;">"buildNativeModuleRegistry");
NativeModuleRegistry nativeModuleRegistry;
try {
nativeModuleRegistry =
new NativeModuleRegistry(moduleSpecs,reactModuleInfoMap);
}
finally {
Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE);
ReactMarker.logMarker(BUILD_NATIVE_MODULE_REGISTRY_END);
}
NativeModuleCallExceptionHandler exceptionHandler = mNativeModuleCallExceptionHandler !=
null
? mNativeModuleCallExceptionHandler
: mDevSupportManager;
CatalystInstanceImpl.Builder catalystInstanceBuilder =
new CatalystInstanceImpl.Builder()
.setReactQueueConfigurationSpec(ReactQueueConfigurationSpec.createDefault())
.setJSExecutor(jsExecutor)
.setRegistry(nativeModuleRegistry)
.setJSModuleRegistry(jsModulesBuilder.build())
.setJSBundleLoader(jsBundleLoader)
.setNativeModuleCallExceptionHandler(exceptionHandler);
ReactMarker.logMarker(CREATE_CATALYST_INSTANCE_START);
Systrace.beginSection(TRACE_TAG_REACT_JAVA_BRIDGE,0);
Box-sizing: border-
Box;">"createCatalystInstance");
final CatalystInstance catalystInstance;
try {
catalystInstance = catalystInstanceBuilder.build();
}
finally {
Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE);
ReactMarker.logMarker(CREATE_CATALYST_INSTANCE_END);
}
if (mBridgeIdleDebugListener !=
null) {
catalystInstance.addBridgeIdleDebugListener(mBridgeIdleDebugListener);
}
reactContext.initializeWithInstance(catalystInstance);
catalystInstance.runJSBundle();
return reactContext;
}
这个方法代码有点多,首先它执行设置了RN自带的和开发者自定义的模块组件(Package\Module),然后同样使用了构造器CatalystInstanceImpl.Builder生成了catalystInstance对象,最后调用了catalystInstance.runJSBundle()。跟进去是一个接口类CatalystInstance,那么我们又要去看它的实现类CatalystInstanceImpl
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
- 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
void runJSBundle() {
Assertions.assertCondition(!mJSBundleHasLoaded,0);
Box-sizing: border-
Box;">"JS bundle was already loaded!");
mJSBundleHasLoaded =
true;
mJSBundleLoader.loadScript(CatalystInstanceImpl.
this);
synchronized (mJSCallsPendingInitLock) {
mAcceptCalls =
true;
for (PendingJSCall call : mJSCallsPendingInit) {
callJSFunction(call.mExecutorToken,call.mModule,call.mMethod,call.mArguments);
}
mJSCallsPendingInit.clear();
}
Systrace.registerListener(mTraceListener);
}
到这里,可以看到mJSBundleLoader调用了loadScript去加载bundle。进去方法看下,发现它又是个抽象类,有两个抽象方法,一个是loadScript加载bundle,一个是getSourceUrl返回bundle的地址,并且提供了4个静态工厂方法。
由之前分析知道,JSBundleLoader默认是使用了JSBundleLoader.createAssetLoader来创建的实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static JSBundleLoader
createAssetLoader(
final Context context,
final String assetUrl) {
new JSBundleLoader() {
@Override
void loadScript(CatalystInstanceImpl instance) {
instance.loadScriptFromAssets(context.getAssets(),assetUrl);
}
public String
getSourceUrl() {
return assetUrl;
}
};
}
我们看到loadScript最后是调用了CatalystInstanceImpl的loadScriptFromAssets。跟进去之后发现,它是一个native方法,也就是最后的实现RN把它放在了jni层来完成最后加载bundle的过程。
并且CatalystInstanceImpl不止loadScriptFromAssets一个native方法,它还提供了loadScriptFromFile和loadScriptFromOptimizedBundle。其中前面两个,分别是从android assets目录下加载bundle,另一个是从android SD卡文件夹目录下加载bundle。而loadScriptFromOptimizedBundle是在UnpackingJSBundleLoader类里调用,但是UnpackingJSBundleLoader目前好像是没有用到,有知道它的作用的朋友们可以告知一下。
至此,bundle的加载流程我们已经走一遍了,下面用一张流程图来总结下
加载bundle文件的几个途径
从上面的分析过程,我们可以得出,bundle的加载路径来源取决于JSBundleLoader的loadScript,而loadScript又调用了CatalystInstanceImpl的loadScriptFromAssets或者loadScriptFromFile,所以,加载bundle文件的途径本质上有两种方式
从android项目下的assets文件夹下去加载,这也是RN发布版的默认加载方式,也就是在cmd命令行下使用gradlew assembleRelease 命令打包签名后的apk里面的assets就包含有bundle文件
如果你打包后发现里面没有bundle文件,那么你将它安装到系统里,运行也是会报错的
react native gradle assembleRelease打包运行失败,没有生成bundle文件
第二种方式是从android文件系统也就是sd卡下去加载bundle。
我们只要事先在sd卡下存放bundle文件,然后在ReactNativeHost的getJSBundleFile返回文件路径即可。
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
- 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
return Arrays.<ReactPackage>asList(
new MainReactPackage()
);
}
@Nullable
protected String
getJSBundleFile() {
File bundleFile =
new File(getCacheDir()+
"/react_native",0);
Box-sizing: border-
Box;">"index.android.bundle");
if(bundleFile.exists()){
return bundleFile.getAbsolutePath();
}
super.getJSBundleFile();
}
protected String
getBundleAssetName() {
super.getBundleAssetName();
}
};
getJSBundleFile首先会尝试在sd卡目录下
1
data/
data@H_
301_2214@/@H_
301_2214@<package
-name@H_
301_2214@>/
cache/react_native@H_
301_2214@/
看是否存在index.android.bundle文件,如果有,那么就会使用该bundle,如果没有,那么就会返回null,这时候就是去加载assets下的bundle了。
热更新的实现
如果你了解React Nativebundle命令,那么就会知道,其实该命令分两部分,一部分是生成bundle文件,一部分是生成图片资源。对android的react.gardle来说,也就是app/build.gradle中下面这句
1
apply
from:
"../../node_modules/react-native/react.gradle"
该脚本就是去执行react native bundle命令,它将生成的bundle文件放在assets下,且将生成的图片资源放在drawable下。
但是当我们自定义getJSBundleFile路径之后,bundle的所有加载过程都是在该目录下,包括图片资源,所以我们服务器上存放的应该是个bundle patch,包括bundle文件和图片资源。关于RN的图片热更新问题,可以看这个React-Native 图片热更新初探
有了前面的分析和了解后,那么就可以自己动手来实现bundle的热更新了。
那么热更新主要包括
- bundle patch从服务器下载到sd卡
- 程序中加载bundle
接下来,进行模拟版本更新:将旧版本中‘我的’tab的列表中‘观看历史’item去掉,也就是新版本中不再有‘观看历史’功能,效果如下
更新之前如下:
更新并加载bundle之后如下:
bundle patch的下载
我这里服务器使用的bmob后台,将要更新的bundle文件存放在服务器上。
先将去掉‘观看历史’后的新版本bundle patchs打包出来,上传到服务器上(bmob)。
通过react-native bundle命令手动将patchs包打包出来
1
2
3
react-native bundle --platform android --dev false --r
eset-cache --entry-file index.android.js --bundle-output F:
\Gray\ReactNative\XiF
an
\bundle\index.android.bundle --assets-dest F:
\XiFan\bundle
上传到服务器
然后,在客户端定义一个实体类来存放更新对象
1
2
3
4
5
AppInfo BmobObject{
private String version;
private String updateContent;
private BmobFile bundle;
}
然后,程序启动的时候去检测更新
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
- 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
@Override
void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
BmobQuery<AppInfo> query =
new BmobQuery<>();
query.setLimit(
1);
query.addWhereGreaterThan(
"version",0);
Box-sizing: border-
Box;">"1.0.0");
query.findObjects(
new FindListener<AppInfo>() {
@Override
void done(List<AppInfo> list,BmobException e) {
if(e ==
null){
if(list!=
null && !list.isEmpty()){
AppInfo info = list.get(
0);
File reactDir =
new File(getCacheDir(),0);
Box-sizing: border-
Box;">"react_native");
if(!reactDir.exists()){
reactDir.mkdirs();
}
BmobFile patchFile = info.getBundle();
final File saveFile =
new File(reactDir,0);
Box-sizing: border-
Box;">"bundle-patch.zip");
if(saveFile.exists()){
return;
}
patchFile.download(saveFile,136);
Box-sizing: border-
Box;">new DownloadFileListener() {
@Override
void done(String s,BmobException e) {
if (e ==
null) {
System.out.println(
"下载完成");
unzip(saveFile);
}
else {
Log.e(
"bmob",e.toString());
}
}
void onProgress(Integer integer,136);
Box-sizing: border-
Box;">long l) {
System.out.println(
"下载中...." + integer);
}
});
}
}
else{
Log.e(
在MainActivity的onCreate,将当前版本当作是1.0.0,发起检测更新。
当进入应用后,就会从服务端获取到更新对象
然后将bundle-patch文件保存到data/data/com.xifan/cache/react_native sd卡路径下
当将bundle-patch保存完并解压之后,接下去就是加载bundle了。
加载bundle
根据bug的紧急/重要程度,可以把加载bundle的时机分为:立马加载和下次启动加载,我这里将它们分别称为热加载和冷加载。
冷加载
冷加载方式比较简单,不用做任何特殊处理,下载并解压完patch.zip包之后,当应用完全退出之后(应用在后台不算完全退出,应用被杀死才算),用户再次启动应用,就会去加载新的bundle了。
热加载
热加载需要特殊处理一下,处理也很简单,只要在解压unzip之后,调用以下代码即可
//MainActivity.java
getReactNativeHost().clear();
recreate();
结合JS端,实现完整热更新流程
热更新的总体思路是,JS端通过Module发起版本检测请求,如果检测到有新版本bundle,就去下载bundle,下载完成后根据更新的紧急程度来决定是冷加载还是热加载。
那么首先我们需要定义一个UpdateCheckModule来建立起JS端和android端之间的检测更新通信。
UpdateCheckModule.java
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
- 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
class UpdateCheckModule extends ReactContextBaseJavaModule {
static final String TAG =
"UpdateCheckModule";
final String BUNDLE_VERSION =
"CurrentBundleVersion";
private SharedPreferences mSP;
UpdateCheckModule(ReactApplicationContext reactContext) {
super(reactContext);
mSP = reactContext.getSharedPreferences(
"react_bundle",Context.MODE_PRIVATE);
}
public String
getName() {
return "UpdateCheck";
}
public Map<String,Object>
getConstants() {
Map<String,Object> constants = MapBuilder.newHashMap();
String bundleVersion = BuildConfig.BUNDLE_VERSION;
String cacheBundleVersion = mSP.getString(BUNDLE_VERSION,0);
Box-sizing: border-
Box;">"");
if(!TextUtils.isEmpty(cacheBundleVersion)){
bundleVersion = cacheBundleVersion;
}
constants.put(BUNDLE_VERSION,bundleVersion);
return constants;
}
@ReactMethod
void check(String currVersion){
BmobQuery<AppInfo> query =
new BmobQuery<>();
query.setLimit(
1);
query.addWhereGreaterThan(
new FindListener<AppInfo>() {
@Override
null){
null && !list.isEmpty()){
final AppInfo info = list.get(
0);
File reactDir =
new File(getReactApplicationContext().getCacheDir(),0);
Box-sizing: border-
Box;">"react_native");
deleteDir(reactDir);
if(!reactDir.exists()){
reactDir.mkdirs();
}
"bundle-patch.zip");
BmobFile patchFile = info.getBundle();
patchFile.download(saveFile,136);
Box-sizing: border-
Box;">new DownloadFileListener() {
@Override
null) {
log(
"下载完成");
boolean result = unzip(saveFile);
if(result){
mSP.edit().putString(BUNDLE_VERSION,info.getVersion()).apply();
if(info.isImmediately()) {
((ReactApplication) getReactApplicationContext()).getReactNativeHost().clear();
getCurrentActivity().recreate();
}
}
else{
File reactDir =
"react_native");
deleteDir(reactDir);
}
}
else {
e.printStackTrace();
log(
"下载bundle patch失败");
}
}
void onProgress(Integer per,136);
Box-sizing: border-
Box;">long size) {
}
});
}
}
else{
e.printStackTrace();
log(
"获取版本信息失败");
}
}
});
}
}
代码中注释已经解释了其中的重要部分,需要注意的是,AppInfo增加了个boolean型immediately字段,来控制bundle是否立即生效
1
2
3
4
5
6
private Boolean immediately;
//要下载的bundle文件
}
还有在getConstants()方法获取当前bundle版本时,使用BuildConfig.BUNDLE_VERSION来标记和apk一起打包的bundle基础版本号,也就是assets下的bundle版本号,该字段是通过gradle的buildConfigField来定义的。打开app/build.gradle,然后在下面所示的位置添加buildConfigField定义,具体如下:
//省略了其它代码
android{
defaultConfig {
buildConfigField "String",0); Box-sizing: border-Box;">"BUNDLE_VERSION",0); Box-sizing: border-Box;">'"1.0.0"'
}
}
接着,不要忘记将自定义的UpdateCheckModule注册到Packages里。如果,你对自定义module还不是很了解,请看这里
最后,就是在JS端使用UpdateCheckModule来发起版本检测更新了。
我们先在XiFan/js/db 创建一个配置文件Config.js
1
2
3
4
const Config = {
bundleVersion:
'1.0.0'
};
export
default Config;
代码很简单,Config里面只是定义了个bundleVersion字段,表示当前bundle版本号。
每次要发布新版bundle时,更新下这个文件的bundleVersion即可。
然后,我们在MainScene.js的componentDidMount()函数中发起版本检测更新
//MainScene.js
import {
NativeModules
} from 'react-native';
import Config from './db/Config';
var UpdateCheck = NativeModules.UpdateCheck;
export default class MainScene extends Component{
componentDidMount(){
console.log('当前版本号:'+UpdateCheck.CurrentBundleVersion);
UpdateCheck.check(Config.bundleVersion)
}
}
这样就完成了,基本的bundle更新流程了。
总结
本篇文章主要分析了RN android端bundle的加载过程,并且在分析理解下,实现了完整bundle包的基本热更新,但是这只是热更新的一部分,还有很多方面可以优化,比如:多模块的多bundle热更新、bundle拆分差量更新、热更新的异常回退处理、多版本bundle的动态切换、bundle的更新和apk的更新相结合等等,这也是之后继续研究学习的方向。
最后,这个是项目的github地址,本章节的内容是在android分支上开发的,如需查看完整代码,克隆下来后请切换分支。