作者:京东零售 何骁
介绍
京喜APP早期开发主要是快速 原生化 迭代替代原有 H5 ,提高用户体验,在这期间也积累了不少性能问题。之后我们开始进行一些性能优化相关的工作,本文主要是介绍 京喜图片库 相关优化策略以及关于图片相关的一些关联知识。
图片性能问题
作为电商APP,图片在各个业务场景被大量使用。我们需要做到尽可能降低 网络消耗 / 内存消耗 / 硬盘消耗 ,同时不降低 图片质量 ,提高图片 加载速度 ,给用户带来更好的使用体验。基于这些性能目标,我们也通过初步性能评估梳理出了一些性能问题:
图片加载慢/流量消耗高
图片链接主要由后台接口下发,下发图片 格式 和 尺寸 由每个业务后台指定。部分业务没有使用更小的图片格式比如 WebP ,或图片 尺寸 过大,都会使图片过大导致网络消耗高。特别是网络状况不佳的场景,图片加载过慢给用户带来不好的体验。同时也会导致更多的 I/O读写 和 解码 耗时,造成更多的电量消耗。
图片内存占用高
经过初步的APP内存使用评估,图片内存消耗占APP总内存消耗的比例 最高 ,特别是大尺寸图片会占用很多内存。一方面APP占用太高内存退到后台容易被系统杀死,导致下次打开重新启动影响体验。另一方面APP大量使用内存,容易被系统杀死产生 OOM 。特别是我们目前有大量的低端设备用户,设备内存相对比较低。
优化方向
基于上面分析出的一些性能问题,我们对图片框架进行了整体重构优化。一方面是 降低 图片网络传输,提高图片加载速度。另一方面是 减少 图片内存消耗。

最小化网络传输
京东 图片服务器 提供了多种处理功能,例如图片 格式转换,图片降质,图片缩放,图片圆角 等功能。这些功能通过在图片 URL 中添加特定参数实现,图片服务器会根据参数设置提前将图片处理完成并保存到 CDN 服务器。我们可以通过添加图片处理参数,减少图片传输大小。
虽然后台可以提前进行 URL预处理 ,下发已添加过图片参数的 图片URL 。但是由于对接后台业务很多,每个业务图片参数设置差异很大无法统一,而且可能会造成性能影响,例如没有使用 webP 图片格式,下发太大的 图片尺寸 。同时考虑到推动各业务后台修改成本也很高,并且前端机型多,不同机型需要使用不同的图片尺寸。另外也不方便灰度降级功能,后续功能修改也不方便。所以在 客户端 进行图片 URL预处理 是更好的方式,可以统一控制,也方便之后功能更新。
图片URL预处理

