评论

收藏

[HarmonyOS] Cube 技术解读 | Cube 渲染设计的前世今生

移动开发 移动开发 发布于:2022-06-13 10:00 | 阅读数:454 | 评论:0

DSC0000.jpg

作者:何瑾(潇珺)
本文为《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),回到主线程给视图进行刷新显示。
DSC0001.jpg

跨平台架构
另一个目标跨平台,是要做到可以快速扩展其他平台,cube将涉及平台的部分分离出来,形成platform 层。
platform
这里提供了各平台通用的标准c++原子接口,在不同平台用平台语言实现,初步只实现了android、iOS两个平台,android通过jni调用java方法,iOS在实现文件中c++、OC混编。如果未来需要扩展其他平台例如macOS,只需实现platform层定义的接口即可,可以达到快速扩展其他平台的目标。
core
library是基于platform原子接口用c++实现的是基础库,例如文件IO、UI控件、图片下载、消息通讯等,供上层引擎使用。library之上,就是cube渲染的核心实现,渲染部分包括数据模型和渲染逻辑,组件库指cube内部支持的一些系统实体控件,或者开发者可外接的实体组件。
下图是第一版cube渲染架构图。
DSC0002.jpg

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团队的主要目标是快速验证 ,列表的实现这种成本过高,不是主要矛盾所在。
DSC0003.jpg

TextureVIew
textureView是google从android4.0开始提供的,它的出现很大程度上是为了弥补SurfaceView、GLSurfaceView与原生View融合的不足,基于上面一节描述的这两个view与原生view一起动画的问题,textureView似乎更适合我们的场景,既能支持独立render线程,又能保证与原生view完美融合。
但是,在实际的调研过程中发现,textureView的渲染机制,不适用于长列表,如果每个列表的item是一个textureView,那么就涉及到出屏回收,进屏创建,否则会带来内存问题。而回收和创建SurfaceTexture是异步过程,出现了闪黑屏问题。除此之外,进一步发现textureView的数量和容量(每个view的尺寸累计)存在某个上限,而且不同手机上限也差异很大。简单说,这是一个看起来很美好,但是兼容性坑无数的技术路线。
DSC0004.jpg

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上。
DSC0005.jpg

数据模型
如下图所示,根据layoutTree构建RenderTree,但非渲染节点不在renderTree上,layerTree只有一个自绘制layer(rootLayer),和其他自定义组件X,最终除自定义组件外,其他所有节点都绘制在rootLayer上。
DSC0006.jpg

渲染流程
bridge线程通过DomApi构建layoutTree,当主线程触发渲染时,主线程根据layoutTree构建RenderTree,构建过程中遇到外接实体组件,创建实例并addSubView,之后切换子线程绘制RenderTree,即rootLayer上的所有虚拟节点,绘制完成后切换主线程贴图(bitmap“产物”)。
DSC0007.jpg

缺点
      
  • 不能支持多layer结构  
  • 实体view没有复用,也就是朋友动态列表中有多少item/cell,就会有多少“赞”、“赏”,“评”实体组件
但这个调研验证了异步渲染的可行性,在列表滚动时帧率大幅提升。
产品化时期——2.0支持多layer
前面验证了可行性,在进行产品化设计时,就必须要满足多layer结构了,即实际的一张卡片中,会有一个或几个不同的节点被设置为layer,这些节点及其子节点,分别绘制在不同画布上,供不同的layer渲染。
数据模型
改进之处时layerTree里有个多layer节点,layer节点下面的子虚拟节点,将绘制在该layer的bitmap“产物”上。
DSC0008.jpg

渲染流程
brige线程构建layoutTree的过程中,每个指令(addNode、removeNode……)都会相应分发到render模块的主线程,render根据指令构建RenderTree,并用指令信息生成task入队,当VSync信号来时,触发任务出队并去重,构建layerTree,不同layer分发到不同draw线程绘制,绘制完成后切主线程贴图(bitmap“产物”)。
DSC0009.jpg

缺点
      
  • 主线程计算量大,可能造成卡顿  
  • render节点既包含绘制信息,是绘制对象,还包含逻辑,例如display:"none"节点忽略不显示,职责不清晰。
优化时期——3.0取长补短
上面可以看到renderTree的构建以及layerTree的构建,都是在UI线程,在节点数比较多活复杂的情况下会造成UI的卡顿,为了追求极致滚动帧率,尽可能减少主线程计算内容,优化3.0版本将renderObject构建layer、以及计算节点变更导致的绘制影响范围,的部分改在子线程完成,形成了现在线上运行的版本。
数据模型
新增了PaintTree这个结构,它挂载在Layer节点上,样式和属性值从RenderTree拷贝而来,但不涉及任何逻辑处理,单纯的是一个绘制对象,每个绘制任务只绘制paintTree上的paint节点,与layerTree和renderTree没有并发问题。
DSC00010.jpg

渲染流程
layout线程构建layoutTree,切换到render线程构建renderTree,当平台层触发渲染,切换到renderTree构建layerTree,并计算影响范围等,切换到主线程将layer对应的实体化View添加在容器View上,生成绘制任务在paint线程执行,绘制结束后切换主线程贴图(bitmap产物)。
DSC00011.jpg

缺点
      
  • 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等多样化场景,团队成员将持续在渲染性能、用户体验、以及工具链等方向持续发力,努力把产品打磨好,把开发者服务好,成长为具有竞争力的跨平台动态化渲染方案。
关注【阿里巴巴移动技术】,阿里前沿移动干货&实践给你思考!

关注下面的标签,发现更多相似文章