前言
本文基于WWDC2018-Image and Graphics Best Practices,對(duì)圖片加載和處理的思考和總結(jié)。
本文不是WWDC翻譯,如果需要了解視頻內(nèi)容可以點(diǎn)擊上面的鏈接觀看。
正文
圖片的顯示分為三步:加載、解碼、渲染。
通常,我們操作的只有加載,解碼和渲染是由UIKit進(jìn)行。

什么是解碼?
以UIImageView為例。當(dāng)其顯示在屏幕上時(shí),需要UIImage作為數(shù)據(jù)源。
UIImage持有的數(shù)據(jù)是未解碼的壓縮數(shù)據(jù),能節(jié)省較多的內(nèi)存和加快存儲(chǔ)。
當(dāng)UIImage被賦值給UIImage時(shí)(例如imageView.image = image;),圖像數(shù)據(jù)會(huì)被解碼,變成RGB的顏色數(shù)據(jù)。
解碼是一個(gè)計(jì)算量較大的任務(wù),且需要CPU來執(zhí)行;并且解碼出來的圖片體積與圖片的寬高有關(guān)系,而與圖片原來的體積無關(guān)。
其體積大小可簡(jiǎn)單描述為:寬 * 高 * 每個(gè)像素點(diǎn)的大小 = width * height * 4bytes。

圖像解碼操作會(huì)造成什么問題?
以我們常見的UITableView和UICollectionView為例,假如我們?cè)谑褂靡粋€(gè)多圖片顯示的功能:

在上下滑動(dòng)顯示圖片的過程中,我們會(huì)在cellFor的方法加載UIImage圖片、賦值給UIImageView,相當(dāng)于在主線程同時(shí)進(jìn)行IO操作、解碼操作等,會(huì)造成內(nèi)存迅速增長(zhǎng)和CPU負(fù)載瞬間提升。
并且內(nèi)存的迅速增加會(huì)觸發(fā)系統(tǒng)的內(nèi)存回收機(jī)制,嘗試回收其他后臺(tái)進(jìn)程的內(nèi)存,增加CPU的工作量。如果系統(tǒng)無法提供足夠的內(nèi)存,則會(huì)先結(jié)束其他后臺(tái)進(jìn)程,最終無法滿足的話會(huì)結(jié)束當(dāng)前進(jìn)程。

那么如何對(duì)這種情況進(jìn)行優(yōu)化 ?
優(yōu)化1:降采樣
在滑動(dòng)顯示的過程中,圖片顯示的寬高遠(yuǎn)比真實(shí)圖片要小,我們可以采用加載縮略圖的方式減少圖片的占用內(nèi)存。
如下圖所示:

我們加載jpeg的圖片,然后進(jìn)行相關(guān)設(shè)置,解碼后根據(jù)設(shè)置生成CGImage縮略圖,最后包裝成UIImage,最終傳遞給UIImageView渲染。
思考:這里的解碼步驟為何不是上文提到的imageView.image=image時(shí)機(jī)?
func downsample(imageAt imageURL: URL, to pointSize: CGSize, scale: CGFloat) -> UIImage {
let imageSourceOptions = [kCGImageSourceShouldCache: false] as CFDictionary
let imageSource = CGImageSourceCreateWithURL(imageURL as CFURL, imageSourceOptions)!
let maxDimensionInPixels = max(pointSize.width, pointSize.height) * scale
let downsampleOptions = [kCGImageSourceCreateThumbnailFromImageAlways: true,
kCGImageSourceShouldCacheImmediately: true,
kCGImageSourceCreateThumbnailWithTransform: true,
kCGImageSourceThumbnailMaxPixelSize: maxDimensionInPixels] as CFDictionary
let downsampledImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, downsampleOptions)!
return UIImage(cgImage: downsampledImage)
}
我的理解:正常的UIImage加載是從APP本地讀取,或者從網(wǎng)絡(luò)下載圖片,此時(shí)不涉及圖片內(nèi)容相關(guān)的操作,并不需要解碼;當(dāng)圖片被賦值給UIImageView時(shí),CALayer讀取圖片內(nèi)容進(jìn)行渲染,所以需要對(duì)圖片進(jìn)行解碼;
而上文的縮略圖生成過程中,已經(jīng)對(duì)圖片進(jìn)行解碼操作,此時(shí)的UIImage只是一個(gè)CGImage的封裝,所以當(dāng)UIImage賦值給UIImageView時(shí),CALayer可以直接使用CGImage所持有的圖像數(shù)據(jù)。
優(yōu)化2:異步處理

從用戶的體驗(yàn)來分析,滑動(dòng)的操作往往是間斷性觸發(fā),在滑動(dòng)的瞬間有較大的工作量,而且由于都是在主線程進(jìn)行操作無法進(jìn)行任務(wù)分配,CPU 2處于閑置。由此引申出兩種優(yōu)化手段:Prefetching(預(yù)處理)和
Background decoding/downsampling(子線程解碼和降采樣)。綜合起來,可以在Prefetching的時(shí)候把降采樣放到子線程進(jìn)行處理,因?yàn)榻挡蓸舆^程就包括解碼操作。

Prefetching回調(diào)中,把降采樣的操作放到同步隊(duì)列serialQueue中,處理完畢之后拋給主線程進(jìn)行update操作。
需要特別注意,此處不能是并發(fā)隊(duì)列,否則會(huì)造成線程爆炸,原因見總結(jié)部分。

