###0.前言
开发环境:windows10 + vscode + react-native 0.57 + Android模拟器
本文主要记录在Android中对 react-native 相关模块进行版本更新(热更新的前期准备)
react-native的更新本质上是对index.android.bundle文件和资源文件的替换
注意:本文只是针对于Android方面的记录。
###1.实现加载本地index.android.bundle文件
加载本地index.android.bundle文件的实现主要由ReactNativeHost实现。
整体的实现流程:
- App -> 实例化ReactNativeHost
- 启动react-native相关模块界面 -> ReactNativeHost -> ReactInstanceManager初始化(为空时)-> 通过getJSBundleFile()或者getBundleAssetName()设置需要加载的bundle文件。
核心代码:
//ReactNativeHost类
protected ReactInstanceManager createReactInstanceManager() {
ReactMarker.logMarker(ReactMarkerConstants.BUILD_REACT_INSTANCE_MANAGER_START);
ReactInstanceManagerBuilder builder = ReactInstanceManager.builder()
.setApplication(mApplication)
...//省略部分参数设置代码
//核心代码段↓↓↓
String jsBundleFile = getJSBundleFile();
if (jsBundleFile != null) {//检测到JSBundleFile则优先加载getJSBundleFile()返回的bundle文件
builder.setJSBundleFile(jsBundleFile);
} else {//检测不到则加载assets目录下的bundle文件
builder.setBundleAssetName(Assertions.assertNotNull(getBundleAssetName()));
}
//核心代码段↑↑↑
ReactInstanceManager reactInstanceManager = builder.build();
ReactMarker.logMarker(ReactMarkerConstants.BUILD_REACT_INSTANCE_MANAGER_END);
return reactInstanceManager;
}
而在Android的项目中,集成react-native模块需要实例化ReactNativeHost对象。
//NowyApp类中
private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
@Override
public boolean getUseDeveloperSupport() {
return BuildConfig.DEBUG;
}
@Override
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(//高级应用:JS调原生模块
new MainReactPackage(),
new CusReactNativePackage()
);
}
//对应react-native项目的入口文件的名称,此项目为index.js
@Override
protected String getJSMainModuleName() {
return mJSMainModuleName;
}
@Override
public ReactInstanceManager getReactInstanceManager() {
return super.getReactInstanceManager();
}
//*******************************
// 热更新
//*******************************
//
// String jsBundleFile = getJSBundleFile();
// if (jsBundleFile != null) {
// builder.setJSBundleFile(jsBundleFile);
// } else {
// builder.setBundleAssetName(Assertions.assertNotNull(getBundleAssetName()));
// }
//*******************************
@Nullable
@Override
protected String getBundleAssetName() {
return super.getBundleAssetName();
}
//此方法为重点,若mJSBundleFile不为空,则优先加载mJSBundleFile指向的bundle文件
@Nullable
@Override
protected String getJSBundleFile() {
if (mJSBundleFile != null && mJSBundleFile.exists()) {
return mJSBundleFile.getPath();
}
return super.getJSBundleFile();
}
};
检测本地bundle文件存在相关代码:
//NowyApp类
private String mJSMainModuleName ="index";
private File mJSBundleFile = null;
private String mJSBundleDirName = "JSBundle";
private String mJSBundleName = "index.android.bundle";
@Override
public void onCreate() {
super.onCreate();
sInstance = this;
SoLoader.init(this, /* native exopackage */ false);
if(checkRNUpdate()){
Logger.t(TAG).e("本地存在JSBundle文件");
}else{
Logger.t(TAG).e("本地不存在JSBundle文件");
}
}
/**
* 检测RN更新
* @return true:存在本地RN文件
*/
private boolean checkRNUpdate(){
mJSBundleFile = null;
File mJSBundleDir = new File(FileUtil.getAppPath(this),mJSBundleDirName);
if(!mJSBundleDir.exists()){
Logger.t(TAG).e("没有检测到文件夹:"+mJSBundleDir.getPath());
mJSBundleDir.mkdirs();
return false;
}else{
String mJSBundleFilePath = mJSBundleDir.getPath()+File.separator+mJSBundleName;
mJSBundleFile = new File(mJSBundleFilePath);
if(mJSBundleFile.exists()){
Logger.t(TAG).e("检测到新的"+mJSBundleName);
return true;
}
return false;
}
}
以上为最基础的RN更新流程,原理就是通过ReactNativeHost实例重写getJSBundleFile()方法,返回外部的bundle文件路径。
项目代码:点击这里
- branche(对应分支):
热更新
- commit(提交版本):
8df83cc3a3d83e4f861bdf80bb4617a45f4fe980
####1.1首次开启react-native界面,存在白屏问题
- 根本原因是ReactContext的初始化时间比较长,而且是通过后台线程进行初始化的。
解决方法,在非react-native页面预先创建ReactContext对象。
此处有两种方法:
- 通过ReactRootView.startReactApplication(…)方法隐式初始化ReactContext对象
- 显式的调用ReactNativeHost.getReactInstanceManager().createReactContextInBackground()初始化。
其中方法一参考文章为:点击此处
由于参考的文章中的代码比较旧,所以根据思路重新实现了预加载。
相对应的类在preLoad目录下:点击查看
方法二核心代码:
//MainActivity
//方法二:通过createReactContextInBackground预加载ReactContext
if(NowyApp.getInstance().getReactNativeHost().getReactInstanceManager().hasStartedCreatingInitialContext()){
Log.e("MainActivity","已经开始创建ReactContext");
}else {
NowyApp.getInstance().getReactNativeHost().getReactInstanceManager().createReactContextInBackground();
}
PS:因为ReactContext初始化需要一定的时间,如果在此时间段内打开react-native界面,一样会白屏。上述两个方法都是迂回方法。
项目代码:点击这里
- branche(对应分支):
热更新
- commit(提交版本):
429c3520d7031ed9e971acd42b4fc5e821c55b1d
###2.实现加载本地图片
react-native如果是通过getJSBundleFile()方法加载外部bundle文件,那么会加载该bundle文件所在目录的资源文件。
核心代码:
//node_modules/react-native/Image/AssetSourceResolver.js
/**
* If the jsbundle is running from a sideload location, this resolves assets
* relative to its location
* E.g. 'file:///sdcard/AwesomeModule/drawable-mdpi/icon.png'
*/
drawableFolderInBundle(): ResolvedAssetSource {
const path = this.jsbundleUrl || 'file://';
return this.fromSource(path + getAssetPathInDrawableFolder(this.asset));
}
所以,最简单的方式就是将react-native模块使用到的全部资源文件拷贝到外部bundle文件对应的目录下。
例子中的bundle文件在:/data/data/<package包名>/JSBundle/
bundle文件:/data/data/<package包名>/JSBundle/index.android.bundle
图片资源目录:
/data/data/<package包名>/JSBundle/drawable-hdpi
/data/data/<package包名>/JSBundle/drawable-mdpi
/data/data/<package包名>/JSBundle/drawable-xhdpi
/data/data/<package包名>/JSBundle/drawable-xxhdpi
/data/data/<package包名>/JSBundle/drawable-xxxhdpi
<package包名>:app对应的applicationId
注:在读取外部图片资源时,如果资源不存在,则只占空间不显示图片(style决定)。
###2.1 既加载App的资源目录图片又加载外部资源目录图片的实现
图片加载的核心代码:
//node_modules/react-native/Image/AssetSourceResolver.js
defaultAsset(): ResolvedAssetSource {
if (this.isLoadedFromServer()) {
return this.assetServerURL();
}
if (Platform.OS === 'android') {
return this.isLoadedFromFileSystem()
? this.drawableFolderInBundle() //读取外部资源目录
: this.resourceIdentifierWithoutScale();//读取app资源目录
} else {
return this.scaledAssetURLNearBundle();
}
}
通过修改isLoadedFromFileSystem()方法对文件读取路径进行调整。
//node_modules/react-native/Image/AssetSourceResolver.js
isLoadedFromFileSystem(): boolean {//判断是否加载外部bundle文件,例如file:///sdcard/AwesomeModule/index.android.bundle
return !!(this.jsbundleUrl && this.jsbundleUrl.startsWith('file://'));
}
实现的思路:
- 判断图片是否存在于扩展资源目录下
- 存在则优先显示外部资源目录下的图片
- 不存在则显示app资源目录下的图片
具体实现:
对应版本记录图片资源
//在app目录下新建一个存放version描述文件的目录(app/version) //imageVersion.js import React, {Component} from 'react'; ////// // react-native使用的资源文件在编译后会根据存放的路径重命名。 // 例如:资源文件路径:/app/images/image.png,会被命名为:app_images_image.png. // android默认存放路径:/android/app/src/main/res/drawable-mdpi/app_images_image.png. // 外部存放路径:file:///sdcard/AwesomeModule/drawable-mdpi/app_images_image.png. ////// //资源文件在/app/images/下,使用此前缀拼接 const ImagesPrefix = "app_images_" //原来的资源文件,存在于APP资源目录中 const originalImgs = [ ImagesPrefix+'ic_user_avatar_def.png' /*头像图*/ ] //增量更新的资源文件,存在于外部bundle文件所在目录中 const externalImgs_V1 = [ ImagesPrefix+'ic_evaluation_satisfied_green.png' /*笑脸图*/ ] export { originalImgs, externalImgs_V1 as externalImgs }
isLoadedFromFileSystem()的替换
核心代码:
//查询externalImgs中是否存在该资源,存在优先使用外部资源目录的图片资源
isLoadedFromFileSystem_V2(): boolean{
//'drawable-mdpi/icon.png'
let imgFolder = getAssetPathInDrawableFolder(this.asset);
var imgName = imgFolder.substr(imgFolder.indexOf("/") + 1);
return !!(this.jsbundleUrl && externalImgs.indexOf(imgName) > -1);
}
###3.附录
参考文章:点击跳转
本文项目地址:点击跳转 branche: 热更新
commit : 4d877896f4a6839f2cffca5190ce7c558ae7e70b
项目说明:
- Android的assets目录中存放了两个版本的index.android.bundle(重命名)
- app的version目录下存放了修改后的AssetSourceResolver.js的源码
- 未完待续…
END
–Nowy
–2018.11.19