一、前言
RN入门调研一文中提到:RN热更新的核心技术是构建JS与原生之间的解释器,基本原理是替换JS Bundle。
JS 解释器(javaScriptCore)是一个翻译系统,它非常复杂,辛亏前人已经做好,我们只要知道通过JS解释器,便可以用JS语言和原生系统进行交流——指挥原生做事情,从原生获取信息。
如何替换JS Bundle则是实现RN热更新需要掌握的,幸运的是替换JS Bundle和更换磁带、CD差不多, 换CD谁不会啊,哈哈~还真不一定⊙︿⊙。所以我们本文主要内容就是教大家如何换CD( ̄▽ ̄):
1、认识播放器,了解它如何使用;
2、介绍CD存放在哪里,它跟播放器都有哪些接触点;
3、什么时候把旧CD取出来,替换为新CD(CD更换策略)。
嘿嘿~算了,大家还是百度如何换磁带、CD吧,这里按照更换CD的流程来介绍如何更换JS Bundle:
1、了解View显示机制;
2、了解JS Bundle存放在哪里,它跟原生代码有哪些结合点;
3、以什么样的策略去更新JS Bundle。
二、View显示机制简介
假如你女朋友突然变得又聋又哑,你必须画一幅有趣的画给她,她才能好起来,那么主要分几步走呢?
1、你要准备好绘画的内容,比如画面中建筑的高矮,天空的颜色,人物的表情等;
2、你需要有绘画能力,能把每一个元素按照设想的大小绘制在正确的位置上;
3、你需要把这幅画展示在女朋友面前,也许还要在她要求下修改一二。
View显示机制也差不多这样:
具体Android中如下:
简单解释:
内容:描述显示和用户交互所有数据。比如显示的图片是什么、显示的文字是什么、文字的颜色是什么等,滑动图片怎么切换到一张图等。
内容提供者:也称为内容管理者,内容管理者跟内容的区别在于他有主动性,他既可以主动去获取内容,也会在合适的时机将内容分发给订阅的机构、人、模块等。
View:相当于绘制系统+触摸反馈系统。它可以将内容数据转换成一张张可以供展示的页面,可以将用户触摸事件传递给内容管理者,从而改变内容数据,当然内容变化后一般绘制-展示也会随之变化。
Activity: Activity是各个管理、控制模块在基层的交汇之处,也是对外服务点,不管是显示、语音、指纹控制、生命周期等,它都要插一脚,有点类似我们基层的行政服务中心,其中就包括展示与交互系统。比如一个Acitity有几个不同的页面,确定让哪一个展示给用户呢?点屏幕事件怎么通过触控系统传递到View绘制系统等。
对于原生来说,RN模块是内容用JS语言表达的一个View:(一种比较特殊的自定义View)
从RN显示机制一图中可以看到,JS Bundle 就是内容管理者管理的内容数据,RN页面之间跳转,不过是JS Bundle不同部分的内容展示。知道了JS Bundle的作用后,接下来便是查找:
三、JS Bundle在哪里?它跟原生代码如何结合的
类比播放器,CD作为内容存储设备,存在播放器可接触到位置,然后有一个磁头可以将CD内容读出来,经过播放器转换为声音放出来。所以JS Bundle也是处于App某一个地方,而且JS 解释器肯定也有一个类似于磁头一样的东西去读写JS Bundle的内容,供显示机制来显示,下面我们就从代码级别看看:
按照RN中文网去创建一个最简单demo,用AS打开目录下的Android部分,经过一番努力就能发现,在如下箭头位置获取并加载JS Bundle:
其中ReactNativeHost相当于内容管理者,getJSBundleFile就是内容管理者去取内容,而builder.setJSBundleFile(jsBundleFile)则代表启用jsBundleFile中的内容,好比说播放器现在播放这个CD。
再回头看下这个一番努力是怎么努力的?上文有提到当RN代码在移动设备上运行时候,原生代码相当于壳,入口、交互一切的起点都在原生发起。我们就从Android进程创建后首先加载的页面(清单中定义的)MainActivity入手,跟踪它生命周期进行分析起来:
public class MainActivity extends ReactActivity {
/**
* 在RN代码中注册的模块名
*/
@Override
protected String getMainComponentName() {
return "AwesomeProject";
}
/**
* 创建一个ReactActivity的代理
*/
@Override
protected ReactActivityDelegate createReactActivityDelegate() {
return new ReactActivityDelegate(this, getMainComponentName()) {
@Override
protected ReactRootView createRootView() {
return new RNGestureHandlerEnabledRootView(MainActivity.this);
}
};
}
}
可以看到MainActivity中,我们做了三件事:
A:填写我们的RN模块名;
一个ReactActivity相当于一个磁带盒/CD盒,每一个ReactActivity启动的时候就会根据getMainComponentName返回的模块名去加载对应的CD(JS 文件集合)。
读者问题1):模块名是在哪里注册?怎么得到这些模块名?
读者问题2):如何根据模块名去寻找对应的JS文件集合呢?
B:创建一个ReactActivity的代理ReactActivityDelegate
所谓代理是什么呢,就是遇到不想做或者不方便做的事,我就把权限发给某一个人让他帮忙做,那么他就是我的代理。
读者问题3):为什么要创建这个代理?
C:最关键的是继承自ReactActivity
ReactActivity是RN应用的基本类,它封装了所有RN与原生相关的东西。
从ReactActivity的源码可以看出来,它主体功能都是用一个代理ReactActivityDelegate来实现:
插一句,这里这个代理就是刚才在MainActivity中创建那个代理对象,目的就是让码农们方便自定义的一些东西,这也是代理的精髓。
继续我们的事件流,Activity的onCreate事件最终执行的就是ReactActivityDelegate的onCreate事件,这里有一个重要方法loadApp()。它的参数是前面提到的模块名,稍微跟踪下就会发现是在MainActivity中getMainComponentName方法中自定义那个。
继续跟踪loadApp到startReactApplication,我们追踪进它第一个参数:
public ReactInstanceManager getReactInstanceManager() {
if (mReactInstanceManager == null) {
ReactMarker.logMarker(ReactMarkerConstants.GET_REACT_INSTANCE_MANAGER_START);
// 创建RN Instance
mReactInstanceManager = createReactInstanceManager();
ReactMarker.logMarker(ReactMarkerConstants.GET_REACT_INSTANCE_MANAGER_END);
}
return mReactInstanceManager;
}
当原生首次加载RN时候,mReactInstanceManager等于null,所以我们继续追踪到createReactInstanceManager,这便是我们最上面指出的那个获取并更新js bundle的地方。
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);
}
// 获取js bundle
String jsBundleFile = getJSBundleFile();
if (jsBundleFile != null) {
// 设置js bundle
builder.setJSBundleFile(jsBundleFile);
} else {
builder.setBundleAssetName(Assertions.assertNotNull(getBundleAssetName()));
}
ReactInstanceManager reactInstanceManager = builder.build();
ReactMarker.logMarker(ReactMarkerConstants.BUILD_REACT_INSTANCE_MANAGER_END);
return reactInstanceManager;
}
跟踪进如getJSBundleFile方法内发现它返回null,是因为官方给的demo并没有接入热更新,所以它取的是本地Assert目录下本地打包的 Bundle文件。而做热更新的关键就是去重写这个getJSBundleFile,在这个方法里面可以从服务器拉取新的js bundle文件,设置到mReactInstanceManager中, 完成热更新。
怎么重写这个getJSBundleFile方法呢,我们看到这个方法位于ReactNativeHost类中,再回到上层找到这个ReactNativeHost对象来源于:
protected ReactNativeHost getReactNativeHost() {
return ((ReactApplication) getPlainActivity().getApplication()).getReactNativeHost();
}
所以我们在:
重写,至于我们怎么从网络获取新的bundle 则是另外一个问题,如上图中封装在UpdateContext.getBundleUrl(MainApplication.this)方法中。
现在我们知道了热更新就是替换js bundle,也找到了替换的地方,那最后的问题就是热更新策略,就是指什么时候更新你的CD,怎么验证你更新的CD对不对等。
热更新策略
热更新策略就是如何按照需求正确地更新JS Bundle,包括什么时候更新js bundle,更新过程中校验,更新失败处理等。最简单的热更新策略如下:
检查更新方案很多,比如最基本方案是给每一个js bundle一个版本号,每次加载的时候先请求服务器JS Bundle最新版本号,如果版本号比本地使用的bundle版本号大,就启动下载程序。
注意:上图可以称之为全量热更新,如果有新的版本的时候,旧的js bundle完全舍弃,替换为新的Js bundle。如果当前js bundle已经非常大,但是你新的js 文件就修改了一个字段,这时候还要这样全量更新,会大大浪费用户流量,也会增加不少更新时间,所以现在已经演变出来很多增量更新的方案,可以参考文章:增量更新RN
热更新思维之光
本文介绍了RN热原理与具体实现,指出了热更新是因为客户端有了内容管理者和内容解析者,使客户端既可以主动去获取数据与命令,又可以理解这些数据与指令,这些数据和指令便是可热更新的部分。思路发散下,如果可以热更新的不止业务数据与指令,而是人、部门、团体、产品配件等等,那该如何构建热更新机制使整个系统平稳、安全、快速的进化呢?
借鉴在技术领域的热更新,这里总结了三个关键点:
1、对可更新的部分构建精确、稳定、 高效的解释系统;
2、可更新的部分有明确和标准的对外接口;
3、对可更新部分有完整的更新策略,成功判断,失败处理等。
从这三点来看,报纸行业-->新闻中心+手机客户端 算是一种热更新转变,纸质信件-->电子邮件 是一种热更新,厨师培训->标准化饮食制作是一种热更新,构建公司人才策略,构建公司整体架构也可以是一种热更新。
在这个节奏越来越快的社会,商品需要快速换代,服务需要快速反馈,个人知识需要快速增长、公司架构需要快速进化 ,只要你能想到需要速度的地方,都可以将热更新之光照耀过去。
附录
关于问题:RN模块名是在哪里注册?怎么得到这些模块名?
答: 在原生中有一个接口AppRegistry,这个接口在RN代码中实现,当我们在原生中调用AppRegistry中的方法的时候,RN代码中对应的实现变开始执行。AppRegistry 是RN代码执行入口点,就是RN代码首先都是从这个实现中的方法开始运行的。
当首次从原生入口到RN代码时,会运行AppRegistry.registerComponent,(在RN模块中修改,有默认值)这个方法会把很多模块注册到RN框架中,参数就是模块名和入口文件名,相当于把CD放到某一个CD盒内,并把播放头调整到特定位置。然后在每次原生调用RN时候,都会执行AppRegistry. runApplication,这个方法传递的主要参数是getMainComponentName返回的模块名,这就是前面提到的根据模块名加载对应的JS文件模块。