iOS开发学无止境 - 异步图片加载优化与常用开源库分析
作者:羅軒(@luoyibu)
網址:http://www.jianshu.com/p/3b2c95e1404f
?
?
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策略的,具體是在添加下載任務的時候,將上次添加的下載任務添加依賴為新添加的下載任務。
?
[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的解決方案:
?
+ (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 關于第二步圖片處理(裁剪,邊框等)
?
一般情況下,對于下載下來的圖片我們可能想要做一些處理,比如說做一些縮放,裁剪,或者添加圓角等等。
?
對于比較通用的縮放,或者圓角等功能,可以集成到控件本身。不過,提供一個接口出來,讓使用者能夠有機會對下載下來的圖片做一些其他的特殊處理是有必要的。
?
/** 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. 常用的開源庫對比
?
| 異步下載圖片 | 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 |
| 接口易用性 | * | * | * |
?
參考資料
FastImageCache-github
SDWebImage-github
AFNetworking-github
File System vs Core Data: the image cache test
iOS image caching. Libraries benchmark (SDWebImage vs FastImageCache)
Avoiding Image Decompression Sickness
iOS圖片加載速度極限優化—FastImageCache解析
?
轉載于:https://www.cnblogs.com/xvewuzhijing/p/5003752.html
總結
以上是生活随笔為你收集整理的iOS开发学无止境 - 异步图片加载优化与常用开源库分析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: LocalDB 静默安装
- 下一篇: iOS 9.0以后支持http协议