作者:京東零售 何驍
介紹
京喜APP早期開發主要是快速原生化迭代替代原有H5,提高用戶體驗,在這期間也積累了不少性能問題,之后我們開始進行一些性能優化相關的作業,本文主要是介紹京喜圖片庫相關優化策略以及關于圖片相關的一些關聯知識,
圖片性能問題
作為電商APP,圖片在各個業務場景被大量使用,我們需要做到盡可能降低網路消耗/記憶體消耗/硬碟消耗,同時不降低圖片質量,提高圖片加載速度,給用戶帶來更好的使用體驗,基于這些性能目標,我們也通過初步性能評估梳理出了一些性能問題:
圖片加載慢/流量消耗高
圖片鏈接主要由后臺介面下發,下發圖片格式和尺寸由每個業務后臺指定,部分業務沒有使用更小的圖片格式比如WebP,或圖片尺寸過大,都會使圖片過大導致網路消耗高,特別是網路狀況不佳的場景,圖片加載過慢給用戶帶來不好的體驗,同時也會導致更多的I/O讀寫和解碼耗時,造成更多的電量消耗,
圖片記憶體占用高
經過初步的APP記憶體使用評估,圖片記憶體消耗占APP總記憶體消耗的比例最高,特別是大尺寸圖片會占用很多記憶體,一方面APP占用太高記憶體退到后臺容易被系統殺死,導致下次打開重新啟動影響體驗,另一方面APP大量使用記憶體,容易被系統殺死產生OOM,特別是我們目前有大量的低端設備用戶,設備記憶體相對比較低,
優化方向
基于上面分析出的一些性能問題,我們對圖片框架進行了整體重構優化,一方面是降低圖片網路傳輸,提高圖片加載速度,另一方面是減少圖片記憶體消耗,
最小化網路傳輸
京東圖片服務器提供了多種處理功能,例如圖片格式轉換,圖片降質,圖片縮放,圖片圓角等功能,這些功能通過在圖片URL中添加特定引數實作,圖片服務器會根據引數設定提前將圖片處理完成并保存到CDN服務器,我們可以通過添加圖片處理引數,減少圖片傳輸大小,
雖然后臺可以提前進行URL預處理,下發已添加過圖片引數的圖片URL,但是由于對接后臺業務很多,每個業務圖片引數設定差異很大無法統一,而且可能會造成性能影響,例如沒有使用webP圖片格式,下發太大的圖片尺寸,同時考慮到推動各業務后臺修改成本也很高,并且前端機型多,不同機型需要使用不同的圖片尺寸,另外也不方便灰度降級功能,后續功能修改也不方便,所以在客戶端進行圖片URL預處理是更好的方式,可以統一控制,也方便之后功能更新,
圖片URL預處理
圖片庫在網路圖片加載前,檢測是否是京東域名的圖片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時需要傳入對應的ptsize大小,圖片庫內部根據設備的scale進行動態計算出真實的像素寬高,
提示:
android設備因為螢屏差異比較大,更適合使用固定的scale,太多的圖片尺寸不利于CDN快取,無快取的時候需要對圖片進行相關引數處理,圖片處理本身是耗時操作,
Scale降級
低端機降級- 對于部分3xscale的低端設備,因為機器本身記憶體比較低,使用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:
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大螢屏設備和高端設備(記憶體比較高),適當增加更多記憶體上限,
優化成果
其他收益
域名統一- 減少了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幀進行解碼之后渲染,因為并不會把解碼過的幀保存,所以會導致頻繁解碼導致記憶體波動大,經過優化,對已經解碼過的幀進行保存,避免重復解碼的消耗,同時避免記憶體的波動,
優化前記憶體波動很明顯
優化后記憶體傾于平穩
提示:保存每一幀也會導致更多的記憶體消耗,目前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
- 手淘圖片庫HEIC使用
- 影片WebP和GIF比較
- WebP支持
- APNG支持
- AVIF
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/552299.html
標籤:iOS
上一篇:跑步課程匯入能力,助力科學訓練
下一篇:返回列表
