react-native实践记录(04)

###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实现。

整体的实现流程:

  1. App -> 实例化ReactNativeHost
  2. 启动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对象。

此处有两种方法:

  1. 通过ReactRootView.startReactApplication(…)方法隐式初始化ReactContext对象
  2. 显式的调用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://'));
  }

实现的思路:

  1. 判断图片是否存在于扩展资源目录下
  2. 存在则优先显示外部资源目录下的图片
  3. 不存在则显示app资源目录下的图片

具体实现:

  1. 对应版本记录图片资源

    //在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
    }
    
  2. 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

项目说明:

  1. Android的assets目录中存放了两个版本的index.android.bundle(重命名)
  2. app的version目录下存放了修改后的AssetSourceResolver.js的源码
  3. 未完待续…

END

–Nowy

–2018.11.19

分享到