共计 6238 个字符,预计需要花费 16 分钟才能阅读完成。
作者:何瑾 (潇珺)
本文为《Cube 技术解读》系列第四篇文章,往期文章欢送大家回顾。
- 《Cube 技术解读 | Cube 小程序技术详解》
- 《Cube 技术解读 | 支付宝新一代动态化技术架构与选型综述》
- 《Cube 技术解读 | Cube 卡片技术栈详解》
阿里是个重经营的公司,前端开发者居多,2016-2017 年,在 Weex 还是 1.0 时代,React Native 开源还没多久,Flutter 还没诞生的时候,如何在贴合前端开发环境的前提下,疾速铺到 android/iOS 双平台是个大热点,支付宝外部孵化一个动态化跨平台计划趁势而生。
后面三篇文章别离介绍了 Cube 以后架构,Cube 卡片和 Cube 小程序技术产品状态。这篇文章次要探讨 Cube 的渲染设计,帮忙大家理解 Cube 卡片渲染技术的前世今生。
Native 原生渲染的问题
咱们都晓得一个原生 view 渲染上屏须要几个步骤,以 android 举例:create、measure、layout、draw,这些须要在主线程实现,当实现原生列表时,即便完满复用 item,对不同数据渲染时,也须要 measure、layout、draw 几步缺一不可,而且随着 view 嵌套层级越深,对主线程资源耗费越大,当列表 fly 起来当前,帧率疾速降落,造成页面卡顿,基于这个问题,cube 在调研期间,如何解决渲染效率是重要的一 part。
通常来说优化列表滚动帧率,也就是 view 层级、布局复杂度、去掉不必要背景色,解决适度绘制,图片懒加载、item 复用等方面下手,但基本还是绕不过 measure、layout、draw。彼时的 weex 和 RN,也都还是将 html 中的标签映射到平台层 view,在某些场景下,开发者又不能像原生开发一样自行优化,在渲染性能上饱受诟病。因而 cube 调研期间渲染指标是:优化渲染效率 + 跨平台。
跨平台异步渲染计划
异步渲染
基于下面提到的背景和需要,那么咱们就想,是否有一种形式,把关键步骤移除出线程呢,即异步渲染。在列表滚动时根本只有零碎手势和列表自身滚动算法、动画须要占用主线程,将大大提高帧率。视图内元素绘制的产物是一个像素缓存(Cube 采纳的设计是 Bitmap),回到主线程给视图进行刷新显示。
跨平台架构
另一个指标跨平台,是要做到能够疾速扩大其余平台,cube 将波及平台的局部分离出来,造成 platform 层。
platform
这里提供了各平台通用的规范 c ++ 原子接口,在不同平台用平台语言实现,初步只实现了 android、iOS 两个平台,android 通过 jni 调用 java 办法,iOS 在实现文件中 c ++、OC 混编。如果将来须要扩大其余平台例如 macOS,只需实现 platform 层定义的接口即可,能够达到疾速扩大其余平台的指标。
core
library 是基于 platform 原子接口用 c ++ 实现的是根底库,例如文件 IO、UI 控件、图片下载、音讯通信等,供下层引擎应用。library 之上,就是 cube 渲染的外围实现,渲染局部包含数据模型和渲染逻辑,组件库指 cube 外部反对的一些零碎实体控件,或者开发者可外接的实体组件。
下图是第一版 cube 渲染架构图。
cube 渲染架构图
异步渲染技术选型
后面提到了,异步渲染计划里异步绘制的“产物”是一张 bitmap 交给“容器”View,为什么是 bitmap 呢,看起来对内存很不敌对,View 又是个什么 View,有没有特殊性,上面聊聊 cube 调研期间都钻研过哪些计划,最终为什么选型 bitmap。
Android 平台技术选型
android 的选型之路崎岖起伏,最先能想到的反对独立渲染线程的 textureView、GLSurfaceView 做为容器,但有显著缺点,是不能用于常见业务的列表场景的,只能利用于特定场景。
SurfaceView、GLSurfaceView
SurfaceView 从 android1.0 开始就有,次要特点是它的渲染能够在子线程中实现,因而存在的问题是,尽管它继承 View,然而它领有独立的 Surface,不在 View hierachy 中,它的显示也不受 View 的属性管制,因而不能像一般 view 一样缩放平移,更不能作为 item 放在 listView/RecycleView 中当作一般 view 应用,滚动起来会有不同步的问题。
GLSurfaceView 继承 SurfaceView,它自带 GLThread,有和 GLSurfaceView 雷同的问题,总之,这两个 view 更适宜单个视频渲染或者像地图类渲染场景。
有人可能要问,整个页面都用 SurfaceView/GLSurfaceView 不就行了,连列表也在 render 线程实现?这里两个问题:
1、如果列表容器也在 render 线程实现,正如当初的 flutter 一样,那么列表滑动手势解决须要本人实现,比方 drag,fling,各种列表滚动个动画,以及滚动加速度计算等,老本很高。并且,touch 事件捕捉依然依赖平台层,而处理事件须要切换到 render 线程,这两头肯定有线程切换老本造成的不跟手的体验问题。当初很多基于 flutter 引擎革新的渲染引擎,正面临着这些问题;
2、在过后 cube 团队的次要指标是疾速验证,列表的实现这种老本过高,不是主要矛盾所在。
TextureVIew
textureView 是 google 从 android4.0 开始提供的,它的呈现很大水平上是为了补救 SurfaceView、GLSurfaceView 与原生 View 交融的有余,基于下面一节形容的这两个 view 与原生 view 一起动画的问题,textureView 仿佛更适宜咱们的场景,既能反对独立 render 线程,又能保障与原生 view 完满交融。
然而,在理论的调研过程中发现,textureView 的渲染机制,不适用于长列表,如果每个列表的 item 是一个 textureView,那么就波及到出屏回收,进屏创立,否则会带来内存问题。而回收和创立 SurfaceTexture 是异步过程,呈现了闪黑屏问题。除此之外,进一步发现 textureView 的数量和容量(每个 view 的尺寸累计)存在某个下限,而且不同手机下限也差别很大。简略说,这是一个看起来很美妙,然而兼容性坑有数的技术路线。
Bitmap+ 一般 View
最终抉择了 bitmap 看起来并不完满的计划,尽管这被大多数 android 开发认为 bitmap 带来大量内存耗费,视为不可承受,但随着 cube 的利用范畴越来越广,这逐步被证实是在过后,最普适的一个计划。
每一个 layer 对应一个零碎 view,每个 view 的绘制内容在子线程通过 CanvasAPI 异步绘制在 bitmap 上,当 view 上屏时,零碎 onDraw 绘制这个 bitmap“产物”。
BitmapCache
尽管用了 Bitmap 绘制计划,但必须要思考内存过载的问题,这里咱们采纳了 BitmapCache,次要针对列表类型场景,依赖零碎的 item 回收回调告诉,将 bitmap 画布放入 Cache,item 上屏渲染时,优先从 cache 取 bitmap 画布应用,优先取雷同大小的,如果不存在,则取 width、height 大于指标 width、height,让 view 只绘制 bitmap 部分,达到正确渲染的目标
iOS 平台技术选型
iOS 的实现原理与 android 大致相同,区别是,iOS 异步线程绘制实现的“产物”,不会在 UIView 的 drawRect 里利用 CoreGraphics 进行渲染,这种形式效率很低,页面卡顿显著,最终采纳的是将画布赋值给 UIView 的 layer,托管给零碎渲染 layer。
渲染技术的演进
下面讲了 cube 异步渲染大体计划和关键技术选型,事实上,从 19 年初上线答答星球,到当初,cube 在支付宝内利用越来越宽泛,这两头也随同着 cube 团队依据理论业务场景一直摸索、优化的过程,渲染链路经验了两次重构。须要强调的,这个演进过程是在严格的内存 / 性能下实现的,而且要对 Android 兼容性做出斗争。一些看起来不那么优雅或者先进的设计,事实上是不得不这么做,比方抉择 Bitmap 作为像素缓冲,比方接入三方组件的设计等。从某种意义上,抛开束缚议论技术优劣也意义不大。咱们已经借鉴 flutter 的局部,但 Cube 最终还是沿着适宜本身场景的技术路线往前走。
常见术语
- LayoutTree:DomApi 通过 add、update、remove 构建的通过 yoga 布局的,用来形容节点父子关系,蕴含布局信息的原始树型构造;
- RenderTree:用来形容绘制节点父子关系,蕴含绘制信息的树型构造,与 layoutTree 的区别举例:一个 layoutNode visible 为 gone,则该节点不会在 RenderTree 中呈现;
- Layer:个别状况下,根节点及其子节点绘制在同一个画布上,定义为一个 layer,对应平台层一个 view,当子节点有动画属性,或者超出父节点范畴,则须要独立出一个 layer;
- LayerTree:下面提到的 layer 节点,构建的树型构造,一个 layer 对应平台层一个 view,咱们叫 ContainerView;
- 实体节点:须要独立 layer 的节点为实体节点;
- 虚构节点:除了实体节点以外,其余节点均会被绘制在父容器的画布上,这些是虚构节点。
演进过程
调研初期——1.0 验证计划的可行性
调研期间验证计划可行性,场景比较简单,以支付宝内敌人动静页面为验证场景,每条状态(一个 item/cell)作为一个渲染单元,这里只思考了 layerTree 只有一个 layer 的状况,头像、昵称、工夫、配图、“赞”、“赏”,“评”等元素均绘制在 root 节点对应的 layer 上,“赞”、“赏”,“评”文本旁边的小图标则作为外接实体组件,通过 addSubView 增加在 rootLayer 的 View 上。
数据模型
如下图所示,依据 layoutTree 构建 RenderTree,但非渲染节点不在 renderTree 上,layerTree 只有一个自绘制 layer(rootLayer),和其余自定义组件 X,最终除自定义组件外,其余所有节点都绘制在 rootLayer 上。
渲染流程
bridge 线程通过 DomApi 构建 layoutTree,当主线程触发渲染时,主线程依据 layoutTree 构建 RenderTree,构建过程中遇到外接实体组件,创立实例并 addSubView,之后切换子线程绘制 RenderTree,即 rootLayer 上的所有虚构节点,绘制实现后切换主线程贴图(bitmap“产物”)。
毛病
- 不能反对多 layer 构造
- 实体 view 没有复用,也就是敌人动静列表中有多少 item/cell,就会有多少“赞”、“赏”,“评”实体组件
但这个调研验证了异步渲染的可行性,在列表滚动时帧率大幅晋升。
产品化期间——2.0 反对多 layer
后面验证了可行性,在进行产品化设计时,就必须要满足多 layer 构造了,即理论的一张卡片中,会有一个或几个不同的节点被设置为 layer,这些节点及其子节点,别离绘制在不同画布上,供不同的 layer 渲染。
数据模型
改良之处时 layerTree 里有个多 layer 节点,layer 节点上面的子虚构节点,将绘制在该 layer 的 bitmap“产物”上。
渲染流程
brige 线程构建 layoutTree 的过程中,每个指令(addNode、removeNode……)都会相应散发到 render 模块的主线程,render 依据指令构建 RenderTree,并用指令信息生成 task 入队,当 VSync 信号来时,触发工作出队并去重,构建 layerTree,不同 layer 散发到不同 draw 线程绘制,绘制实现后切主线程贴图(bitmap“产物”)。
毛病
- 主线程计算量大,可能造成卡顿
- render 节点既蕴含绘制信息,是绘制对象,还蕴含逻辑,例如 display:”none” 节点疏忽不显示,职责不清晰。
优化期间——3.0 舍短取长
下面能够看到 renderTree 的构建以及 layerTree 的构建,都是在 UI 线程,在节点数比拟多活简单的状况下会造成 UI 的卡顿,为了谋求极致滚动帧率,尽可能减少主线程计算内容,优化 3.0 版本将 renderObject 构建 layer、以及计算节点变更导致的绘制影响范畴,的局部改在子线程实现,造成了当初线上运行的版本。
数据模型
新增了 PaintTree 这个构造,它挂载在 Layer 节点上,款式和属性值从 RenderTree 拷贝而来,但不波及任何逻辑解决,单纯的是一个绘制对象,每个绘制工作只绘制 paintTree 上的 paint 节点,与 layerTree 和 renderTree 没有并发问题。
渲染流程
layout 线程构建 layoutTree,切换到 render 线程构建 renderTree,当平台层触发渲染,切换到 renderTree 构建 layerTree,并计算影响范畴等,切换到主线程将 layer 对应的实体化 View 增加在容器 View 上,生成绘制工作在 paint 线程执行,绘制完结后切换主线程贴图(bitmap 产物)。
毛病
- render 线程忙碌时造成的闪白率升高
以上就是 cube 渲染从诞生到当初线上计划的演进,目前在支付宝端内卡片状态接入业务超过 20+,线上运行的卡片模版个数达到 500 多个,显示 PV 过百亿,禁受住了各业务方的考验。
但在技术支持中也发现了一些问题,例如渲染工作过多时,render 线程阻塞排队,不能及时生产导致白屏概率变大,最近 cube 也在持续钻研优化计划。
存在的问题
两端一致性问题
- cube 目前的绘制 api,采纳的零碎平台层提供的 CanvasApi(iOS 是 CoreGraphics),这就导致了两个平台在绘制点线面的细节上必须两端人工代码对齐,否则就会产生成果差别,当新增一些 feature,例如反对点划线,须要两个平台各自实现 DrawDottedLine 接口,但这个问题,cube 团队正调研自绘制,即应用 skia api 将绘制接口下沉到 c ++,实现跨平台自绘制;
- 文本也是容易产生差别的一个点,利用平台层 api 对文本进行布局,在绘制时调用布局的 api 进行绘制,因而可能会产品平台差别,但 cube 团队目前曾经在 Cube 小程序上把文本布局,布局算法下沉在 c ++ 层,不依赖平台 api,实现双平台统一;限于内存 / 性能的束缚尚未在 Cube 卡片上利用。
闪白问题
因为滚动采纳的异步渲染,所以必然会产生主线程卡片曾经上屏,异步绘制还未实现造成的闪白问题,线程切换有老本,这个闪白实践上肯定存在,只是工夫长短问题,cube 团队致力于进步渲染效率,将线程切换带来的损耗降到最低,使用户在列表滚动中体验晋升。
将来布局
针对目前已知的问题,cube 团队致力于继续优化,次要优化点包含但不限于以下:
- 渲染快照,进步冷启的渲染效率,缩小闪白工夫;
- 渲染策略,例如预渲染、同异步绘制自适应、线程模型优化、组件缓存和预加载等,缩小闪白率,晋升渲染效率;
- 用于 Cube 卡片的 yoga 布局引擎优化,晋升 layout 布局效率;
- skia 自绘制实现,实现双端一致性;
cube 的渲染技术的利用蕴含卡片和小程序两种技术状态,场景包含支付宝端内、端外、IOT 等多样化场景,团队成员将继续在渲染性能、用户体验、以及工具链等方向继续发力,致力把产品打磨好,把开发者服务好,成长为具备竞争力的跨平台动态化渲染计划。
关注【阿里巴巴挪动技术】,阿里前沿挪动干货 & 实际给你思考!