【Android】RN学习4——QDaily基于RN 0.30的混合app的通信和热修复实践

React Native现在已经到了0 37版本了,应用初期使用的0 30版本还不支持将resource打入bundle实施热更新,0 37版本已经解决了这些问题,如果再

React Native现在已经到了0.37版本了,应用初期使用的0.30版本还不支持将resource打入bundle实施热更新,0.37版本已经解决了这些问题,如果再不写篇文章,炒炒这份冷饭,那就过气了。

QDaily现在在Android和iOS的版本中都集成了React Native,用其做广告效果页的展示。

本文介绍基于Android平台,在RN进行混合app研发过程中,native部分做过的一些工作和踩过一些坑。

本该双平台一起介绍的,但Android的坑多,先说Android,iOS的对应工作会在下个文章描述。

一、Android中RN的View初始化及传参

马上看代码

//React相关组件初始化

ReactRootView mReactRootView = new ReactRootView(this);

mReactInstanceManager = ReactInstanceManager.builder()

.setApplication(getApplication())

.setJSBundleFile(getBundleFilePath())

.setJSMainModuleName("index.android")

.addPackage(new MainReactPackage())

.setUseDeveloperSupport(BuildConfig.DEBUG)

.setInitialLifecycleState(LifecycleState.BEFORE_RESUME)

.setUseOldBridge(true)

.build();

//准备参数

Bundle bundle = new Bundle();

bundle.putString("url", "http://www.qdaily.com");

bundle.putFloat("totalSeconds", 12);

bundle.putInt("style", 1);

bundle.putString("extras", "{/"key/" : /"value/"}");

bundle.putStringArray("imagePaths", new String[]{"url1", "url1"});

//组件启动

mReactRootView.startReactApplication(mReactInstanceManager, "adImageLaunch", bundle);

先解释下方法名和对应的参数

  • 上面的 getBundleFilePath() 可以指向本地路径,assert://开头或者file://开头,如果是测试情况,也可以以 http://开头,最终对应一个JSBundle文件。
  • “index.android”为MainModule的名字,相当于当前JSBundle的程序入口。
  • addPackage()用来增加支持的package,基本意思就是用于js端调用的native部分,包含函数和原生自定义组件。
  • setUseOldBridge(true) 在当前版本需要设定,否则会报错,应该是用于兼容旧版本使用。最新版本(0.37)还没测试
  • “adImageLaunch”意思是该view对应的bundle中的对应component,该component会生命在JSBundle的MainModule(当前为”index.android”)中声名。

再重点说下支持的传参(android.os.Bundle)。因为同名,为消除歧义,Bundle特指Android中的android.os.Bundle,React Native中的Bundle用JSBundle命名。

RN的传参仅支持这一种方式(文件、网络自行读取不算),但也不是Bundle支持的全部支持,具体参见Arguments.java源码,它将Bundle转换成为WritableMap组件(类json数据结构)。查看源码可知:

仅支持通过Bundle传递 Number (int、float、double等基本数据类型)、 StringBoolean 、或者包含以上类型的 数组 数据、或者包含以上类型的 Bundle 数据。

其它无论是否支持Parcelable、Serializable,或List都会在传输过程中收到IllegalArgumentException异常。

此处仅仅Android如此,iOS可以直接传输NSDictionary,系统级转换成JSON数据格式。

如果传输(透传)复杂数据类型,请自行转成JSON用字符串用String传输,ReactNative端用 JSON.parse() 进行转换

二、UI集成

此处集成可以分为Activity继承和fragment集成,以及View的集成(未测试)

Activity集成

onCreate 方法中,调用 setContentView(mReactRootView); ,直接将上面生成的ReactRootView赋给Activity的ContentView。

Activity需要 implements DefaultHardwareBackBtnHandler 接口,用于处理返回按键。

同时在生命周期的template方法中透传生命周期给ReactInstanceManager:

@Override

protected void onPause() {

super.onPause();

if (mReactInstanceManager != null) {

mReactInstanceManager.onHostPause();

}

}

@Override

protected void onResume() {

super.onResume();

if (mReactInstanceManager != null) {

mReactInstanceManager.onHostResume(this, this);

}

}

@Override

