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

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

  1. 下載圖片
  2. 圖片處理(裁剪,邊框等)
  3. 寫入磁盤
  4. 從磁盤讀取數據到內核緩衝區
  5. 從內核緩衝區複製到用戶空間(內存級別拷貝)
  6. 解壓縮爲位圖(耗cpu較高)
  7. 如果位圖數據不是字節對齊的,CoreAnimationcopy一份位圖數據並進行字節對齊
  8. CoreAnimation渲染解壓縮過的位圖
  9. 以上4,5,6,7,8步是在UIImageViewsetImage時進行的,所以默認在主線程進行(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;
        }

另外一種解決方案是:

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

同時,仍然管理一個執行隊列,爲了避免佔用太多的資源,通常會對執行隊列設置一個最大的併發量。此外,爲了保證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解析

轉載自:https://segmentfault.com/a/1190000002776279
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章