皮皮网

【语聊厅源码】【startbbs源码】【gmtool源码】rn 源码

2024-11-18 21:36:01 来源:mathgpt源码

1.网易云音乐 RN 低代码体系建设思考与实践
2.什么是源码跨终端
3.Expo 搭建 React-native 项目代码目录分析
4.ReactNative资源更新增量包的优化实践

rn 源码

网易云音乐 RN 低代码体系建设思考与实践

       在构建低代码平台时,Tango是源码一个用于快速构建低代码平台的低代码设计器框架,以源代码为中心,源码执行和渲染前端视图,源码为用户提供低代码可视化搭建能力。源码借助Tango构建的源码语聊厅源码低代码工具或平台,可以实现源码进,源码源码出的源码效果,无缝与企业内部现有的源码研发体系进行集成。当前,源码Tango设计引擎部分已经开源,源码正在积极推进中,源码欢迎大家加入社区共同参与建设。源码

       RN(React Native)作为主流的源码跨端方案,具有建全的源码社区生态,每周下载次数稳步上升,下载量相比5年前已翻倍,GitHub Star数量超过万。RN具有较低的上手成本,可以快速迁移React技术栈,减少客户端开发同学的编译时间,提高开发效率。同时,RN提供了丰富的组件库和API,支持动态更新,满足跨端、动态更新以及复杂业务需求的场景。在国内,动态更新能降低产品的试错成本,快速上线和修复线上问题。重构已久的RN新架构已经确定将在年正式推出,将为RN开启一个全新的阶段,带来了更好的启动性能和渲染机制、通信性能的提升。

       云音乐在RN场景下的研发现状显示,RN具有众多优势,云音乐也有大量的RN需求。在需求迭代和研发投入的过程中,暴露了一些问题。基于C端场景的特点,云音乐在开发过程中的核心流程包括:准备开发环境、静态页面开发与还原视觉(交互)稿、进入业务开发阶段。然而,在实际操作中,云音乐遇到了DSL方案的局限性,即在面对灵活性高的移动端场景时,DSL无法满足业务需求,导致需要开发介入定制DSL或升级组件,增加了研发成本。为解决这些问题,云音乐引入了Tango,通过AST驱动提供源码为中心的RN在线搭建能力,支持快速交付,并提供标准化的线上研发流程。

       在移动端搭建上,云音乐面临一系列问题,包括DSL方案的局限性、传统搭建平台的问题等。为解决这些问题,云音乐采用Tango提供的源码为中心的在线搭建能力,支持RN应用快速交付,并通过标准化的线上研发流程提升效能。此外,云音乐还构建了在线真机预览调试环境,解决了模拟器运行环境和实时画面传输问题,实现与现有RN研发生态的融合。

       云音乐在构建在线真机预览调试环境时,考虑了多种方案,最终选用SRS流媒体服务器、OBS进行窗口捕获及推流,并利用WebRTC进行拉流。经过实践,实现了0.5s至2s内的平均响应速度。此外,云音乐还构建了基于Socket网关的云手机调度与通信交互方案,实现了高并发下的云手机分配与一致性的保证,为在线联调提供了必要的工具。

       在与低码结合方面,云音乐通过Tango提供了多维度的可视化搭建模拟,包括节点选中效果、startbbs源码结构树可视化编排等。同时,云音乐也提供了双模式切换、源码模式下的开发体验增强、多形式的代码生成等能力,以降低从0搭建成本。此外,Tango低码生态建设旨在构建一个以源码为中心的完整低码研发生态体系,包括运行时框架、组件、数据资产沉淀与可视化编排等,以提升开发效率。

       云手机的应用场景丰富,可以平替依赖物理手机的扫码类场景,远程联调客户端协议,查看应用程序在不同设备和屏幕尺寸上的显示效果,以及用于测试回归等。在低码平台的构建中,平衡点的寻找是一个持续探讨的问题,如何在通用性和贴合业务场景之间找到最佳平衡,以实现高内聚、低耦合的T型架构,是低码从业者需要不断思考和实践的。

