作者:落影loyinglin
連結:https://www.jianshu.com/p/7d8a82115060
前言
本文基於WWDC2018-Image and Graphics Best Practices:https://developer.apple.com/videos/play/wwdc2018/219/,對圖片載入和處理的思考和總結。
本文不是WWDC翻譯,如果需要瞭解影片內容可以點選上面的連結觀看。
正文
圖片的顯示分為三步:載入、解碼、渲染。
通常,我們操作的只有載入,解碼和渲染是由UIKit進行。
什麼是解碼?
以UIImageView為例。當其顯示在螢幕上時,需要UIImage作為資料源。
UIImage持有的資料是未解碼的壓縮資料,能節省較多的記憶體和加快儲存。
當UIImage被賦值給UIImage時(例如imageView.image = image;
),影象資料會被解碼,變成RGB的顏色資料。
解碼是一個計算量較大的任務,且需要CPU來執行;並且解碼出來的圖片體積與圖片的寬高有關係,而與圖片原來的體積無關。
其體積大小可簡單描述為:寬 * 高 * 每個畫素點的大小 = width * height * 4bytes。
影象解碼操作會造成什麼問題?
以我們常見的UITableView和UICollectionView為例,假如我們在使用一個多圖片顯示的功能:
在上下滑動顯示圖片的過程中,我們會在cellFor的方法載入UIImage圖片、賦值給UIImageView,相當於在主執行緒同時進行IO操作、解碼操作等,會造成記憶體迅速增長和CPU負載瞬間提升。
並且記憶體的迅速增加會觸發系統的記憶體回收機制,嘗試回收其他後臺行程的記憶體,增加CPU的工作量。如果系統無法提供足夠的記憶體,則會先結束其他後臺行程,最終無法滿足的話會結束當前行程。
那麼如何對這種情況進行最佳化 ?
最佳化1:降取樣
在滑動顯示的過程中,圖片顯示的寬高遠比真實圖片要小,我們可以採用載入縮圖的方式減少圖片的佔用記憶體。
如下圖所示:
我們載入jpeg的圖片,然後進行相關設定,解碼後根據設定生成CGImage縮圖,最後包裝成UIImage,最終傳遞給UIImageView渲染。
思考:這裡的解碼步驟為何不是上文提到的imageView.image=image
時機?
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本地讀取,或者從網路下載圖片,此時不涉及圖片內容相關的操作,並不需要解碼;當圖片被賦值給UIImageView時,CALayer讀取圖片內容進行渲染,所以需要對圖片進行解碼;
而上文的縮圖生成過程中,已經對圖片進行解碼操作,此時的UIImage只是一個CGImage的封裝,所以當UIImage賦值給UIImageView時,CALayer可以直接使用CGImage所持有的影象資料。
最佳化2:非同步處理
從使用者的體驗來分析,滑動的操作往往是間斷性觸發,在滑動的瞬間有較大的工作量,而且由於都是在主執行緒進行操作無法進行任務分配,CPU 2處於閑置。由此引申出兩種最佳化手段:Prefetching(預處理)和
Background decoding/downsampling(子執行緒解碼和降取樣)。綜合起來,可以在Prefetching的時候把降取樣放到子執行緒進行處理,因為降取樣過程就包括解碼操作。
Prefetching回呼中,把降取樣的操作放到同步佇列serialQueue中,處理完畢之後拋給主執行緒進行update操作。
需要特別註意,此處不能是非同步佇列,否則會造成執行緒爆炸,原因見總結部分。
最佳化3:使用Image Asset Catalogs
Apple推薦的圖片資源管理工具,壓縮效率更高,在iOS 12的機器上有10~20%的空間節約,並且每個版本Apple都會持續對其進行最佳化。
內容較多,詳細可點Session:https://developer.apple.com/videos/play/wwdc2018/227/。
總結
應用上述的最佳化策略,已經能對圖片載入有比較好的最佳化。
WWDC後續還有對CustomDrawing和CALayer的BackingStore的介紹,因為與圖片關係不大,不在此贅述。
下麵再介紹我對WWDC學習的看法。
附錄
我們可以先主觀假設兩個前提:
1、大部分蘋果工程師對iOS系統內部實現都比我們要清楚;
2、能到WWDC分享的工程師在蘋果內部也是優秀的工程師;
那麼WWDC所講的內容我們可以認為是事實上的結果。
於是可以使用我們所掌握的基礎知識,還有對iOS系統的瞭解來分析WWDC上面所提到的現象,看我們的iOS知識體系是否存在缺陷;另外,WWDC介紹的很多知識點同樣免驗證的加入自己的知識體系。
這就是我比較喜歡的一種看WWDC影片的學習方式。
以上文提到的執行緒爆炸為例,看看這種方式的好處。
原文如下:
Thread Explosion(執行緒爆炸)
More images to decode than available CPUs(解碼影象數量大於CPU數量)
GCD continues creating threads as new work is enqueued(GCD建立新執行緒處理新的任務)
Each thread gets less time to actually decode images(每個執行緒獲得很少的時間解碼影象)
從這個案例我們學習到如何避免影象解碼的執行緒爆炸,但還能擴散思維:
我們分析蘋果工程師的邏輯:
原因(解碼任務過多)==> 過程(GCD開啟更多執行緒) ==> 結果( 每個執行緒獲得更少的時間)
延伸出來的問題有:
GCD是如何處理非同步佇列?為何會啟動多個執行緒處理?
多少的執行緒數量是合適的?執行緒的cpu時間分配和切換代價如何?
…
舉一反三,類似的問題太多。但是這樣的思考稍顯混亂,仍有最佳化的空間。
把腦海關於GCD的認知提煉出來:
1、GCD是用來處理一系列任務的同步和非同步執行,佇列有序列和併發兩種,與執行緒的關係只有主執行緒和非主執行緒的區別;
2、序列佇列是執行完當前的任務,才會執行下一個block任務;並行佇列是多個block任務並行執行,GCD會根據任務的執行情況分配執行緒,原則是儘快完成所有任務;
接下來的表現是作業系統相關的知識:
1、iOS系統中行程和執行緒的關聯,每個啟動的APP都是一個行程,其中有多個執行緒;
2、cpu的時間是分為多個時間片,每個執行緒輪詢執行;
3、執行緒切換執行有代價,但比行程切換小得多;
4、每個cpu核心在同一時刻只能執行一個執行緒;
至此我們可以結合作業系統和GCD的知識,猜測底層GCD的實現思路和執行緒爆炸情況下的表現:
主執行緒把多個任務block放到併發佇列,GCD先啟動一個執行緒處理解碼任務,執行緒執行過程中遇到耗時操作時(IO等待、大量CPU計算),短時間內無法完成,為了不阻塞後續任務的執行,GCD啟動新的執行緒處理新的任務。
集合此案例,我們能回答相關問題:
1、現在有一個很複雜的計算任務,例如是統計一個5000×5000圖片中畫素點的RGB顏色通道,如果用分為25個任務放到GCD併發佇列,把大圖切分成25個1000×1000小圖分別統計,是否會速度的提升?
2、GCD的序列佇列和併發佇列的應用場景有何不同?以上一些平時學習的感受。
如果能對你有所觸動,十分榮幸;
如果你覺得能改進,歡迎提出來幫助我成長;
如果你覺得毫無用處,至少你知道一種錯誤的學習方法。
●編號312,輸入編號直達本文
●輸入m獲取文章目錄
Web開發
更多推薦《18個技術類微信公眾號》
涵蓋:程式人生、演演算法與資料結構、駭客技術與網路安全、大資料技術、前端開發、Java、Python、Web開發、安卓開發、iOS開發、C/C++、.NET、Linux、資料庫、運維等。