1. 前言
在電商APP中野宜,圖片在整個(gè)頁(yè)面中占比最大燃逻,清晰高質(zhì)量的圖片能夠明顯提升轉(zhuǎn)化率坐儿。但是APP運(yùn)行環(huán)境錯(cuò)綜復(fù)雜薪前,往往我們會(huì)遇到 圖片壓縮導(dǎo)致模糊奏篙、列表加載長(zhǎng)時(shí)間顯示空白圖柴淘、查看大圖黑屏過(guò)久、甚至因?yàn)閳D片過(guò)大導(dǎo)致crash等秘通,如下效果展示:
針對(duì)以上問題为严,我們需要:
尋求合適的壓縮方案,保證畫質(zhì)的同時(shí)肺稀,增加壓縮率第股,提高圖片上傳速度和下載顯示速度。
排除網(wǎng)絡(luò)波動(dòng)影響下话原,利用CDN固定尺寸預(yù)熱夕吻,統(tǒng)一不同分辨率機(jī)型命中預(yù)熱規(guī)則,加速圖片加載顯示繁仁。
利用清晰度分階段加載及分塊加載解決黑屏和crash涉馅。
2. 方案綜述
綜上問題,我們探索的方案整體流程圖如下:
3. 圖片上傳前-圖片壓縮
3.1 Android常見壓縮方式
在討論壓縮方案前改备,先讓我們了解一下Android設(shè)備中一張網(wǎng)絡(luò)圖片所占用的內(nèi)存大小是如何計(jì)算的
圖片長(zhǎng)度 x 圖片寬度 x 一個(gè)像素點(diǎn)占用的字節(jié)數(shù)
一個(gè)像素點(diǎn)占用的字節(jié)數(shù)與圖片的壓縮格式有關(guān):
ALPHA_8:一個(gè)像素點(diǎn)占用1個(gè)字節(jié)控漠,沒有顏色,只有透明度悬钳。
ARGB_4444:A=4位盐捷,R=4位,G=4位默勾,B=4位碉渡,總共16位,2個(gè)字節(jié)母剥,成像質(zhì)量不好通常不用滞诺。
ARGB_8888:同上計(jì)算方式共32位形导,4個(gè)字節(jié),成像質(zhì)量最佳习霹。
RGB_565:同上計(jì)算方式共16位朵耕,2個(gè)字節(jié),成像質(zhì)量次之淋叶。
所以阎曹,為了達(dá)到圖片占用更小內(nèi)存的目的,可以從圖片長(zhǎng)度煞檩、寬度以及一個(gè)像素點(diǎn)占用的字節(jié)數(shù)入手处嫌,如果不改變以上三個(gè)參數(shù)的壓縮方式,將只能降低圖片的文件大小斟湃。 而Android中常見的幾種壓縮方式如下:
- 質(zhì)量壓縮
//quality 圖像壓縮率熏迹,0-100。 0 壓縮100%凝赛,100意味著不壓縮
bufferedOutputStream.compress(Bitmap.CompressFormat.JPEG, quality, bos);
這種方式注暗,通過(guò)算法扣掉(同化)了圖片中的一些某個(gè)點(diǎn)附近相近的像素,達(dá)到降低質(zhì)量減少文件大小的目的哄酝。
- 采樣率壓縮(Luban代表)
//inSampleSize 采樣率(采樣頻率)友存,是指每隔多少個(gè)樣本采樣一次作為結(jié)果,比如將這個(gè)結(jié)果設(shè)置為4陶衅,意味著從原本圖片的4個(gè)像素中取一個(gè)像素作為返回結(jié)果,其余的都被丟棄
BitmapFactory options.inSampleSize =calculateSampleSize(options);
通過(guò)特定的采樣算法直晨,可以原始圖片像素進(jìn)行丟棄處理搀军,采樣率越大,圖片越小勇皇,越失真罩句。
- 縮放壓縮
//scale 縮放的比例,如0.8f敛摘,則為縮小到原來(lái)的80%
Matrix matrix.setScale(scale, scale);
通過(guò)減少單位尺寸的像素值门烂,真正意義上的降低像素值,但是是在原bitmap的基礎(chǔ)之上生成的兄淫,占內(nèi)存屯远,效率低。 以上的壓縮方式是Android中java層的調(diào)用函數(shù)捕虽,最終的Android圖片編碼邏輯是java層->Native層->Skia引擎->libjpeg慨丐。
3.2 基于libjpeg-turbo庫(kù)的壓縮方式
研究libjpeg庫(kù)后發(fā)現(xiàn),libjpeg在編碼圖像時(shí)有一個(gè)參數(shù)是optimize_coding泄私。如果是false時(shí)房揭,libjpeg會(huì)使用標(biāo)準(zhǔn)的哈夫曼表來(lái)做壓縮备闲;如果設(shè)置為true,libjpeg會(huì)基于圖像數(shù)據(jù)生成對(duì)應(yīng)的哈夫曼表做壓縮捅暴,這個(gè)計(jì)算過(guò)程會(huì)消耗一定的時(shí)間和空間恬砂。
在Android7.0之前(不包含),Google考慮到設(shè)備性能兼容性問題蓬痒,optimize_coding設(shè)置的是false觉既。在7.0之上(包含),默認(rèn)已改為true乳幸。
針對(duì)計(jì)算過(guò)程中消耗的時(shí)間和空間問題瞪讼,后推出其升級(jí)版本 libjpeg-turbo,其核心是利用SIMD指令集來(lái)加速JPEG編碼和解碼基礎(chǔ)效率粹断。并且在之后符欠,Mozilla實(shí)驗(yàn)室推出針對(duì)libjpeg-turbo的升級(jí)版mozjpeg,其核心是通過(guò)優(yōu)化霍夫曼編碼樹瓶埋,從而達(dá)到減少文件大小和不改變圖像質(zhì)量的前提下提高編碼效率希柿。
我們是否可以利用Mozilla來(lái)提高壓縮率和減少消耗的時(shí)間呢?
3.3 基于Spectrum庫(kù)的壓縮方式
Spectrum是來(lái)自Meta(前身Facebook)的跨平臺(tái)圖片轉(zhuǎn)碼庫(kù)养筒。它可以輕松的集成到Android或iOS的項(xiàng)目中曾撤,以高效執(zhí)行常見的圖像操作。
優(yōu)勢(shì):
基于mozjpeg晕粪,實(shí)現(xiàn)高質(zhì)量輸出并提高壓縮率挤悉。
易于使用,支持對(duì)EXIF元數(shù)據(jù)的處理巫湘。
支持更多自定義配置装悲,功能齊全,包括色度采樣模式和指定分辨率等
跨平臺(tái)尚氛,實(shí)現(xiàn)Android和iOS一致的壓縮效果诀诊。
缺點(diǎn):
編碼圖片時(shí)間增加
不支持HEIC和HIEF圖片
個(gè)別圖片可能會(huì)出現(xiàn)編碼失敗
3.4 數(shù)據(jù)對(duì)比
為了對(duì)比libjpeg-turbo的系統(tǒng)壓縮與以mozjpeg為核心的Spectrum庫(kù)壓縮效果,我們比較不同壓縮質(zhì)量及色度采樣模式下的壓縮效果阅嘶,壓縮效果依據(jù)butteraugli質(zhì)量差異属瓣、文件大小、壓縮耗時(shí)讯柔。
butteraugli是Google開發(fā)的一個(gè)測(cè)量圖像之間感知差異的工具抡蛙,數(shù)值越小,人眼越不能察覺出差異磷杏。
S444采樣模式 4:4:4 以相同的分辨率存儲(chǔ)亮度和色度信息;S420采樣模式 4:2:0 每個(gè)色度信息使用 4 個(gè)亮度信息
測(cè)試環(huán)境:樣圖 3468x4624溜畅、壓縮設(shè)備:meizu 17 Android 11 、比較環(huán)境:macOS Monterey 12.3.1
壓縮質(zhì)量 | 核心 | 色度采樣模式 | butteraugli質(zhì)量差異 | 文件大小 | 耗時(shí) |
---|---|---|---|---|---|
原圖 | 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 |
從表中數(shù)據(jù)可知以下對(duì)比結(jié)論:
mozjpeg S444色度采樣模式下极祸,質(zhì)量差異表現(xiàn)優(yōu)秀慈格,但是耗時(shí)較長(zhǎng)怠晴,壓縮后文件大小降低并不理想。
jpeg-turbo S444 和 mozjpeg S420 各有長(zhǎng)短浴捆,對(duì)比數(shù)據(jù)蒜田,mozjpeg S420相對(duì)jpeg-turbo S444的壓縮率平均提高26.6%,相對(duì)的壓縮耗時(shí)平均增長(zhǎng)了592ms选泻。
實(shí)際業(yè)務(wù)中冲粤,圖片上傳包括圖片壓縮處理及網(wǎng)絡(luò)上傳輸,由于后者網(wǎng)絡(luò)傳輸?shù)目深A(yù)測(cè)性較差并且通常是主要瓶頸页眯,所以通常更傾向于在預(yù)處理步驟中進(jìn)行更多的計(jì)算以減少需要傳輸?shù)臄?shù)據(jù)量梯捕。更小的數(shù)據(jù)量不僅會(huì)加速上傳流程,對(duì)后續(xù)的提高下載展示流程速度也能起到關(guān)鍵作用窝撵。
結(jié)合業(yè)務(wù)傀顾,我們采用壓縮質(zhì)量為70%、mozjpeg的S420壓縮模式碌奉。對(duì)比數(shù)據(jù)短曾,采用此模式時(shí),質(zhì)量差異尚可赐劣,壓縮后文件大小縮減了85%嫉拐,且壓縮耗時(shí)控制更好。
4. 圖片上傳后-展示圖片
4.1魁兼、轉(zhuǎn)碼成Webp格式
Webp是谷歌提供的一種支持有損壓縮和無(wú)損壓縮的圖片文件格式婉徘,而且可以提供比JPEG或PNG更好的壓縮,CDN可支持在線轉(zhuǎn)碼Webp璃赡。
隨機(jī)抽取平臺(tái)200張圖片判哥,平均數(shù)據(jù),已采用第三節(jié)方案的進(jìn)行壓縮碉考。
原圖(JPG) | WEBP | PNG |
---|---|---|
280KB | 117KB | 490KB |
采用webp的圖片格式,尺寸是原先的50%挺身,而PNG更大侯谁。
4.2、CDN裁切預(yù)熱
因玩物得志App中使用Glide圖片加載框架章钾,所以以Glide為例墙贱,描述如何進(jìn)行實(shí)際業(yè)務(wù)中的優(yōu)化展示:
Glide在實(shí)際加載網(wǎng)絡(luò)圖片時(shí),已經(jīng)利用控件的寬高對(duì)原圖裁切出對(duì)應(yīng)的bitmap作為展示贱傀。雖然每張小圖僅加載了極小的bitmap內(nèi)存惨撇,但是下載的依舊是原圖數(shù)據(jù)。對(duì)用戶感知上來(lái)說(shuō)府寒,會(huì)有較長(zhǎng)時(shí)間的空白或者占位圖魁衙。
那么报腔,我們是否可以降低下載原圖的數(shù)據(jù)大小呢?
我們可以利用CDN圖片裁切技術(shù)剖淀,并且Glide提供了BaseGlideUrlLoader 的getUrl 方法對(duì)訪問的URL進(jìn)行處理纯蛾。但是如果以實(shí)際的View 尺寸去裁切,不同機(jī)型的控件尺寸會(huì)有些許差異纵隔,并且CDN的流量暴漲明顯翻诉,費(fèi)用較高,首圖第一次裁切預(yù)熱也比較慢捌刮。為了解決機(jī)型設(shè)備產(chǎn)生的預(yù)熱尺寸漂移值碰煌。所以我們按每50px做一個(gè)間隔范圍進(jìn)行規(guī)整。假設(shè)控件寬度是310px绅作,則指定寬度300px的CDN裁切芦圾。圖片經(jīng)過(guò)CDN裁切后,需要下載的數(shù)據(jù)量和時(shí)間會(huì)大大降低棚蓄,如下所示:
隨機(jī)抽取平臺(tái)200張圖片堕扶,平均數(shù)據(jù),裁切寬300px梭依,高等比縮小稍算,原始圖片格式,已采用第三節(jié)方案的進(jìn)行壓縮役拴。
場(chǎng)景 | 大泻健(平均) |
---|---|
原圖情況下 | 280KB |
CDN裁切情況下 | 14KB |
實(shí)際業(yè)務(wù)中可以直接分為小圖、大圖河闰、原圖三套標(biāo)準(zhǔn)尺寸進(jìn)行預(yù)熱科平。無(wú)需每50px這么精準(zhǔn)。
采用每50px間隔是為了解決機(jī)型設(shè)備上的預(yù)熱尺寸漂移值姜性,但是在業(yè)務(wù)上也會(huì)存在該類可取舍的情況瞪慧,如列表中展示310px的圖片,詳情中展示360px的圖片部念∑茫可以對(duì)這類情況進(jìn)行特殊的裁切規(guī)則處理,如均使用350px儡炼,達(dá)到一次預(yù)熱的目的妓湘。
綜合上述
隨機(jī)抽取平臺(tái)200張圖片,平均數(shù)據(jù)乌询,裁切寬300px榜贴,高等比縮小,轉(zhuǎn)化webp妹田,已采用第三節(jié)方案的進(jìn)行壓縮唬党。
場(chǎng)景 | 大芯楣病(平均) | 下載時(shí)間(平均) |
---|---|---|
優(yōu)化前 | 280KB | 495ms |
優(yōu)化后 | 9KB | 48ms |
4.3 大圖預(yù)覽特殊手段
解決對(duì)小圖的優(yōu)化方案后,我們繼續(xù)解決全屏大圖打黑屏或crash問題初嘹。
現(xiàn)在假設(shè)一臺(tái)設(shè)備屏幕分辨率1080x1920和一張4320x7680的圖及汉,需要全屏大圖顯示。
按Android官網(wǎng)高效加載大型位圖的介紹屯烦,我們可以計(jì)算得到inSampleSize = 4坷随,最終4320x7680的圖片會(huì)被采樣壓縮為1080x1920的圖片,清晰度稍微降低驻龟,肉眼幾乎難以察覺温眉。但是如果該控件支持放大查看,就會(huì)出現(xiàn)模糊或者細(xì)節(jié)有差異翁狐。在電商APP中类溢,尤其字畫類目圖片,該情況特別明顯露懒。
我們可以利用 BitmapRegionDecoder 類闯冷,在decodeRegion方法中,只解碼其中指定的rect區(qū)域懈词,進(jìn)行分塊加載蛇耀。這就是Subsampling Scale Image View為我們解決問題的核心所在。
4.3.1 核心邏輯-分組切片集合
Subsampling Scale Image View按類似上述的計(jì)算采樣率方式坎弯,會(huì)得到一個(gè)fullImageSampleSize采樣值纺涤,
當(dāng)fullImageSampleSize = 1時(shí),整個(gè)控件足夠顯示整張圖片抠忘,所以不需要做分塊加載撩炊。
當(dāng)fullImageSampleSize > 1時(shí),需要切片加載崎脉,會(huì)使用LinkedHashMap<int,List<Tile>>來(lái)存儲(chǔ)不同縮放時(shí)需要取的切片數(shù)據(jù)拧咳,key是不同縮放時(shí)需要使用的采樣率inSampleSize,value是對(duì)應(yīng)的切片集合囚灼。
例如一開始提到的例子呛踊,屏幕是1080x1920,全圖是4320x7680啦撮,此時(shí)會(huì)存儲(chǔ)3組切片數(shù)據(jù):
inSampleSize是1,切片有16個(gè)汪厨,當(dāng)放大4倍時(shí)展示赃春,如下方右圖;
inSampleSize是2劫乱,切片有4個(gè)织中,當(dāng)放大2倍時(shí)展示锥涕,如下方中間圖;
inSampleSize是4狭吼,切片有1個(gè)层坠,當(dāng)未縮放時(shí)展示,如下方左圖刁笙。
在實(shí)際的顯示過(guò)程中破花,如果控件顯示區(qū)域涵蓋多個(gè)分塊區(qū)域時(shí),如下圖所示:
注意:以上的切片圖示只是為了方便演示說(shuō)明疲吸,實(shí)際切片集合會(huì)根據(jù)原圖大小及顯示控件大小按一定的算法座每,得出對(duì)應(yīng)采樣率下最佳的切片集合
此時(shí)會(huì)命中inSampleSize是1,切片集合16個(gè)摘悴。而由于放大峭梳,此時(shí)控件展示的區(qū)域只是編號(hào)7、8蹂喻、11和12這4塊切片的一部分葱椭。為了優(yōu)化內(nèi)存管理,默認(rèn)會(huì)將其他非全圖采樣率對(duì)應(yīng)的切片集合中已加載的bitmap回收口四,同時(shí)只啟動(dòng)7孵运、8、11和12這4塊區(qū)域的切片bitmap解碼任務(wù)窃祝。以上4個(gè)切片bitmap解碼任務(wù)任一完成后觸發(fā)繪制掐松,通過(guò)切片本身記錄的在原始圖片中的位置關(guān)系,經(jīng)過(guò)縮放和移動(dòng)相對(duì)調(diào)整成在屏幕中顯示到的區(qū)域粪小,對(duì)解碼的bitmap做相應(yīng)的矩陣變換最終繪制在控件的相應(yīng)位置上大磺。
如對(duì)詳細(xì)源碼解析感興趣的可以查看subsampling-scale-image-view加載長(zhǎng)圖源碼分析總結(jié)。
4.3.2 大圖片清晰度分階段加載
我們雖然進(jìn)行了大圖的分塊加載探膊,但是依舊是需要先獲取到原圖像素杠愧,可能需要等候較長(zhǎng)時(shí)間。但是一般業(yè)務(wù)場(chǎng)景中逞壁,我們是從小圖點(diǎn)擊跳轉(zhuǎn)到大圖預(yù)熱頁(yè)面中的流济。所以我們可以利用圖片緩存,做進(jìn)一步的展示優(yōu)化腌闯。方案設(shè)計(jì)如下:
這套方案就解決了我們之前提的問題绳瘟,通過(guò)區(qū)塊加載功能,不同縮放級(jí)別時(shí)姿骏,加載對(duì)應(yīng)的切片集合糖声,避免了一次性加載大圖crash,也確保了畫質(zhì)清晰。通過(guò)尺寸攜帶蘸泻,優(yōu)先展示緩存圖片琉苇,再靜默展示大圖,從而解決黑屏等待過(guò)長(zhǎng)問題悦施。
4.3.3 數(shù)據(jù)對(duì)比
最后我們對(duì)以上討論的幾種流程進(jìn)行對(duì)比測(cè)試并扇,以跳轉(zhuǎn)大圖頁(yè)面后開始計(jì)時(shí)。
緩存情況 | 首圖展示平均消耗時(shí)間(ms) | 原圖展示平均消耗時(shí)間(ms) | |
---|---|---|---|
存在緩存 | 直接加載原圖顯示 | 10 | 10 |
檢測(cè)到原圖緩存直接優(yōu)先加載原圖 | 36 | 36 | |
不存在緩存 | 直接加載原圖顯示 | 865 | 865 |
先加載指定像素再加載原圖 | 93 | 786 |
由表中數(shù)據(jù)可知:無(wú)緩存時(shí)抡诞,首圖展示時(shí)間足足提高了772ms穷蛹;有緩存時(shí),僅慢了20多ms沐绒。結(jié)果來(lái)看俩莽,優(yōu)先展示指定像素的預(yù)覽圖再展示原圖的方案體驗(yàn)提升明顯。
5. 總結(jié)
最后乔遮,我們回到最初提出的問題扮超,看一下經(jīng)過(guò)這套圖片優(yōu)化加載方案實(shí)施后,所呈現(xiàn)的效果如何蹋肮。如下: 經(jīng)過(guò)我們的優(yōu)化后出刷,滑動(dòng)加載圖片流暢很多,大圖清晰度分階段加載坯辩,黑屏幾乎沒有馁龟,特殊長(zhǎng)圖展示流暢,不crash漆魔。
從數(shù)據(jù)上來(lái)看坷檩,保證圖片質(zhì)量幾乎不變的情況下,圖片壓縮率提高了3.8倍改抡,這部分提高的壓縮率對(duì)于后期展示也同樣有對(duì)應(yīng)的提高矢炼。后期展示圖片時(shí),列表小圖展示平均加載速度提升了10.3倍阿纤,大圖展示的首圖展示時(shí)間提速了9.3倍句灌。 目前來(lái)看,這一套圖片加載方案是能滿足玩物得志APP現(xiàn)階段的使用體驗(yàn)的欠拾,當(dāng)然胰锌,也依然有許多還可以優(yōu)化的點(diǎn)。比如藐窄,進(jìn)一步減少壓縮耗時(shí)资昧;查看大圖時(shí)自然的過(guò)渡動(dòng)畫;原圖加載進(jìn)度條等荆忍。