什么是跨终端

       链接:/post/

       鉴于很多人对跨端技术感觉很神秘,虽然我实际上还没有写过一个从0到1的跨端框架,但是我曾经用Yoga(布局引擎Yoga(React-Native)做过一些简单的跨端的事情,后来用了Weex。研究跨端有一段时间了,想科普一下。

       科普之前,首先你要知道,为什么需要跨端技术?我们通常会把Weex和React-Native(本文统称为RN)说成是跨端技术吗(Flutter没有单独提到)?

       其实不是,好像Android/iOS本来是两个人的,但最终变成了一个人。我的人力减少了一半!

       但前提是这个人力需要懂Android,iOS,JavaScript,更懂,不然出了问题,怎么修?

       所以在中国的互联网环境下,很难招到这样的人。大家都在研究PPT架构技术,职场生存理论,岁如何解脱财富。我们如何有时间扩展我们的技术堆栈?

       端上开发很惨,总有崩溃(使用崩溃,闪退)而且没有办法远程修复。只能等下一个版本给使用市场推一个修复bug的新版本。

       但如果推送新版本,用户可能不会升级。因此,许多公司研究了各种热修复框架,尤其是在Android平台上。有很多热修复框架,主要是由DexClassLoader来完成。

       但是,最早的时候,WebView有一个很大的问题,尤其是Android。而且加载网页肯定要花时间,过程中屏幕会一片空白等等。所以很多人围绕这些做了很多优化。我个人觉得最有用的其实是线下套餐。同时,每一代WebView也在更新升级。然后一些有实力的公司开发了自己的所谓浏览器内核,各种黑科技,如何提速,支持各种特性等等。但是好像没有开源:dog:

       不算。这只是跨安卓和iOS,不把我的PC当目的?

       其实浏览器是跨端的,每个平台都可以用Chrome(其他浏览器主要是想做不做)!但是它也有自己的问题,因为各家都有自己的浏览器,内核不同,划分越来越大。chrome(Blink)/Safari(WebKit)/Firefox(Gecko?)等等,尤其是gmtool源码对css的支持。

       Developer.mozilla.org/zh-CN/docs/.这个网站可以检查一些浏览器的兼容性。例如,边框宽度的兼容性如下:

       其实也不是不可以,但是这样做相当于直接为OpenGL或者其他图形引擎编程,而且要自下而上的搭建一套渲染机制,打包各种基础UI组件给开发者使用,或者留下很多漏洞让开发者自定义自己的UI,非常复杂。但其实Flutter就是这么做的,所以Flutter2.0又开始向桌面端发展了,而且不局限于Android/iOS,但不知道能走多远。还有的是搞React-Native-Skia的,所以用js代码直接调Skia(2D图形渲染引擎)?(具体没看过)

       你写的JavaScript代码为什么能运行?这取决于JavaScript引擎。

       扔给它一段js代码(实际上是一个文本字符串),它就能帮你计算结果,处理逻辑。

       常见的Weex、RN、Hippy也依赖于此(MLN使用Lua)进行逻辑处理。

       这个时候会有很多概念。

       有些人喜欢把JavaScript引擎称为JavaScriptCore(不知道为什么,可能是因为iOS开发者才是研究这些比较深入的人,因为苹果的JavaScript引擎叫JavaScriptCore。苹果的这个JavaScriptCore呢?很多人喜欢称之为JSCore或者JSC)。所以,后来看到这些名词,我总是把它们带入语境中去感受他想说的是JavaScript引擎还是苹果的JavaScript引擎 JavaScript Core (JSCore/JSC)。

       先说JavaScript引擎。

       是的,有这么多!当然还有JavaScriptCore(不在图中)。

       最后一行是跑分,越多越好。有JIT的V8在3w挂所有东西。其中QuikJS极小,得分很高。估计很多人会用QuikJS做跨端JavaScript引擎吧?赫尔墨斯是由脸书创造的。看来Android目前在RN中使用的JavaScript引擎已经取代了之前使用的JavaScriptCore。RN为什么一直不用V8?这个我也不知道.

       但是很多人都在搞Android的V8项目,Github上也有一些开源项目。其次,iOS不支持JIT,有自己的JavaScriptCore,没有JIT改V8似乎意义不大。

       一个正常的跨端框架最简单的情况如下(后面会讨论问题,逐步丰富):

       用一个

       简单的例子看

       假设我的 js 文件中就是要 展示一个红色的 div 方块 。那么首先,端会把这个文本传给 JavaScript Runtime,它解析完后形成一个约定的格式,比如如下的 JSON 格式(里面的值用来描述是一个*红色方块,我随便定义的)

       { "name":"div", "width":"", "height":"", "background":"red"}

       通过 JavaScript Runtime 和 端(Android/iOS) 通信,把这个消息传回去。

       端拿到了消息,发现要创建一个 * 的叫做 div 的东西,没有 div 啊!这就需要端上提前埋好代码,比如 Android 里有 FrameLayout,那么就有类似的注册代码

       // 伪代码register("div", FrameLayout.class);

       然后端就知道了,oh!我需要创建一个长宽的正方形。

       首先,这是框架设计提前思考好的,究竟要支持哪些基础组件,比如 image 、text 等等。而且一般这里都会开个口子,让开发者可以自己扩展组件,比如你需要一个横滑列表,没提供怎么办?看看 div 怎么注册的,按照它的过程注册一个列表就好了。这也可以 PPT 吹成: 扩展跨端框架 ,其实 门槛比自定义 View 还要低 。

       前面说了 JavaScript Engine,这里咋又来了个 Runtime?

       JavaScript Engine 能做什么?

       什么都做不了,只能解析执行 js 代码

       那么问题来了,我怎么去 描述 我写的 js 代码代表的 视图 呢?其实不用描述,js 代码只要在 内存中 维护好一个树形结构就好了,就是一个 Object,因为实体在具体的源码合购端上,怎么理解呢?

       左边只要在内存中维护好这样一个树形结构就好了,传递给客户端时,转为

       { "name":"div", "children":[ { "name":"image" }, { "name":"div", "children":[] } // 等等 ]}

       端上拿到消息,创建视图为右图中的结构即可。

       如何维护好这个模型呢?调用什么 js 的方法发送消息呢?怎么给这些个 div 加上 css 来描述它的大小形状呢?等等更复杂的一系列的前端问题,都需要 写代码 来实现。

       所以一般都会有个 core.js 或者 framework.js 类似的一堆 js 代码,就是用来处理这些事情,而这些代码同样依靠 JavaScript Engine 来执行。

       从而所谓的 JavaScript Runtime ,我觉得可以单纯的理解为 JavaScript Engine 自身的代码跑起来后的环境,也可以理解为 core.js 等被跨端框架所需要的、包含了各种逻辑的前端代码被加载运行后的环境。

       当你用这些跨端框架的时候,你会发现他们只支持 css 子集 ,而且布局方式基本都是 flexbox(一种布局模型) 。

       那么比如你写了一个横着容纳了三个小方块的大方块,你的前端 css 代码肯定要写成, flex-diretion:row ,那么抛给端上的消息可能如下:

       { "name":"div", "attribute":{ // 使用布局 "flex-diretion":"row" }, "children":[ { "name":"div" }, { "name":"div" }, { "name":"div" } ]}

       端上拿到这个消息,都不知道 flex-direction 是什么。当然,你可以自己写一个解析库来解,但是 Yoga 帮你做了这件事!

       所以 RN 使用的是 Yoga 布局引擎(支持 flexbox,也是 Facebook 搞的)。

       Weex 似乎一开始是用的 Yoga,后来自己写了一套?

       这个地方就出现了一个名词 Layout Engine ,它就是帮我们处理各种布局参数的,然后帮我们算好每个视图的坐标,然后端上拿到坐标后设置对应的视图的坐标,一个井井有条的视图便展示了出来。如果你觉得你写的布局解析算法超越了 Yoga 等等,那么你完全可以自己写一套。

       比如从 JavaScript Runtime 处理完各种属性了,要渲染视图了!传了一段 JSON 给端。

       端上手指点了一下这个视图,那也要封装成一个消息传递给 JavaScript Runtime,然后触发你之前写的 js 的监听代码,比如点击后弹一个弹窗,那就又要封装一个调用弹窗方法的消息给端。

       就是这样来来回回。

       所以两边都有自己的消息队列。

       而且当你做动画还想监听动画过程的时候,肯定在短时间内发送了大量消息,这些过程肯定是 需要优化 的。

       并且!据我个人用 Weex 的经验,有的 flexbox 属性两端都不统一(可能是 Weex 的 Bug,毕竟 KPI 项目,都不维护了)

       我记得当时还开玩笑说,用了 Weex 终于领悟了跨端的真谛:

       if(platform === 'Andoird') { // 差异化逻辑} else if(platform === 'iOS') { // 差异化逻辑}

       跨端的代价就是,你 本以为 真的可以一套代码两端跑,后来发现真的有点做梦了(连 H5 有时候 Andoird/iOS 都不一致,因为用的内核都不是一个),代码里有不少的 if-else。

       所以经过上面的一系列科普,一个跨端框架成了这样:

       这其中一般是需要一个客户端、一个前端、一个懂 JavaScript Engine 会 C/C 的来分别开发。

       我虽然没开发过,但是感觉会有很多问题。

       比如 JavaScript Runtime 在另一个进程的话,跨进程通信?

       比如消息通信过于频繁是不是就会有各种连锁反应,掉帧啊、事件响应不及时、动画不流畅啊,怎么优化?

       其实我本身一直自诩喜欢研究原理,但是直至今日我也没真的一行行看过跨端框架的源码,我知道的这些也未必是对的,只是之前做过 Weex 的一些工作稍微研究了一下,还是挺惭愧的。

       既然你自称喜欢研究原理,为什么不看呢?

       链接:/post/

       相关问答:相关问答:手机端和电脑端各是什么?

       电脑端和手机端,实际上说的就是平台问题。

       当我们使用电脑的时候,电脑基本使用的操作平台是windows,或者苹果等常用操作系统。

       而手机上用的gba源码平台,如安卓,苹果的IOS,当年诺基亚的塞班,黑莓的系统,都叫做手机端。

       那么怎么定义手机端和电脑端呢,我们可以这么理解,如果用电脑操作系统的设备,即便是平板电脑,你也可以理解成是电脑端。

       如微软平板电脑surface,他的定位是平板也是电脑,

       我们很多的平板,多数使用的是安卓系统,苹果的当然就是IOS,但是平板使用基本使用的移动平台,也就可以看成是手机端。

       但是,如果这个移动设备的平台使用的是电脑的操作系统的时候,他所使用的平台,也就成了电脑端。

Expo 搭建 React-native 项目代码目录分析

       创建React-native项目时,Expo提供了多种工具简化开发过程。根据项目需求,选择不同的模板:空白模板(blank)适合演示、组件预览和个人项目;带有底部tab菜单的模板(tabs);需要直接控制原生代码时选择(minimal);遇到未知问题则选择RN方式。[1]

       React Native的典型目录结构包括以下几个部分:[2]

       src:存放组件源代码,是项目开发的核心目录。

       test:用于编写和运行组件的测试用例。

       demo:包含一个独立的Expo项目,App.js是核心文件,通过引用src中的组件进行展示和开发。

       其他文件如.eslintrc.js, babel.config.js, README.md, .gitignore, package.json等,分别负责代码风格规范、编译配置、项目介绍和版本管理。

       引入Expo时,需注意src目录与demo目录的配置协调,以确保metro(打包工具)能够正确处理。首先安装Expo CLI,然后创建项目,通过yarn start预览组件。配置metro时,重点在于新版本的metro.config.js,用于添加providesModuleNodeModules,解决src目录依赖的解析问题。[3]

       总结起来,开发过程中App.js是关键,负责组件的集成和展示。app.json和package.json分别用于设置应用配置和依赖管理。assets存放资源文件,babel.config.js用于代码转换,index.js是应用入口,metro.config.js负责项目打包,而yarn.lock则保证了依赖版本的一致性。eas.json则提供了EAS平台的云构建和部署选项。每个文件都有其特定的功能,共同构建React-native项目的开发流程。[4]

ReactNative资源更新增量包的优化实践

       本文首发于微信公众号“Shopee技术团队”。

1.背景

       Shopee的许多手机应用是原生与ReactNative(下文简称“RN”)的混合(hybrid)应用。在用户打开App时,客户端会发起请求查看是否有新版RN资源。若有,则后台静默下载最新资源,重新加载RN,以实现RN更新。这样的更新过程免却了用户去GooglePlay/AppStore重新下载App的麻烦,也能迅速把最新资源推送给所有用户。

       另一方面,RN资源虽会更新迭代,但新旧版的差异其实只占小部分,让用户下载全量资源不仅浪费用户流量,也影响用户对App的使用体验(因为后台静默更新仍然会挤占带宽资源)。

       自然而然,我们会想到“增量(差量)更新”,客户端仅下载新旧RN资源的差异部分。这个差异部分汇总到一个文件里,这个文件被称之为增量包(或补丁包)。下载完成并验证补丁包的合法性后,方可与旧版本合并为新版本,以此节约流量。

       考虑到Shopee主要市场的网络条件,数据流量的节约尤为重要。但这个增量包应该是怎样的呢?本文会以循序渐进的思路分析各种方案的优缺点,包括Shopee公司内的探索实践过程所产生的方案,以及业界提供的方案,最后提出FolderBsdp算法,对直接资源进行差分,实现增量更新。

2.增量包实践方案进化之路2.1直接传输差异的(原始)文件

       RN资源包括转译后的JavaScript代码、各语言翻译JSON文件、等媒体资源、配置文件。我们把这些资源称为“直接资源”。它们是RN打包的直接产物,也是日常供客户端调用的存在形式。如果这些“直接资源(原始文件)”在CDN上,保留新旧资源的文件目录,即可对应下载新增的和修改的文件。

       其缺点主要有两方面:

1)分散传输易出错

       如果有新增和修改翻译文件等修改,需要发起多次HTTP请求到CDN下载文件,这样会复杂化更新的异常处理情况,比如部分文件下载过程中断的恢复问题。

2)传输量大费流量

       和翻译JSON文件用原始文件传输浪费数据流量,因为这些资源本可以被压缩。翻译文件体积较大,但每个版本的修改一般是细微新增若干行,没有必要直传整个文件。因此,所谓传输“差异”的部分,文件级别的“差异”颗粒度还是太大了。

2.2文件间的差分算法BSDiff

       为了避免2.1中的问题,我们引入了差分算法,以实现增量更新。

2.2.1差分算法

       文件间的差分算法是一种实现增量更新的方法。差分算法的入参是新旧两个文件,其输出是两个文件的差异部分,称为Patch,即补丁包(也称增量包)。

       一个差分算法还有与之配套的打补丁算法,其入参是旧文件和Patch,输出是新文件。不同的差分算法各有优劣,体现在差分和打补丁操作的时空复杂度、Patch体积、Patch内容可读性等。

2.2.2Bsdp(BSDiff&BSPatch)算法

       BSDiff算法是一个非常流行的差分算法,它专注于得到尽可能小的Patch体积(适用于Patch要通过网络传输的更新方式),被GoogleChrome等软件广泛使用,非常适合代码升级这种具有局域性的稀疏的改动。

       BSDiff算法开源且免费,用C语言写成,源代码可以在服务端、Android/iOS端执行。BSDiff算法所搭配的打补丁算法BSPatch可以将旧文件和Patch结合,恢复出新文件。BSDiff和BSPatch算法并称Bsdp算法。

2.2.3直接使用BSDiff算法的优缺点

       对比于“2.1直接传输差异的(原始)文件”,如果只传输BSDiff所生成的差异Patch,的确能够减少传输的数据流量,加快下载过程。但是,它没有解决频繁发起多次HTTP请求去下载各个文件的问题。我们需要一个方法把资源合并起来,一起下载。

2.3压缩资源的BSDiff

       考虑到各个资源文件分散传输(2.1与2.2)的缺点,改进方式是在编译时把RN各直接资源文件放在一个文件夹里,然后用ZIP工具压缩为一个文件,采用ZIP压缩资源全量包以及压缩资源的Patch来实现初始安装和后续更新。

       具体说来,RN打包的直接产物包含了一定目录结构的JS代码、翻译JSON、、配置文件。所有这些文件压缩为一个ZIP压缩包。在App初始安装时,里面有这个ZIP包,以及其解压的RN资源。

       在用户使用App的过程中,若有新版RN资源,则会下载对应Patch文件,运用BSPatch算法与先前的ZIP压缩包(客户端打包时并不删除当时RN资源的ZIP压缩包)结合,即可得到新版本的ZIP压缩包,这个新版本的ZIP压缩包保留在用户手机里,以便下一次生成新ZIP包,如此反复,持续更新。ZIP压缩包解压后得到直接资源,即可被使用。

       从时间线上来看,这样的更新也就是版本迭代,形成如下图所示的更新关系,称为“RN更新链”。

       此方案的优点是克服了分列文件方案的缺点。通过ZIP压缩文件,多个文件合为一体,资源形式变得简单,HTTP请求数也减少了。对于全量包来说,它减少了总体积。Shopee某业务包未压缩时所有资源一共有MB,压缩后约为4MB。

       但是,此方案隐藏了一个难以发现的缺陷,使得这一方案存在很大的改进空间。这一缺陷就是ZIP压缩过程影响了文件的样貌,使得新旧文件的差异扩大,由此,ZIP压缩包之间的Patch体积也过大。以下详细解释这一缺陷。

2.3.1BSDiff生成Patch的过程

       BSDiff属于“区块移动”算法。为了记录新旧文件的差异以便恢复出来,它使用了动态规划算法——最长公共子序列。

       对于新文件的内容,它尝试从旧文件找到匹配的最长公共子序列,并记录了新文件的内容相对于旧文件的位置变化,站在新文件面向旧文件的视角,尽可能地用旧文件的内容拼装成新文件。

       而对于额外的差异,它记录了差异内容以及其在新文件的位置。这些记录的内容和位置信息经过bzip2压缩后即得到Patch文件。Patch文件包含的复制和插入信息,在运行BSPatch时可以把旧文件变成新文件。

       简单的示意图如上。蓝色和绿色区块是相同的内容,但是它们出现在文件中的位置不一样,Patch记录下位置信息。红色的是额外(Extra)的内容,Patch文件记录下其在新文件的位置及其内容本身。这些记录形成的指令让我们用旧文件和Patch文件恢复出新文件。

       由此,我们领悟到,对重复内容,记录位置偏移(蓝色、绿色区块)所需要的信息量少;对于额外区块,我们不仅要记录位置偏移,还要在Patch文件里留存其内容本身(尽管可以压缩)。位置偏移所占的Patch文件体积要远小于内容所占体积。额外内容越少(新旧文件越相似),Patch文件体积越小。

2.3.2压缩资源使用BSDiff的缺陷

       Lempel-Ziv编码算法是ZIP压缩算法的重要环节,它又被称为“滑动窗口压缩”。该算法的核心思想是在之前的历史数据中寻找重复字符串。

       但是,并不是一路回溯到文件头,而是考虑到重复内容的局部性,它设定了一个区间(常见是KB)。这个KB的窗口随着编码不断进行而向前滑动。理论上来说,这个滑动窗口(SlidingWindow)如果更大,我们有更大概率找到重复字符串,但这样会增大计算量。KB是ZIP的折中设定。

       在上图中,当编码来到“OK.”(右侧,蓝色标记)时,在滑动窗口回溯发现有重复的“OK.”(左侧,蓝色标记)。LZ算法记录下左侧“OK.”相对于当前位置的距离(Distance)和长度(Length),然后再推进到下一个字符(NextCharacter)。在压缩的结果文件中,此处就会保留一个“OK.”,然后用Distance+Length的方式把SlidingWindow里重复的“OK.”表示出来。这样就避免了“OK.”重复出现,节约了体积。若重复字符若更长,压缩效果会更明显。

       若这段字符出现了细微修改,LZ算法的编码结果会产生巨大的改动。

       考虑上图第二行的情况,若插入了“!”(绿色标记),则编码可能产生两个变化。

       第一个变化就是编码时扫描到右侧“OK.”时,其对应的左侧重复字符的Distance发生了改变(Distance值的变化会产生一系列其他副作用)。

       第二个可能的变化是因为插入了字符,左侧的“OK.”已经超出了滑动窗口的左沿,因此右侧“OK.”无法找到此重复字符(此处当然在KB滑动窗口内,仅作示意)。

       考虑上图第三行的情况,若插入了“!”(橙色标记),则原有的“OK.”重复字符(Literal)不复存在,编码产生了巨大的改动。

       此外,在ZIP压缩算法中还有霍夫曼编码环节,无论是字符(Literal),距离(Distance),还是长度(Length),都不会用其真值表示,而是采用了霍夫曼编码,越常出现的就用越短的字符表示,越不常出现的就用越长的字符表示。另外再保留一张霍夫曼编码后的字符与原字符的对应表格。

       上面第二行和第三行的情况,会影响到Literal、Distance、Length不同值出现的频率,也就影响到了其对应的编码。这个编码的影响是全局而非局部的。

       其他主流压缩算法与ZIP压缩算法原理本质相同,为了追求压缩比,细微的差异也会有全局的影响。

       由此,我们可知:压缩不利差分性质。

       压缩过的文件有可能破坏了新旧RN资源的字节流的相似性,基于压缩过的文件进行BSDiff计算所得的Patch体积存在进一步缩小的可能。

       计算BSDiff基于原始RN资源的字节流是一个值得探讨的方向。

2.4Store文件的BSDiff

       为了解决2.3存在的缺陷(压缩不利差分性质),我们尝试把包含各文件的直接资源以不压缩的方式合并成一个大文件,让原始字节流以其原有的样貌留在文件中,它还可以“解绑”恢复原有的文件及目录结构。通过这个方法,原始的字节流特征就不会被破坏。我们把这样的文件称之为Store文件,即没有压缩功能的Archive文件。

       其主流实现有ZIP的Store模式(也是使用ZIP工具,但是不进行ZIP压缩算法)以及Tar文件格式。ZIPStore模式的资源执行BSDiff算法,(相较于压缩文件)大概%的体积。但是,这一方案也有其缺陷。

2.4.1ZIP的Store模式

       要生成ZIP的Store文件非常简单,所有的ZIP工具都有Store模式,一般来说是生成ZIP包时把入参布尔值store设置为true,解压/解绑时不需要额外提供参数。考虑到使用ZIP的Store模式主要是因为AndroidSDK原生支持ZIP文件的生成和解压/解绑,因此对于包含许多App的Shopee公司来说是一大优势,不仅节约包体积(重要),还减少协调难度。

       精简依赖考量——在客户端,我们尽可能不安装额外的依赖包。

       看似是最优解,但在实践尝试中发现此方案有一个问题:ZIPStore文件在各端并不兼容。

       具体的问题是,在后端Node.js中,运用通用的archiver库所生成的Store文件不被Android原生的ZIP库识别,无法解绑。这一点在网上也有过广泛讨论,见StackOverflow。

       其中提出的解决建议是Android另外装Oracle的ZIP库,这与精简依赖考量相违背,不是优秀的选项。

       那么,在Android客户端运用直接资源去生成ZIPStore文件行不行呢?答案是否定的。因为Android客户端生成的Store文件和服务端生成的不同,而服务端的Patch是基于服务端的Store文件求BSDiff所得。哪怕有一个字节的差异,都无法打补丁成功。所以我们不能让客户端去生成Store文件。哪怕我们去掉了平台所特有的metadata,ZIP本身也是非确定性算法(Non-deterministicAlgorithm),这会让打补丁算法失效。所以:

       ZIP不确定性——跨平台差异性使得ZIP压缩包或ZIPStore包不能由客户端运用直接资源去生成。

2.4.2Tar文件格式

       Tar文件格式在类UNIX系统非常流行,是不执行压缩的Archive-onlyFile(本文续称Store文件)。Tar文件格式不可避免地包含各种平台特有的metadata,在一部分平台的库中没有彻底去除metadata的选项,即便可以,Android和iOS客户端也要为此安装Tar文件的解绑依赖包,这与精简依赖考量相违背。

2.4.3Store文件格式难以避免的缺点

       上文讲解了ZIPStore文件和Tar文件的缺陷,假设我们寻觅到了一个优秀的Store文件格式,它支持跨平台统一的Archive操作,但还是避免不了一个难以接受的缺陷——增大了客户端存储空间的占用。

       相比于ZIP压缩文件,Store文件由于未压缩,体积很大。在某些情况下传输全量Store包的流量消耗也就变大了。如果我们对于全量Store包再进行压缩,则给客户端带来了新的复杂度。那就是资源包具有多种格式,有的要解压两次,有许多异常的可能性,这给客户端带来的负担太沉重。我们需要有另外的方法来差分资源。

3.最佳实践方案:FolderBsdp

       为解决上一章的问题,我们创新性地提出FolderBsdp方案。FolderBsdp是以文件间的Bsdp算法为基础,对有目录层级结构的文件夹进行差分。具体来说,它由FolderBSDiff(求差分)和FolderBSPatch(打补丁)两个算法相结合。

3.1FolderBSDiff的具体算法

       先比较新旧文件夹的目录结构和内含文件,新文件夹相对于旧文件夹所产生的变动包括五种情况:新增目录、删除目录、新增文件、删除文件、修改文件。

       对于新增目录、删除目录、删除文件的情况,记录下对应的操作;对于修改文件,调用BSDiff函数。我们基于直接资源里的每一个文件的内容修改求BSDiff,留下其Patch文件体积足够小,避开了压缩不利差分性质的困境;对于新增文件,则记录下所增文件的相对路径,并拷贝此文件。

       我们把这些操作按照一定顺序(新增目录要在最前,删除目录要在最后)汇总到一个JSON文件里。并且把BsDiff求出的各个Patch文件和新增的文件保存下来,这些文件通过ZIP压缩算法为一个ZIP文件,即为FolderBSDiff的Patch文件。这个文件的体积与Store文件之间求BSDiff类似,也同样(相对于ZIP压缩包之间的BSDiff)节约了约%的体积。这个Patch可以用于对应的打补丁操作(FolderBSPatch)。

优点一:在客户端侧打补丁时内存占用低

       在打补丁操作时,我们需要把旧文件和补丁包加载到内存里,在执行完成前,新文件也在内存里。FolderBSPatch针对逐个资源文件,按顺序串行操作,“化整为零”,相比于ZIP包的BSPatch,内存占用更低,更不影响用户体验。

       根据实际测算,相比于ZIP包的BSPatch,使用FolderBSPatch在客户端侧打补丁的内存峰值占用少了%。

       内存考量——在客户端侧降低内存占用是一个优点。

       在RN资源包更新这个情境中,FolderBSPatch这一优点非常有必要,因为打补丁的过程是在App运行时的后台进行,与此同时用户很可能还在积极使用App。此外,我们预期Shopee主要市场的用户手机内存配置较低。

优点二:节约存储空间

       在客户端侧,基于FolderBsdp可以舍弃掉储备文件,无论是ZIP压缩资源包,还是上文讨论的Store资源包。

       在之前的算法中,因为ZIP不确定性,我们留存ZIP压缩包或Store文件在客户端。文件留存的唯一目的就是为了打补丁,没有其他用处,非常浪费存储空间。

       从时间线上来看,根据FolderBsdp也形成了客户端侧的“RN更新链”,如下图所示:

       以Shopee内某个App为例,客户端总包体积MB,其中RN的ZIP压缩包约8MB,使用FolderBsdp后,不再需要ZIP包,App也就“瘦身”了。

       空间考量——节约掉储备文件所占的体积是一个重要的优点,我们要尽量做到。

优点三:不需要ZIP库

       在客户端,我们不再需要调用ZIP的解压算法,iOS端也就不再需要依赖ZIP库或其他压缩库(Android内置无法删除)。虽然我们需要引入FolderBSPatch,但其代码不过一百行左右,没有大型依赖,体积远小于ZIP库,符合精简依赖考量。

优点四:附带更精确的MD5校验

       我们可以把每一个新增、删除、修改文件的MD5值保存在JSON文件中,在打补丁操作时进行校验,更自信地确认我们的操作是正确无误的。

4.FolderBsdp与业界其他方案的对比4.1Google的archive-patcher

       Google考虑到了ZIP压缩文件之间求BSDiff破坏了原始字节流相似性的缺陷(压缩不利差分性质),产生了逐个文件恢复出其原始字节流并且求差异的方案,很好地解决了这个问题。

       另一方面,因为跨平台的差异性(ZIP不确定性),此方案不可避免地需要在客户端保留ZIP压缩资源包。

       这与我们追求尽可能节约存储空间的目标背道而驰(空间考量),因此不应该被采用。而本文提出的FolderBsdp方案很好地克服了archive-patcher的这一缺点。

4.2其他方案

       增量更新领域中有另一知名的开源项目,作者使用C++自行编写算法,将目录下各文件以不压缩的状态合并起来进行差分,即本文Store文件思路。FolderBsdp与此方案对比的优点是打补丁时更加节约内存(内存考量)。本文所述的RN资源更新,是在用户活跃使用App时后台静默更新,也考虑到Shopee主要用户群体手机内存配置较低的特点,FolderBsdp算法这一优点很重要。

       此外,有方案对差分包的压缩格式进行扩展,使其不局限于ZIP格式,也可以支持其他压缩库。这个灵活性在某些场景适用。但在本文情境中,App的精简依赖考量更加重要。本文提出的FolderBsdp算法利用标准bsdp和AndroidSDK自带的ZIP库,不需要安装其他格式的压缩库。FolderBsdp的补丁包体积与其他压缩格式差异不大。

5.总结

       本文所介绍的FolderBsdp方案降低了打补丁时的内存峰值,避免了在客户端保留多余的ZIP包占用空间,额外提供了逐个文件的MD5精准校验,无需新增依赖库,因为用各端原生开发语言实现而具有兼容性。兼具各种优点,FolderBsdp成功实现了增量更新功能,是多番探索之下总结出来的RN资源增量更新的最佳实践。

       本文作者

       Pei,来自Shopee商家服务前端团队。

原文:/post/