抖音 Android 端圖片優化實踐
背景介紹
抖音為什么要持續優化圖片能力
圖片能力作為抖音最基礎的能力之一,服務于抖音各個業務。隨著抖音圖文、電商、IM等多圖業務體量的增長,圖片加載量級越來越大,對應的圖片帶寬成本也在日益增加。為了降低圖片成本、提升用戶瀏覽圖片體驗,需要持續不斷的探索和優化圖片能力,在保證圖片展示質量的前提下,提升圖片加載速度,降低圖片整體成本,實現圖片的 "好快省" 。
BDFresco簡介
BDFresco是火山引擎veImageX團隊基于開源Fresco拓展優化的Android端通用基礎的網絡圖片框架,主要提供圖片網絡加載、圖像解碼、圖片基礎處理與變換、圖片服務質量監控上報、自研HEIF軟解、內存緩存策略、云控配置下發等能力,目前已覆蓋到字節幾乎所有App。
下面將從抖音視角出發,介紹抖音基于BDFresco在圖片方向做了哪些優化。
優化思路
一張網絡圖片完整的加載流程如下:
客戶端通過網絡獲取業務數據,響應內容包括對應的圖片數據,通過將圖片Url數據交給BDFresco加載,正式開始圖片的加載流程。BDFresco會判斷當前圖片是否在內存緩存及磁盤緩存,若存在則執行對應解碼或渲染操作,若不存在則直接走veImageX-CDN下載,將圖片資源下載到本地后再進行解碼和渲染操作。
圖片加載過程不僅占用了客戶端內存、存儲和CPU等資源,也消耗了網絡流量和服務端資源。
圖片的加載流程本質上是一個多級緩存邏輯,可以將圖片加載流程拆分成4大核心階段,內存緩存、圖片解碼、磁盤緩存、網絡加載,結合指標監控體系,分別針對各階段進行優化:
- 內存緩存優化:當前Android內存緩存命中率高達50%,內存緩存以占用App寶貴的內存為代價,使得我們可以快速地訪問圖片;但內存緩存的存在并不會直接導致App的OOM或者卡頓情況變嚴重,相反,根據特定場景配置合理的內存緩存配置能夠減少圖片頻繁的解碼和內存申請,甚至可以帶來OOM和ANR的優化。
- 圖片解碼優化:當內存緩存失敗后,圖片文件會進行解碼,最終以bitmap形式在內存中存在,目前解碼后的bitmap平均大小為800KB,90分位為5MB,99分位更是高達夸張的11MB,解碼流程需要頻繁申請內存,同時有超過15%的圖片存在一倍尺寸的浪費,對客戶端的性能影響非常大,因此如何減少解碼階段的內存申請是我們需要重點解決的問題。
- 磁盤緩存優化:盡管對比內存緩存命中率,磁盤緩存命中率只有10%,但理論上內存中的bitmap在磁盤中都有對應的原始文件存在,因此想要整體緩存命中率,我們更關注磁盤緩存的優化,需要通過合理的磁盤配置,讓存儲空間利用率更高。
- 網絡加載優化:雖然網絡階段失敗率高達2.5%,但經過數據排查和修復,實際失敗率< 0.1%,優化空間不多,考慮到網絡加載是整體流程耗時最長的,耗時占了近90%,其中主要影響為文件過大導致的加載耗時長,因此需要重點解決下發大文件問題,優化網絡加載耗時。
優化過程
指標建設
在進行圖片優化之前,需要對圖片整體質量完成一次數據盤點,指標建設是至關重要的一步。通過建立指標系統,能夠幫助我們了解圖片現狀、確定優化方向和評估優化后的效果。
BDFresco提供日志上報能力,上報的圖片日志經過veImageX云端數據清洗,最終可以在veImageX云端控制臺查看圖片質量相關指標。從觸發圖片加載,到內存、解碼、磁盤、網絡各個階段都建立了完備的數據監控體系,覆蓋各階段加載耗時、成功率、客戶端和CDN緩存命中率、文件大小、內存占用、大圖異常監控等幾百項指標。
具體舉措
1 內存緩存優化
1.1 內存查找優化
內存緩存原理
BDFresco是通過Producer/Consumer接口來實現圖片加載的流程,例如網絡數據獲取、緩存數據獲取、圖片解碼等多種工作,不同階段由不同Producer實現類處理,所有的Producer都是一層嵌套一層,產生的結果由Consumer進行消費。一個簡化后的圖片內存緩存邏輯如下:
其中,讀取內存或磁盤緩存是通過緩存key來進行匹配,緩存key是通過Uri做轉換的,可以簡單理解成cacheKey==uri,抖音在之前上線過一個緩存key優化的實驗:對于同個資源的不同域名,會剔除host和query參數,即cacheKey被簡化為scheme://path/name
優化方案
業務在進行圖片加載時,BDFresco支持傳入Uri數組,Uri均是同一資源,指向的是不同veImageX-CDN地址,實際上內部會將該批Uri(A-B-C)識別成同一個緩存key。
如下圖所示,ABC3個Uri并不完全是按照【A全流程查找->B全流程查找->C全流程查找】的順序執行,而是會先對ABC各進行一次內存緩存查找,再按順序進行ABC的全流程查找。
由于ABC為同一資源,只是域名不同,在端上生成的緩存key一致,實際上的ABC各自的內存緩存查找為無效操作,由于該環節在UI線程執行,且抖音存在多圖場景,一次滑動會觸發多次圖片加載邏輯,因此部分場景會導致卡頓丟幀等情況發生。
通過將多余的內存查找流程去除,對大盤幀率有明顯提升。
1.2 動靜圖緩存拆分
抖音圖片的內存緩存大小,是根據 java 堆內存大小來進行配置,默認大小為1/8,即32M或者64M。由于Android 8后,圖片內存數據不再存儲在java堆上,而是存在native堆,如果繼續使用堆內存大小來進行圖片內存緩存大小的配置是不合理的,因此通過將內存緩存大小*2,希望能減少解碼操作,優化OOM和ANR指標。
實驗后的穩定性指標顯示,OOM雖然減少了,但是問題轉換成了native崩潰和ANR都顯著劣化,實驗并不符合預期。
圖片的緩存命中率和緩存大小成正相關,緩存大小越大,命中率越高,但隨著緩存大小的增大,命中率提升空間會越來越小。
結合實驗結果來看,單純增大緩存大小會導致內存水位上升,引發ANR和native崩潰問題,方案并不可行。
目前動圖和靜圖的內存緩存使用同一塊緩存塊,BDFresco的緩存管理是LRU的淘汰策略,如果播放動圖幀數過多,很容易把靜圖緩存給替換掉,重新切換回來靜圖就需要重新解碼,重新解碼勢必帶來性能的損耗和用戶體驗的降低,抖音上存在較多此類場景,如IM、個人頁動靜圖混搭場景。
同時,考慮到直接增大內存緩存大小,命中率提升的空間不高,所以嘗試將動圖和靜圖緩存做隔離,動靜圖各使用一塊內存緩存,能夠有效地提升命中率,減少解碼操作。
最終實驗收益:
- 抖音通過拆分動靜圖緩存,單塊緩存大小不變,整體緩存增大,日活顯著提升,OOM顯著降低,大盤幀率顯著正向。
- 抖極通過拆分動靜圖緩存,單塊緩存大小變為1/2,整體緩存不變,日活顯著提升,人均使用時長顯著正向,OOM顯著降低,大盤幀率顯著正向。
2 圖片解碼優化
2.1 解碼格式優化
的內存大小圖片長度圖片寬度
單位像素點占用的字節數
單位像素占用的字節數由顏色模式Bitmap.Config決定,即ARGB 顏色通道,主要有6種類型:
- ALPHA_8:只有一個alpha通道,8bit,每個像素占1Byte;
- ARGB_4444:包含紅綠藍alpha4個通道,每個通道4bit,每個像素占2Byte;
- ARGB_8888:包含紅綠藍alpha4個通道,每個通道8bit,每個像素占4Byte;
- RGB_565:包含紅綠藍3個通道,其中紅色占5bit,綠色占6bit,藍色占5bit,每個像素占2Byte;
- RGBA_F16:包含紅綠藍alpha4個通道,每個通道8bit,每個像素占4Byte;
- HARDWARE:ARGB_8888的特殊配置,Bitmap會直接存儲在顯存中。
目前抖音主要使用ARGB_8888和RGB_565兩種配置,ARGB_8888支持透明通道,且顏色質量更高,RGB_565不支持透明通道,但整體內存占用少了一半,抖音的優化思路如下:
- 低端機默認使用RGB_565進行解碼,減少內存占用。
- 抖音部分圖片不攜帶透明通道,如所有的heic圖,但業務指定為ARGB_8888,導致透明通道做無效占用,在內存上造成浪費,因此可以在解碼階段將不攜帶透明通道的圖片強制降級為RGB_565,在犧牲一定程度的顏色質量下減少近一半的內存占用和解碼性能損耗。
- 由于部分bitmap的操作如圓角、高斯模糊等依賴透明通道的渲染,若強制將無透明通道圖片降級成565,可能會導致部分業務無法正常展示,因此需要針對這類業務進行加白處理。
2.2 heif解碼內存優化
優化原理:
BDFresco中heic圖解碼原邏輯是通過jni調用解碼器的解碼接口,返回解碼后像素數據,返回到java層再轉換成Bitmap對象展示。原邏輯中存在使用超大臨時對象問題,會導致java內存開銷以及GC,優化后減少大對象創建,直接在native層完成Bitmap對象構建,預期減少heif圖片解碼耗時,提升一定流暢度。
將原有heif圖片解碼流程從:
優化為流程:
修復前:每個heic圖片解碼時使用兩個大數組:
- 圖片原始數據,大小為圖片文件大小,一般在40K-700K之間
- 圖片解碼后數據:大小為圖片寬高4,一般在1-11M之間
修復后: 無java層大數組使用,只使用一個40K-700K的native層的DirectByteBuffer數組。減少兩個java層大數組創建,減少GC發生概率以及因為大數組創建導致的OOM問題,從而帶來流暢度以及ANR收益。
在抖音上開實驗,性能相關指標均有顯著提升:java內存占用減少,heic解碼耗時減少,Android ANR減少,從而顯著提升圖文的消費市場,帶動了整體使用時長收益。
2.3 自適應控件解碼
在前面,我們提到有超過15%的圖片存在一倍尺寸的浪費,導致解碼階段需要申請大量的內存,最終展示在控件上并不需要這么大的bitmap,我們通過將圖片尺寸resize至控件大小后進行解碼,最終解碼出小分辨率的Bitmap,能夠將解碼內存申請極致化。
但考慮到圖片浪費主要是服務端下發過大的圖片,單純在解碼階段限制大小,無法解決網絡階段的大圖片問題,帶寬浪費和網絡加載耗時長問題仍然沒有解決,因此我們將該階段做了前置遷移,在網絡加載階段進行優化,具體方案可看4.2節按需縮放方案。
3 磁盤緩存優化
通過優化客戶端的磁盤緩存配置來提升緩存命中率,減少圖片請求量級,在提升圖片加載速度的情況下,也能降低圖片帶寬成本。
磁盤緩存分為3種:主磁盤、small磁盤、獨立磁盤;各磁盤空間存在上限,采用LRU替換算法,目前抖音主要使用主磁盤和獨立磁盤,整體流程如下:圖片默認存儲在主磁盤,圖片被替換概率較高;若業務指定獨立磁盤cacheName,則指定圖片會單獨使用一個磁盤,被替換概率低。
- 主磁盤存儲空間增大:抖音Android端存儲空間上限為40M,考慮到該值為fresco的默認值,配置值主要參考當年設備的存儲空間,因此可以針對存儲空間較多的設備,增加圖片存儲配置,提升磁盤緩存命中率。
- 實驗結果表明:隨著存儲空間的增大,磁盤緩存命中率顯著上漲,進一步帶來圖片量級的減少,當圖片存儲上限提升至80M時,Android大盤量級-5%
- 獨立磁盤推廣:針對復用率高的圖片場景,推薦接入獨立磁盤緩存,可以減少被其他業務圖片LRU替換的幾率,提升圖片的磁盤緩存命中率。
- 以IM表情包為例,我們拉取IM業務的圖片緩存命中率數據分析,
表情包
命中率僅有7%,對比同樣使用獨立磁盤的IM
普通圖片的28%和個人頁主態
的31%,表情包磁盤命中率偏低。 - 將IM表情包接入獨立磁盤后,表情包請求量減少27%
4 網絡加載優化
4.1 圖片格式優化
常見圖片格式
- image:原圖,未經過veImageX壓縮處理。
- JPEG:全稱為Joint Photographic Experts Group(聯合圖像專家組),于1992發布,是一種有損壓縮的光柵圖像文件格式,壓縮率越高圖片質量越差,同時不支持透明通道。
- PNG:全稱為Portable Network Graphics(便攜式網絡圖形),在1997年3月作為知識性RFC 2083發布,于2004年作為ISO/IEC標準發布,PNG也是一種柵格圖形格式,但支持無損壓縮,同時也支持攜帶透明通道信息。
- WebP:是一種由谷歌開發的圖片格式,于2010年發布,支持有損壓縮和無損壓縮圖片文件格式,提供更高的壓縮率和更快的加載速度。對比jpeg和png格式,在相同圖片質量的情況下,文件體積能減少30%+,同時WebP 圖片格式還支持透明通道和動畫,目前抖音Android所有版本均支持Webp格式。
- HEIC(BVC1):基于火山引擎自研BVC算法進行封裝的圖片(17項第一,火山自研編碼器在MSU大賽多項奪冠[https://www.toutiao.com/article/6951287905268843011/?upstream_biz=doubao&source=m_redirect]),通常的文件后綴名為heic,對比Webp格式,在相同圖片質量的情況下,文件體積能再減少30%+,帶寬收益更加明顯。但heic格式也存在缺點:由于高效編碼會導致解碼性能損耗略有增加,但體積較小也會帶來網絡耗時的降低,最終總的加載耗時基本打平或略有降低,目前抖音Android端已全量使用自研BVC軟解實現解碼。
- vvic:字節基于 BVC2算法自研的圖片格式,采用的是VVC的圖片編碼格式,又稱BVC2編碼格式,對比heic的BVC1壓縮率更高。
heic格式推廣
當前veImageX平臺支持最好的是heic編碼格式,但到22年初,抖音Android端覆蓋率不足50%,直接通過提升業務的heic占比能夠大幅減少帶寬成本,提升圖片加載速度。
- JPEG->heic,大幅減少帶寬成本80%以上,加載速度提升30%+
- webp->heif,個人頁動圖平均文件大小-25.33%,加載速度提升30%+
在做heif動圖實驗推廣時,發現個人頁UI幀率存在大幅劣化,在高低端設備均有6-8幀的幀率下降,實驗無法上線,針對該問題,我們對heif動圖的解碼緩存邏輯進行一次優化,提出了heif動圖獨立緩存優化方案。
heif動圖獨立緩存
動圖原理
在圖片文件下載完成解析成字節流,動圖正式播放之前,BDFresco會進行預解碼,當動圖正式播放時,會根據動圖調度器的播放順序將Bitmap渲染到屏幕上,并且在播放過程中會主動預解碼下一幀,如當前需要播放第5幀,會同步解碼第6幀率。其中預解碼操作均在子線程中進行。
不同調度器的核心區別為:當子線程預解碼速度過慢,下一幀需要播放的Bitmap不存在時,是繼續返回當前幀重復播放,等待子線程進行解碼,還是返回下一幀,直接在主線程進行解碼渲染。
- SmoothSlidingFrameScheduler:默認調度器,在子線程預解碼速度跟不上播放速度時,會降低動圖的播放速度,如重復播放當前幀,保證不在主線程進行解碼,會導致動圖播放不流程,但對頁面性能非常好,不會引起卡頓。
- DropFramesFrameScheduler:嚴格按照圖片的時間標準進行播放,若預解解碼速度太慢,則直接在主線程進行解碼,以保證對應幀能夠在對應時間內進行解碼并且渲染到屏幕上,缺點是會在主線程進行解碼,可能會引起頁面的卡頓。
- 自定義調度器:業務自定義實現getFrameNumberToRender接口,支持倒序播放、跳幀播放等特殊邏輯。
獨立緩存
heif動圖掉幀問題經過排查,發現heif動圖采用了一個新的播放調度邏輯FixedSlidingHeifFrameScheduler:動圖無任何預解碼邏輯,在需要播放對應幀時,直接在主線程進行解碼,即播放一幀解碼一幀,這也導致了Heif動圖在播放過程中需要在主線程占用大量CPU資源進行解碼。
為什么heif動圖必須在主線程解碼呢?
對比其他動圖支持任意幀解碼,heif動圖采用了幀間壓縮的方式,引入了I幀P幀的概念,I幀為關鍵幀,包含了當前圖像的完整信息,能夠獨立解碼;P幀為差別幀,沒有完整的畫面數據,只有與前一幀的畫面差別的數據,無法獨立進行解碼,解碼需要依賴前一幀數據。
由于AndroidBDFresco的內存緩存為LRU替換,Bitmap隨時有可能被回收,因此針對Heif動圖的解碼,必須嚴格按照動圖順序進行解碼,否則會導致Heif動圖播放過程中出現花屏綠屏等問題。
方案思考:
- 從源頭解決,優化heif動圖的編碼解碼邏輯,但目前Heif的幀結構就決定了解碼器的解碼邏輯,如果需要支持指定幀解碼,就得改造Heif編碼格式,方案不可行。
- 不在主線程進行解碼,專門開一個子線程做heif動圖的解碼,主線程需要渲染某一幀的時候,就切到子線程去解碼,解碼完成通知主線程做渲染,但方案對BDFresco的解碼流程改造較大,且不支持內存緩存,方案待定。
- 抖音Android&iOS雙端共用一個解碼器,但iOS實驗并無幀率劣化,原因在于iOS的圖片內存緩存是可控的,不會有不符合預期的緩存釋放,因此Android端可以嘗試借鑒該思路
- 給heif動圖單獨開辟一個新的內存緩存塊,且對解碼后的Bitmap進行強引用,即不會被動釋放內容,也不會被其他圖片LRU替換。方案優點在于能夠完美復用老的解碼邏輯,也支持子線程預解碼,只需要將Bitmap單獨緩存即可實現。
- 由于Bitmap是強引用,緩存塊也無上限,方案存在內存無限增長的可能,因此需要有一個主動釋放時機,即能減少內存占用,也能保證解碼順序不被影響。因此我們嘗試關聯view的detach方法,當動圖控件在快速滑動時,會主動釋放不可見View上對應的Bitmap。
經過實驗,最終采取了獨立緩存方案,在取得帶寬收益的同時,個人頁幀率無明顯劣化。
4.2 按需縮放
背景
圖片加載流程最終會將解碼后的bitmap渲染在控件上,當bitmap大小大于控件時,實際對用戶感官并無影響,圖片最終展示的像素值不會超過控件占據的空間,當圖片大小 >> 控件大小時:
- 造成一定程度的帶寬浪費;
- 圖片過大,客戶端性能損耗嚴重;
- 不同業務對同一張圖片進行圖片裁剪,沒有考慮圖片尺寸碎片化問題,導致veImageX-CDN緩存命中率顯著下降,最終造成回源成本的暴漲。
解決方案
在圖片展示時上報對應的bitmap和控件大小,從上報的數據來看,存在大量業務請求的圖片大小遠大于控件。因此,需要采用一種通用的方案,在滿足圖片質量的前提下,客戶端提供一套控件規范,根據控件大小將圖片收斂至固定大小,保證圖片尺寸和展示控件基本一致,同時減少圖片碎片化問題。
個人頁、同城、推薦等多個業務均存在雙列封面場景,這里以雙列封面為例子:
收益
- 視覺搜索場景文件大小 -83.39%,內存大小 -66.57%
- veImageX-CDN緩存命中率提升 + 6.99%,回源請求數減少 -23.79%
5 異常恢復
盡管前面我們對圖片的加載流程做了一系列優化,但因為抖音本身圖片量級大,部分業務如電商、IM等對圖片清晰度有較高的要求,且存在圖片放大和長圖展示等操作,業務會進行超大圖加載,直接將圖片直接加載進內存,單張圖片內存甚至高達100M+,無論在磁盤IO階段,還是內存解碼或者Bitmap拷貝過程中均會申請大量內存,最終導致卡頓、ANR甚至OOM崩潰,因此需要一套兜底方案來解決圖片OOM頻發問題,提升圖片加載的可靠性。
抖音在系統內存觸頂時,會通過釋放圖片內存來緩解壓力:監聽系統內存的告警回調,根據不同級別釋放不同大小的圖片內存緩存,降低發生OOM和ANR的幾率,但因大圖存在,仍然存在大量OOM。
OOM兜底
內存是一個全局指標,并不能直接通過OOM堆棧確定異常原因,因為OOM發生的時候內存可能處于高水位狀態,有可能申請了一個小對象就直接觸發異常。但關注到崩潰中Top5的堆棧大部分和圖片堆棧有關系,可以合理懷疑是App內圖片頻繁申請大內存導致。
因此針對高頻的圖片解碼和內存拷貝邏輯,增加兜底邏輯,當代碼發生OOM,主動catch,并通過清除圖片占用的內存緩存來釋放部分內存,降低內存水位:
- 清除兩級內存緩存,解碼內存緩存+未解碼內存緩存
- 清除接入層緩存的動圖預覽幀
實驗結果表明,盡管部分OOM轉換成native崩潰,但整體影響用戶大幅下降,實驗符合預期。
總結
總體來看,抖音在建設了圖片的全鏈路監控后,根據數據分析對圖片加載流程做了不少優化。
- 提升了圖片加載速度和性能
- 減少了圖片的總成本
從收益角度來看,大致可以分為成本優化和客戶端體驗優化兩方面。成本收益主要是圖片帶寬成本的降低,體驗收益體現在日活和OOM指標上,并且隨著各種優化方案推廣到更多的業務線,收益也在持續增加。
本文簡要介紹了抖音基于BDFresco的圖片優化最佳實踐、經驗沉淀、業務收益。由于篇幅所限,本文對探索歷程、具體實現等細節內容有所省略,但仍希望能給業內同仁們一點啟發或者參考借鑒。目前BDFresco已集成到火山引擎veImageX產品,對行業開放使用中,如需體驗抖音同款圖片優化能力,可以到火山引擎veImageX官網申請使用。
參考:火山引擎veImageX提供端到端一站式的整體圖片解決方案,包含圖片及素材托管、圖像處理與壓縮、分發、客戶端編解碼及圖片加載SDK全鏈路能力,官網地址:https://www.volcengine.com/product/imagex