作者:京東零售 何驍
京喜APP早期開發主要是快速原生化
迭代替代原有H5
,提高使用者體驗,在這期間也積累了不少效能問題。之後我們開始進行一些效能優化相關的工作,本文主要是介紹京喜圖片庫
相關優化策略以及關於圖片相關的一些關聯知識。
作為電商APP,圖片在各個業務場景被大量使用。我們需要做到儘可能降低網路消耗
/記憶體消耗
/硬碟消耗
,同時不降低圖片質量
,提高圖片載入速度
,給使用者帶來更好的使用體驗。基於這些效能目標,我們也通過初步效能評估梳理出了一些效能問題:
圖片連結主要由後臺介面下發,下發圖片格式
和尺寸
由每個業務後臺指定。部分業務沒有使用更小的圖片格式比如WebP
,或圖片尺寸
過大,都會使圖片過大導致網路消耗高。特別是網路狀況不佳的場景,圖片載入過慢給使用者帶來不好的體驗。同時也會導致更多的I/O讀寫
和解碼
耗時,造成更多的電量消耗。
經過初步的APP記憶體使用評估,圖片記憶體消耗佔APP總記憶體消耗的比例最高
,特別是大尺寸圖片會佔用很多記憶體。一方面APP佔用太高記憶體退到後臺容易被系統殺死,導致下次開啟重新啟動影響體驗。另一方面APP大量使用記憶體,容易被系統殺死產生OOM
。特別是我們目前有大量的低端裝置使用者,裝置記憶體相對比較低。
基於上面分析出的一些效能問題,我們對圖片框架進行了整體重構優化。一方面是降低
圖片網路傳輸,提高圖片載入速度。另一方面是減少
圖片記憶體消耗。
京東圖片伺服器
提供了多種處理功能,例如圖片格式轉換,圖片降質,圖片縮放,圖片圓角
等功能。這些功能通過在圖片URL
中新增特定引數實現,圖片伺服器會根據引數設定提前將圖片處理完成並儲存到CDN
伺服器。我們可以通過新增圖片處理引數,減少圖片傳輸大小。
雖然後臺可以提前進行URL預處理
,下發已新增過圖片引數的圖片URL
。但是由於對接後臺業務很多,每個業務圖片引數設定差異很大無法統一,而且可能會造成效能影響,例如沒有使用webP
圖片格式,下發太大的圖片尺寸
。同時考慮到推動各業務後臺修改成本也很高,並且前端機型多,不同機型需要使用不同的圖片尺寸。另外也不方便灰度降級功能,後續功能修改也不方便。所以在使用者端
進行圖片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降級
低端機降級
- 對於部分3x
scale的低端裝置,因為機器本身記憶體比較低,使用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
會直接儲存到快取中,下次使用可以直接返回。快取key
由URL
+相關圖片轉換引數
拼接組成。
圖片處理引數通過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
圖片,導致記憶體佔用極高。例如一張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
檢測工具,當檢測到大圖片時,通過圖片翻轉
/高亮背景顏色
的方式提醒業務開發同學進行優化。
目前京喜APP有10+
個二級介面是基於Flutter
開發,所以我們也針對Flutter
圖片載入做了一些優化。
因為Flutter
框架自帶圖片庫只提供記憶體圖片快取,並不支援硬碟快取,所以會導致圖片重複下載。所以我們通過重寫ImageProvider
,當載入網路圖片時,通過Channel
呼叫原生圖片庫,原生圖片庫下載圖片到本地磁碟後,返回圖片檔案目錄。之後Flutter
通過檔案目錄載入解碼圖片顯示。這樣一方面可以利用原生圖片庫相關優化能力,同時也可以複用
圖片硬碟快取避免重複下載。
使用Image
元件時,通過設定cacheHeight
/cacheWidth
,將圖片解碼為置頂畫素
寬高的點陣圖尺寸,減少記憶體消耗。同時因為Flutter
記憶體消耗相對原生
更高,所以在Flutter
介面關閉時,通過呼叫imageCache
方法清除圖片記憶體消耗降低記憶體消耗。
動畫優化
- 因為通常使用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
引擎建立紋理
。之後講圖片紋理id
傳遞給Flutter
進行渲染。這樣可以統一在原生側管理圖片記憶體快取,優化之前Flutter
和原生
都分別有一份記憶體快取的方式。而且針對於混合棧的導航棧方式,也可以更好的進行圖片記憶體回收。另外針對Flutter
,需要提供更靈活的圖片記憶體回收策略,避免記憶體消耗過高。
提示:紋理可以複用記憶體中的
點陣圖
快取,所以並不會導致更多的記憶體佔用。紋理方式大概能減少30%
的記憶體消耗相比Flutter引擎圖片庫,主要是一些其他物件使用導致。
我們可以通過攔截WebView
圖片載入的方式,讓原生圖片庫來下載圖片之後傳遞圖片二進位制
資料給WebView
顯示。
通過這種方式,我們可以將原生圖片庫URL預處理
相關功能支援到H5
圖片,減少H5
載入過程中圖片流量消耗,提高圖片載入速度。同時因為APP原生
和WebView
圖片快取機制是相互獨立的,所以通過統一在原生側管理圖片快取,可以減少相同圖片的重複下載。
例如在iOS
系統上,WKWebView
目前只支援PNG
/JPG
/GIF
圖片格式。所以我們可以通過在原生端實現下載WebP
/HEIC
圖片,之後對圖片進行解碼
再傳給WebView
,這樣就可以支援其他圖片格式的顯示。
提示:因為
WebView
不支援直接傳遞點陣圖
二進位制資料顯示,所以需要提前轉換為PNG
/JPG
二進位制資料傳遞。所以對於其他圖片格式增加一次PNG
/JPG
編碼過程會造成更多的效能消耗。不過對於Android
系統應該可以在web核心層優化減少這塊消耗。
本文並沒有講底層圖片載入庫的具體實現,目前圖片庫不管是直接用第三方庫還是自研圖片庫實現方式通常差異不大。我們更多是關注自身業務以及如何利用圖片伺服器能力最大化改善網路圖片載入效能。所以部分策略可能不一定針對所有APP都合適,應該針對自身業務場景仔細評估優化方案。