为什么会有纯Native的动态化计划
业内许多的动态化计划都是经过JS虚拟机来完成的,好处有许多,逻辑能够完成动态化,有现成的JavaScriptCore(iOS)或者V8(Android)来做动态化引擎,能够覆盖90%的场景诉求
可是对于中心页面,比方首页Feeds,小黄车,下单,商详这类页面,经过这类动态化计划就会存在稳定性和功能问题(究竟JS作为解释性语言以及单线程存在天然瓶颈,依据寄存器的指令集,导致内存耗费更多,异步回调也是主线程派发到作业线程处理后的音讯通知机制完成,再加上bridge底层也是经过调用Native的办法来完成,还有做JS和Native的类型转化)
我用ReactNative官方demo做了些改动,机型iPhoneX,运用FlatList(RN的高功能list组件)快速滑动下帧率表现如下,快速滑动的时分最低帧率在52帧
做了一个类似的Native列表,滑动表现如下,最低帧率58帧
布局是两个label加一个imageView,一起cell依据数据来展现不同高度来模拟不定高的状况,属于十分典型的UI结构比较简单的场景。这次状况下Native和RN的功能差异也会比较显着,所以在cell结构比较杂乱的状况下差异肯定会愈加显着了
比照完业界通用计划后,作为ReactNative场景的补充,页面有动态化需求,且对逻辑的动态性要求没有那么高,烘托功能好的Native动态化计划也就有业务价值了
高功能的Native动态化计划一般是经过约定好的二进制文件格局,运用定制的解码器在app内将二进制文件转化成OriginTree,然后流水线生成视图树终究烘托出一个Native的View。
比照下自界说二进制以及通用文件格局的好坏
才能比照 | 通用文件比方JSON、XML | 自界说二进制文件 |
---|---|---|
通用性 | 是 | 否 |
文件巨细(以弹窗为例) | 17KB | 2KB |
解析同一文件iOS耗时比例 | 6 | 1 |
安全性 | 差 | 比较好,不知道解析规则的状况下无法获取对应内容 |
需求额定开发环境 | 不必 | 需求前端建立编写环境、服务端,客户端定制编解码器 |
拓展性 | 差 | 高 |
比照以上好坏点,大型APP在资源充足的状况下往往更重视功能、安全性以及后续扩展性方面。
接下来我会大致聊聊端上相关的开发思路。
拟定文件格局
咱们能够参阅zhuanlan.zhihu.com/p/20693043 进行二进制文件格局规划
客户端能够运用JSON来描绘UI:
//ShopBannerComponent
{
"componentName": "ViewComponent",
"width": "375",
"height": "70",
"backgroundColor": "#fff",
"onClick": "customClick(mdnGetData(data.jumpUrl))",
"children": [
{
"componentName": "ListComponent",
"width": "100%",
"height": "50",
"listData": "mdnGetData(data.list)",
"orientation": "horizontal",
"children": [
{
"componentName": "TextComponent",
"width": "mdnGetSubData(item.width)",
"height": "mdnGetSubData(item.height)",
"maxLines": "1",
"textSize": "15",
"textColor": "#fff",
"text": "mdnGetSubData(item.content)"
}
]
},
{
"componentName": "ImageComponent",
"width": "100%",
"height": "20",
"contentMode": "aspectFill",
"imageUrl": "mdnGetData(data.backgroudPic)"
},
{
"componentName": "TextComponent",
"width": "44",
"height": "15",
"maxLines": "1",
"textSize": "15",
"textColor": "#fff",
"text": "mdnGetData(data.desc)"
}
]
}
经过和后端协商定制协议后,生成的二进制文件如下:
Header(固定巨细区域)
- 标志符:也叫MagicNumber,判别是否是指定文件格局
- MainVersion:用来判别二进制文件编译的版别号,和本地解码器版别做比照,当二进制版别号大于本地时,判别文件不可用,最大值1btye,也便是版别号不能大于127
- SubVersion:当新增feature的时分需求晋级,本地解码器依据版别做逻辑判别,最大值不能大于short的最大值32767
大的版别迭代比方1.0晋级到2.0,规定必须是依据中心逻辑的晋级,整个二进制文件结构可能会从头规划,这时分经过主版别号比对,假如版别号小于文件版别号,那么就直接不读取,回来为空。小的迭代比方二进制文件内新增了某个小feature,在对应SDK内部逻辑增加一个版别判别,大于指定版别就读取对应区域,运用新的feature,老版别还是能够正常运用基本功能,做到向上兼容。
- ExtraData:预留空间,用于后续扩展,能够包含文件巨细,checksum等内容,用来查验文件是否被篡改
Body
FileNameLength用于读取文件名长度,然后依据FileNameLength读取详细文件名,比方FileNameLength为19,往后读取19byte长度数据,UTF8Decode成对应文件名ShopBannerComponent
读取流程
大致流程图
参阅Flutter的烘托管线机制,设置如下流程图
整个烘托流程都是在一个流水线内履行,能够确保从恣意节点开端到恣意节点完毕
日常运用场景比方:咱们在TableView里要尽快的回来Cell的高度,这时分流水线履行到MDNRenderStageCalculateFrame即可,一起会依照indexPath进行索引值Cache,后续需求回来cell的时分,取到对应indexPath的Component,后续再履行MDNRenderStageFlatten以及后边逻辑,确保每个component的各个节点只会履行一次,大致流程如下
流水线履行一直围绕在Component,只不过每道工序都会让Component更挨近NativeView
就和轿车工厂里一样,最开端只有一个车架,后边经过依照引擎、零部件、喷漆等等工序终究组装成咱们能够驾驶的轿车
组件解析
将本地二进制文件转化原始视图树,这个阶段不会绑定动态数据,经过大局缓存一份, 后续以Copy的形式生成对应副本,能够有效的进步功能以及下降内存,然后在副本进行数据绑定以及生成IntermediateTree
- OriginObjectTree:直接经过二进制数据解析出来的树,大局只有一个,类似于Flutter的
WidgetTree
- IntermediateTree:经过
OriginObjectTree
克隆后,将数据填充进去核算布局后,然后经过层级剪枝的树,将没有点击事情以及无特殊UI作用的Node进行合并,目的是为了下降烘托树生成实在view的视图层级,削减View实例,防止了创立无用view 目标的资源耗费,CPU生成更少的bitmap,顺带下降了内存占用,GPU 防止了多张 texture 合成和烘托的耗费,下降Vsync期间的耗时 - RenderTree:和IntermediateTree一一对应,递归生成原生View
和ReactNative类似,所有的组件都继承自基类,基类提供一些生命周期办法让子类重写
@interface MDNBaseComponent : NSObject {
//子类重写测量办法
- (void)onMeasureSizeWidth:(MDNMeasureValue)widthValue height:(MDNMeasureValue)heightValue;
//子类重写布局办法
- (void)onLayout:(CGRect)rect;
//子类重写烘托对应的NativeView办法
- (void)onRender:(UIView *)view;
//子类重写事情相关办法
- ((BOOL)onEvent:(MDNEvent *)event;
//子类被加载的办法
- (void)componentDidLoad;
//子类被卸载的办法
- (void)componentDidUnload;
- 字符串存储区域存的是对应的常量、枚举、事情、办法、表达式,比方代码中宽度375 ,枚举值
aspectFill
,表达式mdnGetData(data.backgroudPic)
,这些值都会有对应的key,用于组件解析的时分进行绑定对应特点
{
"componentName": "ImageComponent",
"width": "100%",
"height": "20",
"contentMode": "aspectFill",
"imageUrl": "mdnGetData(data.backgroudPic)"
}
- 表达式区域存储的是全部用到的表达式字段,每个表达式都有对应的key,与
component
的特点进行关联,因为表达式能够相互嵌套,因而咱们能够考虑设置成树型结构。startToken
以及endToken
代表表达式的开端和完毕,经过遍历将表达式exprNode
入栈,一起将入栈的exprNode
增加到之前栈顶的exprNode
中children
,形成一个单节点树,便利表达式组合运用 - 组件区域是依照DSL代码次序,从上往下遍历,因为Component也是能够相互嵌套,也是树形结构,经过
startToken
以及endToken
代表一个component
的开端和完毕,客户端层面也是依照区域次序读取,遇到startToken
创立一个component
,期间会绑定特点、事情、办法,以及动态表达式,然后入栈,遇到endToken
出栈,一起设置栈顶的Component
为父组件,终究得到一个Component
的OriginTree
组件动态绑定
当ViewComponent需求进行动态绑定,将表达式进行遍历扫描,以customClick(mdnGetData(data.jumpUrl))
为例,在二进制文件中,会经过对应的key解析成事情表达式Node,然后mdnGetData(data.jumpUrl)
在二进制文件中,解析成办法表达式Node,终究在办法表达式里data.jumpUrl会进行以下操作,大致流程如下:
这个解析流程参阅了SQL的解析原理
留意:合法判别里面有许多状况切换的状况需求考虑,比方如何从上个扫描的字符串到当时扫描字符串的状况切换是合法的
- 前一个是a-z,A-Z相关的字母,那么后边的扫描结果也只能是a-z,A-Z、[、.,假如扫描到了],便是不合法的
- 前一个是[,那么后边的扫描只能是0-9
- 前一个是0-9,后边则只能是0-9、]
因为一个组件内肯定有大量的表达式逻辑,进行上千甚至上万次遍历是很正常的状况,这种状况判别积累的功能损耗也是很大的,因而这种状况判别逻辑最好是经过矩阵来做from到to的处理,达到优化功能的作用,经测验,随机状况履行一万次,履行时刻缩短了20%
组件宽高核算&布局
绑定好终究的特点后,就能够核算组件以及子组件的宽高了,以最简单的固定宽高的父容器为例,父容器遍历子视图传递自身的束缚条件,比方父容器的最大宽高,子容器依据父容器的束缚来核算自身的size,然后依据DFS算法进行束缚递归终究确定各个子视图的布局
拿图一的布局来做演示
核算完所有Component的布局后,就需求将无用的层级Component进行剪枝,防止烘托树层级过高,优化杂乱视图结构的功能
组件烘托
当咱们拿到完整的扁平树后,就能够递归生成对应Native的View了,烘托前咱们需求进行diff,尽可能削减UIView的创立和销毁,有助于提高功能,尤其是在低端机且视图结构杂乱的组件上,复用能下降大量的烘托时刻
一起因为安卓iOS对View的操作必须在主线程,因而假如提早创立View,并对数据或者布局进行修改,会触发许多无用transcation提交,因而将数据以及frame算好后,终究只设置一次能确保功能最优
diff算法能够参阅flutter的diff,经过O(n)遍历,决定每个子节点是否能被复用
diff完毕后,便是将Component对应的frame,以及事情绑定到对应的view上,比方
ViewComponent对应MDNView
ListComponent对应MDNCollectionView
ImageComponent对应MDNImageView
TextComponent对应MDNLabelView
终究咱们就得到了一个纯端上逻辑支持点击手势的动态化View啦\
hi, 我是快手电商的heyang
快手电商无线技能团队正在招贤纳士! 咱们是公司的中心业务线, 这儿云集了各路高手, 也充满了机会与应战. 伴随着业务的高速发展, 团队也在快速扩张. 欢迎各位高手加入咱们, 一起发明世界级的电商产品~
热招岗位: Android/iOS 高档开发, Android/iOS 专家, Java 架构师, 产品司理(电商布景), 测验开发… 大量 HC 等你来呦~
内部推荐请发简历至 >>>我的邮箱: heyang06@kuaishou.com <<<, 补白诨名成功率更高哦~
参阅文档:
ParseSQLToken
FlutterInside
动态界面:DSL & 布局引擎