優(yōu)化3:使用Image Asset Catalogs
Apple推薦的圖片資源管理工具,壓縮效率更高,在iOS 12的機(jī)器上有10~20%的空間節(jié)約,并且每個(gè)版本Apple都會(huì)持續(xù)對(duì)其進(jìn)行優(yōu)化。
內(nèi)容較多,詳細(xì)可點(diǎn)Session。
總結(jié)
應(yīng)用上述的優(yōu)化策略,已經(jīng)能對(duì)圖片加載有比較好的優(yōu)化。
WWDC后續(xù)還有對(duì)CustomDrawing和CALayer的BackingStore的介紹,因?yàn)榕c圖片關(guān)系不大,不在此贅述。
下面再介紹我對(duì)WWDC學(xué)習(xí)的看法。
附錄
我們可以先主觀假設(shè)兩個(gè)前提:
1、大部分蘋果工程師對(duì)iOS系統(tǒng)內(nèi)部實(shí)現(xiàn)都比我們要清楚;
2、能到WWDC分享的工程師在蘋果內(nèi)部也是優(yōu)秀的工程師;
那么WWDC所講的內(nèi)容我們可以認(rèn)為是事實(shí)上的結(jié)果。
于是可以使用我們所掌握的基礎(chǔ)知識(shí),還有對(duì)iOS系統(tǒng)的了解來分析WWDC上面所提到的現(xiàn)象,看我們的iOS知識(shí)體系是否存在缺陷;另外,WWDC介紹的很多知識(shí)點(diǎn)同樣免驗(yàn)證的加入自己的知識(shí)體系。
這就是我比較喜歡的一種看WWDC視頻的學(xué)習(xí)方式。
以上文提到的線程爆炸為例,看看這種方式的好處。
原文如下:
Thread Explosion(線程爆炸)
More images to decode than available CPUs(解碼圖像數(shù)量大于CPU數(shù)量)
GCD continues creating threads as new work is enqueued(GCD創(chuàng)建新線程處理新的任務(wù))
Each thread gets less time to actually decode images(每個(gè)線程獲得很少的時(shí)間解碼圖像)
從這個(gè)案例我們學(xué)習(xí)到如何避免圖像解碼的線程爆炸,但還能擴(kuò)散思維:
我們分析蘋果工程師的邏輯:
原因(解碼任務(wù)過多)==> 過程(GCD開啟更多線程) ==> 結(jié)果( 每個(gè)線程獲得更少的時(shí)間)
延伸出來的問題有:
GCD是如何處理并發(fā)隊(duì)列?為何會(huì)啟動(dòng)多個(gè)線程處理?
多少的線程數(shù)量是合適的?線程的cpu時(shí)間分配和切換代價(jià)如何?
...
舉一反三,類似的問題太多。但是這樣的思考稍顯混亂,仍有優(yōu)化的空間。
把腦海關(guān)于GCD的認(rèn)知提煉出來:
1、GCD是用來處理一系列任務(wù)的同步和異步執(zhí)行,隊(duì)列有串行和并發(fā)兩種,與線程的關(guān)系只有主線程和非主線程的區(qū)別;
2、串行隊(duì)列是執(zhí)行完當(dāng)前的任務(wù),才會(huì)執(zhí)行下一個(gè)block任務(wù);并行隊(duì)列是多個(gè)block任務(wù)并行執(zhí)行,GCD會(huì)根據(jù)任務(wù)的執(zhí)行情況分配線程,原則是盡快完成所有任務(wù);
接下來的表現(xiàn)是操作系統(tǒng)相關(guān)的知識(shí):
1、iOS系統(tǒng)中進(jìn)程和線程的關(guān)聯(lián),每個(gè)啟動(dòng)的APP都是一個(gè)進(jìn)程,其中有多個(gè)線程;
2、cpu的時(shí)間是分為多個(gè)時(shí)間片,每個(gè)線程輪詢執(zhí)行;
3、線程切換執(zhí)行有代價(jià),但比進(jìn)程切換小得多;
4、每個(gè)cpu核心在同一時(shí)刻只能執(zhí)行一個(gè)線程;
至此我們可以結(jié)合操作系統(tǒng)和GCD的知識(shí),猜測(cè)底層GCD的實(shí)現(xiàn)思路和線程爆炸情況下的表現(xiàn):
主線程把多個(gè)任務(wù)block放到并發(fā)隊(duì)列,GCD先啟動(dòng)一個(gè)線程處理解碼任務(wù),線程執(zhí)行過程中遇到耗時(shí)操作時(shí)(IO等待、大量CPU計(jì)算),短時(shí)間內(nèi)無法完成,為了不阻塞后續(xù)任務(wù)的執(zhí)行,GCD啟動(dòng)新的線程處理新的任務(wù)。
集合此案例,我們能回答相關(guān)問題:
1、現(xiàn)在有一個(gè)很復(fù)雜的計(jì)算任務(wù),例如是統(tǒng)計(jì)一個(gè)5000x5000圖片中像素點(diǎn)的RGB顏色通道,如果用分為25個(gè)任務(wù)放到GCD并發(fā)隊(duì)列,把大圖切分成25個(gè)1000x1000小圖分別統(tǒng)計(jì),是否會(huì)速度的提升?
2、GCD的串行隊(duì)列和并發(fā)隊(duì)列的應(yīng)用場(chǎng)景有何不同?
以上一些平時(shí)學(xué)習(xí)的感受。
如果能對(duì)你有所觸動(dòng),十分榮幸;
如果你覺得能改進(jìn),歡迎提出來幫助我成長(zhǎng);
如果你覺得毫無用處,至少你知道一種錯(cuò)誤的學(xué)習(xí)方法。