简介:flutter图片内存大?加载慢?本地资源利用率低?看这篇就够了git
做者:闲鱼技术-意境github
图片对一个端侧研发来讲是一老生常谈的话题了。闲鱼做为业界在Flutter技术方向上最先一批投入的团队。从使用Flutter之初,图片就是咱们核心关注和重点优化的功能。图片算是闲鱼业务场景下最为重要的内容表现形式之一。图片展现体验的好坏会对闲鱼用户的使用体验产生巨大影响。大家是否也曾遇到过:web
针对上述问题,从初版Flutter业务上线开始,闲鱼对图片框架的优化就从未中止。从开始的原生优化,到后面黑科技的外接纹理;从内存占用,到包大小;文本会逐一介绍。但愿其中的优化思路和手段,能给你们带去一些启发。算法
从技术层面看图片加载,其实简单来讲,追求的是无非是加载的效率的最大化—用尽量小的资源成本,尽量快地加载尽量多的图片。缓存
闲鱼图片的第一个版本其实基本上是纯原生的方案。若是你不想魔改不少底层的逻辑,原生方案确定是最简单和经济的方案。原生方案的功能模块以下:性能优化
若是你啥都没作直接上了,那么你可能会发现效果并无达到你预期的那么美好。那么若是从原生的方案入手,咱们有哪些具体的优化手段呢?网络
没错猜对了,是缓存。对于图片加载,最能想到的方案就是使用缓存。首先原生Image的组件是支持自定义图片缓存的,具体的实现类是ImageCache。ImageCache的设置维度是两个方向: 1.缓存图片的张数。经过maximumSize设置。默认是1000张。2. 缓存空间的大小。 经过maximumSizeBytes 来设置。默认值100M。相比张数的限制,其实大小的设置方式更加符合咱们的最终的预期。架构
经过合理设置ImageCache的大小,能充分利用缓存机制加速图片加载。不只如此,闲鱼在这个点上还作了额外两个重要优化:框架
在上线之后,咱们陆续收到线上舆情的反馈,发现所有机型设置同一个缓存大小的作法并不是最优。特别是大缓存设置在低端机器上面,不只会出现体验变差,甚至还会影响稳定性。基于实际状况,咱们实现了一个能从Native侧获取机器基础信息的Flutter 插件。经过获取的信息,咱们根据不一样手机的配置设置不一样的缓存策略。在低端机器上面适当下降图片缓存的大小,同时在高端手机上将其适当放大。这样能在不一样配置的手机上获取最优的缓存性能。性能
熟悉APP开发的同窗都知道,成熟的图片加载框架通常都有多级缓存。除了常见的内存缓存,通常都会配置一个文件缓存。从加载效率上来讲,是经过空间换时间,提高加载速度。从稳定性来讲,这又不会过度占用宝贵的内存资源,出现OOM。可是惋惜的是,Flutter自带的图片加载框架并无独立的磁盘缓存。因此咱们在原生方案的基础上扩展了磁盘缓存能力。
在具体的架构实现上,咱们并无彻底本身撸一个磁盘缓存。咱们的策略仍是复用现有能力。首先咱们将Native图片加载框架的磁盘缓存的功能经过接口暴露出来。而后经过桥接的方式,将Native 磁盘缓存能力嫁接到Flutter层。Flutter侧进行图片加载的时候,若是内存没有命中,就去磁盘缓存中进行二次搜索。若是都没有命中才会走网络请求。
经过增长磁盘缓存,Flutter图片加载效率进一步提高。
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的部分会被强制回收掉。
基于上述的原理,咱们有两点建议:
尽可能避免大型的cell出现,这样能大幅下降cell频繁建立过程当中的性能损耗。其实这里影响的不只仅是图片加载过程。文字,视频等其余组件也都应该避免cell过于复杂致使的性能问题。
ListView能够经过设置cacheExtent 来设置预先加载的内容大小。经过预先加载能够提高view渲染的速度。可是这个值须要合理设置,并不是越大越好。由于预加载缓存越大,对页面总体内存的压力就越大。
这里须要客观指出:若是是一个纯Flutter APP,原生方案是完善,够用的。可是若是从混合APP的角度来讲,有以下两个缺陷:
1. 没法复用Native图片加载能力
毫无疑问,原生的图片方案是彻底独立的图片加载方案。对于一个混合APP来讲,原生方案和Native的图片框架相互独立,能力没法复用。例如CDN裁剪&压缩等能力须要重复建设。特别是Native一些独特的图片解码能力,Flutter就很难使用。这会形成APP范围内的图片格式的支持不统一。
2. 内存性能不足
从整个APP的视角来讲,采用原生图片方案的状况下,其实咱们维护了两个大的缓存池:一个是Native的图片缓存,一个是Flutter侧的图片缓存。两个缓存没法互通,这无疑是一个巨大的浪费。特别是对内存的峰值内存性能产生了很是大的压力。
通过多轮优化,基于原生的方案已经得到了很是大的性能提高。可是整个APP的内存水位线依然比较高(特别是Ios端)。现实的压力迫使咱们继续对图片框架进行更深度的优化。基于上述原生方案缺点的分析,咱们有了一个大胆的想法:可否彻底复用Native的图片加载能力?
怎样打通Flutter和Native的图片能力?咱们想到了外接纹理。外接纹理并不是是Flutter自有的技术,他是音视频领域经常使用的一种性能优化手段。
这个阶段咱们基于shared-Context的方案实现了Flutter和Native的纹理外接。经过该方案,Flutter能够经过共享纹理的方式,拿到Native图片库加载好的图片并展现。为了实现这个纹理共享的通道,咱们对engine层作了深度定制。细节过程以下:
该方案不只打通了Native和Flutter的图片架构,整个过程图片加载的性能也获得了优化。想要了解细节的同窗能够继续阅读这篇文章:万万没想到——Flutter外接纹理。
外接纹理是闲鱼图片方案的一次大跨越。经过该技术,咱们不只实现图片方案的本地能力复用,并且还能实现视频能力的纹理外接。这避免了大量重复的建设,提高了整个APP的性能。
这个优化策略是真真被逼出来的。在对线上数据分析之后,咱们发现Flutter页面栈有一个很是有意思的特色:
多页面栈状况下,底层的页面不会被释放。即使是在内存很是紧张的状况下,也不会执行回收。这样就会致使一个问题:随着页面的增多,内存消耗会线性增加。这里占比最高的就是图片资源的占比了。
是否是能够在页面处于页面栈底层的时候直接回收掉该页面内的图片呢?
在这个想法的驱动下,咱们对图片架构进行了新一轮的优化。整个图片框架中的图片都会监听页面栈的变化。当方发现本身已经处于非栈顶的时候,就自动回收掉对应的图片纹理释放资源。这种方案能使图片占用的内存大小不会随着页面数的变多呈现持续线性增加。原理以下:
须要注意的是:这个阶段页面判断位置实际上是须要页面栈(具体来讲就是混合栈)提供额外的接口来实现的。系统之间的耦合相对较高。
打通Native和Flutter侧图片框架之后,咱们发现了一个意外收获: Native和Flutter能够共用本地图片资源了。也就是说,咱们再也不须要将相同的图片资源在Flutter和Native侧各保留一份了。这样能大幅提高本地资源的复用率,从而下降总体的包大小。基于这个方案,咱们实现了一套资源管理的功能,脚本能自动同步不一样端的本地图片资源。经过这样提高本地资源利用率,下降包大小。
原生的Image是没有PlaceHolder功能的。若是想用原生方案的话,须要使用FadeInImage。针对闲鱼的场景咱们有不少定制,因此咱们本身实现了一套PlaceHolder的机制。
从核心功能上来讲,咱们引入了加载状态的概念分为: 1. 未初始化 2. 加载中 3. 加载完成 等。针对不一样的状态,能够细粒度的控制PlaceHolder的展现逻辑。
随着闲鱼业务的不断推动,engine的升级的成本是咱们必需要考虑的事情。可否不改engine实现一样的功能是咱们核心的述求。(PS: 我认可咱们是贪心的)
外接纹理的方案须要经过桥的方式跟native的能力作通讯。这里包括图片请求的传递和图片加载各类状态的同步。特别是在listview快速滑动的时候,经过桥发送的数据量仍是可观的。当前方案每一个图片加载时都会单独进行桥的调用。在图片数量比较多的状况下,这显然会是一个瓶颈。
在实现图片回收方案的时候,目前方案须要栈提供是否在栈底层的接口。这里就产生方案耦合,很难抽象出一个独立干净的图片加载方案。
时间来到了2020年,随着对Flutter基础能力理解的逐步深刻,咱们实现了一个总体方案更优的图片框架。
外接纹理能够不用修改engine么?答案是确定的。
其实Flutter是提供了官方的外接纹理方案的。
并且Native操做的texture和Flutter侧显示的texture在底层是同一对象,并没有产生额外的数据copy。这样就保证了纹理共享的足够高效。那为何闲鱼以前会单独基于shared-Context本身实现一套呢?1.12版本以前,官方Ios的外接纹理方案有性能问题。每次渲染的过程当中(无论纹理是否有更新)都会频繁获取CVPixelBuffer,形成没必要要的性能损耗(过程有加锁损耗)。该问题已经在1.12版本中修复(官方commit地址),这样官方方案也足够知足需求。在这样的背景下,咱们从新启用官方方案来实现外接纹理功能。
以前提到过,老版本的基于页面栈的图片资源回收须要强依赖栈功能的接口。一方面产生了没必要要的依赖,更重要的是,总体方案没法独立成通用方案。为了解决这个问题,咱们对Flutter底层进行了深刻的研究。咱们发现Flutter的layer层能够稳定感知到页面栈的变化。
而后每一个页面经过context获取的router对象做为标识对一个页面中的全部的图片对象进行从新组织。全部获取到同一个router对象的标识成同一个页面。这样就能以页面为单位对全部的图片进行管理。总体上经过LRU的算法来模拟虚拟页面栈结构。这样就能对栈底页面的图片资源实现回收了。
1. 通道的高度复用
首先咱们以一帧为单位对这一帧中的图片请求进行聚合,而后在一次通道请求中传递给Native的图片加载框架。这样能避免频繁的桥调用。特别在快速滚动等场景下优化效果尤其明显。
2. 高效的纹理复用
使用外接纹理进行图片加载之后,咱们发现复用纹理能够进一步提高性能。举一个简单的场景。咱们知道电商场景中,商品展现常常会有标签,打底图这样的图片。这类图片每每在不一样的商品上会出现大量重复。这时候,能够将已经渲染好的纹理,直接复用给不一样的显示组件。这样能进一步优化GPU内存的占用,避免重复建立。为了精确对纹理进行管理,咱们引入了引用计数的算法来管理纹理的复用。经过这些方案,咱们实现了纹理跨页面高效复用。
此外,咱们将纹理和请求的映射关系移动到了Flutter侧。这样能在最短路径上完成纹理的复用,进一步减小了桥的通讯的压力。
因为最新的版本目前还在灰度,具体数据后续会写文跟你们详细介绍。下属数据主要以方案二为主。
内存优化
多页面栈的内存优化,在多页面场景下对内存优化做用明显。咱们作了一个极限试验效果以下:(**测试环境**,非闲鱼APP)
可见多页面栈的优化,能够将多Flutter页面的内存占用控制得更好。
经过接入外接纹理,本地资源获得了更好的复用,包大小下降1M。早期闲鱼接入Flutter,会以改造现有页面为切入点。资源重复状况比较严重,可是随着闲鱼Flutter 新业务愈来愈多。Flutter和Native的重复资源愈来愈少。外接纹理对包大小的影响已经逐步变弱。
本文介绍了闲鱼在Flutter图片框架方向上所作的持续优化。介绍了闲鱼不一样时期,典型的图片技术方案的细节。但愿能够给到读者一些启发。这是一场没有尽头的旅行,咱们对闲鱼图片的优化还会持续。特别是咱们最新的方案,受限篇幅,本文只是作了初步介绍。更多技术细节,包括测试数据,咱们随后还会专门写文继续给你们作介绍。方案完善之后,咱们也会逐步开源。