React-Native 熱更新嘗試(Android)
前言:由於蘋果釋出的ios的一些rn的app存在安全問題,主要就是由於一些第三方的熱更新庫導致的,然而訊息一出就鬧得沸沸揚揚的,導致有些人直接認為“學了大半年的rn白學啦~~!!真是哭笑不得。廢話不多說了,馬上進入我們今天的主題吧。“
因為一直在做android開發,所以今天也只是針對於android進行熱更新嘗試(ios我也無能為力哈,看都看不懂,哈哈~~~)。
先看一下效果:
怎麼樣?效果還是不錯的吧?其實呢,實現起來還是不是很難的,下面讓我們一點一點的嘗試一下吧(小夥伴跟緊一點哦)。
首先我們來看看當我們執行:
react-native init xxxx
命令的時候,rn會自動幫我們建立一個android專案跟ios專案,然後我們看看rn幫我們建立的android專案長啥樣:
我們看到,幫我們建立了一個MainActivity跟一個MainApplication,我們先看一下MainActivity:
package com.businessstore;
import com.facebook.react.ReactActivity;
public class MainActivity extends ReactActivity {
/**
* Returns the name of the main component registered from JavaScript.
* This is used to schedule rendering of the component.
*/
@Override
protected String getMainComponentName() {
return "BusinessStore";
}
}
很簡單,就一行程式碼getMainComponentName,然後返回我們的元件名字,這個名字即為我們在index.android.js中註冊的元件名字:
然後我們看看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;
}
@Override
public void onCreate() {
super.onCreate();
SoLoader.init(this, /* native exopackage */ false);
}
}
也是沒有幾行程式碼…..
好啦~那我們的rn頁面是怎麼出來的呢? 不急,我們來一步一步往下看,首先點開MainActivity:
public class MainActivity extends ReactActivity {
/**
* Returns the name of the main component registered from JavaScript.
* This is used to schedule rendering of the component.
*/
@Override
protected String getMainComponentName() {
return "BusinessStore";
}
}
一個activity要顯示一個頁面的話肯定得setContentView,既然我們的activity沒有,然後就找到它的父類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();
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
mDelegate.onActivityResult(requestCode, resultCode, data);
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
return mDelegate.onKeyUp(keyCode, event) || super.onKeyUp(keyCode, event);
}
@Override
public void onBackPressed() {
if (!mDelegate.onBackPressed()) {
super.onBackPressed();
}
}
@Override
public void invokeDefaultOnBackPressed() {
super.onBackPressed();
}
@Override
public void onNewIntent(Intent intent) {
if (!mDelegate.onNewIntent(intent)) {
super.onNewIntent(intent);
}
}
@Override
public void requestPermissions(
String[] permissions,
int requestCode,
PermissionListener listener) {
mDelegate.requestPermissions(permissions, requestCode, listener);
}
@Override
public void onRequestPermissionsResult(
int requestCode,
String[] permissions,
int[] grantResults) {
mDelegate.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
protected final ReactNativeHost getReactNativeHost() {
return mDelegate.getReactNativeHost();
}
protected final ReactInstanceManager getReactInstanceManager() {
return mDelegate.getReactInstanceManager();
}
protected final void loadApp(String appKey) {
mDelegate.loadApp(appKey);
}
}
程式碼也不是很多,可見,我們看到了activity的很多生命週期方法,然後都是由一個叫mDelegate的類給處理掉了,所以我們繼續往下走看看mDelegate:
// Copyright 2004-present Facebook. All Rights Reserved.
package com.facebook.react;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
import android.support.v4.app.FragmentActivity;
import android.view.KeyEvent;
import android.widget.Toast;
import com.facebook.common.logging.FLog;
import com.facebook.infer.annotation.Assertions;
import com.facebook.react.bridge.Callback;
import com.facebook.react.common.ReactConstants;
import com.facebook.react.devsupport.DoubleTapReloadRecognizer;
import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler;
import com.facebook.react.modules.core.PermissionListener;
import javax.annotation.Nullable;
/**
* Delegate class for {@link ReactActivity} and {@link ReactFragmentActivity}. You can subclass this
* to provide custom implementations for e.g. {@link #getReactNativeHost()}, if your Application
* class doesn't implement {@link ReactApplication}.
*/
public class ReactActivityDelegate {
private final int REQUEST_OVERLAY_PERMISSION_CODE = 1111;
private static final String REDBOX_PERMISSION_GRANTED_MESSAGE =
"Overlay permissions have been granted.";
private static final String REDBOX_PERMISSION_MESSAGE =
"Overlay permissions needs to be granted in order for react native apps to run in dev mode";
private final @Nullable Activity mActivity;
private final @Nullable FragmentActivity mFragmentActivity;
private final @Nullable String mMainComponentName;
private @Nullable ReactRootView mReactRootView;
private @Nullable DoubleTapReloadRecognizer mDoubleTapReloadRecognizer;
private @Nullable PermissionListener mPermissionListener;
private @Nullable Callback mPermissionsCallback;
public ReactActivityDelegate(Activity activity, @Nullable String mainComponentName) {
mActivity = activity;
mMainComponentName = mainComponentName;
mFragmentActivity = null;
}
public ReactActivityDelegate(
FragmentActivity fragmentActivity,
@Nullable String mainComponentName) {
mFragmentActivity = fragmentActivity;
mMainComponentName = mainComponentName;
mActivity = null;
}
protected @Nullable Bundle getLaunchOptions() {
return null;
}
protected ReactRootView createRootView() {
return new ReactRootView(getContext());
}
/**
* Get the {@link ReactNativeHost} used by this app. By default, assumes
* {@link Activity#getApplication()} is an instance of {@link ReactApplication} and calls
* {@link ReactApplication#getReactNativeHost()}. Override this method if your application class
* does not implement {@code ReactApplication} or you simply have a different mechanism for
* storing a {@code ReactNativeHost}, e.g. as a static field somewhere.
*/
protected ReactNativeHost getReactNativeHost() {
return ((ReactApplication) getPlainActivity().getApplication()).getReactNativeHost();
}
public ReactInstanceManager getReactInstanceManager() {
return getReactNativeHost().getReactInstanceManager();
}
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 void onPause() {
if (getReactNativeHost().hasInstance()) {
getReactNativeHost().getReactInstanceManager().onHostPause(getPlainActivity());
}
}
protected void onResume() {
if (getReactNativeHost().hasInstance()) {
getReactNativeHost().getReactInstanceManager().onHostResume(
getPlainActivity(),
(DefaultHardwareBackBtnHandler) getPlainActivity());
}
if (mPermissionsCallback != null) {
mPermissionsCallback.invoke();
mPermissionsCallback = null;
}
}
protected void onDestroy() {
if (mReactRootView != null) {
mReactRootView.unmountReactApplication();
mReactRootView = null;
}
if (getReactNativeHost().hasInstance()) {
getReactNativeHost().getReactInstanceManager().onHostDestroy(getPlainActivity());
}
}
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (getReactNativeHost().hasInstance()) {
getReactNativeHost().getReactInstanceManager()
.onActivityResult(getPlainActivity(), 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(getContext())) {
if (mMainComponentName != null) {
loadApp(mMainComponentName);
}
Toast.makeText(getContext(), REDBOX_PERMISSION_GRANTED_MESSAGE, Toast.LENGTH_LONG).show();
}
}
}
}
public boolean onKeyUp(int keyCode, KeyEvent event) {
if (getReactNativeHost().hasInstance() && getReactNativeHost().getUseDeveloperSupport()) {
if (keyCode == KeyEvent.KEYCODE_MENU) {
getReactNativeHost().getReactInstanceManager().showDevOptionsDialog();
return true;
}
boolean didDoubleTapR = Assertions.assertNotNull(mDoubleTapReloadRecognizer)
.didDoubleTapR(keyCode, getPlainActivity().getCurrentFocus());
if (didDoubleTapR) {
getReactNativeHost().getReactInstanceManager().getDevSupportManager().handleReloadJS();
return true;
}
}
return false;
}
public boolean onBackPressed() {
if (getReactNativeHost().hasInstance()) {
getReactNativeHost().getReactInstanceManager().onBackPressed();
return true;
}
return false;
}
public boolean onNewIntent(Intent intent) {
if (getReactNativeHost().hasInstance()) {
getReactNativeHost().getReactInstanceManager().onNewIntent(intent);
return true;
}
return false;
}
@TargetApi(Build.VERSION_CODES.M)
public void requestPermissions(
String[] permissions,
int requestCode,
PermissionListener listener) {
mPermissionListener = listener;
getPlainActivity().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;
}
}
};
}
private Context getContext() {
if (mActivity != null) {
return mActivity;
}
return Assertions.assertNotNull(mFragmentActivity);
}
private Activity getPlainActivity() {
return ((Activity) getContext());
}
}
我們終於看到了一些有用的程式碼了,這個類就是處理跟activity生命週期相關的一些方法,包括(給activity新增contentview、監聽使用者回退、按鍵、6.0的一些執行時許可權等等…)我們看到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();
}
我們看到這麼一個判斷,這個是做什麼的呢?是為了檢測是不是具有彈出懸浮窗的許可權:
if (getReactNativeHost().getUseDeveloperSupport() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// Get permission to show redbox in dev builds.
if (!Settings.canDrawOverlays(getContext())) {
getReactNativeHost().getUseDeveloperSupport()返回的即為我們在MainApplication中寫的:
@Override
protected boolean getUseDeveloperSupport() {
return BuildConfig.DEBUG;
}
也就是說,當我們執行debug包的時候,會去檢測app是不是具有彈出懸浮窗的許可權,沒有許可權的話就會去請求許可權,懸浮窗即為rn的除錯menu:
好啦~!有點偏離我們今天的主題了,我們繼續往下走…往下我們看到會去執行一個叫loadApp的方法:
if (mMainComponentName != null && !needsOverlayPermission) {
loadApp(mMainComponentName);
}
我們點開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);
}
好啦~~! 看到這裡我們看到直接給activity設定了一個叫mReactRootView的元件,而這個元件正是rn封裝的元件,我們在js中寫的元件都會被轉換成native元件,然後新增進mReactRootView這個元件中,那麼問題來了,這些rn的元件又是在何時新增進我們的mReactRootView這個元件的呢???我們繼續往下走,看到rootview有一個startReactApplication方法:
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;
mLaunchOptions = launchOptions;
if (!mReactInstanceManager.hasStartedCreatingInitialContext()) {
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) {
attachToReactInstanceManager();
}
}
我們看到這麼一行程式碼:
if (!mReactInstanceManager.hasStartedCreatingInitialContext()) {
mReactInstanceManager.createReactContextInBackground();
}
看名字就知道肯定是載入了某些東西,可是點進去我們發現居然是一個抽象的方法,尷尬了~~!!:
public abstract void createReactContextInBackground();
那麼肯定有它的實現類,我們看到這個mReactInstanceManager是我們在呼叫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);
}
而mReactInstanceManager又是呼叫getReactNativeHost().getReactInstanceManager()方法獲取的,getReactNativeHost()這個返回的物件即為我們在MainApplication中建立的host物件:
public class MainApplication extends Application implements ReactApplication {
private static final String FILE_NAME = "index.android";
private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
然後我們順著getReactNativeHost().getReactInstanceManager()一直往下找最後發現mReactInstanceManager的實現類在這裡被建立了:
我們趕緊找到XReactInstanceManagerImpl類,然後看一下createReactContextInBackground這個方法:
@Override
public void createReactContextInBackground() {
Assertions.assertCondition(
!mHasStartedCreatingInitialContext,
"createReactContextInBackground should only be called when creating the react " +
"application for the first time. When reloading JS, e.g. from a new file, explicitly" +
"use recreateReactContextInBackground");
mHasStartedCreatingInitialContext = true;
recreateReactContextInBackgroundInner();
}
然後我們繼續往下:
private void recreateReactContextInBackgroundInner() {
...
@Override
public void onPackagerStatusFetched(final boolean packagerIsRunning) {
UiThreadUtil.runOnUiThread(
new Runnable() {
@Override
public void run() {
...
recreateReactContextInBackgroundFromBundleLoader();
}
}
});
}
});
}
return;
}
我們找到recreateReactContextInBackgroundFromBundleLoader繼續往下:
private void recreateReactContextInBackgroundFromBundleLoader() {
recreateReactContextInBackground(
new JSCJavaScriptExecutor.Factory(mJSCConfig.getConfigMap()),
mBundleLoader);
}
然後看到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這就是我們今天需要找的方法,傳遞了兩個引數:一個是執行js程式碼的執行緒池、另外一個是我們bundle檔案的載入器(bundle檔案可以是我們的npm伺服器中的檔案(debug模式),也可以是我們assert目錄中的bundle檔案(釋出版))。
既然如此,那我們熱更新方案是不是可以這樣呢?
1、請求伺服器介面,當介面中返回的版本號跟我們rn中儲存的版本號不一致的時候,那麼這個時候就需要更新版本了。
2、伺服器介面返回一個jsbundle檔案的下載地址,然後我們app中拿到地址下載到本地,替換掉當前版本的jsbundle檔案。
3、重新執行一下recreateReactContextInBackground方法,讓app重新載入新的jsbundle檔案。
好啦~! 有了思路以後,我們就可以寫我們的程式碼了:
首先,我們模擬一個後臺介面:
{
"url": "/business/version",
"method": "post",
"response": {
"code": "0",
"message": "請求成功",
"body": {
"versionName": "2.0.0",
"description":"添加了熱更新功能",
"url":"http://www.baidu.com"
}
}
},
然後在我們的rn中我們對應定義了一個常量叫version:
可以看到我們rn中定義的為1.0.0,所以待會我去請求介面,當介面返回的2.0.0不等於1.0.0的時候,我就去下載更新bundle檔案了,於是在我們rn主頁面的時候,我們就傳送一個請求,然後做判斷:
componentDidMount() {
this._versionCheck();
}
_versionCheck() {
this.versionRequest = new HomeMenuRequest(null, 'POST');
this.versionRequest.start((version)=> {
version = version.body;
if (version && version.versionName != AppConstant.version) {
if (Platform.OS == 'android') {
Alert.alert(
'發現新版本,是否升級?',
`版本號: ${version.versionName}\n版本描述: ${version.description}`,
[
{
text: '是',
onPress: () => {
this.setState({
currProgress: Math.random() * 80,
modalVisible: true
});
NativeModules.UpdateAndroid.doUpdate('index.android.bundle_2.0', (progress)=> {
let pro = Number.parseFloat('' + progress);
if (pro >= 100) {
this.setState({
modalVisible: false,
currProgress: 100
});
} else {
this.setState({
currProgress: pro
});
}
});
}
},
{
text: '否'
}
]
)
}
}
}, (erroStr)=> {
});
}
}
會彈出一個對話方塊:
當我們點選是的時候:
NativeModules.UpdateAndroid.doUpdate('index.android.bundle_2.0', (progress)=> {
let pro = Number.parseFloat('' + progress);
if (pro >= 100) {
this.setState({
modalVisible: false,
currProgress: 100
});
} else {
this.setState({
currProgress: pro
});
}
});
我們執行了native中的doUpdate並傳遞了兩個引數,一個是下載地址,一個是當native完成熱更新後的回撥:
NativeModules.UpdateAndroid.doUpdate()
這裡宣告一下,因為我這邊用的伺服器是mock的伺服器,所以沒法放一個檔案到伺服器上,我就直接把需要下載的bundle_2.0放在了跟1.0同級的一個目錄中了,然後我們去copy 2.0到記憶體卡(模擬從網路上獲取),替換掉1.0的版本。
再次宣告,我們釋出apk的時候需要把本地的js檔案打成bundle,然後丟到assets目錄中,所以最初的版本應該是index.android.bundle_1.0,這裡出現了一個index.android.bundle_2.0是為了模擬從伺服器下載,我就直接丟在了assert目錄了(正常這個檔案是在我們的遠端伺服器中的)。
如果還不知道怎麼釋出apk的童鞋可以去看我前面的一篇部落格:
接下來就看看我們native的程式碼如何實現了….
我就直接拿釋出版的例子來說了,我們首先看看我們的MainApplication中該怎麼寫:
public class MainApplication extends Application implements ReactApplication {
private static final String FILE_NAME = "index.android";
private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
@Override
protected boolean getUseDeveloperSupport() {
//這裡返回false的話即為釋出版,否則為測試版
//釋出版的話,app預設就會去assert目錄中找bundle檔案,
// 如果為測試版的話,就回去npm伺服器上獲取bundle檔案
return false;
}
@Override
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage(),
new VersionAndroidPackage(),
new UpdateAndroidPackage()
);
}
@Nullable
@Override
protected String getJSBundleFile() {
File file = new File(getExternalCacheDir(), FILE_NAME);
if (file != null && file.length() > 0) {
return file.getAbsolutePath();
}
return super.getJSBundleFile();
}
};
@Override
public ReactNativeHost getReactNativeHost() {
return mReactNativeHost;
}
@Override
public void onCreate() {
super.onCreate();
copyBundle();
SoLoader.init(this, /* native exopackage */ false);
}
private void copyBundle(){
if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
return;
}
File file = new File(getExternalCacheDir(), FILE_NAME);
if (file != null && file.length() > 0) {
return;
}
BufferedInputStream bis = null;
BufferedOutputStream bos = null;
try {
bis = new BufferedInputStream(getAssets().open("index.android.bundle_1.0"));
bos = new BufferedOutputStream(new FileOutputStream(file));
int len = -1;
byte[] buffer = new byte[512];
while ((len = bis.read(buffer)) != -1) {
bos.write(buffer, 0, len);
bos.flush();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (bis != null) {
bis.close();
}
if (bos != null) {
bos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
可以看到我們多重寫了一個方法,然後還寫了一段copy bundle檔案到記憶體卡的程式碼:
@Nullable
@Override
protected String getJSBundleFile() {
File file = new File(getExternalCacheDir(), FILE_NAME);
if (file != null && file.length() > 0) {
return file.getAbsolutePath();
}
return super.getJSBundleFile();
}
};
因為rn預設是去assert目錄中載入bundle檔案的,當指定了bundle檔案的地址後,rn會去載入我們指定的目錄。所以當我們第一次執行app的時候,我們首先把assert中的bundle檔案拷貝到了記憶體卡,然後讓rn去記憶體卡中加在bundle檔案。
好啦~~!!此時的rn已經知道去記憶體卡中載入bundle檔案了,我們要做的就是:
1、根據rn 中傳遞的地址去下載最新的bundle檔案。
2、替換掉記憶體卡中的bundle檔案。
3、呼叫createReactContextInBackground方法重新載入bundle檔案。
至於rn怎麼去跟native互動,我這裡簡單的說一下哈:
首先我們需要建一個叫UpdateAndroid去繼承ReactContextBaseJavaModule,然後註釋宣告為react的module:
@ReactModule(name = "UpdateAndroid")
public class UpdateAndroid extends ReactContextBaseJavaModule {
然後重寫裡面的一個叫getName的方法給這個module取一個名字:
@Override
public String getName() {
return "UpdateAndroid";
}
最後宣告一個類方法,讓rn調取:
@ReactMethod
public void doUpdate(String url, Callback callback) {
if (task == null) {
task = new UpdateTask(callback);
task.execute("index.android.bundle_2.0");
}
}
全部程式碼:
@ReactModule(name = "UpdateAndroid")
public class UpdateAndroid extends ReactContextBaseJavaModule {
private UpdateTask task;
public UpdateAndroid(ReactApplicationContext reactContext) {
super(reactContext);
}
@ReactMethod
public void doUpdate(String url, Callback callback) {
if (task == null) {
task = new UpdateTask(callback);
task.execute("index.android.bundle_2.0");
}
}
@Override
public String getName() {
return "UpdateAndroid";
}
private class UpdateTask extends AsyncTask<String, Float, File> {
private Callback callback;
private static final String FILE_NAME = "index.android";
private UpdateTask(Callback callback) {
this.callback = callback;
}
@Override
protected File doInBackground(String... params) {
return downloadBundle(params[0]);
}
@Override
protected void onProgressUpdate(Float... values) {
// if (callback != null && values != null && values.length > 0){
// callback.invoke(values[0]);
// Log.e("TAG", "progress-->" + values[0]);
// }
}
@Override
protected void onPostExecute(File file) {
if (callback != null) callback.invoke(100f);
//重寫初始化rn元件
onJSBundleLoadedFromServer(file);
}
private void onJSBundleLoadedFromServer(File file) {
if (file == null || !file.exists()) {
Log.i(TAG, "download error, check URL or network state");
return;
}
Log.i(TAG, "download success, reload js bundle");
Toast.makeText(getCurrentActivity(), "Downloading complete", Toast.LENGTH_SHORT).show();
try {
ReactApplication application = (ReactApplication) getCurrentActivity().getApplication();
Class<?> RIManagerClazz = application.getReactNativeHost().getReactInstanceManager().getClass();
Method method = RIManagerClazz.getDeclaredMethod("recreateReactContextInBackground",
JavaScriptExecutor.Factory.class, JSBundleLoader.class);
method.setAccessible(true);
method.invoke(application.getReactNativeHost().getReactInstanceManager(),
new JSCJavaScriptExecutor.Factory(JSCConfig.EMPTY.getConfigMap()),
JSBundleLoader.createFileLoader(file.getAbsolutePath()));
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
}
}
/**
* 模擬bundle下載連結url
*
* @param url
*/
private File downloadBundle(String url) {
if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
return null;
}
//刪除以前的檔案
File file = new File(getReactApplicationContext().getExternalCacheDir(), FILE_NAME);
if (file != null && file.length() > 0) {
file.delete();
}
BufferedInputStream bis = null;
BufferedOutputStream bos = null;
try {
//模擬網路下載過程,我這直接放在了assert目錄了
long size = getReactApplicationContext().getAssets().open(url).available();
bis = new BufferedInputStream(getReactApplicationContext().getAssets().open(url));
bos = new BufferedOutputStream(new FileOutputStream(file));
int len = -1;
long total = 0;
byte[] buffer = new byte[100];
while ((len = bis.read(buffer)) != -1) {
total += len;
bos.write(buffer, 0, len);
bos.flush();
float progress = total * 1.0f / size;
publishProgress(progress);
}
return file;
} catch (Exception e) {
e.printStackTrace();
} finally