通过对RN热更新的剖析来感受热更新思维

一、前言

    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显示机制也差不多这样:

通过对RN热更新的剖析来感受热更新思维_第1张图片
View System.png

    具体Android中如下:

通过对RN热更新的剖析来感受热更新思维_第2张图片
Android View显示机制.png

     简单解释:
    内容:描述显示和用户交互所有数据。比如显示的图片是什么、显示的文字是什么、文字的颜色是什么等,滑动图片怎么切换到一张图等。
    内容提供者:也称为内容管理者,内容管理者跟内容的区别在于他有主动性,他既可以主动去获取内容,也会在合适的时机将内容分发给订阅的机构、人、模块等。
    View:相当于绘制系统+触摸反馈系统。它可以将内容数据转换成一张张可以供展示的页面,可以将用户触摸事件传递给内容管理者,从而改变内容数据,当然内容变化后一般绘制-展示也会随之变化。
    Activity: Activity是各个管理、控制模块在基层的交汇之处,也是对外服务点,不管是显示、语音、指纹控制、生命周期等,它都要插一脚,有点类似我们基层的行政服务中心,其中就包括展示与交互系统。比如一个Acitity有几个不同的页面,确定让哪一个展示给用户呢?点屏幕事件怎么通过触控系统传递到View绘制系统等。
     对于原生来说,RN模块是内容用JS语言表达的一个View:(一种比较特殊的自定义View)
通过对RN热更新的剖析来感受热更新思维_第3张图片
RN显示机制.png

    从RN显示机制一图中可以看到,JS Bundle 就是内容管理者管理的内容数据,RN页面之间跳转,不过是JS Bundle不同部分的内容展示。知道了JS Bundle的作用后,接下来便是查找:

三、JS Bundle在哪里?它跟原生代码如何结合的

    类比播放器,CD作为内容存储设备,存在播放器可接触到位置,然后有一个磁头可以将CD内容读出来,经过播放器转换为声音放出来。所以JS Bundle也是处于App某一个地方,而且JS 解释器肯定也有一个类似于磁头一样的东西去读写JS Bundle的内容,供显示机制来显示,下面我们就从代码级别看看:

    按照RN中文网去创建一个最简单demo,用AS打开目录下的Android部分,经过一番努力就能发现,在如下箭头位置获取并加载JS Bundle:

通过对RN热更新的剖析来感受热更新思维_第4张图片
原生获取加载JS Bundle位置.png

    其中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与原生相关的东西。

通过对RN热更新的剖析来感受热更新思维_第5张图片
ReactActivity.png

    从ReactActivity的源码可以看出来,它主体功能都是用一个代理ReactActivityDelegate来实现:

通过对RN热更新的剖析来感受热更新思维_第6张图片
ReactActivityDelegate.png

    插一句,这里这个代理就是刚才在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();
  }

所以我们在:


通过对RN热更新的剖析来感受热更新思维_第7张图片
RN 热更新接入点.png

重写,至于我们怎么从网络获取新的bundle 则是另外一个问题,如上图中封装在UpdateContext.getBundleUrl(MainApplication.this)方法中。
    现在我们知道了热更新就是替换js bundle,也找到了替换的地方,那最后的问题就是热更新策略,就是指什么时候更新你的CD,怎么验证你更新的CD对不对等。

热更新策略

    热更新策略就是如何按照需求正确地更新JS Bundle,包括什么时候更新js bundle,更新过程中校验,更新失败处理等。最简单的热更新策略如下:

通过对RN热更新的剖析来感受热更新思维_第8张图片
RN简单热更新.png

    检查更新方案很多,比如最基本方案是给每一个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文件模块。

你可能感兴趣的:(通过对RN热更新的剖析来感受热更新思维)