图片加载是APP最常见最基本的功能,也是影响用户体验的因素之一。看似简单的图片加载背后,隐藏着诸多技术难点。本文介绍闲鱼技术团队在Flutter图片优化方面的尝试,分享闲鱼典型图片处理方案的技术细节,希望能给大家带来一些启发。早在闲鱼开始使用Flutter的那些年,图像就是我们重点关注和优化的功能。图片展示体验的好坏将对闲鱼的用户体验产生巨大的影响。你有没有遇到过:图片加载占用内存太大?使用Flutter后,本地资源重复,利用率不高?混合方案下Flutter原生图片加载效率不高?针对以上问题,从第一版Flutter商业上线开始,闲鱼就一直没有停止对图片框的优化。从一开始的原生优化到背后黑科技的外在质感;从内存使用到包大小;正文将一一介绍。希望里面的优化思路和手段能给大家带来一些启发。原生模式从技术层面看图片加载。其实简单来说,它追求的无非就是加载效率最大化——以尽可能小的资源消耗,尽可能快地加载尽可能多的图片。闲鱼影业第一版基本是纯原生解决方案。如果不想改动很多底层逻辑,原生方案绝对是最简单、最经济的方案。原生方案的功能模块如下:如果什么都不做直接上去,可能会发现效果并没有想象中的那么好。那么如果我们从原来的解决方案入手,具体有哪些优化方法呢?设置图片缓存没错,你猜对了,就是缓存。对于图片加载,最能想到的方案就是使用缓存。首先,原生Image组件支持自定义图片缓存,具体实现类为ImageCache。ImageCache的设置维度有两个方向:缓存图片的数量。由maximumSize设置。默认值为1000页。缓存空间的大小。由maximumSizeBytes设置。默认值为100M。相比张数限制,尺寸设置方式更符合我们最终的预期。通过合理设置ImageCache的大小,可以充分利用缓存机制来加快图片加载速度。不仅如此,闲鱼在这一点上还额外做了两项重要优化:低端手机适配上线后,我们陆续收到网络舆论反馈,发现设置所有模型的缓存大小相同。出色的。尤其是在低端机上设置大缓存,不仅体验变差,甚至会影响稳定性。根据实际情况,我们实现了一个Flutter插件,可以从Native端获取机器基本信息。通过获取到的信息,我们根据不同手机的配置设置不同的缓存策略。图片缓存在低端机器上适当减小,在高端手机上适当放大。这样可以在不同配置的手机上获得最优的缓存性能。磁盘缓存熟悉APP开发的同学都知道,成熟的图片加载框架一般都有多级缓存。除了普通的内存缓存外,一般还会配置一个文件缓存。在加载效率方面,通过以空间换时间来提高加载速度。在稳定性方面,这样不会占用太多宝贵的内存资源而导致OOM。但遗憾的是,Flutter自带的图片加载框架并没有独立的磁盘缓存。因此,我们在原生方案的基础上扩展了磁盘缓存能力。在具体的架构实现上,我们并没有完全自己推出一个磁盘缓存。我们的策略是重用现有功能。首先我们通过接口暴露Native图片加载框架的磁盘缓存功能。然后通过桥接的方式,将Native的磁盘缓存能力嫁接到Flutter层。图片在Flutter端加载时,如果内存没有命中,会去磁盘缓存进行二次查找。如果没有命中,将接受网络请求。通过增加磁盘缓存,进一步提升了Flutter的图片加载效率。设置CDN优化CDN优化是图像优化的另一个非常重要的手段。CDN优化的效率提升主要是尽量减少传输图片的大小。常见策略包括:根据显示尺寸裁剪简单地说,您要加载的图像的实际尺寸可能比您实际显示窗口的尺寸大。那么就不需要加载完整的大图了,只需要加载一张能够覆盖窗口大小的图片即可。以这种方式,可以通过裁剪掉不需要的部分来最小化传输图像的大小。从终端侧来看,一来可以提高加载速度,二来可以减少内存占用。这里适当压缩图片大小主要是根据实际情况增加图片压缩的比例。在不影响显示效果的情况下,通过压缩进一步缩小图片尺寸。图片格式建议优先使用webp格式,图片资源比较少。Flutter原生支持webp(包括动画)。这里特别强调一下,webp动画不仅比gif小很多,而且对透明效果的支持也更好。webp动画是gif方案的理想替代方案。基于以上原因,闲鱼图片框架在Flutter端实现了CDN大小匹配算法。通过该算法,请求的图片会自动匹配到最合适的尺寸,并根据实际显示的尺寸进行适当的压缩。如果图片格式允许,尽量将图片转成webp格式发送。这样,CDN图片的传输就可以尽可能的高效。其他优化除了上述策略,Flutter还有一些其他的手段来优化图片的性能。图片预加载如果你想尽可能快的展示图片,官方也提供了一套预加载机制:precacheImage。precacheImage可以将图片预加载到内存中,实际使用时秒释放。元素重用优化其实是Flutter常用的一种优化方案。重写didWidgetUpdate方案,通过比较前后两个widget中图片的描述是否一致来决定是否重新渲染Element。这可以避免不必要地重复渲染同一图像。长列表优化总的来说,Listview是flutter最常用的滚动容器。Listview中的性能直接影响到最终的用户体验。Flutter的Listview和Native的实现不太一样。它最大的特点是viewPort的概念。超出viewPort的部分将被强制回收。基于以上原则,我们有两点建议:1)在cell分裂时尽量避免大cell,这样可以大大减少频繁创建cell过程中的性能损失。事实上,这里受影响的不仅仅是图片加载过程。文本、视频等组件也应避免过于复杂的单元格导致的性能问题。2)合理使用缓冲区ListView可以通过设置cacheExtent来设置预加载内容的大小。视图渲染的速度可以通过预加载来提高。但是这个值需要合理设置,并不是越大越好。因为preloadcache越大,对页面整体内存的压力就越大。需要客观的指出这个方案的不足之处:如果是纯FlutterAPP,原生方案已经很完美,足够用了。但是从HybridAPP的角度来看,存在以下两个缺陷:1)无法复用原生图片加载能力毫无疑问,原生图片方案是一种完全独立的图片加载方案。对于混合APP,原生解决方案和原生镜像框架是相互独立的,能力不能复用。CDN裁剪压缩等能力需要重复建设。尤其是Native独有的一些图片解码能力,很难与Flutter搭配使用。这样会导致APP内部对图片格式的支持不一致。2)内存性能不足从整个APP来看,在使用原生图片方案的时候,我们其实维护了两个很大的缓存池:一个是Native图片缓存,一个是Flutter端的图片缓存。两个缓存不能互相通信,这无疑是一种巨大的浪费。尤其是内存的峰值存储性能非常有压力。经过Native的多轮优化,基于native的方案取得了非常大的性能提升。但是整个APP的内存水位还是比较高的(尤其是Ios端)。现实的压力迫使我们继续更深入地优化画框。基于对上述原生方案不足的分析,我们有一个大胆的想法:能否充分复用Native的图片加载能力?外部贴图如何打通Flutter和Native的图像能力?我们想到了外部纹理。ExternalTexture并不是Flutter自己的技术,是音视频领域常用的一种性能优化手段。这个阶段,我们基于shared-Context的方案实现了Flutter和Native的纹理连接。通过该方案,Flutter可以通过共享纹理的方式获取并显示Native图片库加载的图片。为了实现这个纹理共享通道,我们对引擎层进行了深度定制。详细流程如下:本方案不仅打通了Native和Flutter的图片架构,还全程优化了图片加载的性能。外部纹理是闲鱼图像解决方案的一大飞跃。通过该技术,我们不仅实现了图像解决方案的局部能力复用,还实现了视频能力的纹理外化。这样就避免了很多重复构建,提高了整个APP的性能。多页内存优化的优化策略真的是被逼出来了。通过分析网上资料,我们发现Flutter页面栈有一个很有意思的特点:在多个页面栈的情况下,底层页面不会被释放。即使在非常紧张的内存条件下,也不会执行任何收集。这会导致一个问题:随着页数的增加,内存消耗会线性增加。这里占比最高的是图片资源占比。是否可以在页面处于页面栈底时直接回收页面中的图片?在这个想法的驱动下,我们对图片结构进行了新一轮的优化。整个图片框里的图片会监听页面栈的变化。当Fang发现自己不在栈顶时,自动回收对应的图片纹理释放资源。这种方案可以防止图片占用的内存大小随着页数的增加而不断线性增加。其原理如下:需要注意的是,这个阶段页面的位置确定实际上需要页面栈(具体来说是混合栈)提供额外的接口。系统之间的耦合度比较高。意外收获:打开packagesize中的Native和Flutter端图片框后,发现一个意外收获:Native和Flutter可以共享本地图片资源。也就是说,我们不再需要在Flutter端和Native端分别保留一份相同的图片资源。这样可以大大提高本地资源的重用率,从而减小整体的包体积。基于这个方案,我们实现了一套资源管理功能,脚本可以自动同步不同端的本地图片资源。这样既提高了本地资源的利用率,又减小了包的体积。其他优化-PlaceHolder增强NativeImage没有PlaceHolder功能。如果要使用原生解决方案,则需要使用FadeInImage。我们有很多针对闲鱼场景的定制,所以我们自己实现了一套PlaceHolder机制。在核心功能方面,我们引入了加载状态的概念:未初始化加载加载完成,针对不同的状态,可以细粒度的控制PlaceHolder的显示逻辑。这个方案整体结构的缺点毕竟换了引擎。随着闲鱼业务的不断推进,引擎升级的成本是我们必须要考虑的。能不能在不换引擎的情况下实现同样的功能是我们的核心需求(PS:我承认我们贪心)。通道性能和优化空间外部肌理的方案需要通过一座桥梁与Native能力进行沟通。这包括图片请求的传递和图片加载各种状态的同步。尤其是listview快速滑动的时候,通过bridge发送的数据量还是相当可观的。在当前的解决方案中,当加载每个图像时,将分别调用桥接器。在图片数量较多的情况下,这显然会成为一个瓶颈。过度耦合在实现图像恢复方案时,目前的方案要求栈提供接口,无论是在栈底。这里发生方案耦合,很难抽象出一个独立干净的图片加载方案。Clean&Efficient时间来到了2020年,随着对Flutter基础能力理解的逐渐深入,我们实现了一个整体解决方案比较好的图片框架。非侵入式外部纹理可以在不修改引擎的情况下使用外部纹理吗?答案是肯定的。事实上,Flutter提供了官方的外部纹理解决方案。而且Native操作的纹理和Flutter端显示的纹理是底层同一个对象,不会产生额外的数据副本。这确保纹理共享足够有效。那为什么之前闲鱼要单独实现一个基于shared-Context的set呢?在1.12版本之前,IOS官方的外部纹理解决方案存在性能问题。在每次渲染过程中(无论是否更新纹理),都会频繁获取CVPixelBuffer,造成不必要的性能损失(进程有锁损失)。该问题在1.12版本(官方commit地址)已经修复,所以官方的解决方案足以满足需求。在此背景下,我们重新启用官方方案来实现外部贴图功能。内存独立优化前面提到过,老版本基于页面栈的图片资源回收需要一个强依赖栈函数的接口。一方面,产生了不必要的依赖,更重要的是,整体解决方案不能独立成一个通用的解决方案。为了解决这个问题,我们对Flutter底层进行了深入的研究。我们发现Flutter的layer层可以稳定感知页面栈的变化。然后每个页面使用从上下文中获取的路由器对象作为标识符,重新组织页面中的所有图像对象。所有获得同一个路由器对象的标识符都是同一个页面。这样就可以对所有图片进行逐页管理。整体上,虚拟页栈结构是通过LRU算法模拟出来的。这样就可以回收栈底页的图片资源。其他优化通道的高复用首先,我们以一帧为单位聚合本帧的图片请求,然后在一个通道请求中传递给Native图片加载框架。这避免了频繁的桥接调用。尤其是在快速滚动等场景下,优化效果尤为明显。高效的纹理重用在使用外部纹理进行图像加载后,我们发现复用纹理可以进一步提高性能。举一个简单的场景。我们知道,在电商场景中,商品展示往往会有标签、背景图片等图片。这些图像往往在不同的产品中大量重复。这时候渲染出来的纹理可以直接复用给不同的显示组件。这样可以进一步优化GPU内存使用,避免重复创建。为了准确地管理纹理,我们引入了一种引用计数算法来管理纹理重用。通过这些解决方案,我们实现了纹理的跨页面高效复用。此外,我们将纹理和请求的映射移到了Flutter端。这样就可以在最短路径上完成纹理的复用,进一步降低网桥的通信压力。整体架构的优化效果还在最新版本的灰度,具体数据会在后面详细写。从属数据主要以方案二为主。通过Native进行内存优化,与第一个在线版本相比,在显示效果不变的情况下,IOS的异常退出率降低了25%,用户体验显着提升。多页栈内存优化多页栈内存优化对多页场景下的内存优化效果显着。我们做了一个极限测试,结果如下(测试环境,非闲鱼APP):可见多页面栈的优化可以更好的控制多个Flutter页面的内存占用。包体积缩减通过访问外部纹理,更好地复用本地资源,包体积缩减1M。闲鱼接入Flutter初期,出发点是修改现有页面。资源重复严重,但随着闲鱼Flutter的新业务越来越多。Flutter和Native的重复资源越来越少。外部纹理对束大小的影响逐渐减弱。后续计划这是一个没有尽头的旅程,我们对闲鱼图片的优化还会继续。尤其是我们最新的解决方案,限于篇幅,本文只是初步介绍。更多技术细节,包括测试数据,我们会专门写一篇文章继续为大家介绍。方案完善后,我们会逐步开源。【本文为专栏作者《阿里巴巴官方技术》原创稿件,转载请联系原作者】点此查看作者更多好文
