您好!
欢迎来到京东云开发者社区
登录
首页
博文
课程
大赛
工具
用户中心
开源
首页
博文
课程
大赛
工具
开源
更多
用户中心
开发者社区
>
博文
>
京喜APP - 图片库优化
分享
打开微信扫码分享
点击前往QQ分享
点击前往微博分享
点击复制链接
京喜APP - 图片库优化
29****
2023-07-04
IP归属:北京
12640浏览
# 介绍 京喜APP早期开发主要是快速`原生化`迭代替代原有`H5`,提高用户体验,在这期间也积累了不少性能问题。之后我们开始进行一些性能优化相关的工作,本文主要是介绍`京喜图片库`相关优化策略以及关于图片相关的一些关联知识。 ## 图片性能问题 作为电商APP,图片在各个业务场景被大量使用。我们需要做到尽可能降低`网络消耗`/`内存消耗`/`硬盘消耗`,同时不降低`图片质量`,提高图片`加载速度`,给用户带来更好的使用体验。基于这些性能目标,我们也通过初步性能评估梳理出了一些性能问题: #### 图片加载慢/流量消耗高 图片链接主要由后台接口下发,下发图片`格式`和`尺寸`由每个业务后台指定。部分业务没有使用更小的图片格式比如`WebP`,或图片`尺寸`过大,都会使图片过大导致网络消耗高。特别是网络状况不佳的场景,图片加载过慢给用户带来不好的体验。同时也会导致更多的`I/O读写`和`解码`耗时,造成更多的电量消耗。 #### 图片内存占用高 经过初步的APP内存使用评估,图片内存消耗占APP总内存消耗的比例`最高`,特别是大尺寸图片会占用很多内存。一方面APP占用太高内存退到后台容易被系统杀死,导致下次打开重新启动影响体验。另一方面APP大量使用内存,容易被系统杀死产生`OOM`。特别是我们目前有大量的低端设备用户,设备内存相对比较低。 # 优化方向 基于上面分析出的一些性能问题,我们对图片框架进行了整体重构优化。一方面是`降低`图片网络传输,提高图片加载速度。另一方面是`减少`图片内存消耗。 ![图片优化.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a95c673969ac4ff78771427deef67368~tplv-k3u1fbpfcp-watermark.image) ## 最小化网络传输 京东`图片服务器`提供了多种处理功能,例如图片`格式转换,图片降质,图片缩放,图片圆角`等功能。这些功能通过在图片`URL`中添加特定参数实现,图片服务器会根据参数设置提前将图片处理完成并保存到`CDN`服务器。我们可以通过添加图片处理参数,减少图片传输大小。 虽然后台可以提前进行`URL预处理`,下发已添加过图片参数的`图片URL`。但是由于对接后台业务很多,每个业务图片参数设置差异很大无法统一,而且可能会造成性能影响,例如没有使用`webP`图片格式,下发太大的`图片尺寸`。同时考虑到推动各业务后台修改成本也很高,并且前端机型多,不同机型需要使用不同的图片尺寸。另外也不方便灰度降级功能,后续功能修改也不方便。所以在`客户端`进行图片`URL预处理`是更好的方式,可以统一控制,也方便之后功能更新。 ### 图片URL预处理 ![URL预处理](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ec3184fa7db14037aa506d4fd3bf8c9e~tplv-k3u1fbpfcp-zoom-1.image) 图片库在网络图片加载前,检测是否是`京东`域名的图片`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: ```swift 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`大屏幕设备和`高端设备`(内存比较高),适当增加更多内存上限。 ## 优化成果 ![图片优化成果](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/25a92e8bbdbf4b0f89d1b47629d9d3db~tplv-k3u1fbpfcp-zoom-1.image) ##### 其他收益 - `域名统一` - 减少了`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`帧进行解码之后渲染。因为并不会把解码过的帧保存,所以会导致频繁解码导致内存波动大。经过优化,对已经解码过的帧进行保存,避免重复解码的消耗,同时避免内存的波动。 > 优化前内存波动很明显 ![优化前](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8e7e52a269ef4638b9a3af563a1d0b0a~tplv-k3u1fbpfcp-zoom-1.image) > 优化后内存倾于平稳 ![优化后](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1fff1d34493a46eb928a88d8c0b72d4d~tplv-k3u1fbpfcp-zoom-1.image) > 提示:保存每一帧也会导致更多的内存消耗。目前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](https://developers.google.com/speed/webp) - [手淘图片库HEIC使用](https://zhuanlan.zhihu.com/p/265881086) - [动画WebP和GIF比较](https://developers.google.com/speed/webp/faq#why_should_i_use_animated_webp) - [WebP支持](https://caniuse.com/?search=webp) - [APNG支持](https://caniuse.com/?search=apng) - [AVIF](https://jakearchibald.com/2020/avif-has-landed/)
上一篇:百万并发场景中倒排索引与位图计算的实践
下一篇:测试环境治理之MYSQL索引优化篇
29****
文章数
6
阅读量
2029
作者其他文章
01
基于 prefetch 的 H5 离线包方案
前言对于电商APP来讲,使用H5技术开发的页面占比很高。由于H5加载速度非常依赖网络环境,所以为了提高用户体验,针对H5加载速度的优化非常重要。离线包是最常用的优化技术,通过提前下载H5渲染需要的HTML/JS/CSS资源,加载时直接使用本地缓存资源避免额外的网络请求提高加载速度。本文主要是介绍团队在离线包技术方案上的探索,以及基于prefetch的离线包实现方案如何减少维护成本和开发成本。现有方
01
京喜APP - 图片库优化
介绍京喜APP早期开发主要是快速原生化迭代替代原有H5,提高用户体验,在这期间也积累了不少性能问题。之后我们开始进行一些性能优化相关的工作,本文主要是介绍京喜图片库相关优化策略以及关于图片相关的一些关联知识。图片性能问题作为电商APP,图片在各个业务场景被大量使用。我们需要做到尽可能降低网络消耗/内存消耗/硬盘消耗,同时不降低图片质量,提高图片加载速度,给用户带来更好的使用体验。基于这些性能目标,
01
使用Swift提高代码质量
前言京喜APP最早在2019年引入了Swift,使用Swift完成了第一个订单模块的开发。之后一年多我们持续在团队/公司内部推广和普及Swift,目前Swift已经支撑了70%+以上的业务。通过使用Swift提高了团队内同学的开发效率,同时也带来了质量的提升,目前来自Swift的Crash的占比不到1%。在这过程中不断的学习/实践,团队内的Code Review,也对如何使用Swift来提高代码质
01
移动端APP组件化架构实践
theme: smartblue前言对于中大型移动端APP开发来讲,组件化是一种常用的项目架构方式。个人最近几年在工作项目中也一直使用组件化的方式来开发,在这过程中也积累了一些经验和思考。主要是来自在日常开发中使用组件化开发遇到的问题以及和其他开发同学的交流探讨。本文通过以下问题来介绍组件化这种开发架构的思想和常见的一些问题:为什么需要组件化组件化过程中会遇到的挑战和选择如何维护一个高质量的组件化
29****
文章数
6
阅读量
2029
作者其他文章
01
基于 prefetch 的 H5 离线包方案
01
使用Swift提高代码质量
01
移动端APP组件化架构实践
添加企业微信
获取1V1专业服务
扫码关注
京东云开发者公众号