图片库在网络图片加载前,检测是否是 京东 域名的图片 URL 。如果 域名 匹配,图片框架先对图片 URL 进行预处理,预处理包括 域名统一 , 添加缩放参数 , 添加webP参数 , 添加降质参数 的方式减少图片网络传输大小。
提示:因为后台返回的图片 URL 可能会带有一部分图片处理参数,例如 https://img11.360buyimg.com/img/pingou-head/25.jpg!webp ,直接追加图片参数可能会导致图片处理参数不生效,或格式错误导致加载失败。所以转换时会先将所有图片参数提前计算出来,之后一起处理,避免添加重复参数。
域名统一
目前图片服务器提供了多个图片域名可使用,例如 m.360buyimg.com , img10.360buyimg.com 等多个域名。 m.360buyimg.com 主要提供给 移动端 使用。但是由于对接了各种业务后台,导致接口会下发不同的域名图片。图片使用 不同域名 可能会导致以下问题:
- 不利于缓存复用 - 图片框架通常默认以 URL 字符串生成图片 缓存key ,不同 域名 导致生成不同的 缓存key 。 硬盘缓存 无法复用会导致图片重复*载下**, 内存缓存 无法复用导致同样的图片占用多份内存。
- 不利于HTTP/2连接复用 - 大部分界面图片比较多,很多场景都会同时加载多张图片,特别是 首屏 通常会加载几十张图片。当加载多个图片时,每个域名都需要重新建立 HTTPS 连接,经历 DNS解析/TCP连接/TLS握手 过程(目前一次HTTPS请求创建过程大概耗时 50-150ms )。如果利用 HTTP/2 链接复用就只需要创建一次 HTTPS 请求,之后的图片请求可以减少这部分的耗时。
所以在预处理时,如果是 京东 域名的图片,将图片URL 域名 统一替换为 m.360buyimg.com 。
追加图片参数
图片缩放
很多业务后台返回的原始 图片URL 的 size 都比客户端实际显示的 size 要大。一方面导致使用更多的网络流量造成浪费。另一方面会导致占用更多内存。同时因为图片 size 和实际显示 size 不一致导致 像素不对齐 , GPU 需要做额外的插值处理,也会一定的影响渲染性能。所以我们通过添加缩放参数的方式,指定图片服务器下发更小和更匹配实际显示 size 的图片尺寸。
动态scale计算尺寸
因为 iOS 设备主要使用 2x/3x 的分辨率,所以业务方使用API时需要传入对应的pt size 大小,图片库内部根据设备的 scale 进行动态计算出真实的像素宽高。
提示: android 设备因为屏幕差异比较大,更适合使用固定的 scale 。太多的图片尺寸不利于 CDN 缓存,无缓存的时候需要对图片进行相关参数处理,图片处理本身是耗时操作。
Scale降级
- 低端机降级 - 对于部分 3x scale的低端设备,因为机器本身内存比较低,使用 3x 分辨率计算出来的图片 像素 宽高比较大,会造成更多的内存消耗以及解码/渲染更多的性能消耗。所以对于宽高超过一定要求的图片,降级到使用 2x 分辨率来计算 像素 宽高,减少设备性能消耗。
- iPad降级 - 因为目前APP并没有针对 iPad 做特定优化,所以iPad设备下默认是放大显示。这会导致在 iPad 下图片尺寸计算出来特别大。所以也是针对iPad图片尺寸做了特定限制,防止下发图片尺寸过大。
- 大图片降级 - 正常情况下图片 宽/高 不应该超过屏幕 宽/高 。为了防止部分业务使用过大的图片 size ,所以添加了一个限制,最终生成的图片 像素 尺寸不能超过屏幕 宽/高 。
降质
图片服务器支持 0-100 的图片质量参数设置,通过降低图片质量可以减少图片大小,但是质量降低太多也会影响图片的观看体验。我们将图片质量参数设置为 q70 ,指定图片服务器下发 70% 质量的图片。对于大部分业务,一方面可以大幅减少图片*载下**大小,同时也可以保证观看体验。通过添加图片降质参数至少可以减少 30-40% 的图片大小。
使用WebP
按照 Google 官方的数据,与 PNG 相比, WebP 无损图像的字节数要少 26% 。 WebP 有损图像比同类 JPG 图像字节数少 25-34% 。图片服务器支持转换 webP 格式,可以减少图片大小。针对 png / jpg 图片格式,添加 webP 参数,指定图片服务器下发 webp 格式。虽然 webP 相比 png / jpg 图片解码需要更长时间,但相对网络传输速度提升还是很大。
提示:由于目前图片服务器并不支持 GIF 转 webP ,GIF并没有做处理。
URL预处理缓存
添加轻量缓存,提高 URL 转换性能。因为 URL 转换本身有一定的耗时,而且单个图片 URL 可能会多次加载/多次转换。转换后的 URL 会直接保存到缓存中,下次使用可以直接返回。缓存 key 由 URL +相关图片 转换参数 拼接组成。
图片API设计
图片处理参数通过 options 设置,默认使用 q70 图片质量以及 webP 格式。业务方在调用加载图片方法时传入,下面是 iOS 端的API:
imageView6.jx.setImage(url: URL(string: "https://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缓存 - 优先淘汰最久未使用的图片内存。对于很多 二级界面 的场景,用户打开界面后并不会再次打开。但是因为这些图片缓存是最后使用,所以清除内存时也会最后移除,但是在这种场景下就不太合适。
- 界面栈管理 - 当界面 关闭 时将该界面的所有的图片内存移除,但是对于经常会打开的界面会导致频繁图片 编解码 也不太合适。
所以针对不同的业务场景使用不同的回收方式可能更加合适:
- 对于 购物车/我的订单 这类界面,用户每次加载的图片基本固定,所以更适合在内存中常驻,当内存消耗过高时再回收。
- 对于 商详/搜索商品列表 这类界面,通常商品列表展示的图片不一样并且用户也不会频繁进某一个特定的商详,所以更适合 优先 移除这部分的内存。
- 对于部分弹窗功能,图片显示后并不会再次使用,可以考虑不添加到内存中。
使用更好的图片格式
使用更好的图片格式通常可以带来更小的图片字节大小。同时因为压缩率的提高,可以在减少大小的同时提高图片质量。
提示:使用系统支持硬解码的图片格式更有优势。硬解码就是使用 GPU 进行解码,相比使用 CPU 软解码性能更好更省电。
- APNG/动画WebP代替GIF - 按照 Google 官方的说法, GIF 转换为 有损WebP 的字节数缩小了64%,而 无损WebP 字节数缩小了19%。所以使用 动画WebP 可以减少更多的网络流量传输。 APNG 是 Mozilla 推出的基于 PNG 的动图格式并且完全支持 RGBA ,相比 GIF 可以减少 20%+ 的图片大小。而且 GIF 本身只支持256色索引颜色以及1位alpha(加上透明度后,边缘会出现明显的锯齿),使用 APNG / WebP 也可以带来相比 GIF 更好的显示效果。
提示:相比 GIF , WebP 的解码比 GIF 占用更多的CPU资源。 有损WebP 的解码时间是 GIF 的2.2倍,而 无损WebP 的解码时间是 GIF 的1.5倍。
- HEIC - HEIC 是基于 H.265 视频编码格式推出的图片格式。 HEIC 相比 WebP 可以减少20%+的图片大小,并且编解码性能更好。在系统兼容性上, Android 9.0 以上的系统支持 HEIC 。苹果在 iOS14 以上系统才提供了 WebP 硬解码,之前的系统只能使用软解码,而 HEIC 在 iOS11 之后的机器上都已经支持硬解码,不过并不支持 浏览器 。
- AVIF - AVIF 是基于 AV1 编码格式推出的图片格式。 AVIF 相比 WebP 可以减少30%+的图片大小。不过目前只有 Android 12 以上的版本支持。
提示:这里主要是以 VP8 编码格式的 WebP , VP9 编码格式的 WebP 整体性能和 HEIC 差异不大。不过这些图片格式需要图片服务器支持之后才能使用。
Flutter
虽然我们对 Flutter 图片库做了一些优化,但总体上还有很多优化空间。包括业界有在使用的基于 纹理 的图片方案。在原生侧将图片解码后,通过 Flutter 引擎创建 纹理 。之后讲图片纹理 id 传递给 Flutter 进行渲染。这样可以统一在原生侧管理图片内存缓存,优化之前 Flutter 和 原生 都分别有一份内存缓存的方式。而且针对于混合栈的导航栈方式,也可以更好的进行图片内存回收。另外针对 Flutter ,需要提供更灵活的图片内存回收策略,避免内存消耗过高。
提示:纹理可以复用内存中的 位图 缓存,所以并不会导致更多的内存占用。纹理方式大概能减少 30% 的内存消耗相比Flutter引擎图片库,主要是一些其他对象使用导致。
优化H5图片加载
我们可以通过拦截 WebView 图片加载的方式,让原生图片库来*载下**图片之后传递图片 二进制 数据给 WebView 显示。
减少流量消耗
通过这种方式,我们可以将原生图片库 URL预处理 相关功能支持到 H5 图片,减少 H5 加载过程中图片流量消耗,提高图片加载速度。同时因为APP 原生 和 WebView 图片缓存机制是相互独立的,所以通过统一在原生侧管理图片缓存,可以减少相同图片的重复*载下**。
支持更多图片格式
例如在 iOS 系统上, WKWebView 目前只支持 PNG / JPG / GIF 图片格式。所以我们可以通过在原生端实现*载下** WebP / HEIC 图片,之后对图片进行 解码 再传给 WebView ,这样就可以支持其他图片格式的显示。
提示:因为 WebView 不支持直接传递 位图 二进制数据显示,所以需要提前转换为 PNG / JPG 二进制数据传递。所以对于其他图片格式增加一次 PNG / JPG 编码过程会造成更多的性能消耗。不过对于 Android 系统应该可以在web内核层优化减少这块消耗。
总结
本文并没有讲底层图片加载库的具体实现,目前图片库不管是直接用第三方库还是自研图片库实现方式通常差异不大。我们更多是关注自身业务以及如何利用图片服务器能力最大化改善网络图片加载性能。所以部分策略可能不一定针对所有APP都合适,应该针对自身业务场景仔细评估优化方案。
扩展链接
- WebP
- 手淘图片库HEIC使用
- 动画WebP和GIF比较
- WebP支持
- APNG支持
- AVIF