1. 網路圖片顯示大體步驟:
- 下載圖片
- 圖片處理(裁剪,邊框等)
- 寫入磁碟
- 從磁碟讀取資料到核心緩衝區
- 從核心緩衝區複製到使用者空間(記憶體級別拷貝)
- 解壓縮為點陣圖(耗cpu較高)
- 如果點陣圖資料不是位元組對齊的,
CoreAnimation
會copy
一份點陣圖資料並進行位元組對齊 CoreAnimation
渲染解壓縮過的點陣圖
以上4,5,6,7,8步是在
UIImageView
的setImage
時進行的,所以預設在主執行緒進行(iOS UI操作必須在主執行緒執行)。
2. 一些優化思路:
- 非同步下載圖片
- image解壓縮放到子執行緒
- 使用快取 (包括記憶體級別和磁碟級別)
- 儲存解壓縮後的圖片,避免下次從磁碟載入的時候再次解壓縮
- 減少記憶體級別的拷貝 (針對第5點和第7點)
- 良好的介面(比如
SDWebImage
使用category
) Core Data
vs 檔案儲存- 圖片預下載
2.1 關於非同步圖片下載:
fastImageCache
主要針對於從磁碟檔案讀取並展示圖片的極端優化,所以並沒有整合非同步圖片下載的功能。這裡主要來看看SDWebImage(AFNetWorking的基本類似)的實現方案:
tableView中,非同步圖片下載任務的管理:
我們知道,tableViewCell是有重用機制的,也就是說,記憶體中只有當前可見的cell數目的例項,滑動的時候,新顯示cell會重用被滑出的cell物件。這樣就存在一個問題:
一般情況下在我們會在cellForRow方法裡面設定cell的圖片資料來源,也就是說如果一個cell的imageview物件開啟了一個下載任務,這個時候該cell物件發生了重用,新的image資料來源會開啟另外的一個下載任務,由於他們關聯的imageview物件實際上是同一個cell例項的imageview物件,就會發生2個下載任務回撥給同一個imageview物件。這個時候就有必要做一些處理,避免回撥發生時,錯誤的image資料來源重新整理了UI。
SDWebImage提供的UIImageView擴充套件的解決方案:
imageView物件會關聯一個下載列表(列表是給AnimationImages用的,這個時候會下載多張圖片),當tableview滑動,imageView重設資料來源(url)時,會cancel掉下載列表中所有的任務,然後開啟一個新的下載任務。這樣子就保證了只有當前可見的cell物件的imageView物件關聯的下載任務能夠回撥,不會發生image錯亂。
同時,SDWebImage管理了一個全域性下載佇列(在DownloadManager中),併發量設定為6.也就是說如果可見cell的數目是大於6的,就會有部分下載佇列處於等待狀態。而且,在新增下載任務到全域性的下載佇列中去的時候,SDWebImage預設是採取LIFO
策略的,具體是在新增下載任務的時候,將上次新增的下載任務新增依賴為新新增的下載任務。
1 2 3 4 5 6 |
[wself.downloadQueue addOperation:operation]; if (wself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) { // Emulate LIFO execution order by systematically adding new operations as last operation's dependency [wself.lastAddedOperation addDependency:operation]; wself.lastAddedOperation = operation; } |
<!–所以在執行SDWebImage的demo的時候,可以看到,如果快速滑下去,然後又滑回來的話,圖片是過了一會才顯示出來,這是因為快速滑動的時候,舊資料來源的下載任務被取消掉了。 –>
另外一種解決方案是:
imageView物件和圖片的url相關聯,在滑動時,不取消舊的下載任務,而是在下載任務完成回撥時,進行url匹配,只有匹配成功的image會重新整理imageView物件,而其他的image則只做快取操作,而不重新整理UI。
同時,仍然管理一個執行佇列,為了避免佔用太多的資源,通常會對執行佇列設定一個最大的併發量。此外,為了保證LIFO
的下載策略,可以自己維持一個等待佇列,每次下載任務開始的時候,將後進入的下載任務插入到等待佇列的前面。
iOS非同步任務一般有3種實現方式:
- NSOperationQueue
- GCD
- NSThread
這幾種方式就不細說了,SDWebImage是通過自定義NSOperation來抽象下載任務的,並結合了GCD來做一些主執行緒與子執行緒的切換。具體非同步下載的實現,AFNetworking與SDWebImage都是十分優秀的程式碼,有興趣的可以深入看看原始碼。
2.2 關於圖片解壓縮:
<!–### 圖片來源 針對app自帶的圖片,xcode在編譯的時候會對png圖片進行優化(據說是通過 pngcrush 這個開源的工具來優化),這樣在顯示的時候就會有一些比較好的體驗。 對於從internet上面下載的圖片,多數情況下,是需要做解壓縮後,才能渲染到螢幕上的。 –>
通用的解壓縮方案
主體的思路是在子執行緒,將原始的圖片渲染成一張的新的可以位元組顯示的圖片,來獲取一個解壓縮過的圖片。
基本上比較流行的一些開源庫都先後支援了在非同步執行緒完成圖片的解壓縮,並對解壓縮過後的圖片進行快取。
這麼做的優點是在setImage
的時候系統省去了上面的第6步,缺點就是圖片佔用的空間變大。
比如1張50*50畫素的圖片,在retina
的螢幕下所佔用的空間為100*100*4 ~ 40KB
下面的程式碼是SDWebImage
的解決方案:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
+ (UIImage *)decodedImageWithImage:(UIImage *)image { if (image.images) { // Do not decode animated images return image; } CGImageRef imageRef = image.CGImage; CGSize imageSize = CGSizeMake(CGImageGetWidth(imageRef), CGImageGetHeight(imageRef)); CGRect imageRect = (CGRect){.origin = CGPointZero, .size = imageSize}; CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(imageRef); int infoMask = (bitmapInfo & kCGBitmapAlphaInfoMask); BOOL anyNonAlpha = (infoMask == kCGImageAlphaNone || infoMask == kCGImageAlphaNoneSkipFirst || infoMask == kCGImageAlphaNoneSkipLast); // CGBitmapContextCreate doesn't support kCGImageAlphaNone with RGB. // https://developer.apple.com/library/mac/#qa/qa1037/_index.html if (infoMask == kCGImageAlphaNone && CGColorSpaceGetNumberOfComponents(colorSpace) > 1) { // Unset the old alpha info. bitmapInfo &= ~kCGBitmapAlphaInfoMask; // Set noneSkipFirst. bitmapInfo |= kCGImageAlphaNoneSkipFirst; } // Some PNGs tell us they have alpha but only 3 components. Odd. else if (!anyNonAlpha && CGColorSpaceGetNumberOfComponents(colorSpace) == 3) { // Unset the old alpha info. bitmapInfo &= ~kCGBitmapAlphaInfoMask; bitmapInfo |= kCGImageAlphaPremultipliedFirst; } // It calculates the bytes-per-row based on the bitsPerComponent and width arguments. CGContextRef context = CGBitmapContextCreate(NULL, imageSize.width, imageSize.height, CGImageGetBitsPerComponent(imageRef), 0, colorSpace, bitmapInfo); CGColorSpaceRelease(colorSpace); // If failed, return undecompressed image if (!context) return image; CGContextDrawImage(context, imageRect, imageRef); CGImageRef decompressedImageRef = CGBitmapContextCreateImage(context); CGContextRelease(context); UIImage *decompressedImage = [UIImage imageWithCGImage:decompressedImageRef scale:image.scale orientation:image.imageOrientation]; CGImageRelease(decompressedImageRef); return decompressedImage; |
2.3 關於位元組對齊
SDWebImage與AFNetworking都沒有對第7點做優化,FastImageCache相對與其他的開源庫,則對第5點與第7點做了優化。這裡我們談談第七點,關於圖片資料的位元組對齊。
Core Animation在某些情況下渲染前會先拷貝一份影像資料,通常是在影像資料非位元組對齊的情況下會進行拷貝處理,官方文件沒有對這次拷貝行為作說明,模擬器和Instrument裡有高亮顯示“copied images”的功能,但似乎它有bug,即使某張圖片沒有被高亮顯示出渲染時被copy,從呼叫堆疊上也還是能看到呼叫了CA::Render::copy_image方法:
那什麼是位元組對齊呢,按我的理解,為了效能,底層渲染影像時不是一個畫素一個畫素渲染,而是一塊一塊渲染,資料是一塊塊地取,就可能遇到這一塊連續的記憶體資料裡結尾的資料不是影像的內容,是記憶體裡其他的資料,可能越界讀取導致一些奇怪的東西混入,所以在渲染之前CoreAnimation要把資料拷貝一份進行處理,確保每一塊都是影像資料,對於不足一塊的資料置空。大致圖示:(pixel是影像畫素資料,data是記憶體裡其他資料)
塊的大小應該是跟CPU cache line有關,ARMv7是32byte,A9是64byte,在A9下CoreAnimation應該是按64byte作為一塊資料去讀取和渲染,讓影像資料對齊64byte就可以避免CoreAnimation再拷貝一份資料進行修補。FastImageCache做的位元組對齊就是這個事情。
從程式碼上來看,主要是在建立上圖解碼的過程中,CGBitmapContextCreate
函式的bytesPerRow
引數必須傳64的倍數。
比較各個開源框架的程式碼,可以看到SDWebImage與AFNetworking的該引數都傳的是0,即讓系統自動來計算該值(那為何系統自動計算的時候不讓圖片資料位元組就位元組對齊呢?)。
2.4 關於第3,4點,記憶體級別拷貝
以上3個開源庫中,FastImageCache對這一點做了很大的優化,其他的2個開源庫則未關注這一點。這一塊木有深入研究,就引用一下FastImageCache團隊對該點的一些說明。有能力的可以去看看原文章(英文):here。
記憶體對映
平常我們讀取磁碟上的一個檔案,上層API呼叫到最後會使用系統方法read()讀取資料,核心把磁碟資料讀入核心緩衝區,使用者再從核心緩衝區讀取資料複製到使用者記憶體空間,這裡有一次記憶體拷貝的時間消耗,並且讀取後整個檔案資料就已經存在於使用者記憶體中,佔用了程式的記憶體空間。FastImageCache採用了另一種讀寫檔案的方法,就是用
mmap
把檔案對映到使用者空間裡的虛擬記憶體,檔案中的位置在虛擬記憶體中有了對應的地址,可以像操作記憶體一樣操作這個檔案,相當於已經把整個檔案放入記憶體,但在真正使用到這些資料前卻不會消耗實體記憶體,也不會有讀寫磁碟的操作,只有真正使用這些資料時,也就是影像準備渲染在螢幕上時,虛擬記憶體管理系統VMS才根據缺頁載入的機制從磁碟載入對應的資料塊到實體記憶體,再進行渲染。這樣的檔案讀寫檔案方式少了資料從核心快取到使用者空間的拷貝,效率很高。
2.5 關於第二步圖片處理(裁剪,邊框等)
一般情況下,對於下載下來的圖片我們可能想要做一些處理,比如說做一些縮放,裁剪,或者新增圓角等等。
對於比較通用的縮放,或者圓角等功能,可以整合到控制元件本身。不過,提供一個介面出來,讓使用者能夠有機會對下載下來的圖片做一些其他的特殊處理是有必要的。
1 2 3 4 5 6 7 8 9 10 11 |
/** SDWebImage * Allows to transform the image immediately after it has been downloaded and just before to cache it on disk and memory. * NOTE: This method is called from a global queue in order to not to block the main thread. * * @param imageManager The current `SDWebImageManager` * @param image The image to transform * @param imageURL The url of the image to transform * * @return The transformed image object. */ - (UIImage *)imageManager:(SDWebImageManager *)imageManager transformDownloadedImage:(UIImage *)image withURL:(NSURL *)imageURL; |
2.6 其他(諸如圖片預下載,gif支援等等,下載進度條)
待補充
3. 常用的開源庫對比
tip | SDWebImage | AFNetworking | FastImageCache |
---|---|---|---|
非同步下載圖片 | YES | YES | NO |
子執行緒解壓縮 | YES | YES | YES |
子執行緒圖片處理(縮放,圓角等) | YES | YES | YES |
儲存解壓縮後的點陣圖 | YES | YES | YES |
記憶體級別快取 | YES | YES | YES |
磁碟級別快取 | YES | YES | YES |
UIImageView category | YES | NO | NO |
減少記憶體級別的拷貝 | NO | NO | YES |
介面易用性 | * | * | * |