本期内容:
项目背景
技术方案
总结
对于一个电商 APP,网络图片加载的速度,会直接影响到用户的体验。如果图片加载缓慢,长时间展示默认占位图,对用户体验是非常大的损害。比如,这是首页优化前的网络图片加载现状,从动图中我们可以发现,存在大量的占位图,需要的图片加载非常缓慢,会极大的影响用户的体验。
正常情况下,我们APP是如何加载一张网络图片的呢?当我们去加载一张图片,会先判断是否存在本地磁盘缓存,否则,再去从网络上下载,而磁盘的读取速度正常情况下,都要快于从网络中下载。所以,在图片加载优化中,我们可以进行网络加载优化和图片解码优化两个部分。在实践过程中,我们发现网络加载优化是收益最大的一个优化方案。同样,这也是本文的主要的内容。
目前 APP 图片地址基本都是 HTTPS,相对于 HTTP 来说,多了加密过程带来的额外耗时,但是对于图片等资源,我们通常不会有安全性上的考虑,所以我们可以将 HTTPS 替换为 HTTP,加快连接速度。
默认 Glide 会使用 HttpUrlConnection 作为图片内置网络库,我们可以将其替换为更优的 Okhttp。
通过添加一个透明的 Fragment 来感应页面的生命周期,从而来管理图片请求,例如,当页面不可见时,会暂停当前的图片请求,当页面销毁时,会清空剩余的网络请求。当新的页面可见时,不可见的页面图片请求会暂停,这时候新页面的图片请求优先级是最高的,这也是能提高用户体验的方法。
之前 APP 中有个 GIF 列表页面,即使在将 GIF 转换为 WebP 格式后,单张图片大小依然在 1M 左右,一般情况下,列表可见区域存在 4~6 张卡片,这意味着,同时会有 4~6 个 GIF 图片请求,再加上占位静态图,大概会有 6 M 的图片加载,当列表快速滑动时,会导致列表图片出现长时间的空白,极大损害了用户体验。
首先,我们可能会想到控制 GIF 的大小,比如采用减少动图帧数等等 ,但这会明显导致动图不流畅,既然不能减少图片大小,那么我们是否可以另辟蹊径呢?
虽然不能减少图片大小,但是我们可以控制加载数量和优先级。从用户角度去思考问题,当用户滑动列表停止时,他最想看到的是当前可见区域的图片,那么我们是否可以优先加载可见区域的图片呢?
正常的图片加载顺序 VS 优化后的图片加载顺序(数字表示图片加载顺序)
我们可以通过优先级队列 PriorityQueue 来处理请求优先级问题,通过 getLocalVisibleRect 方法获取当前控件可见区域,然后根据可见区域大小来判断优先级。接着,我们可以只在列表滚动停止时,才去加载 GIF,其他情况,正常加载静态图。
通过增加一个自定义的 OkHttp 拦截器,我们可以获取网络图片加载的数据响应,当超过一定的阀值时,我们可以 Toast 提示,并打印相关的图片地址。
当拼接后的图片请求失败时,我们会使用原始图片地址重新发起一次请求,同样的,这也是在拦截器里面去做,对业务来说,是无感知的。
虽然七牛云 CDN 支持转换 WebP 格式图片,但这个处理时间取决于图片的大小,通常在将 GIF 转换成 WebP 的时候,处理时间过长,导致图片请求超时,这种情况,我们可以让服务端在上传 GIF 图片时候,同时生成一张对应的 WebP 格式图片。
通过网络加载优化,我们能够提升一些图片加载的速度,但是原始图过大的情况,提升效果并不明显。而对于原图过大问题,可以考虑用图片裁剪,压缩,格式转换等多种方式去解决它。
当运营配置活动或者商品图片时候,通常会使用原始图片,它的像素尺寸对于移动端来说是过大的,例如,原图是 1280x1280,但我们只需要展示一个 100x100 可视区域,那超出的尺寸就是一种浪费,这种浪费不仅仅是流量上的,同时也是内存上的浪费。
打个比方,Android 要展示一张图片,需要转码成 Bitmap(位图)格式,一张 1280x1280 像素的图片,如果用 ARGB 格式存储,那么它需要 1280x1280x4 Byte 内存空间,大概为 6M 左右,中低端机默认分配的虚拟机内存一般只有 100 ~ 200 多M,所以,一张没有裁剪的原图,对于内存的压力可想而知。不同的 Android 版本对于 Bitmap 的内存管理策略是不一样的
Android 2.3.3 以及更低的版本,Bitmap 的像素数据存储在 Native 内存中
Android 3.0 ~ Android 7.1,则是存储在 Dalvik 堆上,即虚拟机 Java 内存中
Android 8.0 以及更高的版本,又重新存储在 Native 内存中
因为目前 APP 使用的图片 CDN 是七牛云,同时它也提供了图片裁剪服务,所以我们可以依赖于七牛云的图片服务。七牛云的 imageView2 图片处理服务提供以下裁剪参数:裁剪的服务有了,接下来是如何接入它。
最笨的方式,可以通过写个工具类,处理裁剪参数的拼接,在加载图片的时候,先调用整个方法,虽然简单,但是却繁琐,而且依赖于使用方自觉调用,这个方案不可行。
目前,APP 使用的图片库是 Glide,我们可以自定义 ModelLoader 用来实现拼接裁剪参数,其中 width 和 height 一般是控件尺寸,如果控件没有具体尺寸,则这两个值会兜底为屏幕尺寸,一般来说,我们不会加载一张超过屏幕尺寸的图片,拼接后参数后,再将新的图片地址返回即可。这里有个小技巧,一般我们不会同时指定宽和高的尺寸,可以优先使用宽度,因为一般业务场景,都是固定了宽度,让高度跟着图片自适应。Android 千奇百怪的机型太多了,如果都要实际尺寸去裁剪,CDN 上可能会保留很多份裁剪尺寸,所以我们可以设置一个梯度值,向下取整就可以。
除了裁剪图片尺寸之外,我们还可以将图片转换为 WebP 格式,相对于 PNG 和 JPG,WebP 无疑要小的多,同时图片效果相差无几,同时 WebP 能支持透明通道和动画,所以完全可以用它来替换 PNG,JPG 和 GIF。
- WebP 无损图片相对于 PNG 平均缩小 26%
- WebP 有损图片相对于 JPG 通常缩小 25%~34%,相对于 PNG 通常小 3 倍。
还是以上面那张新人红包图片作为例子:
PNG 格式的原图需要 727 KB,WebP 格式仅仅只需要 51 KB。同样,依赖于七牛云的图片 CDN 服务,可以支持将其他图片格式转成 WebP,这样不需要特意上传 WebP 格式图片了。
Android 4.2.1 开始支持 WebP 格式图片,如果要支持带透明度的 WebP 图片,则需要 4.3 系统以上,因为目前 APP 的最低版本是 5.0,所以对 WebP 能完美支持。
不管网络加载优化还是图片解码优化,我们都希望可以快速引入和不影响原始业务。所以,最终我们输出了统一的图片加载库:WwdzImage,同时增加相应的开关配置,做到:可配置,可灰度。优化图片上线后,根据图片CDN流量及用户体验上,都有了明显的提升。
这个是当月的图片 CDN 流量折线图,可以看到流量有明显的拐点,并且拐点的时间点,跟优化上线时间基本吻合,相对于上个月,大概有 15% 的降幅,可见优化还是带来了不小的收益。
在用户的体验上 模拟环境如下:首先我们使用 Charles 模拟弱网环境(3G):
最大上行速度 1024 kbp/s,125 KB/s
最大下行速度 4096 kbp/s,500 KB/s
其次,保证 APP 没有对应的图片磁盘缓存。
优化前的数据:
总耗时 | 图片数 | 平均耗时 | 总流量 |
---|---|---|---|
73145ms | 40 | 1828ms | 5.25MB |
76195ms | 40 | 1904ms | 5.46MB |
优化后的数据:
总耗时 | 图片数 | 平均耗时 | 总流量 |
---|---|---|---|
8734ms | 40 | 218ms | 0.58MB |
8845ms | 40 | 221ms | 0.57MB |
9557ms | 40 | 238ms | 0.59MB |
优化前 VS 优化后:
牛年邀牛人
一起战斗、一起成长
技术、产品、UED、运营、职能等海量岗位
玩物得志期待你的加入
七牛云图片基本处理(imageView2)
【腾讯Bugly干货分享】WebP原理和Android支持现状介绍
How WebP works (lossly mode)
缩减图片下载大小
Glide 管理位图内存