iOS 異步圖片加載優化與經常使用開源庫分析

1. 網絡圖片顯示大致步驟:

  1. 下載圖片
  2. 圖片處理(裁剪,邊框等)
  3. 寫入磁盤
  4. 從磁盤讀取數據到內核緩衝區
  5. 從內核緩衝區複製到用戶空間(內存級別拷貝)
  6. 解壓縮爲位圖(耗cpu較高)
  7. 若是位圖數據不是字節對齊的,CoreAnimationcopy一份位圖數據並進行字節對齊
  8. CoreAnimation渲染解壓縮過的位圖

以上4,5,6,7,8步是在UIImageViewsetImage時進行的,因此默認在主線程進行(iOS UI操做必須在主線程執行)。html

2. 一些優化思路:

  • 異步下載圖片
  • image解壓縮放到子線程
  • 使用緩存 (包括內存級別和磁盤級別)
  • 存儲解壓縮後的圖片,避免下次從磁盤加載的時候再次解壓縮
  • 減小內存級別的拷貝 (針對第5點和第7點)
  • 良好的接口(好比SDWebImage使用category
  • Core Data vs 文件存儲
  • 圖片預下載

2.1 關於異步圖片下載:

fastImageCache主要針對於從磁盤文件讀取並展現圖片的極端優化,因此並無集成異步圖片下載的功能。這裏主要來看看SDWebImage(AFNetWorking的基本相似)的實現方案:ios

tableView中,異步圖片下載任務的管理:

咱們知道,tableViewCell是有重用機制的,也就是說,內存中只有當前可見的cell數目的實例,滑動的時候,新顯示cell會重用被滑出的cell對象。這樣就存在一個問題:git

通常狀況下在咱們會在cellForRow方法裏面設置cell的圖片數據源,也就是說若是一個cell的imageview對象開啓了一個下載任務,這個時候該cell對象發生了重用,新的image數據源會開啓另外的一個下載任務,因爲他們關聯的imageview對象其實是同一個cell實例的imageview對象,就會發生2個下載任務回調給同一個imageview對象。這個時候就有必要作一些處理,避免回調發生時,錯誤的image數據源刷新了UI。github

SDWebImage提供的UIImageView擴展的解決方案:web

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;
        }

另一種解決方案是:併發

imageView對象和圖片的url相關聯,在滑動時,不取消舊的下載任務,而是在下載任務完成回調時,進行url匹配,只有匹配成功的image會刷新imageView對象,而其餘的image則只作緩存操做,而不刷新UI。app

同時,仍然管理一個執行隊列,爲了不佔用太多的資源,一般會對執行隊列設置一個最大的併發量。此外,爲了保證LIFO的下載策略,能夠本身維持一個等待隊列,每次下載任務開始的時候,將後進入的下載任務插入到等待隊列的前面。框架

iOS異步任務通常有3種實現方式:

  • NSOperationQueue
  • GCD
  • NSThread

這幾種方式就不細說了,SDWebImage是經過自定義NSOperation來抽象下載任務的,並結合了GCD來作一些主線程與子線程的切換。具體異步下載的實現,AFNetworking與SDWebImage都是十分優秀的代碼,有興趣的能夠深刻看看源碼。

2.2 關於圖片解壓縮:

通用的解壓縮方案

主體的思路是在子線程,將原始的圖片渲染成一張的新的能夠字節顯示的圖片,來獲取一個解壓縮過的圖片。

基本上比較流行的一些開源庫都前後支持了在異步線程完成圖片的解壓縮,並對解壓縮事後的圖片進行緩存。

這麼作的優勢是在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. 經常使用的開源庫對比

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
接口易用性 *** *** *

參考資料

  1. FastImageCache-github
  2. SDWebImage-github
  3. AFNetworking-github
  4. File System vs Core Data: the image cache test
  5. iOS image caching. Libraries benchmark (SDWebImage vs FastImageCache)
  6. Avoiding Image Decompression Sickness
  7. iOS圖片加載速度極限優化—FastImageCache解析

轉載請註明出處哦,個人博客: luoyibu

相關文章
相關標籤/搜索