1. 前言
在电商APP中,图片在整个页面中占比最大,清晰高质量的图片能够明显提升转化率。但是APP运行环境错综复杂,往往我们会遇到 图片压缩导致模糊、列表加载长时间显示空白图、查看大图黑屏过久、甚至因为图片过大导致crash等,如下效果展示:
针对以上问题,我们需要:
寻求合适的压缩方案,保证画质的同时,增加压缩率,提高图片上传速度和下载显示速度。
排除网络波动影响下,利用CDN固定尺寸预热,统一不同分辨率机型命中预热规则,加速图片加载显示。
利用清晰度分阶段加载及分块加载解决黑屏和crash。
2. 方案综述
综上问题,我们探索的方案整体流程图如下:
3. 图片上传前-图片压缩
3.1 Android常见压缩方式
在讨论压缩方案前,先让我们了解一下Android设备中一张网络图片所占用的内存大小是如何计算的
图片长度 x 图片宽度 x 一个像素点占用的字节数
一个像素点占用的字节数与图片的压缩格式有关:
ALPHA_8:一个像素点占用1个字节,没有颜色,只有透明度。
ARGB_4444:A=4位,R=4位,G=4位,B=4位,总共16位,2个字节,成像质量不好通常不用。
ARGB_8888:同上计算方式共32位,4个字节,成像质量最佳。
RGB_565:同上计算方式共16位,2个字节,成像质量次之。
所以,为了达到图片占用更小内存的目的,可以从图片长度、宽度以及一个像素点占用的字节数入手,如果不改变以上三个参数的压缩方式,将只能降低图片的文件大小。 而Android中常见的几种压缩方式如下:
- 质量压缩
//quality 图像压缩率,0-100。 0 压缩100%,100意味着不压缩
bufferedOutputStream.compress(Bitmap.CompressFormat.JPEG, quality, bos);
这种方式,通过算法扣掉(同化)了图片中的一些某个点附近相近的像素,达到降低质量减少文件大小的目的。
- 采样率压缩(Luban代表)
//inSampleSize 采样率(采样频率),是指每隔多少个样本采样一次作为结果,比如将这个结果设置为4,意味着从原本图片的4个像素中取一个像素作为返回结果,其余的都被丢弃
BitmapFactory options.inSampleSize =calculateSampleSize(options);
通过特定的采样算法,可以原始图片像素进行丢弃处理,采样率越大,图片越小,越失真。
- 缩放压缩
//scale 缩放的比例,如0.8f,则为缩小到原来的80%
Matrix matrix.setScale(scale, scale);
通过减少单位尺寸的像素值,真正意义上的降低像素值,但是是在原bitmap的基础之上生成的,占内存,效率低。 以上的压缩方式是Android中java层的调用函数,最终的Android图片编码逻辑是java层->Native层->Skia引擎->libjpeg。
3.2 基于libjpeg-turbo库的压缩方式
研究libjpeg库后发现,libjpeg在编码图像时有一个参数是optimize_coding。如果是false时,libjpeg会使用标准的哈夫曼表来做压缩;如果设置为true,libjpeg会基于图像数据生成对应的哈夫曼表做压缩,这个计算过程会消耗一定的时间和空间。
在Android7.0之前(不包含),Google考虑到设备性能兼容性问题,optimize_coding设置的是false。在7.0之上(包含),默认已改为true。
针对计算过程中消耗的时间和空间问题,后推出其升级版本 libjpeg-turbo,其核心是利用SIMD指令集来加速JPEG编码和解码基础效率。并且在之后,Mozilla实验室推出针对libjpeg-turbo的升级版mozjpeg,其核心是通过优化霍夫曼编码树,从而达到减少文件大小和不改变图像质量的前提下提高编码效率。
我们是否可以利用Mozilla来提高压缩率和减少消耗的时间呢?
3.3 基于Spectrum库的压缩方式
Spectrum是来自Meta(前身Facebook)的跨平台图片转码库。它可以轻松的集成到Android或iOS的项目中,以高效执行常见的图像操作。
优势:
基于mozjpeg,实现高质量输出并提高压缩率。
易于使用,支持对EXIF元数据的处理。
支持更多自定义配置,功能齐全,包括色度采样模式和指定分辨率等
跨平台,实现Android和iOS一致的压缩效果。
缺点:
编码图片时间增加
不支持HEIC和HIEF图片
个别图片可能会出现编码失败
3.4 数据对比
为了对比libjpeg-turbo的系统压缩与以mozjpeg为核心的Spectrum库压缩效果,我们比较不同压缩质量及色度采样模式下的压缩效果,压缩效果依据butteraugli质量差异、文件大小、压缩耗时。
butteraugli是Google开发的一个测量图像之间感知差异的工具,数值越小,人眼越不能察觉出差异。
S444采样模式 4:4:4 以相同的分辨率存储亮度和色度信息;S420采样模式 4:2:0 每个色度信息使用 4 个亮度信息
测试环境:样图 3468x4624、压缩设备:meizu 17 Android 11 、比较环境:macOS Monterey 12.3.1
压缩质量 | 核心 | 色度采样模式 | butteraugli质量差异 | 文件大小 | 耗时 |
---|---|---|---|---|---|
原图 | S444 | - | 4.07M | - | |
70% | jpeg-turbo | S444 | 5.404388 | 0.84M | 457ms |
mozjpeg | S444 | 5.500376 | 0.73M | 1368ms | |
mozjpeg | S420 | 5.562090 | 0.60M | 975ms | |
80% | jpeg-turbo | S444 | 5.648004 | 1.15M | 439ms |
mozjpeg | S444 | 5.369581 | 1.01M | 1475ms | |
mozjpeg | S420 | 5.648684 | 0.84M | 1025ms | |
90% | jpeg-turbo | S444 | 4.891199 | 1.94M | 498ms |
mozjpeg | S444 | 5.022236 | 1.74M | 1585ms | |
mozjpeg | S420 | 5.048512 | 1.47M | 1169ms |
从表中数据可知以下对比结论:
mozjpeg S444色度采样模式下,质量差异表现优秀,但是耗时较长,压缩后文件大小降低并不理想。
jpeg-turbo S444 和 mozjpeg S420 各有长短,对比数据,mozjpeg S420相对jpeg-turbo S444的压缩率平均提高26.6%,相对的压缩耗时平均增长了592ms。
实际业务中,图片上传包括图片压缩处理及网络上传输,由于后者网络传输的可预测性较差并且通常是主要瓶颈,所以通常更倾向于在预处理步骤中进行更多的计算以减少需要传输的数据量。更小的数据量不仅会加速上传流程,对后续的提高下载展示流程速度也能起到关键作用。
结合业务,我们采用压缩质量为70%、mozjpeg的S420压缩模式。对比数据,采用此模式时,质量差异尚可,压缩后文件大小缩减了85%,且压缩耗时控制更好。
4. 图片上传后-展示图片
4.1、转码成Webp格式
Webp是谷歌提供的一种支持有损压缩和无损压缩的图片文件格式,而且可以提供比JPEG或PNG更好的压缩,CDN可支持在线转码Webp。
随机抽取平台200张图片,平均数据,已采用第三节方案的进行压缩。
原图(JPG) | WEBP | PNG |
---|---|---|
280KB | 117KB | 490KB |
采用webp的图片格式,尺寸是原先的50%,而PNG更大。
4.2、CDN裁切预热
因玩物得志App中使用Glide图片加载框架,所以以Glide为例,描述如何进行实际业务中的优化展示:
Glide在实际加载网络图片时,已经利用控件的宽高对原图裁切出对应的bitmap作为展示。虽然每张小图仅加载了极小的bitmap内存,但是下载的依旧是原图数据。对用户感知上来说,会有较长时间的空白或者占位图。
那么,我们是否可以降低下载原图的数据大小呢?
我们可以利用CDN图片裁切技术,并且Glide提供了BaseGlideUrlLoader 的getUrl 方法对访问的URL进行处理。但是如果以实际的View 尺寸去裁切,不同机型的控件尺寸会有些许差异,并且CDN的流量暴涨明显,费用较高,首图第一次裁切预热也比较慢。为了解决机型设备产生的预热尺寸漂移值。所以我们按每50px做一个间隔范围进行规整。假设控件宽度是310px,则指定宽度300px的CDN裁切。图片经过CDN裁切后,需要下载的数据量和时间会大大降低,如下所示:
随机抽取平台200张图片,平均数据,裁切宽300px,高等比缩小,原始图片格式,已采用第三节方案的进行压缩。
场景 | 大小(平均) |
---|---|
原图情况下 | 280KB |
CDN裁切情况下 | 14KB |
实际业务中可以直接分为小图、大图、原图三套标准尺寸进行预热。无需每50px这么精准。
采用每50px间隔是为了解决机型设备上的预热尺寸漂移值,但是在业务上也会存在该类可取舍的情况,如列表中展示310px的图片,详情中展示360px的图片。可以对这类情况进行特殊的裁切规则处理,如均使用350px,达到一次预热的目的。
综合上述
随机抽取平台200张图片,平均数据,裁切宽300px,高等比缩小,转化webp,已采用第三节方案的进行压缩。
场景 | 大小(平均) | 下载时间(平均) |
---|---|---|
优化前 | 280KB | 495ms |
优化后 | 9KB | 48ms |
4.3 大图预览特殊手段
解决对小图的优化方案后,我们继续解决全屏大图打黑屏或crash问题。
现在假设一台设备屏幕分辨率1080x1920和一张4320x7680的图,需要全屏大图显示。
按Android官网高效加载大型位图的介绍,我们可以计算得到inSampleSize = 4,最终4320x7680的图片会被采样压缩为1080x1920的图片,清晰度稍微降低,肉眼几乎难以察觉。但是如果该控件支持放大查看,就会出现模糊或者细节有差异。在电商APP中,尤其字画类目图片,该情况特别明显。
我们可以利用 BitmapRegionDecoder 类,在decodeRegion方法中,只解码其中指定的rect区域,进行分块加载。这就是Subsampling Scale Image View为我们解决问题的核心所在。
4.3.1 核心逻辑-分组切片集合
Subsampling Scale Image View按类似上述的计算采样率方式,会得到一个fullImageSampleSize采样值,
当fullImageSampleSize = 1时,整个控件足够显示整张图片,所以不需要做分块加载。
当fullImageSampleSize > 1时,需要切片加载,会使用LinkedHashMap<int,List<Tile>>来存储不同缩放时需要取的切片数据,key是不同缩放时需要使用的采样率inSampleSize,value是对应的切片集合。
例如一开始提到的例子,屏幕是1080x1920,全图是4320x7680,此时会存储3组切片数据:
inSampleSize是1,切片有16个,当放大4倍时展示,如下方右图;
inSampleSize是2,切片有4个,当放大2倍时展示,如下方中间图;
inSampleSize是4,切片有1个,当未缩放时展示,如下方左图。
在实际的显示过程中,如果控件显示区域涵盖多个分块区域时,如下图所示:
注意:以上的切片图示只是为了方便演示说明,实际切片集合会根据原图大小及显示控件大小按一定的算法,得出对应采样率下最佳的切片集合
此时会命中inSampleSize是1,切片集合16个。而由于放大,此时控件展示的区域只是编号7、8、11和12这4块切片的一部分。为了优化内存管理,默认会将其他非全图采样率对应的切片集合中已加载的bitmap回收,同时只启动7、8、11和12这4块区域的切片bitmap解码任务。以上4个切片bitmap解码任务任一完成后触发绘制,通过切片本身记录的在原始图片中的位置关系,经过缩放和移动相对调整成在屏幕中显示到的区域,对解码的bitmap做相应的矩阵变换最终绘制在控件的相应位置上。
如对详细源码解析感兴趣的可以查看subsampling-scale-image-view加载长图源码分析总结。
4.3.2 大图片清晰度分阶段加载
我们虽然进行了大图的分块加载,但是依旧是需要先获取到原图像素,可能需要等候较长时间。但是一般业务场景中,我们是从小图点击跳转到大图预热页面中的。所以我们可以利用图片缓存,做进一步的展示优化。方案设计如下:
这套方案就解决了我们之前提的问题,通过区块加载功能,不同缩放级别时,加载对应的切片集合,避免了一次性加载大图crash,也确保了画质清晰。通过尺寸携带,优先展示缓存图片,再静默展示大图,从而解决黑屏等待过长问题。
4.3.3 数据对比
最后我们对以上讨论的几种流程进行对比测试,以跳转大图页面后开始计时。
缓存情况 | 首图展示平均消耗时间(ms) | 原图展示平均消耗时间(ms) | |
---|---|---|---|
存在缓存 | 直接加载原图显示 | 10 | 10 |
检测到原图缓存直接优先加载原图 | 36 | 36 | |
不存在缓存 | 直接加载原图显示 | 865 | 865 |
先加载指定像素再加载原图 | 93 | 786 |
由表中数据可知:无缓存时,首图展示时间足足提高了772ms;有缓存时,仅慢了20多ms。结果来看,优先展示指定像素的预览图再展示原图的方案体验提升明显。
5. 总结
最后,我们回到最初提出的问题,看一下经过这套图片优化加载方案实施后,所呈现的效果如何。如下: 经过我们的优化后,滑动加载图片流畅很多,大图清晰度分阶段加载,黑屏几乎没有,特殊长图展示流畅,不crash。
从数据上来看,保证图片质量几乎不变的情况下,图片压缩率提高了3.8倍,这部分提高的压缩率对于后期展示也同样有对应的提高。后期展示图片时,列表小图展示平均加载速度提升了10.3倍,大图展示的首图展示时间提速了9.3倍。 目前来看,这一套图片加载方案是能满足玩物得志APP现阶段的使用体验的,当然,也依然有许多还可以优化的点。比如,进一步减少压缩耗时;查看大图时自然的过渡动画;原图加载进度条等。