Flutter与Native混合开发将是接下来很长时间的主流开发方式。一套稳定、高效、与官方体系无缝融合的外接图片缓存方案是必不可少的。在AliFlutter系列第三场直播中,由阿里巴巴新零售淘系技术部无线开发专家王乾元为大家介绍AliFlutter提供的适合混合应用的外接图片库方案。首先对Flutter官方原生方案进行了分析,并提出了AliFlutter方案的切入点以及具体优化手段。
演讲嘉宾简介:王乾元,花名神漠,13年加入阿里,先后负责过天猫、支付宝、手机淘宝App的iOS架构工作。目前在AliFlutter团队负责基础组件、iOS架构,以及引擎、工具链等方面的研究。
以下内容根据演讲视频以及PPT整理而成。
观看回放http://mudu.tv/watch/5624777
本次分享主要围绕以下三个方面:
一、Flutter如何显示、加载图片 二、AliFlutter图片解决方案优化 三、如何选择最适图片解决方案
一、Flutter如何显示、加载图片
介绍Flutter如何加载、显示图片,以及在线程、缓存设计层面的特点。
线程
闲鱼分享的文章《深入理解Flutter引擎线程模式》中,详细讲解了Flutter引擎线程模型的原理以及作用。
Platform Thread:IOS与安卓平台层应用的主线程。进行Flutter Engine接口的调用,用户手势和输入等也通过Platform Thread输入给Flutter Engine。
UI Thread:Flutter的主线程,也称为Dart线程。同时可运行C++代码。
IO Thread:进行图片上传。图片在IO Thread进行异步上传生成GPU纹理.
GPU Thread:负责Flutter最终的GPU调用。
Worker Thread:Flutter中的fml会创建若干个并发工作线程。可进行图片解码等工作。
如上图所示,图片在Worker Thread完成解码,在IO Thread进行异步上传,在引擎启动时创建的ShellIOManager会创建OpenGL Context。同时GPU Thread创建GPU Context。IO Context与GPU Context将存放在Share Group中共享纹理。Flutter中纹理对象是C++的对象,在Flutter底层不会对纹理对象进行任何缓存,而是通过Dart层的ui.Image对象通过引用计数进行管理。
图片加载、显示流程
下图为Flutter从图片加载到显示的相关类关系图,包括类所在文件。
图片加载用到Flutter的Image Widget,一般是使用其“.network”接口加载网络图片。Image Widget进行显示绘制时需要ImageState。ImageState有两个功能,一是驱动Provider下载图片,二是调用State管理底层Render object。Render object负责图片的渲染上屏。
NetworkImage(Provider)在自身resolve方法中异步调用http下载图片。resolve方法调用Provider获取自己的ImageStream。ImageStream会添加到StreamCompleter作为Listener。StreamCompleter可以添加多个ImageStream作为Listeners。图片下载完成后通过Dart层和C++层的接口函数instantiateImageCodec创建底层C++解码器的C++对象。解码器对象获取图片流后在底层进行异步解码,并生成纹理。ImageState接收到事件后获取纹理对象绘制图片。上层获取图片纹理后会调用ImageState的SetState方法将纹理对象传给底层Render object,排版完成后图片就会绘制到屏幕。
底层纹理对象会被上层Dart对象引用,具体为以下几个对象。StreamCompleter负责驱动底层解码器获取纹理对象。因此StreamCompleter会持有底层GPU纹理,并通过Listeners通知所有ImageState。因此ImageState也会持有纹理对象。ImageState将图片传给底层Render object,因此Render object也会持有纹理对象。当上层Image Widget被销毁,Image Cache清空时,触发底层纹理的释放。
Flutter加载显示图片的流程包括了图片的组件、下载、解码、上传、绘制等工作,看似复杂,但是其逻辑较为简单。
二、AliFlutter图片解决方案优化
问题
首先,利用Flutter制作淘宝商品详情页面,图片多,内存、CPU等占用非常高,性能要求高。Flutter图片管理能力较弱,缺乏本地缓存能力,图片的重复下载极易造成内存飙高,易发生OOM(OutOfMemory)情况。因此Flutter原生方案无法满足需求,需要构建适合的AliFlutter方案。
第二,电商APP需要与Native图片库对接,共享缓存、CDN能力以及监控设施。
第三,在使用简单的基础上,AliFlutter需要基于Flutter的强大扩展能力,支持小程序、Canvas等多种场景。
第四,希望AliFlutter与官方Flutter体系尽可能兼容与融合。
AliFlutter图片解决方案总体架构
下图红色标签为AliFlutter方案的重点。在Dart层实现了新的Provider,在C++层实现了新的解码器对象,并基于Flutter规范提供了不同平台的ObjC、安卓的Java接口。
AliFlutter图片解决方案追求以下三个特点。
一致性:与官方体系无缝融合。仅在官方基础上添加代码。
高性能:优化CPU、内存占用,增强List回收能力。
易用性:适配简单、使用简单、易扩展。
AliFlutter:如下图所示,高亮部分为AliFlutter改进部分。
Image Widget添加了新类型的Provider,ExternalAdapterImage。新Provider接收的参数是URL、图片尺寸信息等。可将参数通过Adapter传给Native图片库,进行图片下载或从缓存中加载。Completer会创建新的解码器对象,通过Adapter对接Native图片库,让Native图片库提供图片的原始Buffer,并进行解码。即不依赖Flutter的图片解码能力,而是依赖平台层例如IOS和安卓原生的图片解码能力,可支持更多图片格式。将平台层解码后的bitmap返回给解码器对象,通过位图数据进行图片纹理的上传。AliFlutter解码器底层的C++对象支持这两种工作模式。
一次完整图片加载过程时序图:首先从Image Widget拿到图片请求URL,调用到底层解码器对象的getNextFrame方法会将请求异步上传给对接的Native图片库。由Native图片库做请求,获取平台层的图片对象或Buffer,将图片对象返回给解码器对象。解码器对象在Worker Thread中进行图片解码。图片解码完成后在IO Thread进行图片的GPU纹理上传。上传完成后在UI Thread将图片返回给Dart。上述流程完成一次图片加载,线程模型与Flutter原生保持一致。
图片取消:AliFlutter方案相比Flutter原生方案新增了Cancel能力。Widget通过State将自己添加到Completer的Listeners中。因此Widget销毁时会将自己从Listeners中移除。当Completer的Listeners全部清空时,表示这次图片请求已经不再需要了,调用底层解码器对象的cancel方法。如果图片还未从Native图片库返回,可以取消下载;如果已经返回,还有解码或上传GPU过程,都可以及时取消操作。Cancel能力可以避免许多无用的CPU和内存的消耗,尤其是电商App中常见的快速滑动商品列表的场景。
性能优化
AliFlutter进行了以下层面的优化,除图片取消外,还包括延迟加载、解码并发控制、GIF逐帧上传纹理、增强List回收能力等。
适配与使用:介绍AliFlutter图片方案最终对接到平台层Native图片库的接口。
Flutter的封装是在IOS平台公开了Objective-C接口,在安卓平台提供了Java接口,因此AliFlutter遵循Flutter规范提供了OC接口与Java接口。
IOS平台OC接口只需要实现一个回调。OC回调在对接图片库时接收的是URL以及一些参数,获取图片后向底层返回UIImage即可。使用时可以直接调用Dart的Image.externalAdapter方法加载一张图片。在此可以指定placeholderProvider,可以是AssetImage或其他网络图片,以此可在主图加载失败时加载一张副图。
增强List回收能力
优化前后对比:下图左侧所示为使用Flutter制作的淘宝商品详情页面,其中有多个Cell。其中一个Cell为宝贝详情。宝贝详情Cell最初的实现方式是解析一段HTML。商家有时会上传多张高清大图,若此时将连续的图文详情放在一个Cell中,用户浏览详情页时会同时加载多张大图。另外Flutter默认对所有Cell添加RepaintBoundary属性,该属性默认将Cell中所有内容绘制到一个纹理中,下次浏览时若Cell中内容不变,直接使用纹理绘制图片会比较快速。因此易导致内存飙高问题。
如下图所示,优化前内存容易暴增到600+MB甚至1G,几乎100%会出现OOM问题。在业务代码不进行修改的情况下,优化后的内存增长变得较为平缓。
Flutter List特点:Flutter List回收以Cell为单位。下图所示红色框部分为屏幕大小。默认情况下Flutter默认对所有Cell添加RepaintBoundary。当列表滚动时,若Cell 1绘制过,下次绘制时直接将及纹理上屏即可,无需绘制内部图文元素。而Cell 2会占用大量内存,首先其图文多,同时RepaintBoundary形成的纹理也会占用大量内存。
因此增强List回收能力首先需要解除对部分Cell的RepaintBoundary设置。
优化流程:假设一个Image Widget在一个Cell中,正常情况下当Cell出现,Image Widget也会被创建并且请求图片。List回收能力的优化中试图解除此约定,根据图片是否在屏来判断是否需要图片纹理。若不需要,则释放,若需要,进行请求。
Image Widget的宽、高已知情况下,其排版信息是有效的。SetState完成后触发底层Render Object排版与绘制。在绘制图片过程中添加一段逻辑判断图片是否在屏。若图片不在屏,不作任何处理。若图片在屏幕中,进行图片请求获取真实图片后重复调用SetState,重新进行图片排版和绘制,并判断是否在屏。若图片随着列表滚动不在屏幕中,则回调通知上层解除纹理引用。
若Image Widget的宽、高未知,Flutter只能在获取图片后根据其真实尺寸进行排版。原本底层解码器对象持有getNextFrame接口,该接口导致GPU纹理的生成。在优化后可以不依赖图片纹理上传完成再进行排版。在流程中添加了Request Size接口,Image Widget的宽、高未知时调用该接口可以预先通知底层C++解码器获取图片尺寸。得到图片尺寸后再从SetState开始流程,避免了无效的纹理上传。
关键代码:判断图片是否在屏是通过Dart层的Image Render Object。其paint方法中进行图片是否在屏的判断,根据其是否在屏向上层ImageState发送回调通知。
实现指定Cell不添加RepaintBoundary是通过建立虚类NoRepaintBoundaryHint。若List检测到上层某个Cell继承自NoRepaintBoundaryHint,则不给该Cell添加RepaintBoundary。因此可以在每次屏幕滚动时重新进行绘制,了解图片的在屏、离屏信息。
图片解码时通过Image Codec接口实现只获取图片尺寸,不上传纹理。图片尺寸可以直接从图片的头部信息获取,并不需要分配内存。
图片排版时可以仅根据图片的尺寸信息进行排版,无需获取真实图片。
总结起来,大Cell优化就是避免图片纹理上传,图片真正在屏时,再获取其纹理,当图片离屏时,立刻清除其纹理。
优化效果:经过以上优化,List回收能力的增强取得了较好效果。当商品详情页面有几十张大图在同一列表的同一个Cell中出现,可以做到仅加载在屏图片,若图片离屏则释放。
案例-优化前:十张图片放在一个Cell中,内存突增突降。
案例-优化后:根据图片是否在屏进行加载或释放,内存增降均较为平缓。
后续改进
AliFlutter图片解决方案还有以下方面可以改进。
功能改进:第一,图片在屏、离屏判断优化。第二,支持图片库返回图片原始文件,精简链路。第三,支持业务定制化缓存策略。
包优化大小:允许定制化裁剪Flutter中的若干图片解码库,同时保证Flutter所有功能正常。
与官方探讨:如何将AliFlutter优化融合到Flutter主干。
三、如何选择最适图片解决方案
Flutter图片解决方案诞生以来,开发者也进行了许多尝试,创建图片库方案。难以定论哪些图片方案更加优秀。
如下图所示,开发者可以考虑图片解决方案是否为纯Flutter应用,网络图片场景多不多,图片有无必要缓存等方面。根据自己的应用场景选择最适合自己的图片解决方案。
关注「淘系技术」微信公众号,一个有温度有内容的技术社区~