发布时间:2023-03-24 17:30
DX全称DinamicX,目前是在淘宝乃至整个阿里集团内广泛使用的Native动态化方案,核心优势是性能和稳定性。过去几年一直有其他淘宝/集团的外部文章中有涉及到DX,但DX一直没有对外做过完整介绍,对外界来说这两个字母颇有些神秘色彩。本系列文章《DX研发模式》我们就将拉下它神秘的面纱,看看过去两年 DX 在做什么。
本文主要阐述 DX 在追求极致的性能体验过程中,所突破的性能瓶颈与实践经验。
第一篇:《淘宝Native研发模式的演进与思考 | DX研发模式》
第二篇:《列表容器&事件链如何帮业务提升发版迭代效率? | DX研发模式》
本文:《如何持续突破性能表现 | DX研发模式》
第四篇:《从0到1,IDE如何提升端侧研发效率 | DX研发模式》
前言
DX 作为一种逻辑和视图分离的跨平台动态化方案,是集团内高性能动态化框架的代表,其性能是最接近 Native 的。根据业务的侧重点不同,技术选型也不同,没有绝对的最优解。在追求高性能极致体验,高稳定性及能够提供部分逻辑动态化的场景下,DX 为当前的第一选择。
现状
DX 为了追求极致的性能体验:
在服务端编译期就将 XML 文件解析成二进制,将生成抽象语法树的过程前置到编译期执行,端上执行时,仅需要拿到二进制文件进行属性解析,对于静态值类型,在编译期就直接转换成了对应的数据类型,减少了端上的拆装箱开销;
代码均使用平台原生语言实现,避免了各种跨语言通信成本;
DX 通过使用轻量、不可变的虚拟树节点进行测量、布局等前置操作,只在最后 render 阶段才会操作平台视图。在渲染 view 之前会再次进行视图 diff 操作,确保尽可能复用 view,只生成必要的 view;
DX在过去的一年通过事件链能力扩展了部分逻辑动态化能力,但这些能力属于可插拔能力,并不参与 DX 视图渲染阶段,以上这些都是 DX 持续保持高性能的基础;
通过对 DX 的管线分析,我们发现 DX 的性能还有进一步提升空间,比如
单线程的管线设计,在较为复杂的模板场景下,DX的虚拟树操作阶段耗时不可忽略;
依托平台本身的绘制能力限制,在大量文本、图片等场景下,系统实现均在主线程进行绘制,在手淘这种大量图文的场景下,平台本身的绘制性能限制也不可忽略;
由于部分业务高频组件缺乏内置通用实现,在庞大的业务方自定义组件生态中,性能、稳定性、通用性均无法得到保证。
基于上述分析流程,当前 DX 面临的性能瓶颈主要分为两个部分:虚拟节点树和视图渲染。
针对渲染管线的虚拟节点操作阶段,我们提供了管线异步化能力。
针对渲染阶段,我们提供了异步绘制框架、通用富文本组件及部分属性能力优化。
针对整体 CPU 计算资源占用,我们提供了离屏资源管控框架
管线优化
由于 DX 的服务端预编译及虚拟节点轻量等特征,一般场景下,虚拟节点的主线程耗时占比并不高,但在业务模板较为复杂时,比如含有大量表达式和复杂嵌套层级场景下,虚拟节点操作耗时占比可能超过 40%,在由 DX 卡片搭建的全页面场景下,虚拟节点的主线程耗时占比甚至超过 10%,所以虚拟节点耗时亟需优化。
在 DX 中,开发者在 XML 中写的组件到端上都会先解析为“三棵树”后再交由真正的平台 view 进行渲染。分别为:原型树、展开树、拍平树。渲染管线会在虚拟节点树上分别将二进制解析、表达式解析、测量、布局、拍平等步骤进行操作,步骤之间可拆分重组,这也就为管线异步化打下了基础。
原型树:从二进制下发后到端上的原始树结构,主要包含静态树节点赋值及单位换算;
展开树:根据初始的原型树进行数据进一步解析,包含动态属性的解析、layout 子节点的转换、节点的测量及布局等操作;
拍平树:根据布局好的展开树进行再进一步的优化,主要包含无用布局节点的拍平以及整体层级的拍平,拍平树是真正需要交由平台 view 进行渲染的最终状态。
而从前面的流程分析我们可以了解到,DX 的虚拟节点并不操作实际平台 view,那么是否可以充分利用 CPU 的多核能力进行多线程管线调度,将非必要占用主线程的工作都异步化执行?基于上述分析,我们设计了 DX 的管线异步化能力。
管线异步化整体思路简化流程如图:将虚拟节点上的加载、表达式解析、测量、布局等操作均借助多核 CPU 的能力异步执行,仅有最后的拍平和渲染操作在主线程进行。
我们通过改造 DX 内部的渲染管线,提供了多种可扩展点,组件可将平台渲染无关的耗时操作在 onPrefetch 异步接口中提前执行,待到 render 阶段时仅需执行 UI 相关流程即可。整体流程图如下:
在 DX 内部,借助管线异步化能力,将图片组件中与 UI 无关的操作 URLParser 步骤提前到 onPrefetch 回调中提前进行,整体减少图片库 30% 以上时间占比,在订阅等图片较多的业务中提升显著。
由于 DX 丰富的生态中含有庞大的业务方自定义组件,所以将该能力作为 DX 基础扩展接口,可由业务方在自定义组件中进行预加载时机的自定义操作处理,为自定义组件提供了一种优化策略。
上述主要介绍了管线异步化的核心思路,针对外部容器和 DX 自建容器也提供了不同的接入策略:
DX 的外部业务接入方,通常都是将 DX 作为一张卡片,嵌入各种不同的自建容器中,针对这种场景,DX 提供了诸多不同的异步化接入方式:单个/批量预加载能力,同步/异步预加载能力,异步渲染接口等,可以方便业务方根据自己的业务需求和业务场景进行选择性接入。而在 DX 内部,为了避免和解决多线程滥用问题导致的线程频繁切换开销和线程爆炸等问题,我们设计了小型多线程管控队列,管线异步化也借助该队列,得以根据当前 CPU 核数等动态调整最大并发数;
针对外部容器虽然 DX 已提供了多种可选的异步化接入接口,但对于业务方来说还是有一定接入成本的。而针对 DX 内部自建的功能强大的 RecycleLayout 容器,DX 提供了内置的管线异步化能力,得以使用 RecycleLayout 的业务方可以通过 prefetch 属性设置,无成本的接入管线异步化。
渲染优化
利用管线异步化方案,将虚拟节点上的操作异步执行,大大减少了主线程压力。但在不同业务场景下,虚拟节点操作和视图渲染操作在主线程所占比例并不尽相同,在大多数场景下更为耗时的为真正的视图渲染阶段。那么是否可以充分利用系统多线程能力,将耗时的绘制操作放到异步线程执行?基于此,我们设计并实现了异步绘制框架以解决这种必须要在 CPU 上进行绘制时的主线程消耗,而自测自绘的富文本组件也成为了异步绘制框架的第一个实际应用场景。
系统 CALayer 通常有两种绘制方式,直接使用纹理或手动绘制。DX 内部的异步绘制框架简易原理时序图如下,在接收到系统 layer 的 display 消息时,将纹理生成步骤在异步线程执行完成后再回到主线程统一提交。
DX 基于系统的绘制流程,模仿系统实现,业务方可选择直接实现位图或是基于 DX 提供的画布进行加工绘制。在该流程中间提供了有较多向外扩展点,满足业务方不同的定制化和时机监控需求。并且由于系统绘制框架 CoreGraphics 为线程安全,天然为我们提供了异步绘制的基础,所以可以将绘制的步骤放到异步线程进行。在提交任务时,通过监听系统提交事务时间,将每个 runloop 中的绘制任务暂存,再统一在系统 commit 时机之后从主线程提交到 renderServer。
在 DX 的丰富生态中,不仅有大量的内置组件,还有极为庞大的开发者自定义组件。在 DX 体系下的技术改造如何不影响现有流程并且易于开发者自定义扩展是一个必须要考虑的问题。所以在设计异步绘制框架时,采用面向接口编程的思想,对 DX 原有渲染逻辑无入侵性,也减轻了类之间的依赖耦合关系,可以实现仅对部分实现该协议的组件进行异步绘制,扩展性较强,针对于每个步骤都有对应的扩展点用于开发者自定义操作。
内置 DXDisplayLayer 和 DXBaseView 作为异步绘制基础类,DXBaseView 作为 displayLayer 的视图代理,用于视图展示和手势处理。DXWidgetNode 节点的 AsyncDisplay 分类作为 DisplayLayer 的 displayLayerDelegate,实现真正的同步/异步自定义绘制能力调度。开发者在自定义组件中使用时,可以通过实现 DXWidgetNodeAsyncDisplayProtocol 接口即可接入异步绘制,实现自己节点的异步绘制能力。
对外暴露 DXDisplayLayer 和 DXBaseView 两个基类,业务方可直接重写自定义节点 view 的 layerClass ,或是直接继承自 DXBaseView 来实现异步绘制相关能力。
DX 之前并未提供统一的通用富文本能力,由各个业务方封装自定义富文本组件。其中大部分都是为了完成业务方自身需求,具有较强的定制化属性,无法做到通用性,并且性能也无法保证。而通用富文本能力,天然可作为异步绘制框架在 DX 中的试验场。
众所周知,iOS 系统上的 UILabel 是在 CPU 上绘制成为一张 bitmap 后再交由 GPU 进行混合、合成等操作。而在大量图文场景,例如手淘信息流场景,含有大量价格标签和各种角标,富文本需求强烈。在这种情况下,文本绘制整体占比主线程 10% 以上。1、由于 UIKit 默认非线程安全,所以默认文本绘制均在主线程进行;2、由于 DX 渲染管线机制,需要先测量、布局、再渲染,在测量的过程中需要借助系统函数对文本测量,这也就导致了文本需要测量两次,对主线程占用较高,对帧率产生了较大负面影响。
iOS