veImageX 演進(jìn)之路:iOS 高性能圖片加載 SDK
1. SDK簡介
圖片在業(yè)務(wù)應(yīng)用場景是一個常見的元素,veImageX(簡稱ImageX)為業(yè)務(wù)提供了靈活、高效的一站式圖片處理解決方案,包括了服務(wù)端 SDK、上傳 SDK 和客戶端圖片加載 SDK。
1.1 業(yè)內(nèi)主流開源圖片加載 SDK
在介紹 veImageX 圖片加載 SDK 之前先看看業(yè)內(nèi)目前有哪些主流的圖片加載 SDK,veImageX 圖片加載 SDK 是使用 Objective-C 語言開發(fā)的,業(yè)內(nèi)使用 Objective-C 語言實(shí)現(xiàn)的主流開源圖片加載 SDK 有 YYWebImage,SDWebImage 等
- YYWebImage:一個異步圖片加載框架(YYKit 的一個組件)。它是作為 SDWebImage、PINRemoteImage 和 FLAnimatedImage 的改進(jìn)替代品而創(chuàng)建的。它使用 YYCache 支持內(nèi)存和磁盤緩存,使用 YYImage 支持 WebP/APNG/GIF 圖片解碼,但可惜的是此優(yōu)秀的框架于 2017 年左右已停止更新;
- SDWebImage:目前使用較廣泛的一個圖片處理框架,可以異步加載網(wǎng)絡(luò)圖片,并支持圖片本地緩存等特性,也是一款優(yōu)秀的圖片加載框架。
1.2 veImaegX 的 SDK 優(yōu)勢
veImageX 圖片加載 SDK 也是借鑒各家之所長,基于一些業(yè)務(wù)實(shí)際線上應(yīng)用的屬性自研了一套圖片加載 SDK,相比于這些開源圖片加載 SDK,主要有以下特性:
- 采用分層與模塊化架構(gòu)設(shè)計(jì),根據(jù)業(yè)務(wù)需要選擇相應(yīng)功能模塊,最大程度精簡包大小;
- 支持 WebP、AVIF、HEIF 這種高壓縮率圖片格式,特別是在自研的高性能HEIF軟件解碼庫支持下,能夠高效解碼 HEIF 格式,并擺脫 HEIF 原生 iOS 系統(tǒng)版本的限制;
- 支持云端加密、客戶端解密,保障圖片隱私安全;
- SDK 的網(wǎng)絡(luò)庫支持 HTTPDNS,可以高效防止內(nèi)容劫持及域名劫持,能夠有效降低圖片解碼失敗率,提升客戶端圖片加載體驗(yàn);
- 支持采集各項(xiàng)圖片相關(guān)數(shù)據(jù)并上報,配合 veImageX 控制臺實(shí)時大盤數(shù)據(jù)查看,可以為業(yè)務(wù)的運(yùn)營及產(chǎn)品的體驗(yàn)提升提供全面的從數(shù)據(jù)發(fā)現(xiàn)、數(shù)據(jù)分析、數(shù)據(jù)監(jiān)控、數(shù)據(jù)診斷、數(shù)據(jù)追蹤等全鏈路支持。
2. SDK 架構(gòu)
隨著時間的推移,SDK 的功能越來越多,各種業(yè)務(wù)對 SDK 的功能選擇也開始多樣化起來,特別是在 App 包體積日益增長需要降低的大背景下,SDK 也需要做包體積瘦身,面對以上種種問題,SDK 對功能的模塊化/插件化能力的要求也越來越高,SDK 的架構(gòu)也就隨之演變成下圖的樣子。
SDK 主要分為三層
- 接口層,也是最上層,這一層提供圖片加載與處理的各種接口,接口設(shè)計(jì)與主流開源圖片加載 SDK 保持一致,在這一層提供適配器,提供了開源圖片加載 SDK(如 YYWebImage,SDWebImage 等)的適配層,方便業(yè)務(wù)快速上手與無縫切換;
- 管理層,作為中間層負(fù)責(zé)各種模塊的交互管理,也包括云控配置管理和授權(quán)管理等;
- 模塊層,這一層包含了圖片加載流程的各個模塊:下載模塊,緩存模塊,解碼模塊,日志上報模塊等,業(yè)務(wù)可以根據(jù)自身需求來選擇性依賴這些模塊的各種功能,達(dá)到最小化依賴的原則。
3. UIImageView 如何通過 SDK 渲染出一張網(wǎng)絡(luò)圖片
業(yè)務(wù)上圖片的主流應(yīng)用場景就是加載網(wǎng)絡(luò)圖片,以 iOS 原生系統(tǒng)控件 UIImageView 為例,通過 SDK 加載一張網(wǎng)絡(luò)圖片的完整流程如下:
發(fā)起圖片請求 -> 查詢內(nèi)存緩存 -> 查詢磁盤緩存 -> 加入下載隊(duì)列 -> 開始下載 -> 獲取到服務(wù)端圖片未解碼數(shù)據(jù) -> 從圖片未解碼數(shù)據(jù)中解碼后得到可以渲染的圖片 -> 將解碼后的圖片和圖片未解碼數(shù)據(jù)分別緩存進(jìn)內(nèi)存和磁盤 -> UIImageView 渲染解碼后的圖片,至此,一張網(wǎng)絡(luò)圖片被成功加載并展示給用戶。
4. SDK 模塊介紹
在了解完 SDK 的主流場景中網(wǎng)絡(luò)圖片的完整加載流程后,下面分別介紹一下 SDK 加載流程中的下載、緩存、解碼、日志上報與圖片后處理這五大主要模塊。
4.1 下載模塊
下載模塊的主要任務(wù)是通過網(wǎng)絡(luò)庫把網(wǎng)絡(luò)圖片從服務(wù)端下載到客戶端,這個過程對圖片加載來講是非常重要的一環(huán),下載的成功與否直接決定了圖片能否正確展示,而網(wǎng)絡(luò)庫的性能也決定了圖片下載的快慢,最終反映到用戶的感受體驗(yàn)上。所以,下載模塊中的下載任務(wù)除了支持蘋果原生系統(tǒng)的網(wǎng)絡(luò)庫實(shí)現(xiàn)外,也支持字節(jié)內(nèi)部強(qiáng)大的自研網(wǎng)絡(luò)庫 TTNetwork 實(shí)現(xiàn),該庫不僅做了一些網(wǎng)絡(luò)相關(guān)優(yōu)化,例如 HTTPDNS,HTTP2+HTTPS 連接復(fù)用優(yōu)化、鏈路選擇、動態(tài)策略等,支持最新的網(wǎng)絡(luò)協(xié)議 QUIC,也提供了更為細(xì)粒度的網(wǎng)絡(luò)監(jiān)控,為 SDK 的圖片下載提供了高效的支持。SDK 默認(rèn)支持原生網(wǎng)絡(luò)庫與自研網(wǎng)絡(luò)庫,如果業(yè)務(wù)有自己的網(wǎng)絡(luò)庫,也可以通過插件化的形式集成進(jìn)來。
業(yè)務(wù)上一般會并發(fā)下載多張圖片,在 Feed 流場景中如果用戶來回滑動圖片,同樣的圖片會發(fā)生多次請求,如果相同圖片的多個請求都去反復(fù)下載圖片,這樣顯然會浪費(fèi)用戶流量,也會增加帶寬成本。SDK 會管理這些并發(fā)的下載任務(wù),并標(biāo)記相同的圖片請求,避免這種問題的發(fā)生。下載任務(wù)的管理與調(diào)度通過 iOS 系統(tǒng)原生的 NSOperation 與 NSOperationQueue 實(shí)現(xiàn),同時會根據(jù)請求參數(shù)生成一個 Identifier,用來唯一標(biāo)識一個下載任務(wù),交由下載管理器去管理,這樣就能避免在同一個時間段內(nèi)重復(fù)多次下載相同的圖片。
4.2 緩存模塊
緩存模塊由內(nèi)存和磁盤共同組成一個二級緩存結(jié)構(gòu),當(dāng)一張圖片被下載到客戶端上時,會被緩存進(jìn)內(nèi)存和磁盤緩存,如果 App 生命周期內(nèi)再請求這張圖片,則可以從內(nèi)存緩存中查到,如果冷啟動 App 后再請求這張圖片,則可以從磁盤緩存中查到。這樣不僅可以加快圖片的加載速度,提升用戶體驗(yàn),也可以降低用戶流量,節(jié)省帶寬成本。再對緩存加上過期時間限制,就可以解決圖片的時效性問題。
內(nèi)存緩存方面除了支持 iOS 原生的 NSCache 外,還支持 Strong-Weak 的弱引用緩存,當(dāng)緩存對象無人持有時會被及時釋放掉,降低內(nèi)存占用,同時也支持 LRU 緩存。在收到內(nèi)存不足的通知時會主動釋放內(nèi)存,緩解內(nèi)存壓力,同時保證線程安全。磁盤緩存方面除了支持最基本的 iOS 系統(tǒng)文件管理 NSFileManager,還支持 LRU 緩存,同時保證線程安全。
整體看,如果 App 內(nèi)只使用同一種固定的緩存算法的話,由于圖片使用場景各不相同,同一種緩存算法無法滿足所有場景,緩存命中率就會偏低。除了 SDK 默認(rèn)支持的緩存算法外,由于內(nèi)存和磁盤緩存都是由協(xié)議定義的,業(yè)務(wù)也可以根據(jù)需求去自定義緩存,在不同場景下使用不同的緩存算法,這樣可以極大的提高緩存命中率。在一些業(yè)務(wù)特定場景上 SDK 的緩存命中率能夠達(dá)到 80% 左右,隨著緩存命中率的提升,帶來的帶寬成本節(jié)省收益也越大。
4.3 解碼模塊
圖片下載到客戶端上后都是未經(jīng)過解碼前的數(shù)據(jù),想要把圖片正確展示給用戶,就必須對它進(jìn)行解碼。圖片解碼上支持通過 iOS 原生系統(tǒng)的解碼框架 ImageIO 進(jìn)行解碼,即蘋果原生能支持的格式,SDK 也能支持。除此之外,像 WebP、AVIF、VVIC(字節(jié)基于 BVC 算法自研的圖片格式)等原生不支持的圖片格式,SDK 通過自研解碼器或者開源解碼器的支持,也都能解碼這些格式的圖片。當(dāng)有新格式的圖片要支持時,只需實(shí)現(xiàn)對應(yīng)格式的動靜圖協(xié)議就能以插件化的形式集成進(jìn) SDK,達(dá)到支持新格式圖片的目的。
4.3.1 SDK 特色能力:iOS 全系統(tǒng)支持 HEIF
HEIF 這種高壓縮率格式的圖片在字節(jié)跳動公司內(nèi)部的應(yīng)用已經(jīng)比較成熟了。帶寬節(jié)省方面,相比 WebP,在同質(zhì)量下還能再節(jié)約 30% 的帶寬成本,為公司節(jié)省了大量的帶寬成本。加載優(yōu)化方面,HEIF 支持漸進(jìn)式加載,可以先加載 HEIF 縮略圖,再加載 HEIF 原圖,在網(wǎng)絡(luò)質(zhì)量不好的場景下也能有不錯的圖片加載體驗(yàn)。SDK 有了公司內(nèi)部自研的高性能 HEIF 軟件解碼庫的支持,讓 HEIF 格式圖片的解碼支持?jǐn)[脫了 iOS 系統(tǒng)的限制,不再局限在 iOS 11 及以上才能使用 HEIF 靜圖,iOS 13 及以上才能使用 HEIF 動圖,在低版本 iOS 上也能支持 HEIF 動靜圖,極大的提升了 HEIF 的應(yīng)用范圍,收獲了大量的帶寬成本節(jié)省收益。
4.4 圖片后處理模塊
在圖片加載完后,業(yè)務(wù)也可以根據(jù)需要再次對圖片進(jìn)行各種實(shí)時轉(zhuǎn)換,比如說加圓角、超分等,這些都是通過圖片后處理來完成。下面介紹下 SDK 的一個特色能力:超分。
4.4.1 SDK 特色能力:超分
超分,即超分辨率,指的是基于機(jī)器學(xué)習(xí)/深度學(xué)習(xí)方法,從給定的低分辨率圖片中恢復(fù)高分辨率的圖片,借助圖片后處理,可以在移動端上做到圖片實(shí)時超分。
一般可以用于兩種場景,一是用于提升用戶體驗(yàn),當(dāng)原圖片分辨率低、清晰度低時,對其進(jìn)行超分后,可以用來提升清晰度,以達(dá)到提升用戶觀看體驗(yàn)的目的;二是用于降檔超分,用戶在請求高分辨率的圖片時,可以在傳輸過程中降低圖片的分辨率,然后在客戶端上進(jìn)行超分,提升到原請求的分辨率,以達(dá)到節(jié)省帶寬成本的目的。
4.5 日志上報模塊
SDK 包含了三大日志模塊,圖片性能日志、用戶感知日志,大圖監(jiān)控日志,為業(yè)務(wù)的運(yùn)營及產(chǎn)品的體驗(yàn)提升提供了全面的數(shù)據(jù)支持。配合火山引擎 veImageX 的控制臺,可以實(shí)時查看各項(xiàng)可視化大盤數(shù)據(jù),全方位的監(jiān)控圖片的各項(xiàng)指標(biāo)。
其中,圖片性能日志包括了圖片 URL、下載耗時、解碼耗時、錯誤碼、圖片來源等數(shù)據(jù),用來監(jiān)控圖片各項(xiàng)性能指標(biāo);用戶感知日志包括了圖片 URL、ImageView 的 Size、ImageView 展示圖片耗時等數(shù)據(jù),用來監(jiān)控用戶體驗(yàn)各項(xiàng)指標(biāo);大圖監(jiān)控日志則包含了大圖 URL、內(nèi)存占用大小、圖片文件體積、圖片分辨率大小等數(shù)據(jù),可以全面的監(jiān)控異常大圖情況。
5. 演進(jìn):性能優(yōu)化
SDK 致力于極致的圖片加載用戶體驗(yàn),為此,SDK 做了很多相關(guān)性能優(yōu)化,下面主要介紹下 SDK 如何提升圖片加載體驗(yàn)、降低內(nèi)存占用、優(yōu)化動圖播放。
5.1 提升圖片加載體驗(yàn)
圖片加載的快慢直接影響到用戶的使用體驗(yàn),高效的圖片加載是 SDK 不可或缺的能力。
- 漸進(jìn)式加載
加載靜圖大圖,或者加載多幀數(shù)動圖,亦或者在弱網(wǎng)場景下,都可以開啟圖片漸進(jìn)式加載來提升圖片的加載體驗(yàn)。
SDK 支持傳統(tǒng)的 PNG、JPEG 靜圖漸進(jìn)式加載,也支持HEIF靜圖漸進(jìn)式加載,先加載 HEIF 縮略圖,再加載HEIF原圖。SDK 同時也支持動圖的漸進(jìn)式加載,動圖可以邊下載邊播放,在正常網(wǎng)絡(luò)下,可以提高首幀的加載速度,在弱網(wǎng)下,類似于視頻播放的緩沖機(jī)制,也可以提升動圖的播放體驗(yàn)。
- Force Decode
在圖片解碼方面,SDK 支持 Force Decode,能夠提前把 Bitmap Buffer 轉(zhuǎn)移到渲染進(jìn)程,減少了未來渲染時再去拷貝的耗時,如果原始解碼出來的 Bitmap Buffer,iOS 硬件屏幕不直接支持,會提前轉(zhuǎn)換好,避免渲染時在主線程的轉(zhuǎn)換開銷,提高圖片的加載幀率。
5.2 優(yōu)雅的內(nèi)存控制
通常情況下,App 內(nèi)圖片的場景還是很多的,當(dāng)加載大量圖片時,圖片所占的內(nèi)存可能會很大,如果內(nèi)存占用過高,會帶來 OOM 問題,給用戶的感受跟 Crash 一樣,都是應(yīng)用突然閃退。
SDK 有如下的幾種方案來降低圖片內(nèi)存占用:
- 釋放內(nèi)存緩存
當(dāng)系統(tǒng)內(nèi)存緊張,收到內(nèi)存不足通知時,緩存模塊會及時釋放內(nèi)存緩存,同時也提供接口,由業(yè)務(wù)在適當(dāng)時機(jī)主動釋放內(nèi)存緩存。
- 全局圖片降采樣
圖片在內(nèi)存中的占用大小可以簡單用如下公式來估算:
memoryCost(單位:字節(jié))= imageWidth(單位:像素)* imageHeight(單位:像素)* 4
由公式可以看出,如果想要降低內(nèi)存,那么就要想辦法在不影響功能和體驗(yàn)的前提下盡量降低圖片的寬高,由此,當(dāng)不能明確下載后的圖片大小是否會遠(yuǎn)大于需要展示的 ImageView 的大小時,可以使用全局圖片降采樣功能。全局圖片降采樣分為以尺寸大小限制進(jìn)行降采樣和以內(nèi)存大小限制進(jìn)行降采樣。
以尺寸大小限制進(jìn)行降采樣:
如果當(dāng)前圖片的長寬都大于降采樣的長寬,那么把原圖片長寬等比例縮放到恰好能貼到降采樣尺寸的輪廓
以內(nèi)存大小限制進(jìn)行降采樣:
如果當(dāng)前圖片的內(nèi)存占用超過內(nèi)存限制,那么把原圖片長寬等比例縮放到恰好低于內(nèi)存限額
- 禁止圖片渲染
每次需要渲染前,都會給業(yè)務(wù)回調(diào)當(dāng)前圖片的元信息,例如圖片的長寬尺寸、動圖的幀數(shù)、以及預(yù)估的內(nèi)存消耗量,業(yè)務(wù)可以根據(jù)此信息來禁止不符合預(yù)期的超大圖渲染。
- 大圖監(jiān)控
實(shí)際業(yè)務(wù)場景中,待展示圖片的分辨率和幀數(shù)都是未知的。在一些極端情況下(線上真實(shí)案例),某個動圖分辨率是 1080p、幀數(shù)上百幀,是用戶錄屏生成的,是個超大的動圖,解碼后有超過 1 個 GB的內(nèi)存占用,在一些低端機(jī)上就直接 OOM了。對于這類 OOM 情況,很難根據(jù)常規(guī)方法排查。那怎么有效監(jiān)控這種不符合預(yù)期的線上大圖呢,SDK 通過圖片展示尺寸,圖片解碼后內(nèi)存占用大小和圖片文件體積這三個維度來定義一個大圖,當(dāng)一張圖片觸發(fā)這三個維度中任意一個維度的閾值限制時,就會被記錄到大圖監(jiān)控日志內(nèi),這些數(shù)據(jù)后續(xù)會被上報。業(yè)務(wù)通過 veImageX 控制臺就可以看到大圖監(jiān)控這個指標(biāo)下的詳細(xì)數(shù)據(jù),當(dāng)發(fā)現(xiàn)內(nèi)存占用大小這個值異常大后,就可以及時查到相應(yīng)的圖片 URL,然后結(jié)合實(shí)際業(yè)務(wù)場景,及時下線這種不符合預(yù)期的超大圖,降低線上 OOM 率。
5.3 動圖播放的優(yōu)化
動圖在業(yè)務(wù)上也是一個常見應(yīng)用場景,如果能做好動圖的優(yōu)化,也可以帶來用戶體驗(yàn)的提升。動圖在播放時,會不斷解碼每一幀圖片,這時會大量消耗 CPU 資源,SDK 內(nèi)部會計(jì)算當(dāng)前可用的內(nèi)存以及渲染動圖的所有幀需要的內(nèi)存,如果當(dāng)前可用內(nèi)存滿足渲染動圖所有幀需要的內(nèi)存時,SDK 會緩存動圖的所有幀,以此來節(jié)省 CPU 資源,如果當(dāng)前可用內(nèi)存不滿足渲染動圖所有幀需要的內(nèi)存時,SDK 會在每一幀圖片播放結(jié)束之后舍棄前一幀,也就是不斷重復(fù)渲染下一幀圖片,通過消耗 CPU 資源節(jié)約內(nèi)存,達(dá)到 CPU 消耗與內(nèi)存節(jié)省的一個平衡。
6. 寫在最后
業(yè)內(nèi)雖然已經(jīng)有很多很成熟的圖片加載 SDK 了,但要契合公司自己業(yè)務(wù)發(fā)展的 SDK 也很重要,圖片加載 SDK 作為 veImageX 整體產(chǎn)品端到端不可或缺的一環(huán),也是在這種背景下應(yīng)運(yùn)而生了。除了一些性能優(yōu)化外,在成本節(jié)省上,HEIF 格式的應(yīng)用為公司節(jié)省了大量帶寬成本,收益非??捎^,并且也在持續(xù)嘗試新的壓縮率更高的圖片格式,例如 VVIC。在前沿能力應(yīng)用上,隨著圖片超分算法的不斷迭代優(yōu)化,相信在未來也能帶來不錯的體驗(yàn)上的提升和成本上的節(jié)省。