知乎移动端动态化探索与实践!
BATcoder技术群,让一部分人先进大厂
大家好,我是刘望舒,腾讯TVP,著有三本业内知名畅销书,连续四年蝉联电子工业出版社年度优秀作者,谷歌开发者社区特邀讲师,百度百科收录的高级技术专家,CSDN 2018年度博客之星。
前华为架构师,现大厂技术总监。
想要加入 BATcoder技术群,公号回复BAT
即可。
作者: 于天航
https://zhuanlan.zhihu.com/p/64968076
移动端需求的用户触达效率低,新的产品样式上线不够快。想要快速上线新样式有两个思路:要么缩短现有客户端发版周期;要么通过云端下发的方式实现样式的动态更新。知乎 App 在发版周期上已经做了大量尝试,但由于 App 端版本发布方式的天然限制,在某些业务场景下无法很好的满足要求;
广告样式实验粒度粗放,实验相关代码管理复杂。这一痛点可以看作是痛点一的延续。AB 实验是产品效果提升的重要手段,为了支持实验,要在 App 发版时预埋多套代码,进行复杂的实验开关及样式代码管理,同时还要考虑实验下线逻辑。如果实验粒度很细,逻辑维护成本会指数增加;
广告落地页(Landing Page)的性能和转化效果亟待提升。非原生广告落地页多为非知乎域的 H5 落地页,App 端对其控制能力极其有限,在对 H5 类型落地页本身进行优化的同时,也希望探索新的方向。(第三方 H5 落地页的打开速度和数据采集,可以采用「CDN 加速」、「XPath」等方案优化,这两个不是本文的重点,在这里就不展开讨论了。)
基于此商业移动端团队开始着手搭建移动端动态化基础能力,并一步步实现了 App 内各个位置广告卡片的动态化开发和上线,我们给这个方案起了个比较「动态」的名字叫 Morph。
下面从「技术选型」、「架构设计」、「动态化基础能力建设」三个方面介绍一下此次的技术方案改造升级。
1. 进行了哪些技术选型?
在方案设计之初,我们就确立了实现商业广告全面动态化的技术愿景,覆盖从信息流广告卡片到广告主落地页的所有功能场景。近些年,移动端动态化方案可谓百花齐放,如何选择一个适合知乎商业化业务场景的动态化方案成为我们第一个要解决的问题。
1.1 为什么是 DSL Native+?
从「支持动态化的程度」、「与原生体验的差异」、「方案集成与功能开发成本的高低」三个维度出发,将市面上的移动端动态化方案分成三个方向:
新的动态化方案需要满足「跨平台」、「热更新」两个基本诉求,因此没有考虑 Android 插件化这个大类下的方案。
Web 增强(Web+) : 主要基于 WebView 实现,能够进行快速的迭代,在知乎已经有了对应的解决方案;GPL 和 DSL 可以看作「Native 增强(Native+)」方向的子方向,Native+ 方向基于 App 内自带的语言解析器,独立于 WebView 实现动态化逻辑的解析、布局和渲染。
基于 GPL 的 Native 增强(GPL) : 这个方向是移动端动态化的热门方向,React Native、Flutter、NativeScript 等等,实现原理各不相同,共同的特点是利用了通用编程语言器,可以是系统提供的(如 Javascript),或者是集成到 App 的(如 Dart),再辅以某个布局系统和渲染方案,由于语言是图灵完备的,可以提供完美的动态化解决方案;
基于 DSL 的 Native 增强(DSL): GPL 方案虽然美好但始终需要在动态化能力、渲染性能、Framework 体积、学习和开发成本等方面取舍,难以达到完美。于是产生了基于领域专用语言的解决方案,此类方案通过采用适合专业场景的的 DSL 解析云端下发的逻辑结构,舍弃一部分动态化灵活性,换取其他方面的优势。
动态化方案选择要考量的因素很多,如渲染效率、动态化灵活程度与用户体验的取舍、方案实现成本等等。其中重点强调一下「方案实现成本」,这个因素应该基于开发团队的现状进行考量,可以包括开发成本、学习成本和人才培养成本,开发成本很好理解,即该方案从调研到落地的工作成本;学习成本和人才培养成本往往是我们容易忽略的,基于团队已经掌握的技术栈,不同的方案需要的需要掌握的知识有所不同,学习成本也不同。而人才培养成本强调的是市场上是否有对应的专业人才,不是很熟悉或者没有亲身实践过这个技术方案的人多久能够熟练掌握这项技能?比如 RN 为代表的基于 Javascript 的 GPL 方案通常需要前端开发技能和移动端开发技能的配合使用,对人力培养成本比 Native+ 和 Web + 的成本要高。
讨论了这么多,我们应该选哪个方向呢?针对公司现有的业务场景、工程现状,我们梳理出需要优先考量的原则:
接近 Native 的高性能: 广告动态化的核心要求。相比于 Web 的灵活性,接近 Native 性能需求更强烈。滑动流畅是信息流产品的基础需求,而对于落地页,打开速度和使用体验也是重中之重; 项目能够快速启动: 高速发展的业务诉求。项目需要能够快速启动,快速上线,本文所讲的动态化方案仅每端投入一人月就完成了第一个版本的上线; 低人力成本 : 在业务快速迭代的过程中,支撑业务开发的同时,技术方案上的投入资源有限,前后端都很难有充足的人力投入,新方案应该占用尽可能少的人力,并且对现有工程的改造降至最低; App 包体积影响小: 知乎 App 重视用户体验,新技术对 App 性能、包体积不能产生负面影响。包体积是影响应用增长的重要因素,App Store 应用如果超过 150M,则无法在 4G 环境下下载,将会极大的影响 App 的增长,所以新的方案需要尽可能少的影响包体积; 平滑迁移: 基于技术架构现状。商业广告卡片是基于信息流的,不能因为广告动态化而要求整个信息流重构,所以需要保证新的方案能够和现有信息流方案完全兼容; 持续演进: 快速启动后的发展方案。整体框架需要支持高扩展性,可以在上线后进行持续的需求迭代。
「Web+」方向在性能和稳定性上略差,而且使用场景有限,不适合在信息流这种流畅性要求高的场景中使用;「GPL」方向提供一套完全不同于 Native 的开发模式,可以在其基础上实现动态化功能,且能够保证较好的性能,但是目前的解决方案接入成本和学习成本都比较高,并且与复杂业务场景结合时均有难以解决的问题,不适合在已有业务中接入。
综上考虑,最终我们决定采用基于 Flexbox 的 DSL Native+ 动态化方案即 Morph。Morph 方案在业务上具有如下优势:
和现有技术栈完美融合。知乎 App 信息流基于 ComponentKit 实现的,几乎几乎不会产生额外的学习成本; 快速上线,后期维护成本较低:整体方案的上手难度低,不需要学习新的技术,能够快速进行功能迭代开发; 对包体积影响小:整个方案只是增加了 Google-FlexboxLayout、Yoga 两个轻量级开源布局框架,对体积影响可以忽略不计; 这个方案还带来了一个额外的好处:支持现有实验系统,很好的满足样式实验、数据采集的需求。
1.2 为什么是 Flexbox?
动态布局方案基于 Flexbox,带来的好处是:
Flexbox 为盒状模型提供最大的灵活性,是目前布局系统的首选; 跨平台方案,双端统一; 和知乎 App 现有技术栈匹配度高,开源环境中有优秀开源库的本地化。 iOS 端 Facebook 开源的 Yoga 已经经过 10+ release 迭代,有 1.1w+ 的 Star,(ComponentKit 底层也是依赖于 Yoga),在生产环境经过长时间验证; Android 端的 Yoga 库有一些难以解决的问题,最终选择 Google 的 FlexboxLayout 框架来解析,该框架也非常成熟,在和 iOS Yoga 配合中仅需要很少的双端适配。
1.3 为什么是 JSON?
布局描述语言上,由于各种描述语言的描述能力差异不大,在这种情况下,开发成本成为我们选型描述语言的重点。
开发成本考虑的是基于知乎 App 现有客户端和后端的交互结构,目前前后端数据传输采用的是 JSON 格式,如果要换成其他格式的数据,客户端和后端都需要进行额外的工作处理,最终我们选择了 JSON 格式来描述布局样式。
2. Morph 动态化方案架构如何设计?
我们将一个使用 Morph DSL 系统,以 JSON 格式组织的描述一个视图树结构的配置称为样式。实现动态化的核心业务流程是实现布局文件云端下发和解析:编写样式文件 -> 上传到服务端 -> 云端下发样式到 App 端 -> App 端收到广告数据后解析样式文件展示广告。
商业广告业务数据存放在业务后端,和 App 端直接交互的是引擎后端,所以整个系统中涉及到三个端的调整。业务端相当于商业广告数据的生产者,业务端分业务前端和业务后端,业务前端提供面向广告主投放广告的平台(投放平台、建站工具),业务后端负责存储和处理广告相关的数据(广告模版、广告订单、排期等)。广告引擎则是从业务端获取广告数据进行过滤和筛选,最后选取符合规则的广告下发给 App 端,同时引擎端实验平台负责对 App 端流量进行分流。
结合现有业务场景,Morph 系统结构设计如下图所示:
广告请求接口 :以信息流为例,用户刷新信息流展现广告卡片,引擎端从业务后端广告数据库中读取广告数据下发给 App 端,广告数据中包含了对应广告卡片的样式名称。 样式下发接口 : 样式文件较多,因此与数据接口分开,通过独立的样式服务接口进行下发。
随着产品的演进,动态化样式数量持续增加,为保证研发效率的规避风险,上线了「样式管理平台」,提供可视化界面进行样式的增删改查等工作。
Morph 的业务数据流向如下图所示:
针对产品提出样式需求,由 App 端和业务端协商出新的广告模版和样式映射关系,客户端通过样式管理平台提交新的样式,存储到云端样式数据库,业务端将模版和样式名称映射存入广告数据库;
App 启动时,访问样式服务接口,样式服务根据请求 Header 中的:App 平台、App 版本,以及 App 端现有基础样式及版本信息集合,增量下发广告样式数据。App 接收到新的广告样式数据后,会依次进行:Hash 校验、JSON 格式校验、实验信息的校验,校验通过后将接收到新的样式文件更新到本地样式数据库,同时进行对旧版本样式数据进行清理删除;
App 端信息流刷新,向引擎请求带有样式名称及实验信息的广告数据,App 端根据数据中的样式名称选择对应的样式文件生成布局,展示广告卡片。
3. 动态化能力建设中最核心的4个部分
这个部分和大家探讨 Morph 动态化基础能力搭建的核心思路。
上图展示了动态化能力建设中的4个核心部分,下文将逐个阐述。
3.1 DSL 定义
基于 DSL Native+ 的动态化方案设计首先要解决的一个问题是 DSL 的选择,Morph DSL 包含三个核心设计思路:
基于前端 Flexbox 布局系统进行视图布局; 使用 JSON 数据交互语言描述视图结构; JSON 节点使用属性「type」区分视图控件类型。
下图是 Morph DSL 属性集中的一个子集:
属性集包含六种属性:布局属性,视图属性,数据属性,交互属性,条件属性,容器属性。
1. 布局属性
决定视图树结构,规划视图布局位置的相关属性,如上图中的FlexStyle,FlexStyle 中各属性命名及含义,与 Flexbox 定义一一对应。得益于 Flexbox 布局系统简洁、强大的弹性布局能力,Morph DSL 几乎可以实现任何形式的布局;
2. 视图属性
与视图 UI 样式相关的属性,如背景色、透明度、圆角等,如 ViewStyle。基于 Flexbox 带来的另一个好处是解决了屏幕适配的问题,而在尺寸换算问题上,我们规定:Morph DSL 布局中,使用 2 倍图下的尺寸,单位为 px,Android 和 iOS 端解析具体的数值时,将分别换算成本地的、与设备无关的 dp 或 pt 来使用;
3. 数据属性
表达节点与数据绑定关系的属性,在不同的视图控件中有所区别,具体细节将在下文「数据绑定」中阐述;
4. 交互属性
设置视图节点的点击等事件的属性,主要由 ViewAction 指定。ViewAction 描述一个视图节点的交互类型,结构如下:
action 指明该交互要完成的动作,extra 携带交互需要的信息和数据。上图展示的布局片段,描述了一个 CLICK 事件,触发后,将从 extra 中取出 URL 信息,并进行跳转。目前 Morph 已定义的通用 action 类型及规则举例:
5. 条件属性
Morph 定义了一套 condition 机制,支持在样式文件中根据业务逻辑做条件判断,如互动数据的展示等。condition 定义如下:
目前 condition 支持主要的逻辑运算:or(逻辑或)、and(逻辑与)、not(逻辑非)、notEmpty(非空)、empty(空)、valueOf(取 bool 值)。
以下是一个使用了 condition 的一个文本节点示例,其中,visibility 决定了该控件最终是否会显示,其类型就是一个 condition 类型:
6. 容器属性:在 Morph DSL 中规定,样式的根节点必须是一个 container,以下是一个根节点示例:
container 对应于 App 端支持了 Flexbox 布局系统的容器控件。container 使用属性 children 来描述容器内嵌套的子视图,children 是一个数组,可以包含任意多个控件节点。通过这种方式,Morph DSL 可以描述任意结构的视图树。
下例中,使用了 height 属性来指定控件的高度。Morph 设计之初,我们将控件的宽高定义为一个复合属性:
在后期技术演进过程中,我们使用了更简洁的字符串类型的 layoutHeight、layoutWidth 来描述高和宽:
3.2 动态视图管理
我们把前文提过的样式文件从开发到写入 App 端本地样式数据库的完整流程画成时序图:
基于自定义 DSL 进行编写样式; 样式文件经由样式管理平台存入云端样式数据库: App 端请求样式服务接口; 样式服务接口收到请求后,根据一定的策略进行样式下发; App 端将接收到的样式进行校验后,写入本地样式数据库,以备后续使用。
云端/本地样式数据库的设计如下:
样式下发接口的参数设计如下:
值得注意的是:
App 端请求样式下发时,会带上当前 App 端已支持的样式信息,样式服务据此判断进行增量而非全量的样式下发,节省传输成本; 某样式单个平台版本号为空时,不向该平台下发该样式; 某样式支持的最低 App 端版本大于当前 App 端版本时,不下发该样式。
3.3 构建视图
客户端为每一个 UI 组件创建专门的解析器。对于每一种样式,使用单独的样式名称 style 来标识,样式文件中引用的每种组件会一一对应到字段 type
表明该样式所使用的控件类型。因此当 App 端获取到样式文件后,通过样式中每个元素的 type
,由解析器决定使用哪种原生控件进行承载。
解析器主要承担 2 项职责:
根据 JSON 节点的属性配置创建一个本地对应的控件,解析控件绑定的具体数据并设置到控件上; 通过每个节点中的 type 字段,将每一个节点交由对应的组件解析器去解析,解析的结果是输出一个对应的 UI 控件。
由于所有节点已按照树形结构存储,最终输出所有 UI 控件时,按照树形结构进行组织,就可以生成最终的目标视图卡片。
以 Android 端为例,简要阐述视图解析器实现的技术要点:
每一类型的视图控件,都在 App 端有唯一对应的 ViewModel(样式文件的布局属性 Model 化的产物)和 ViewParser(视图解析器,利用 ViewModel 创建本地View并设置属性) ViewModel 及 ViewParser 的注册,由自定义注解 @ViewModel("type") 及 @ViewParser("type") 完成,在编译期将通过插件自动收集并插入注册代码; 构建视图时,遍历 model 树,通过 model 的 type 字段,获取已注册的 ViewParser,反射构造一个解析器实例,传入 ViewModel,完成单个视图控件的创建; 由于使用递归遍历,某个节点的视图控件构建完成后,其父节点必定已构建完成,因此直接将当前视图控件,加入父控件即可。
3.4 绑定数据
样式文件的主要功能是设置视图显示约束,其中包含了与视图显示属性相关的设置,以及对应显示的内容数据字段。因此在样式解析后,还需要进行显示数据的绑定。下图是一个图片控件节点的布局示例。
「url」为该控件的一个数据属性,表示该图片控件要显示的图片链接的地址,其值 指向了一个 JSON 数据体中的一个具体节点:
我们规定指向数据体节点的引用使用 作为结束标识,数组直接使用数字下标指定其具体位置。如上面的引用 中,ads 指数据体中的一个数组节点,而0标识取数组的第一个元素,creative 表示一个 JSONObject 节点,image 表示 creative 节点下的一个具体字段值。
以上文的图片控件为例,数据绑定过程如下:
调用数据绑定接口,将数据体传入接口; 遍历已构建好的视图树,遍历每一个节点时,将从该节点控件中取出对应的 ViewModel 对象(视图构建时,会作为 tag 与控件绑定),再获取对应的控件解析器;3.调用解析器,解析器中将使用数据引用和数据体,共同完成解析,拿到最终的数据(如图片链接 url); 解析器按照控件自身具体的规则,完成数据设置。
4. 写在最后
Morph 移动端动态化技术方案自上线以来,经过多轮迭代,支持了大量控件并覆盖了多数业务场景。
虽然项目尚未开源,且文章中的一些技术可能时至今日已经有了新的方案(文章完成于 2 年前),但是文中分享的关于技术选型的考量以及具体实现思路,对大家仍然具有参考意义。
推荐阅读
• 耗时2年,Android进阶三部曲第三部《Android进阶指北》出版!
为了防止失联,欢迎关注我的小号
微信改了推送机制,真爱请星标本公号👇