作者:何进(小军)本文是《Cube 技术解读》系列的第四篇。欢迎回顾往期文章。《Cube 技术解读 | Cube 小程序技术详解》《Cube 技术解读 | 支付宝新一代动态化技术架构与选型综述》《Cube 技术解读 | Cube 卡片技术栈详解》阿里巴巴是一家专注于运营的公司,前端开发人员最多。2016-2017,Weex还是1.0的时候,ReactNative很长一段时间没有开源,Flutter还没有诞生的时候,如何去契合前端开发环境?,迅速蔓延到android/iOS双平台,是一大热点。支付宝在内部孵化了一个动态的跨平台解决方案以利用这一趋势。前面三篇分别介绍了Cube目前的架构、Cube卡片和Cube小程序技术产品形态。本文主要探讨Cube的渲染设计,帮助大家了解Cube卡片渲染技术的前世今生。原生渲染的问题我们都知道在屏幕上渲染一个原生视图需要几个步骤。以android为例:创建、测量、布局、绘制。这些都需要在主线程中完成。在实现原生列表时,即使item被完美复用,在渲染不同数据时,measure、layout、draw这几个步骤也是必不可少的,而且view的嵌套层次越深,对资源的消耗就越大主线程。当列表飞起来时,帧率快速下降,导致页面卡顿,基于这个问题,研究期间如何解决Cube的渲染效率是一个重要的环节。一般来说,优化列表滚动的帧率,即视图层级,布局复杂度,去除不必要的背景色,解决过度绘制,图片延迟加载,item复用等,但还是绕不开measure,layout,并绘制。当时weex和RN都还是将html中的标签映射到平台层视图。在某些场景下,开发者无法像原生开发一样进行自我优化,导致渲染性能饱受诟病。因此,立方体研究期间的渲染目标是:优化渲染效率+跨平台。跨平台异步渲染方案异步渲染是基于上面提到的背景和需求,所以我们想有没有办法把关键步骤从线程中去掉,那就是异步渲染。滚动列表时,基本上只有系统手势和列表本身的滚动算法,动画需要占用主线程,会大大提高帧率。视图中元素绘制的产物是像素缓存(Cube采用的设计是Bitmap),返回主线程为视图刷新显示。跨平台架构跨平台的另一个目标是快速扩展其他平台。Cube将平台中涉及的部分分开,形成一个平台层。这里的平台为各个平台提供了通用的标准c++原子接口,在不同的平台上用平台语言实现。最初只实现了android和iOS两个平台。Android通过jni调用java方法,iOS在实现文件中混合了c++和OC。如果以后需要扩展macOS等其他平台,只需要实现平台层定义的接口即可达到快速扩展其他平台的目的。corelibrary是基于平台原子接口用C++实现的基础库,如文件IO、UI控件、图片下载、消息通信等,供上层引擎使用。库之上是立方体渲染的核心实现。渲染部分包括数据模型和渲染逻辑。组件库是指cube内部支持的一些系统实体控件,或者是开发者可以连接的外部实体组件。下图是第一版立方体渲染架构图。立方体渲染架构图异步渲染技术选择前面提到,异步渲染方案中异步绘制的“产品”,是交给“容器”View的位图。为什么是位图?看起来对记忆很不友好,而且View也是一个什么是视图,有什么特殊性吗?说说cube研究期间研究了哪些方案,最后为什么选择bitmap。Android平台技术选型Android选型之路坎坷坎坷。支持独立渲染线程的textureView和GLSurfaceView是最早可以认为是容器的,但是它们有明显的缺陷。不能用于普通业务列表场景,只能用于特定场景。SurfaceView,GLSurfaceViewSurfaceView从android1.0开始就有了。主要特点是它的渲染可以在子线程中实现,所以问题是它虽然继承了View,但是有一个独立的Surface,不在Viewhierachy中。它的显示也不受View的属性控制,所以不能像普通视图一样进行缩放和平移,也不能作为一个item在listView/RecycleView中作为普通视图使用。滚动的时候会出现异步问题。GLSurfaceView继承了SurfaceView,它是GLThread自带的,和GLSurfaceView存在同样的问题。总之,这两种视图更适合单视频渲染或者类地图渲染场景。可能有人会问,难道整个页面都用SurfaceView/GLSurfaceView就够了吗,连列表都在render线程中实现?这里有两个问题:1、如果列表容器也是在render线程中实现的,就像现在的flutter一样,那么列表滑动手势处理需要自己实现,比如drag,fling,各种列表滚动动画,以及滚动加速计算等,成本非常高。而且触摸事件的捕获还是依赖于平台层,事件的处理需要切换到render线程。一定有线程切换成本导致的体验不尽如人意。现在很多基于flutter引擎改造的渲染引擎都面临着这些问题;2、当时立方体团队的主要目标是快速验证,实现列表的成本太高,这不是主要矛盾。TextureVIewtextureView是从android4.0开始由谷歌提供的。它的出现很大程度上是为了弥补SurfaceView、GLSurfaceView和nativeView整合的不足。基于上一节描述的这两个视图和原生视图之间的动画问题,textureView似乎更适合我们的场景。它不仅可以支持独立的渲染线程,还可以保证与原生视图的完美结合。但是在实际研究过程中,发现textureView的渲染机制并不适合长列表。如果每个list的item都是一个textureView,那么就涉及到屏幕外的回收和屏幕内的创建,否则会造成内存问题。但是回收和创建SurfaceTexture是一个异步过程,存在闪黑屏的问题。另外进一步发现,textureView的个数和容量(每个view的累计大小)都有一定的上限,不同手机的上限也有很大差异。简单来说,这是一条看起来很美,但兼容性坑数不胜数的技术路线。Bitmap+OrdinaryView最终选择了位图看起来不完美的解决方案。bitmap带来的大量内存消耗虽然被大多数android开发者认为是不能接受的,但是随着cube的应用范围越来越广,逐渐被接受。它被证明是当时最常见的解决方案。每一层对应一个系统视图,每个视图的绘制内容在子线程中通过CanvasAPI异步绘制在位图上。当视图在屏幕上时,系统onDraw绘制位图“product”。BitmapCache虽然采用了Bitmap的绘制方案,但是必须考虑内存过载的问题。这里我们使用BitmapCache,主要针对列表类型的场景,依赖于系统的item回收回调通知,将位图画布放入Cache中。当项目在屏幕上呈现时,优先使用缓存中的位图画布,并首先使用相同的大小。如果不存在,则取大于目标宽高的宽高,使view只绘制位图部分,达到正确渲染的目的iOS平台技术选择iOS实现原理大致和android一样,区别就是iOS异步线程绘制的“产品”不会在UIView的drawRect中用CoreGraphics渲染。UIView的图层由系统托管来渲染图层。渲染技术的演进上面提到了立方体异步渲染的总体方案和关键技术的选择。事实上,从19年初AnswerPlanet上线至今,魔方在支付宝中的应用越来越广泛,而这也与魔方团队相伴相生。在实际业务场景的不断探索和优化过程中,渲染环节进行了两次重构。需要强调的是,这个演进过程是在严格的内存/性能条件下完成的,必须在Android兼容性上做出妥协。一些看似不优雅或高级的设计其实是要做的,比如选择Bitmap作为像素缓冲,比如访问三方组件的设计等等。从某种意义上讲,高低谈何意义和缺点的技术没有限制。以前我们都是学习flutter的部分,但是Cube最终还是沿着适合自己场景的技术路线往前走。常用术语LayoutTree:通过yoga布局add、update、remove构建的DomApi,用于描述节点的父子关系,包括布局信息的原始树结构;RenderTree:用于描述绘图节点的父子关系,包括绘图信息的树状结构结构,与layoutTree的区别示例:一个可见的layoutNode没有了,则该节点不会出现在RenderTree中;Layer:一般是根节点和它的子节点绘制在同一个画布上,定义为一个层,对应平台Layer一个视图,当子节点有动画属性,或者超出父节点的范围时,一个层需要独立创建;LayerTree:上面提到的层节点,构建的树状结构,一个层对应平台层的一个视图,我们称之为ContainerView;实体节点:需要独立层的节点为实体节点;虚拟节点:除了实体节点,其他节点都会绘制在父容器的画布上,这些都是虚拟节点。演化过程研究初期——调查期间1.0验证方案的可行性,验证方案的可行性比较简单,以支付宝好友动态页作为验证场景,各状态(oneitem/cell)作为渲染单元,这里只考虑在layerTree只有一层的情况下,头像、昵称、时间、图片、“赞”、“打赏”、“评论”等元素都是绘制在根节点对应的图层上。"Like","Reward","Comment""文字旁边的小图标作为外部实体组件,通过addSubView添加到rootLayer的View中,数据模型如下图所示。RenderTree是根据layoutTree构建的,但是非渲染节点不在renderTree上。layerTree只有一个自绘层(rootLayer),其他自定义组件X。最后除了自定义组件外,其他所有节点都在rootLayer上绘制。渲染过程桥接线程通过DomApi构建layoutTree,当主线程触发渲染时,主线程根据layoutTree构建RenderTree,构建过程中遇到外部实体组件,创建实例并添加SubView,然后切换子线程到绘制RenderTree,即rootLayer上的所有虚拟节点,绘制完成后切换主线程纹理(位图“product”)。缺点:不能支持多层结构实体视图不复用,即好友动态列表有多少item/cell,就会有多少“赞”、“打赏”、“评论”实体组件但是本次调研验证了异步渲染性能的可行性,滚动列表时帧率大幅提升。产品化期——2.0支持多层。可行性之前已经验证过了。在进行产品化设计时,需要满足多层结构,即在一张实际的卡片中,会设置一个或几个不同的节点。对于层来说,这些节点和它们的子节点绘制在不同的画布上,由不同的层渲染。数据模型的改进在于layerTree中有一个多层节点,层节点下的子虚拟节点会绘制在层的位图“积”上。在渲染进程的brige线程构建layoutTree的过程中,每条指令(addNode,removeNode...)都会分发到渲染模块的主线程。触发任务dequeue和deduplicate,构建layerTree,将不同的layer分配给不同的drawthread进行绘制,绘制完成后切割主线程纹理(位图“product”)。缺点:主线程计算量大,可能造成卡顿。渲染节点不仅包含绘图信息,还包含逻辑。例如display:"none"节点被忽略不显示,其职责不明确。优化期-3.0取长补短上面可以看到renderTree和layerTree的构建都是在UI线程中。当节点数量较多且复杂时,会导致UI卡顿。为了追求极致的滚动帧率,尽量减少主线程的计算内容,优化3.0版本改变了renderObject构造层以及计算节点变化对子线程造成的渲染影响范围-thread来完成,形成目前在线运行的版本。数据模型增加了PaintTree结构,挂载在Layer节点上,样式和属性值从RenderTree中复制过来,但不涉及任何逻辑处理。它只是一个绘图对象,每个绘图任务只在paintTree上绘制。绘制节点与layerTree和renderTree没有并发问题。渲染过程布局线程构建layoutTree,切换到渲染线程构建renderTree,当平台层触发渲染时,切换到renderTree构建layerTree,计算影响范围等,切换到主线程将图层对应的物化View添加到容器View中,并生成绘制任务在paint线程上执行,绘制完成后切换主线程纹理(位图产品)。缺点渲染线程繁忙时,闪烁的白色速率会增加。以上就是立方体渲染从诞生到现在线上方案的演进过程。目前,支付宝卡表接入服务已超过20个,在线运行的卡模板数量已达500多个,显示PV超过100亿,经受住了各业务方的考验。但是,在技术支持方面也发现了一些问题。比如当渲染任务过多时,渲染线程阻塞在队列中,未能及时消费导致白屏概率较高。最近,Cube也在继续研究优化方案。存在问题两端一致性Cube目前的绘图api使用的是系统平台层提供的CanvasApi(iOS是CoreGraphics),这就导致在两个平台上绘制点、线、面的细节需要手动在两端进行代码对齐。否则,效果会有所不同。当添加一些特性时,比如支持虚线,两个平台需要分别实现DrawDottedLine接口。不过Cube团队正在研究自绘,即利用skiaAPI将绘图界面下沉到c++,实现跨平台的自绘;文字也是一个容易产生分歧的点。使用platformlayerapi布局文字,绘制时调用layoutapi进行绘制,所以产品平台可能会有差异,但cube团队目前在Cube。程序中,将文字排版和排版算法淹没在c++层,独立于平台api,实现两个平台的一致性;仅限于内存/性能的限制尚未应用于Cube卡。因为滚动使用的是异步渲染,难免会出现主线程卡已经上传到屏幕,异步绘制还没有完成导致的闪退问题。线程切换是有代价的。这种闪烁在理论上肯定是存在的,但这只是时间问题。立方体团队致力于提高渲染效率,最大限度减少线程切换带来的损失,提升列表滚动的用户体验。未来规划针对目前已知的问题,立方体团队致力于持续优化。主要优化点包括但不限于:渲染快照,提高冷启动渲染效率,减少闪烁时间;渲染策略,如预渲染、同步和异步绘制自适应、线程模型优化、组件缓存和预加载等,降低闪烁率,提高渲染效率;针对Cube卡片的yoga排版引擎优化,提升排版效率;skia自绘实现实现双端一致性;cube渲染技术的应用包括卡片和小程序两种技术形态。场景包括支付宝内部、外部、IOT等多样化场景。团队成员将继续在渲染性能、用户体验、工具链等方向努力。努力把产品打磨好,服务好开发者,成长为有竞争力的跨平台动态渲染解决方案。关注【阿里移动技术】,阿里前沿移动干货&实践给你思考!
