前言
以下内容仅针对安卓处理。做RN开发知道RN项目启动实际上是打包好的js文件,一般打包到android项目assets目录下,启动加载这里的bundle.js文件,当然RN也可以加载手机内存中的bundle.js
本人项目没有用code-push热更新,而是自己实现的,根据是否有新版本的bundle.js文件 下载重新加载本地bundle.js
之前下载新的bundle后都需要重新启动app才能实现重新加载本地bundle.js 体验比较差
目的:不用杀死进程重启应用,可以重新加载JS代码,重启页面。
分析源码
安卓RN项目 applicaiton 实现了ReactApplicaiton接口
package com.facebook.react;
public interface ReactApplication {
/**
* Get the default {@link ReactNativeHost} for this app.
*/
ReactNativeHost getReactNativeHost();
}
自己的Application部分实现
private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
@Override
public boolean getUseDeveloperSupport() {
return BuildConfig.DEBUG;
}
@Override
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage(),
new RNGestureHandlerPackage(),
new VectorIconsPackage(),
//new RNSentryPackage(),
mCommPackage // 自定义模块
);
}
@Nullable
@Override
protected String getJSBundleFile() {
String fileName = PathFactory.getExtraDownloadPath(appContext)+ SPUtil.getStringValue(appContext, PathFactory.REACT)+ File.separator+ FileConstant.JS_BUNDLE_LOCAL_FILE;
// String fileName = PathFactory.getExtraDownloadPath(appContext)+ "1551941117"+File.separator+FileConstant.JS_BUNDLE_LOCAL_FILE;
// File file = new File (FileConstant.JS_BUNDLE_LOCAL_PATH);
File file = new File (fileName);
if(file != null && file.exists()) {
// return FileConstant.JS_BUNDLE_LOCAL_PATH;
return fileName;
} else {
return super.getJSBundleFile();
}
}
@Override
protected String getJSMainModuleName() {
return "index";
}
};
@Override
public ReactNativeHost getReactNativeHost() {
return mReactNativeHost;
}
getJSBundleFile方法是自己实现的加载本地bundle.js文件完整路径或者super.getJSBundleFile() 【这个等价于"assets://你的bundle文件名" 例如assets://index.android.bundle】
因为Applicaiton是单例启动app后再不关闭的情况下只实例一次,所以对应的getJSBundleFile这个方法也就只执行了一遍
设想
所以如果更新了新的bundle文件,想要重新加载就必须重新让RN调用getJSBundleFile()方法
接着看源码
我们的MainActivity界面是集成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());
}
由以上代码得知 关键点在 ReactActivityDelegate这个代理,部分代码如下
protected ReactNativeHost getReactNativeHost() {
return ((ReactApplication) getPlainActivity().getApplication()).getReactNativeHost();
}
public ReactInstanceManager getReactInstanceManager() {
return getReactNativeHost().getReactInstanceManager();
}
protected void onCreate(Bundle savedInstanceState) {
if (mMainComponentName != null) {
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);
}
由上可知getReactNativeHost方法获取的nativehost就是我们自己application实现的,loadApp就是加载启动RN界面的入口方法;其核心关键就再getReactNativeHost().getReactInstanceManager()的方法里,下面我们继续看看这个方法到底做了什么
ReactNativeHost是一个抽象类
/**
* Get the current {@link ReactInstanceManager} instance, or create one.
*/
public ReactInstanceManager getReactInstanceManager() {
if (mReactInstanceManager == null) {
ReactMarker.logMarker(ReactMarkerConstants.GET_REACT_INSTANCE_MANAGER_START);
mReactInstanceManager = createReactInstanceManager();
ReactMarker.logMarker(ReactMarkerConstants.GET_REACT_INSTANCE_MANAGER_END);
}
return mReactInstanceManager;
}
protected ReactInstanceManager createReactInstanceManager() {
ReactMarker.logMarker(ReactMarkerConstants.BUILD_REACT_INSTANCE_MANAGER_START);
ReactInstanceManagerBuilder builder = ReactInstanceManager.builder()
.setApplication(mApplication)
.setJSMainModulePath(getJSMainModuleName())
.setUseDeveloperSupport(getUseDeveloperSupport())
.setRedBoxHandler(getRedBoxHandler())
.setJavaScriptExecutorFactory(getJavaScriptExecutorFactory())
.setUIImplementationProvider(getUIImplementationProvider())
.setJSIModulesPackage(getJSIModulePackage())
.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()));
}
ReactInstanceManager reactInstanceManager = builder.build();
ReactMarker.logMarker(ReactMarkerConstants.BUILD_REACT_INSTANCE_MANAGER_END);
return reactInstanceManager;
}
核心代码思想就在createReactInstanceManager,创建React实例管理器,一般情况下我们JSMainModuleName是不变更的,所以应该关注的是builder设置JSBundleFile的这个环节。
当然ReactInstanceManager这个类也需要仔细看一遍因为这个类有JSBundleLoader mBundleLoader;JSBundle的加载器
根据以上思路和推断,在自己的MainActivity启动类中做一下逻辑,当新的bundle下载到本地后重新加载bundle
解决方案
private void loadBundle() {
final ReactInstanceManager instanceManager;
try {
instanceManager = resolveInstanceManager();
if (instanceManager == null) {
return;
}
//获取本地的js代码 这里就不给出代码了。 如果本地没有就返回assets目录的
// String latestJSBundleFile = Utils.getJSBundleFileInternal();
String latestJSBundleFile = getJSBundleFileInternal();
setJSBundle(instanceManager, latestJSBundleFile);
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
try {
instanceManager.recreateReactContextInBackground();
} catch (Exception e) {
// The recreation method threw an unknown exception
// so just simply fallback to restarting the Activity (if it exists)
loadBundleLegacy();
}
}
});
} catch (Exception e) {
e.printStackTrace();
loadBundleLegacy();
}
}
private String getJSBundleFileInternal() {
String fileName = PathFactory.getExtraDownloadPath(getApplication())+ SPUtil.getStringValue(getApplication(), PathFactory.REACT)+ File.separator+ FileConstant.JS_BUNDLE_LOCAL_FILE;
File file = new File(fileName);
if(file.exists()){
file= null;
return fileName;
}
return FileConstant.JS_BUNDLE_DEFAULT;
}
private ReactInstanceManager resolveInstanceManager(){
ReactInstanceManager instanceManager;
final Activity currentActivity = MainActivity.this;
if (currentActivity == null) {
return null;
}
ReactApplication reactApplication = (ReactApplication) currentActivity.getApplication();
instanceManager = reactApplication.getReactNativeHost().getReactInstanceManager();
return instanceManager;
}
private void setJSBundle(ReactInstanceManager instanceManager, String latestJSBundleFile) throws IllegalAccessException {
try {
JSBundleLoader latestJSBundleLoader;
if (latestJSBundleFile.toLowerCase().startsWith("assets://")) {
latestJSBundleLoader = JSBundleLoader.createAssetLoader(MainApplication.getContext(), latestJSBundleFile, false);
} else {
latestJSBundleLoader = JSBundleLoader.createFileLoader(latestJSBundleFile);
}
Field bundleLoaderField = instanceManager.getClass().getDeclaredField("mBundleLoader");
bundleLoaderField.setAccessible(true);
bundleLoaderField.set(instanceManager, latestJSBundleLoader);
} catch (Exception e) {
throw new IllegalAccessException("Could not setJSBundle");
}
}
private void loadBundleLegacy() {
Log.d("loadBundleLegacy","loadBundle #3 loadBundleLegacy...");
final Activity currentActivity = MainActivity.this;
if (currentActivity == null) {
// The currentActivity can be null if it is backgrounded / destroyed, so we simply
// no-op to prevent any null pointer exceptions.
return;
}
currentActivity.runOnUiThread(new Runnable() {
@Override
public void run() {
currentActivity.recreate();
}
});
}
在需要重新加载的地方 调用loadBundle()方法便可。希望对自主实现更新加载bundle的朋友有所帮助。