protected void onDestroy() {

super.onDestroy();

mReactInstanceManager.destroy();

}

@Override

public void onBackPressed() {

if (mReactInstanceManager != null) {

mReactInstanceManager.onBackPressed();

} else {

super.onBackPressed();

}

}

Fragment集成

和Activity集成相似,不过需要在onCreateView中返回上文的mReactRootView。生命周期自行处理。

View的集成(不推荐)

此处没做测试,感觉在处理生命周期会有问题,仅仅提供方法,欢迎交流

mNativeView = (FrameLayout) findViewById(R.id.nativeView);//原生布局中的view

//上文 mReactRootView 的初始化

mNativeView.addView(mReactRootView);//添加react布局

三、Native部分被调用的Module实现

说的太拗口了,其实就是React端调用原生方法时候,java部分需要做的一些工作,参见 原生模块

1、定义JavaModule

官方文档其实很清楚了,我这里简单说下我们这边的使用情况:

public class LaunchBridgeModule extends ReactContextBaseJavaModule {

public LaunchBridgeModule(ReactApplicationContext reactContext) {

super(reactContext);

}

@ReactMethod

public void dismissSplash(Callback errorCallback) {

Activity activity = MManagerCenter.getManager(ActivityController.class).getTopActivity();

if (activity != null) {

activity.finish();

}

errorCallback.invoke("");

}

@ReactMethod

public void open(String url, Callback errorCallback) {

Activity activity = MManagerCenter.getManager(ActivityController.class).getTopActivity();

if (activity != null) {

activity.setResult(Activity.RESULT_OK, new Intent().setData(Uri.parse(url)));

activity.finish();

}

errorCallback.invoke("");

}

@Override

public String getName() {

return "LaunchBridgeModule";

}

@Override

public Map<String, Object> getConstants() {

final Map<String, Object> constants = new HashMap<>();

constants.put("STATUS_BAR_HEIGHT", LocalDisplay.STATUS_BAR_HEIGHT_DP);

constants.put("SCREEN_HEIGHT", LocalDisplay.SCREEN_HEIGHT_DP);

constants.put("SCREEN_WIDTH", LocalDisplay.SCREEN_WIDTH_DP);

constants.put("NAVIGATION_BAR_HEIGHT", LocalDisplay.NAVIGATION_BAR_HEIGHT_DP);

return constants;

}

}

此处我们暴露了3个方法给JS端,分别为关闭页面、跳转页面和获取常量3个,其中前两个方法为异步方法,最后一个是同步方法。

异步方法用 @ReactMethod 注解标记,可以随便自定义方法名,所有被标记的方法会再addPackage后通过反射放在一个映射表中,一个类型的Module在同一ReactInstanceManager中只有一个对象。

同步方法在父类接口里面定义,默认返回null。之所以是同步方法,是因为在ReactInstanceManager时就进行了调用,将返回值封成了固定格式的String给了ReactBridge中,该bridge为配置表,JS端直接有这个表,因此能同步执行,但也存在问题,就是不能运行时动态使用,只能进行常量初始化(名字干脆就叫getConstants……)。方法调用堆栈如下:

【Android】RN学习4——QDaily基于RN 0.30的混合app的通信和热修复实践

getName()方法用于JS端调用时候使用的名字(不是类名),请不要重复!

一个注意点: 请相同作用的Module在Android和iOS部分getName,getConstants,以及所有异步方法名称完全一致,异步方法的入餐类型完全一致 。否则JS端调取原生方法时候还需要判断平台。

2、定义ReactPackage

所有Native部分的定义的方法以及View组件,最终都要用自定义的ReactPackage进行包装,来告诉ReactBridge,bridge用映射表供JS使用。

public class LaunchReactPackage implements ReactPackage {

@Override

public List<Class<? extends JavaScriptModule>> createJSModules() {

return Collections.emptyList();

}

@Override

public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {

return Collections.emptyList();

}

@Override

public List<NativeModule> createNativeModules(

ReactApplicationContext reactContext) {

List<NativeModule> modules = new ArrayList<>();

modules.add(new LaunchBridgeModule(reactContext));

return modules;

}

}

