很多人在技術選型的時候,會選擇RN是由于它具有熱更新,而且這是它的1個特性,所以實現起來會相對照較簡單,不像原生那樣,原生的熱更新是1個大工程。那就目前來看,RN的熱更新方案已有的,有微軟的CodePush和reactnative中文網的pushy。實話說,這兩個我還沒有體驗過。1來是當初選擇RN是由于它不但具有接近原生的體驗感還具有熱更新特性,那末就想自己來實現1下熱更新,研究1下它的原理;2來,把自己的東西放在他人的服務器上總是覺得不是最好的辦法,為何不自己實現呢?因此,這篇文章便是記錄自己的1些研究。
這篇文章是基于RN android 0.38.1
當我們創建完RN的基礎項目后,打開android項目,項目只有MainActivity和MainApplication。
打開MainActivity,只有1個重寫方法getMainComponentName,返回主組件名稱,它繼承于ReactActivity。
我們打開ReactActivity,它使用了代理模式,通過ReactActivityDelegate mDelegate對象將Activity需要處理的邏輯放在了代理對象內部,并通過getMainComponentName方法來設置(匹配)JS端AppRegistry.registerComponent端啟動的入口組件。
Activity渲染出界眼前,先是調用onCreate,所以我們進入代理對象的onCreate方法
//ReactActivityDelegate.java
protected void onCreate(Bundle savedInstanceState) {
//判斷是不是支持dev模式,也就是RN常見的那個紅色彈窗
if (getReactNativeHost().getUseDeveloperSupport() && Build.VERSION.SDK_INT >= 23) {
// Get permission to show redbox in dev builds.
if (!Settings.canDrawOverlays(getContext())) {
Intent serviceIntent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
getContext().startActivity(serviceIntent);
FLog.w(ReactConstants.TAG, REDBOX_PERMISSION_MESSAGE);
Toast.makeText(getContext(), REDBOX_PERMISSION_MESSAGE, Toast.LENGTH_LONG).show();
}
}
if (mMainComponentName != null) {
//加載app
loadApp(mMainComponentName);
}
//android摹擬器dev 模式下,雙擊R重新加載
mDoubleTapReloadRecognizer = new DoubleTapReloadRecognizer();
}
上面的代碼并沒甚么實質的東西,主要是調用了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);
}
生成了1個ReactRootView對象,然后調用它的startReactApplication方法,最后setContentView將它設置為內容視圖。再跟進startReactApplication里
//ReactRootView.java
public void startReactApplication(
ReactInstanceManager reactInstanceManager,
String moduleName,
@Nullable Bundle launchOptions) {
UiThreadUtil.assertOnUiThread();
// TODO(6788889): Use POJO instead of bundle here, apparently we can't just use WritableMap
// here as it may be deallocated in native after passing via JNI bridge, but we want to reuse
// it in the case of re-creating the catalyst instance
Assertions.assertCondition(
mReactInstanceManager == null,
"This root view has already been attached to a catalyst instance manager");
//配置項管理
mReactInstanceManager = reactInstanceManager;
//入口組件名稱
mJSModuleName = moduleName;
//用于傳遞給JS端初始組件props參數
mLaunchOptions = launchOptions;
//判斷是不是已加載過
if (!mReactInstanceManager.hasStartedCreatingInitialContext()) {
//去加載bundle文件
mReactInstanceManager.createReactContextInBackground();
}
// We need to wait for the initial onMeasure, if this view has not yet been measured, we set which
// will make this view startReactApplication itself to instance manager once onMeasure is called.
if (mWasMeasured) {
//去渲染ReactRootView
attachToReactInstanceManager();
}
}
startReactApplication傳入3個參數,第1個ReactInstanceManager配置項管理類(非常重要);第2個是MainComponentName入口組件名稱;第3個是Android Bundle類型,用于傳遞給JS端初始組件的props參數。首先,會根據ReactInstanceManager的配置去加載bundle進程,然后去渲染ReactRootView,將UI展現出來。現在我們不用去管attachToReactInstanceManager是如何去渲染ReactRootView,我們主要是研究如何加載bundle的,所以,我們跟進createReactContextInBackground,發現它是抽象類ReactInstanceManager的1個抽象方法。那它具體實現邏輯是甚么呢?那我們就需要知道ReactInstanceManager的具體類的實例對象是誰了【1】。
好了,現在我們回到ReacActivityDelegate.java的loadApp,在ReactRootView的startReactApplication傳入的ReactInstanceManager對象是getReactNativeHost().getReactInstanceManager()
//ReacActivityDelegate.java
mReactRootView.startReactApplication(
getReactNativeHost().getReactInstanceManager(),
appKey,
getLaunchOptions());
getReactNativeHost(),又是甚么呢?
//從Application獲得ReactNativeHost
protected ReactNativeHost getReactNativeHost() {
return ((ReactApplication) getPlainActivity().getApplication()).getReactNativeHost();
}
所以我們在打開MainApplication類
public class MainApplication extends Application implements ReactApplication {
private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
@Override
protected boolean getUseDeveloperSupport() {
return BuildConfig.DEBUG;
}
@Override
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()
//ReactNativeHost.java
protected ReactInstanceManager createReactInstanceManager() {
ReactInstanceManager.Builder builder = ReactInstanceManager.builder()
.setApplication(mApplication)
.setJSMainModuleName(getJSMainModuleName())
.setUseDeveloperSupport(getUseDeveloperSupport())
.setRedBoxHandler(getRedBoxHandler())
.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構造器來設置1些配置并生成對象。從這里看,我們可以從MainApplication的mReactNativeHost對象來配置ReactInstanceManager,比如JSMainModuleName、UseDeveloperSupport、Packages、JSBundleFile、BundleAssetName等,也能夠重寫createReactInstanceManager方法,自己手動生成ReactInstanceManager對象。
這里看下jsBundleFile的設置,先判斷了getJSBundleFile()是不是為null,項目默許是沒有重寫的,所以默許就是null,那末走builder.setBundleAssetName分支,看下getBundleAssetName(),默許是返回”index.android.bundle”
//builder.setBundleAssetName
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()方法看下。
//ReactInstanceManager.Builder
public ReactInstanceManager build() {
Assertions.assertNotNull(
mApplication,
"Application property has not been set with this builder");
Assertions.assertCondition(
mUseDeveloperSupport || mJSBundleAssetUrl != null || mJSBundleLoader != null,
"JS Bundle File or Asset URL has to be provided when dev support is disabled");
Assertions.assertCondition(
mJSMainModuleName != null || mJSBundleAssetUrl != null || mJSBundleLoader != null,
"Either MainModuleName or JS Bundle File needs to be provided");
if (mUIImplementationProvider == null) {
// create default UIImplementationProvider if the provided one is null.
mUIImplementationProvider = new UIImplementationProvider();
}
return new XReactInstanceManagerImpl(
mApplication,
mCurrentActivity,
mDefaultHardwareBackBtnHandler,
(mJSBundleLoader == null && mJSBundleAssetUrl != null) ?
JSBundleLoader.createAssetLoader(mApplication, mJSBundleAssetUrl) : mJSBundleLoader,
mJSMainModuleName,
mPackages,
mUseDeveloperSupport,
mBridgeIdleDebugListener,
Assertions.assertNotNull(mInitialLifecycleState, "Initial lifecycle state was not set"),
mUIImplementationProvider,
mNativeModuleCallExceptionHandler,
mJSCConfig,
mRedBoxHandler,
mLazyNativeModulesEnabled,
mLazyViewManagersEnabled);
}
從上面看來,XReactInstanceManagerImpl的第4個參數,傳入的是1個JSBundleLoader,并且默許是JSBundleLoader.createAssetLoader。
new的是XReactInstanceManagerImpl對象,也就是說,XReactInstanceManagerImpl是抽象類ReactInstanceManager的具體實現類。
好了,在【1】處留下的疑問,我們現在就解決了。也就是,說調用ReactInstanceManager的createReactContextInBackground方法,是去履行XReactInstanceManagerImpl的reateReactContextInBackground方法。
進去reateReactContextInBackground方法后,它調用了recreateReactContextInBackgroundInner()1個內部方法,直接看下recreateReactContextInBackgroundInner的實現代碼
//XReactInstanceManagerImpl.java
private void recreateReactContextInBackgroundInner() {
UiThreadUtil.assertOnUiThread();
//判斷是不是是dev模式
if (mUseDeveloperSupport && mJSMainModuleName != null) {
final DeveloperSettings devSettings = mDevSupportManager.getDevSettings();
// If remote JS debugging is enabled, load from dev server.
if (mDevSupportManager.hasUpToDateJSBundleInCache() &&
!devSettings.isRemoteJSDebugEnabled()) {
// If there is a up-to-date bundle downloaded from server,
// with remote JS debugging disabled, always use that.
onJSBundleLoadedFromServer();
} else if (mBundleLoader == null) {
mDevSupportManager.handleReloadJS();
} else {
mDevSupportManager.isPackagerRunning(
new DevServerHelper.PackagerStatusCallback() {
@Override
public void onPackagerStatusFetched(final boolean packagerIsRunning) {
UiThreadUtil.runOnUiThread(
new Runnable() {
@Override
public void run() {
if (packagerIsRunning) {
mDevSupportManager.handleReloadJS();
} else {
// If dev server is down, disable the remote JS debugging.
devSettings.setRemoteJSDebugEnabled(false);
recreateReactContextInBackgroundFromBundleLoader();
}
}
});
}
});
}
return;
}
recreateReactContextInBackgroundFromBundleLoader();
}
由于我們發布出去的apk包,最后都是關閉了dev模式的,所以dev模式下的bundle加載流程我們先不需要太多的關注,那末mUseDeveloperSupport就是false,它就不會走進if里面,而是調用了recreateReactContextInBackgroundFromBundleLoader()方法。其實,你簡單看下if里面的判斷和方法調用也能知道,其實它就是去拉取通過react-native start啟動起來的packages服務器窗口,再者如果打開了遠程調試,那末它就走閱讀器代理去拉取bundle。
recreateReactContextInBackgroundFromBundleLoader又調用了recreateReactContextInBackground
private void recreateReactContextInBackground(
JavaScriptExecutor.Factory jsExecutorFactory,
JSBundleLoader jsBundleLoader) {
UiThreadUtil.assertOnUiThread();
ReactContextInitParams initParams =
new ReactContextInitParams(jsExecutorFactory, jsBundleLoader);
if (mReactContextInitAsyncTask == null) {
// 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 {
// Background task is currently running, queue up most recent init params to recreate context
// once task completes.
mPendingReactContextInitParams = initParams;
}
}
到這里,recreateReactContextInBackground使用了ReactContextInitAsyncTask(繼承AsyncTask)開啟線程去履行,并且將ReactContextInitParams當作參數,傳遞到了AsyncTask的doInBackground。ReactContextInitParams只是將jsExecutorFactory、jsBundleLoader兩個參數封裝成1個內部類,方便傳遞參數。
那末ReactContextInitAsyncTask開啟線程去履行了甚么?該類也是個內部類,我們直接看它的doInBackground方法。
@Override
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) {
// Pass exception to onPostExecute() so it can be handled on the main thread
return Result.of(e);
}
}
好像也沒處理甚么,就是使用ReactContextInitParams傳遞進來的兩個參數,去調用了createReactContext
private ReactApplicationContext createReactContext(
JavaScriptExecutor jsExecutor,
JSBundleLoader jsBundleLoader) {
FLog.i(ReactConstants.TAG, "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,
"createAndProcessCoreModulesPackage");
try {
CoreModulesPackage coreModulesPackage =
new CoreModulesPackage(this, mBackBtnHandler, mUIImplementationProvider);
processPackage(
coreModulesPackage,
reactContext,
moduleSpecs,
reactModuleInfoMap,
jsModulesBuilder);
} finally {
Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE);
}
// TODO(6818138): Solve use-case of native/js modules overriding
for (ReactPackage reactPackage : mPackages) {
Systrace.beginSection(
TRACE_TAG_REACT_JAVA_BRIDGE,
"createAndProcessCustomReactPackage");
try {
processPackage(
reactPackage,
reactContext,
moduleSpecs,
reactModuleInfoMap,
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, "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);
// CREATE_CATALYST_INSTANCE_END is in JSCExecutor.cpp
Systrace.beginSection(TRACE_TAG_REACT_JAVA_BRIDGE, "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()。跟進去是1個接口類CatalystInstance,那末我們又要去看它的實現類CatalystInstanceImpl
//CatalystInstanceImpl.java
@Override
public void runJSBundle() {
Assertions.assertCondition(!mJSBundleHasLoaded, "JS bundle was already loaded!");
mJSBundleHasLoaded = true;
// incrementPendingJSCalls();
mJSBundleLoader.loadScript(CatalystInstanceImpl.this);
synchronized (mJSCallsPendingInitLock) {
// Loading the bundle is queued on the JS thread, but may not have
// run yet. It's save to set this here, though, since any work it
// gates will be queued on the JS thread behind the load.
mAcceptCalls = true;
for (PendingJSCall call : mJSCallsPendingInit) {
callJSFunction(call.mExecutorToken, call.mModule, call.mMethod, call.mArguments);
}
mJSCallsPendingInit.clear();
}
// This is registered after JS starts since it makes a JS call
Systrace.registerListener(mTraceListener);
}
到這里,可以看到mJSBundleLoader調用了loadScript去加載bundle。進去方法看下,發現它又是個抽象類,有兩個抽象方法,1個是loadScript加載bundle,1個是getSourceUrl返回bundle的地址,并且提供了4個靜態工廠方法。
由之前分析知道,JSBundleLoader默許是使用了JSBundleLoader.createAssetLoader來創建的實例
//JSBundleLoader.java
public static JSBundleLoader createAssetLoader(
final Context context,
final String assetUrl) {
return new JSBundleLoader() {
@Override
public void loadScript(CatalystInstanceImpl instance) {
instance.loadScriptFromAssets(context.getAssets(), assetUrl);
}
@Override
public String getSourceUrl() {
return assetUrl;
}
};
}
我們看到loadScript最后是調用了CatalystInstanceImpl的loadScriptFromAssets。跟進去以后發現,它是1個native方法,也就是最后的實現RN把它放在了jni層來完成最后加載bundle的進程。
并且CatalystInstanceImpl不止loadScriptFromAssets1個native方法,它還提供了loadScriptFromFile和loadScriptFromOptimizedBundle。其中前面兩個,分別是從android assets目錄下加載bundle,另外一個是從android SD卡文件夾目錄下加載bundle。而loadScriptFromOptimizedBundle是在UnpackingJSBundleLoader類里調用,但是UnpackingJSBundleLoader目前好像是沒有用到,有知道它的作用的朋友們可以告知1下。
至此,bundle的加載流程我們已走1遍了,下面用1張流程圖來總結下
從上面的分析進程,我們可以得出,bundle的加載路徑來源取決于JSBundleLoader的loadScript,而loadScript又調用了CatalystInstanceImpl的loadScriptFromAssets或loadScriptFromFile,所以,加載bundle文件的途徑本質上有兩種方式
從android項目下的assets文件夾下去加載,這也是RN發布版的默許加載方式,也就是在cmd命令行下使用gradlew assembleRelease 命令打包簽名后的apk里面的assets就包括有bundle文件
如果你打包后發現里面沒有bundle文件,那末你將它安裝到系統里,運行也是會報錯的
react native gradle assembleRelease打包運行失敗,沒有生成bundle文件
第2種方式是從android文件系統也就是sd卡下去加載bundle。
我們只要事前在sd卡下寄存bundle文件,然后在ReactNativeHost的getJSBundleFile返回文件路徑便可。
//MainApplication.java
private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
@Override
protected boolean getUseDeveloperSupport() {
return BuildConfig.DEBUG;
}
@Override
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage()
);
}
@Nullable
@Override
protected String getJSBundleFile() {
File bundleFile = new File(getCacheDir()+"/react_native","index.android.bundle");
if(bundleFile.exists()){
return bundleFile.getAbsolutePath();
}
return super.getJSBundleFile();
}
@Nullable
@Override
protected String getBundleAssetName() {
return super.getBundleAssetName();
}
};
getJSBundleFile首先會嘗試在sd卡目錄下
data/data/<package-name>/cache/react_native/
看是不是存在index.android.bundle文件,如果有,那末就會使用該bundle,如果沒有,那末就會返回null,這時候候就是去加載assets下的bundle了。
如果你了解react native bundle命令,那末就會知道,其實該命令分兩部份,1部份是生成bundle文件,1部份是生成圖片資源。對android的react.gardle來講,也就是app/build.gradle中下面這句
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以后以下:
我這里服務器使用的bmob后臺,將要更新的bundle文件寄存在服務器上。
先將去掉‘觀看歷史’后的新版本bundle patchs打包出來,上傳到服務器上(bmob)。
通過react-native bundle命令手動將patchs包打包出來
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:\Gray\ReactNative\XiFan\bundle
上傳到服務器
然后,在客戶端定義1個實體類來寄存更新對象
public class AppInfo extends BmobObject{
private String version;//bundle版本
private String updateContent;//更新內容
private BmobFile bundle;//要下載的bundle patch文件
}
然后,程序啟動的時候去檢測更新
//MainActivity.java
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
BmobQuery<AppInfo> query = new BmobQuery<>();
query.setLimit(1);
query.addWhereGreaterThan("version","1.0.0");
query.findObjects(new FindListener<AppInfo>() {
@Override
public 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(),"react_native");
if(!reactDir.exists()){
reactDir.mkdirs();
}
BmobFile patchFile = info.getBundle();
final File saveFile = new File(reactDir,"bundle-patch.zip");
if(saveFile.exists()){
return;
}
//下載bundle-patch.zip文件
patchFile.download(saveFile, new DownloadFileListener() {
@Override
public void done(String s, BmobException e) {
if (e == null) {
System.out.println("下載完成");
//解壓patch文件到react_native文件夾下
unzip(saveFile);
} else {
Log.e("bmob", e.toString());
}
}
@Override
public void onProgress(Integer integer, long l) {
System.out.println("下載中...." + integer);
}
});
}
}else{
Log.e("bmob",e.toString());
}
}
});
}
在MainActivity的onCreate,將當前版本當作是1.0.0,發起檢測更新。
當進入利用后,就會從服務端獲得到更新對象
然后將bundle-patch文件保存到data/data/com.xifan/cache/react_native sd卡路徑下
當將bundle-patch保存完并解壓以后,接下去就是加載bundle了。
根據bug的緊急/重要程度,可以把加載bundle的時機分為:立馬加載和下次啟動加載,我這里將它們分別稱為熱加載和冷加載。
冷加載方式比較簡單,不用做任何特殊處理,下載并解壓完patch.zip包以后,當利用完全退出以后(利用在后臺不算完全退出,利用被殺死才算),用戶再次啟動利用,就會去加載新的bundle了。
熱加載需要特殊處理1下,處理也很簡單,只要在解壓unzip以后,調用以下代碼便可
//MainActivity.java
//清空ReactInstanceManager配置
getReactNativeHost().clear();
//重啟activity
recreate();
熱更新的整體思路是,JS端通過Module發起版本檢測要求,如果檢測到有新版本bundle,就去下載bundle,下載完成后根據更新的緊急程度來決定是冷加載還是熱加載。
那末首先我們需要定義1個UpdateCheckModule來建立起JS端和android端之間的檢測更新通訊。
UpdateCheckModule.java
class UpdateCheckModule extends ReactContextBaseJavaModule {
private static final String TAG = "UpdateCheckModule";
private static final String BUNDLE_VERSION = "CurrentBundleVersion";
private SharedPreferences mSP;
UpdateCheckModule(ReactApplicationContext reactContext) {
super(reactContext);
mSP = reactContext.getSharedPreferences("react_bundle", Context.MODE_PRIVATE);
}
@Override
public String getName() {
return "UpdateCheck";
}
@Nullable
@Override
public Map<String, Object> getConstants() {
Map<String,Object> constants = MapBuilder.newHashMap();
//跟隨apk1起打包的bundle基礎版本號
String bundleVersion = BuildConfig.BUNDLE_VERSION;
//bundle更新后確當前版本號
String cacheBundleVersion = mSP.getString(BUNDLE_VERSION,"");
if(!TextUtils.isEmpty(cacheBundleVersion)){
bundleVersion = cacheBundleVersion;
}
constants.put(BUNDLE_VERSION,bundleVersion);
return constants;
}
@ReactMethod
public void check(String currVersion){
BmobQuery<AppInfo> query = new BmobQuery<>();
query.setLimit(1);
query.addWhereGreaterThan("version",currVersion);
query.findObjects(new FindListener<AppInfo>() {
@Override
public void done(List<AppInfo> list, BmobException e) {
if(e == null){
if(list!=null && !list.isEmpty()){
final AppInfo info = list.get(0);
File reactDir = new File(getReactApplicationContext().getCacheDir(),"react_native");
//獲得到更新消息,說明bundle有新版,在解壓前先刪除掉舊版
deleteDir(reactDir);
if(!reactDir.exists()){
reactDir.mkdirs();
}
final File saveFile = new File(reactDir,"bundle-patch.zip");
BmobFile patchFile = info.getBundle();
//下載bundle-patch.zip文件
patchFile.download(saveFile, new DownloadFileListener() {
@Override
public void done(String s, BmobException e) {
if (e == null) {
log("下載完成");
//解壓patch文件到react_native文件夾下
boolean result = unzip(saveFile);
if(result){//解壓成功后保存當前最新bundle的版本
mSP.edit().putString(BUNDLE_VERSION,info.getVersion()).apply();
if(info.isImmediately()) {//立即加載bundle
((ReactApplication) getReactApplicationContext()).getReactNativeHost().clear();
getCurrentActivity().recreate();
}
}else{//解壓失敗應當刪除掉有問題的文件,避免RN加載毛病的bundle文件
File reactDir = new File(getReactApplicationContext().getCacheDir(),"react_native");
deleteDir(reactDir);
}
} else {
e.printStackTrace();
log("下載bundle patch失敗");
}
}
@Override
public void onProgress(Integer per, long size) {
}
});
}
}else{
e.printStackTrace();
log("獲得版本信息失敗");
}
}
});
}
}
代碼中注釋已解釋了其中的重要部份,需要注意的是,AppInfo增加了個boolean型immediately字段,來控制bundle是不是立即生效
public class AppInfo extends BmobObject{
private String version;//bundle版本
private String updateContent;//更新內容
private Boolean immediately;//bundle是不是立即生效
private BmobFile bundle;//要下載的bundle文件
}
還有在getConstants()方法獲得當前bundle版本時,使用BuildConfig.BUNDLE_VERSION來標記和apk1起打包的bundle基礎版本號,也就是assets下的bundle版本號,該字段是通過gradle的buildConfigField來定義的。打開app/build.gradle,然后在下面所示的位置添加buildConfigField定義,具體以下:
//省略了其它代碼
android{
defaultConfig {
buildConfigField "String","BUNDLE_VERSION",'"1.0.0"'
}
}
接著,不要忘記將自定義的UpdateCheckModule注冊到Packages里。如果,你對自定義module還不是很了解,請看這里
最后,就是在JS端使用UpdateCheckModule來發起版本檢測更新了。
我們先在XiFan/js/db 創建1個配置文件Config.js
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包的基本熱更新,但是這只是熱更新的1部份,還有很多方面可以優化,比如:多模塊的多bundle熱更新、bundle拆分差量更新、熱更新的異常回退處理、多版本bundle的動態切換、bundle的更新和apk的更新相結合等等,這也是以后繼續研究學習的方向。
最后,這個是項目的github地址 ,本章節的內容是在android分支上開發的,如需查看完全代碼,克隆下來后請切換分支。
上一篇 使用純CSS3實現轉動時鐘案例
下一篇 Java集合類詳解