//img11.360buyimg.com/img/pingou-head/25.jpg"),
placeholder: nil, options: [.imageSize(CGSize(width: 40, height: 40))])
磁盘缓存优化
图片缓存查找优化
设置图片不同的size
参数会导致更多的图片下载和磁盘缓存,例如同样一张图片100px
、200px
、300px
尺寸因为URL
不同会下载3次,同时缓存也无法不同。由于图片库通常默认使用URL
作为图片缓存key
,所以我们需要针对图片缓存key
查找图片进行优化改造。简单来讲,相同的图片小size
的图片可以直接复用更大size
的缓存,这样当存在更大尺寸图片时,可以避免图片直接下载并且复用磁盘缓存。
降低图片内存消耗
png
/jpg
等图片格式在显示之前都需要经过解码
生成一张位图,之后根据位图创建纹理
传给GPU做渲染。一张位图的内存消耗大概是像素宽
x像素高
x位深
。通常图片使用的是RGBA
,位深为32位。一张500px_500px
的大概1MB
内存。对于GIF
图片因为本身有多帧,所以最终的内存消耗为单帧内存
x帧数
。
我们的优化方向一方面是通过图片缩放的方式,减少图片位图的内存消耗。另一方面限制图片缓存上限避免缓存使用过高。
图片缩放
通过上面URL
预处理过程让图片服务器下发更小的图片格式,已经降低了一部分内存。但是URL
预处理只处理了jd
域名的jpg
/png
图片,对于GIF
或京东
域名外的图片没有处理,包括一部分URL
转换后加载失败的图片。所以对于这部分图片,我们会在端侧做图片缩放的处理,降低内存消耗。例如一张300px_300px
包含100帧
的GIF图片,实际显示区域只有50px_50px
,优化后总内存消耗可从30MB+
内存降低到3MB
。
GIF动态帧率播放
之前根据线上监控数据发现,部分页面场景偶尔会配置尺寸大/帧数多
的GIF
图片,导致内存占用极高。例如一张500x400px
播放200帧
的GIF图片会占用100MB+
内存消耗。所以针对这种场景,我们针对GIF
做了减帧播放改造。当GIF
图片总内存消耗大于一定量级时(例如图片内存缓存上线的20%),将GIF
播放的帧数适当减少,每一帧的播放时间增加,这样可以将内存控制在一定范围之内。
提示:这里也可以通过 GIF 图片缓存 Buffer 控制内存总量,但是会导致更频繁的解码造成更多的 CPU 消耗。
图片内存缓存上限
图片缓存的设计目的是减少图片解码
消耗。图片第一次使用的时候,将图片进行解码
后的位图保存在内存中,这样可以避免下次使用时避免重复解码
。虽然图片内存高可以尽量避免图片重复解码,但是占用太高内存也会导致APP后台被系统杀掉或产生OOM
等问题。所以我们应该将内存缓存控制在一定范围内。
例如iOS
的第三方图片库SDWebImage
/Kingfisher
默认都使用系统库NSCache
来实现内存缓存。虽然NSCache
会在设备内存紧张时回收内存,但是默认并不限制可保存内存最大字节数,所以在设备内存可用的情况下内存可以一直增加。所以通过设置图片缓存上限,防止图片缓存占用太高内存。图片缓存定义了一个默认的初始值上限,之后对于3x
大屏幕设备和高端设备
(内存比较高),适当增加更多内存上限。
优化成果
其他收益
域名统一
- 减少了10%+
的重复图片下载和内存消耗。同时减少之前多域名
图片加载时重复创建HTTPS
请求的过程,减少图片加载时间。
其他策略
加载异常处理
因为少量图片通过URL
预处理转换后,可能会存在图片不存在的异常场景导致加载失败
。所以当发生图片加载失败时,我们还是需要加载原始图片URL。但是这里需要屏蔽一些特殊的加载错误,避免非必要的加载,例如无网络
/网络超时
/主动取消加载
等错误。之后会将错误图片URL
上报到后台,方便之后调整URL
转换策略,也可以发现一部分错误的图片URL
推动业务修改。同时将这部分连接加入到错误连接
缓存中,避免下次重复执行预处理和重复上报。
线上配置
目前存在的一些功能,例如URL预处理
/统一域名
/WebP
使用等功能,都添加了线上配置,方便灰度/降级。一在出现问题时可以降级某些功能,新功能上线时也可以进行灰度测试。
大图检测
需要有一个机制及时发现图片不符合规范的问题。一方面我们通过线上灰度检测的方式,当发现大图片时会进行上报,后续推动业务方进行优化。另一方面我们在日常测试阶段,会开启Debug
检测工具,当检测到大图片时,通过图片翻转
/高亮背景颜色
的方式提醒业务开发同学进行优化。
Flutter图片库优化
目前京喜APP有10+
个二级界面是基于Flutter
开发,所以我们也针对Flutter
图片加载做了一些优化。
对接原生图片库
因为Flutter
框架自带图片库只提供内存图片缓存,并不支持硬盘缓存,所以会导致图片重复下载。所以我们通过重写ImageProvider
,当加载网络图片时,通过Channel
调用原生图片库,原生图片库下载图片到本地磁盘后,返回图片文件目录。之后Flutter
通过文件目录加载解码图片显示。这样一方面可以利用原生图片库相关优化能力,同时也可以复用
图片硬盘缓存避免重复下载。
减少内存消耗
使用Image
组件时,通过设置cacheHeight
/cacheWidth
,将图片解码为置顶像素
宽高的位图尺寸,减少内存消耗。同时因为Flutter
内存消耗相对原生
更高,所以在Flutter
界面关闭时,通过调用imageCache
方法清除图片内存消耗降低内存消耗。
GIF优化
动画优化
- 因为通常使用Flutter
都是混合栈的机制,原生
和Flutter
界面在页面导航中相互跳转。所以当Flutter
界面存在GIF
图片时,跳转到原生以后GIF
动画还会一直执行。所以我们通过在Image
组件内监听Flutter engine
发送的生命周期通知,当Flutter界面不在栈顶时,停止GIF
动画执行,减少内存和CPU消耗。
减少解码次数
- Flutter框架内部对GIF
渲染的处理方式,在屏幕每一帧判断当前需要显示的GIF帧,之后对该GIF
帧进行解码之后渲染。因为并不会把解码过的帧保存,所以会导致频繁解码导致内存波动大。经过优化,对已经解码过的帧进行保存,避免重复解码的消耗,同时避免内存的波动。
优化前内存波动很明显
优化后内存倾于平稳
提示:保存每一帧也会导致更多的内存消耗。目前APP中通常是小尺寸的GIF所以整体可控。可以考虑设置缓冲区上限来控制缓存的图片帧数避免内存过高。
后续优化方向
更优的缓存算法
优先移除最大内存
- iOS系统NSCache
实现。通过设置最大内存数,当内存不足时优先移除最大的值。
LRU缓存
- 优先淘汰最久未使用的图片内存。对于很多二级界面
的场景,用户打开界面后并不会再次打开。但是因为这些图片缓存是最后使用,所以清除内存时也会最后移除,但是在这种场景下就不太合适。
界面栈管理
- 当界面关闭
时将该界面的所有的图片内存移除,但是对于经常会打开的界面会导致频繁图片编解码
也不太合适。
所以针对不同的业务场景使用不同的回收方式可能更加合适:
- 对于
购物车/我的订单
这类界面,用户每次加载的图片基本固定,所以更适合在内存中常驻,当内存消耗过高时再回收。
- 对于
商详/搜索商品列表