我们这里没有自定义UI组件(比较复杂,如果自定义还要有JS部分和iOS部分的开发)。ReactPackage中定义的三个方法:

  • createViewManagers 为JS端可以使用的自定义Native View。
  • createNativeModules 为可使用的方法,我们只用了这里。
  • createJSModules 没使用过,也没在0.30的代码中看到实现,先忽略。

之后只要在需要使用自定义View或者方法的ReactRootView的ReactInstanceManager的初始化时,将响应的package add进去即可。

四、0.30版本下的Android热修复实现

JSBundle的更新为在app启动之后会向后台sync RN的最新JSBundle,如果server有新的且本地不存在,则下载,否则什么也不做。逻辑很简单,不做介绍了。

这里主要介绍资源的热更新。JS部分默认支持的协议包括 assert://file:// 以及 http:// 协议,一些资源icon采用http协议肯定是不合适的(那也就没有热更新的必要了),assert是打在app包里的,Android的热更新肯定是file协议了,具体实现分为两个部分:

1、java部分将资源从assert copy到data下的固定文件夹中

private void copyFileOrDir(String path) {

AssetManager assetManager = mContext.getAssets();

String assets[] = null;

try {

assets = assetManager.list(path);

if (assets.length == 0) {

copyFile(path);

} else {

File dir = new File(mContext.getFilesDir(), path);

if (!dir.exists())

dir.mkdir();

for (int i = 0; i < assets.length; ++i) {

copyFileOrDir(path + "/" + assets[i]);

}

}

} catch (IOException ex) {

QLog.e("tag", "I/O Exception", ex);

}

}

private void copyFile(String filename) {

AssetManager assetManager = mContext.getAssets();

InputStream in = null;

OutputStream out = null;

try {

in = assetManager.open(filename);

out = new FileOutputStream(new File(mContext.getFilesDir(), filename));

byte[] buffer = new byte[1024];

int read;

while ((read = in.read(buffer)) != -1) {

out.write(buffer, 0, read);

}

in.close();

in = null;

out.flush();

out.close();

out = null;

} catch (Exception e) {

QLog.e(TAG, e.getMessage());

}

}

这样热更新只需要将资源覆盖到对应的文件夹中进行处理即可。此处Native部分很简单,关键是JS部分操作,因为按照写法,资源路径应该是file:///data/data/packagename/file/*,显然iOS部分不能正常读取,下面介绍JS部分。

2、JS部分操作需要在JSBundle动态替换资源路径

此处的规范比较严格,资源需要放在固定路径中,资源请求需要用严格的cage协议

#####(1)固定资源存放位置

首先请将所有资源文件(主要是image)放在根目录的resources下,不允许有嵌套文件夹;

#####(2)固定js中资源访问方式

将所有js代码中请求的本地资源代码由 require('./xxx.png') 改为 {uri:'http://qdaily.cage/xxx.png'} 。其中xxx.png为 上一条 中提到的resources目录下的同名资源:

<Image source={require('./cancel.png')>

改为

<Image source={{uri:'http://qdaily.cage/cancel.png'}}>

#####(3)修改0.30中react-native的js源码

  • 1、node_modules/react-native/Libraries/Image/image.android.js

render: function() {

...

...

//else后的代码块为新增

if (source && source.uri === '') {

console.warn('source.uri should not be an empty string');

} else {

var prefix = 'http://qdaily.cage/';

var match = source.uri.indexOf(prefix);

if (match == 0) { //qdaily.cage://开头

console.log(__DEV__);

var realUrl = source.uri.substring(prefix.length, source.uri.length);

if (__DEV__) { //debug 情况 RN 服务器

source.uri = resolveAssetSource.getDevServerURL() + 'react/resources/' + realUrl;

} else { //release 情况

source.uri = 'file://' + '/data/data/com.qdaily.ui/files/rnimgs/' + realUrl;

}

}

}

...

...

}

  • 2、node_modules/react-native/Libraries/Image/resolveAssetSource.js

    在文件结尾处增加:

module.exports.getDevServerURL = getDevServerURL;

在这一部分,我自己写了一个0.30情况下的shell脚本,在npm install之后运行脚本替换即可。可以点击这个 链接 进行下载。

下一篇文章会介绍iOS部分针对热修复在Native部分需要做的工作。

未登录用户
全部评论0
到底啦