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

View System.png

    具体Android中如下:

Android View显示机制.png

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

原生获取加载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与原生相关的东西。

ReactActivity.png

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

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 热更新接入点.png

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

热更新策略

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

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文件模块。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容

  • 背景 公司要开发一款新的应用,一方面希望能像H5一样跨平台和动态更新,另一方面又不满原生嵌入H5这种性能瓶颈,于是...
    蚍蜉一生阅读 7,861评论 13 26
  • 所谓热更新就是在不重新安装的前提下进行代码和资源的更新,相信在整个宇宙中还不存在觉得热更新不重要的程序猿。 增量热...
    3z鸿杰Libra阅读 28,538评论 33 97
  • 导语 React Native是一套由 Facebook 开源的跨平台、动态更新的 Javascript 框架,其...
    滴嗒嗒阅读 9,082评论 5 36
  • react vr中文网:www.vr-react.com react vr qq群:481244084 示例源码 ...
    liu_520阅读 3,635评论 4 6
  • “抽一丝发把秋捆住,用五彩的珍馐,燃烧羞赧,轻拍抚平,青色的布衣,难盖梦美的笑嫣,悄地藏进秀发,盘起……”简书的首...
    Lnrsyao阅读 363评论 2 1