iOS異步圖片加載優(yōu)化與常用開源庫分析

1. 網(wǎng)絡(luò)圖片顯示大體步驟:

  1. 下載圖片
  2. 圖片處理(裁剪疚颊,邊框等)
  3. 寫入磁盤
  4. 從磁盤讀取數(shù)據(jù)到內(nèi)核緩沖區(qū)
  5. 從內(nèi)核緩沖區(qū)復(fù)制到用戶空間(內(nèi)存級別拷貝)
  6. 解壓縮為位圖(耗cpu較高)
  7. 如果位圖數(shù)據(jù)不是字節(jié)對齊的盹沈,CoreAnimationcopy一份位圖數(shù)據(jù)并進(jìn)行字節(jié)對齊
  8. CoreAnimation渲染解壓縮過的位圖

以上4赃磨,5瞬浓,6惠遏,7舀武,8步是在UIImageViewsetImage時進(jìn)行的拄养,所以默認(rèn)在主線程進(jìn)行(iOS UI操作必須在主線程執(zhí)行)。

2. 一些優(yōu)化思路:

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

2.1 關(guān)于異步圖片下載:

fastImageCache主要針對于從磁盤文件讀取并展示圖片的極端優(yōu)化瘪匿,所以并沒有集成異步圖片下載的功能跛梗。這里主要來看看SDWebImage(AFNetWorking的基本類似)的實(shí)現(xiàn)方案:

tableView中,異步圖片下載任務(wù)的管理:

我們知道棋弥,tableViewCell是有重用機(jī)制的核偿,也就是說,內(nèi)存中只有當(dāng)前可見的cell數(shù)目的實(shí)例顽染,滑動的時候宪祥,新顯示cell會重用被滑出的cell對象。這樣就存在一個問題:

一般情況下在我們會在cellForRow方法里面設(shè)置cell的圖片數(shù)據(jù)源家乘,也就是說如果一個cell的imageview對象開啟了一個下載任務(wù)蝗羊,這個時候該cell對象發(fā)生了重用,新的image數(shù)據(jù)源會開啟另外的一個下載任務(wù)仁锯,由于他們關(guān)聯(lián)的imageview對象實(shí)際上是同一個cell實(shí)例的imageview對象耀找,就會發(fā)生2個下載任務(wù)回調(diào)給同一個imageview對象。這個時候就有必要做一些處理业崖,避免回調(diào)發(fā)生時野芒,錯誤的image數(shù)據(jù)源刷新了UI。

SDWebImage提供的UIImageView擴(kuò)展的解決方案:

imageView對象會關(guān)聯(lián)一個下載列表(列表是給AnimationImages用的双炕,這個時候會下載多張圖片)狞悲,當(dāng)tableview滑動,imageView重設(shè)數(shù)據(jù)源(url)時妇斤,會cancel掉下載列表中所有的任務(wù)摇锋,然后開啟一個新的下載任務(wù)。這樣子就保證了只有當(dāng)前可見的cell對象的imageView對象關(guān)聯(lián)的下載任務(wù)能夠回調(diào)站超,不會發(fā)生image錯亂荸恕。

同時,SDWebImage管理了一個全局下載隊列(在DownloadManager中),并發(fā)量設(shè)置為6.也就是說如果可見cell的數(shù)目是大于6的死相,就會有部分下載隊列處于等待狀態(tài)融求。而且,在添加下載任務(wù)到全局的下載隊列中去的時候算撮,SDWebImage默認(rèn)是采取LIFO策略的生宛,具體是在添加下載任務(wù)的時候,將上次添加的下載任務(wù)添加依賴為新添加的下載任務(wù)肮柜。

        [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相關(guān)聯(lián)陷舅,在滑動時,不取消舊的下載任務(wù)素挽,而是在下載任務(wù)完成回調(diào)時蔑赘,進(jìn)行url匹配狸驳,只有匹配成功的image會刷新imageView對象预明,而其他的image則只做緩存操作缩赛,而不刷新UI。

同時撰糠,仍然管理一個執(zhí)行隊列酥馍,為了避免占用太多的資源,通常會對執(zhí)行隊列設(shè)置一個最大的并發(fā)量阅酪。此外旨袒,為了保證LIFO的下載策略,可以自己維持一個等待隊列术辐,每次下載任務(wù)開始的時候砚尽,將后進(jìn)入的下載任務(wù)插入到等待隊列的前面。

iOS異步任務(wù)一般有3種實(shí)現(xiàn)方式:

  • NSOperationQueue
  • GCD
  • NSThread

這幾種方式就不細(xì)說了辉词,SDWebImage是通過自定義NSOperation來抽象下載任務(wù)的必孤,并結(jié)合了GCD來做一些主線程與子線程的切換。具體異步下載的實(shí)現(xiàn)瑞躺,AFNetworking與SDWebImage都是十分優(yōu)秀的代碼敷搪,有興趣的可以深入看看源碼。

2.2 關(guān)于圖片解壓縮:

通用的解壓縮方案

主體的思路是在子線程幢哨,將原始的圖片渲染成一張的新的可以字節(jié)顯示的圖片赡勘,來獲取一個解壓縮過的圖片。

基本上比較流行的一些開源庫都先后支持了在異步線程完成圖片的解壓縮捞镰,并對解壓縮過后的圖片進(jìn)行緩存闸与。

這么做的優(yōu)點(diǎn)是在setImage的時候系統(tǒng)省去了上面的第6步,缺點(diǎn)就是圖片占用的空間變大岸售。
比如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 關(guān)于字節(jié)對齊

SDWebImage與AFNetworking都沒有對第7點(diǎn)做優(yōu)化,F(xiàn)astImageCache相對與其他的開源庫冰评,則對第5點(diǎn)與第7點(diǎn)做了優(yōu)化映胁。這里我們談?wù)劦谄唿c(diǎn),關(guān)于圖片數(shù)據(jù)的字節(jié)對齊甲雅。

Core Animation在某些情況下渲染前會先拷貝一份圖像數(shù)據(jù)解孙,通常是在圖像數(shù)據(jù)非字節(jié)對齊的情況下會進(jìn)行拷貝處理,官方文檔沒有對這次拷貝行為作說明抛人,模擬器和Instrument里有高亮顯示“copied images”的功能弛姜,但似乎它有bug,即使某張圖片沒有被高亮顯示出渲染時被copy妖枚,從調(diào)用堆棧上也還是能看到調(diào)用了CA::Render::copy_image方法:

那什么是字節(jié)對齊呢廷臼,按我的理解,為了性能,底層渲染圖像時不是一個像素一個像素渲染荠商,而是一塊一塊渲染寂恬,數(shù)據(jù)是一塊塊地取,就可能遇到這一塊連續(xù)的內(nèi)存數(shù)據(jù)里結(jié)尾的數(shù)據(jù)不是圖像的內(nèi)容莱没,是內(nèi)存里其他的數(shù)據(jù)初肉,可能越界讀取導(dǎo)致一些奇怪的東西混入,所以在渲染之前CoreAnimation要把數(shù)據(jù)拷貝一份進(jìn)行處理饰躲,確保每一塊都是圖像數(shù)據(jù)牙咏,對于不足一塊的數(shù)據(jù)置空。大致圖示:(pixel是圖像像素數(shù)據(jù)嘹裂,data是內(nèi)存里其他數(shù)據(jù))

塊的大小應(yīng)該是跟CPU cache line有關(guān)妄壶,ARMv7是32byte,A9是64byte寄狼,在A9下CoreAnimation應(yīng)該是按64byte作為一塊數(shù)據(jù)去讀取和渲染盯拱,讓圖像數(shù)據(jù)對齊64byte就可以避免CoreAnimation再拷貝一份數(shù)據(jù)進(jìn)行修補(bǔ)。FastImageCache做的字節(jié)對齊就是這個事情例嘱。

從代碼上來看狡逢,主要是在創(chuàng)建上圖解碼的過程中,CGBitmapContextCreate函數(shù)的bytesPerRow參數(shù)必須傳64的倍數(shù)拼卵。

比較各個開源框架的代碼奢浑,可以看到SDWebImage與AFNetworking的該參數(shù)都傳的是0,即讓系統(tǒng)自動來計算該值(那為何系統(tǒng)自動計算的時候不讓圖片數(shù)據(jù)字節(jié)就字節(jié)對齊呢腋腮?)雀彼。

2.4 關(guān)于第3,4點(diǎn)即寡,內(nèi)存級別拷貝

以上3個開源庫中徊哑,F(xiàn)astImageCache對這一點(diǎn)做了很大的優(yōu)化,其他的2個開源庫則未關(guān)注這一點(diǎn)聪富。這一塊木有深入研究莺丑,就引用一下FastImageCache團(tuán)隊對該點(diǎn)的一些說明。有能力的可以去看看原文章(英文):here墩蔓。

內(nèi)存映射
平常我們讀取磁盤上的一個文件梢莽,上層API調(diào)用到最后會使用系統(tǒng)方法read()讀取數(shù)據(jù),內(nèi)核把磁盤數(shù)據(jù)讀入內(nèi)核緩沖區(qū)奸披,用戶再從內(nèi)核緩沖區(qū)讀取數(shù)據(jù)復(fù)制到用戶內(nèi)存空間昏名,這里有一次內(nèi)存拷貝的時間消耗,并且讀取后整個文件數(shù)據(jù)就已經(jīng)存在于用戶內(nèi)存中阵面,占用了進(jìn)程的內(nèi)存空間轻局。

FastImageCache采用了另一種讀寫文件的方法洪鸭,就是用mmap把文件映射到用戶空間里的虛擬內(nèi)存,文件中的位置在虛擬內(nèi)存中有了對應(yīng)的地址仑扑,可以像操作內(nèi)存一樣操作這個文件览爵,相當(dāng)于已經(jīng)把整個文件放入內(nèi)存,但在真正使用到這些數(shù)據(jù)前卻不會消耗物理內(nèi)存夫壁,也不會有讀寫磁盤的操作拾枣,只有真正使用這些數(shù)據(jù)時沃疮,也就是圖像準(zhǔn)備渲染在屏幕上時盒让,虛擬內(nèi)存管理系統(tǒng)VMS才根據(jù)缺頁加載的機(jī)制從磁盤加載對應(yīng)的數(shù)據(jù)塊到物理內(nèi)存,再進(jìn)行渲染司蔬。這樣的文件讀寫文件方式少了數(shù)據(jù)從內(nèi)核緩存到用戶空間的拷貝邑茄,效率很高。

2.5 關(guān)于第二步圖片處理(裁剪俊啼,邊框等)

一般情況下肺缕,對于下載下來的圖片我們可能想要做一些處理,比如說做一些縮放授帕,裁剪同木,或者添加圓角等等。

對于比較通用的縮放跛十,或者圓角等功能彤路,可以集成到控件本身。不過芥映,提供一個接口出來洲尊,讓使用者能夠有機(jī)會對下載下來的圖片做一些其他的特殊處理是有必要的。

/** 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 其他(諸如圖片預(yù)下載奈偏,gif支持等等,下載進(jìn)度條)

待補(bǔ)充

3. 常用的開源庫對比

tip SDWebImage AFNetworking FastImageCache
異步下載圖片 YES YES NO
子線程解壓縮 YES YES YES
子線程圖片處理(縮放坞嘀,圓角等) YES YES YES
存儲解壓縮后的位圖 YES YES YES
內(nèi)存級別緩存 YES YES YES
磁盤級別緩存 YES YES YES
UIImageView category YES NO NO
減少內(nèi)存級別的拷貝 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圖片加載速度極限優(yōu)化—FastImageCache解析

轉(zhuǎn)載請注明出處哦,我的博客: luoyibu

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末惊来,一起剝皮案震驚了整個濱河市丽涩,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌裁蚁,老刑警劉巖内狸,帶你破解...
    沈念sama閱讀 211,194評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異厘擂,居然都是意外死亡昆淡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,058評論 2 385
  • 文/潘曉璐 我一進(jìn)店門刽严,熙熙樓的掌柜王于貴愁眉苦臉地迎上來昂灵,“玉大人避凝,你說我怎么就攤上這事≌2梗” “怎么了管削?”我有些...
    開封第一講書人閱讀 156,780評論 0 346
  • 文/不壞的土叔 我叫張陵,是天一觀的道長撑螺。 經(jīng)常有香客問我含思,道長,這世上最難降的妖魔是什么甘晤? 我笑而不...
    開封第一講書人閱讀 56,388評論 1 283
  • 正文 為了忘掉前任含潘,我火速辦了婚禮,結(jié)果婚禮上线婚,老公的妹妹穿的比我還像新娘遏弱。我一直安慰自己,他們只是感情好塞弊,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,430評論 5 384
  • 文/花漫 我一把揭開白布漱逸。 她就那樣靜靜地躺著,像睡著了一般游沿。 火紅的嫁衣襯著肌膚如雪饰抒。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,764評論 1 290
  • 那天诀黍,我揣著相機(jī)與錄音袋坑,去河邊找鬼。 笑死蔗草,一個胖子當(dāng)著我的面吹牛咒彤,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播咒精,決...
    沈念sama閱讀 38,907評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼镶柱,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了模叙?” 一聲冷哼從身側(cè)響起歇拆,我...
    開封第一講書人閱讀 37,679評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎范咨,沒想到半個月后故觅,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,122評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡渠啊,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,459評論 2 325
  • 正文 我和宋清朗相戀三年输吏,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片替蛉。...
    茶點(diǎn)故事閱讀 38,605評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡贯溅,死狀恐怖拄氯,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情它浅,我是刑警寧澤译柏,帶...
    沈念sama閱讀 34,270評論 4 329
  • 正文 年R本政府宣布,位于F島的核電站姐霍,受9級特大地震影響鄙麦,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜镊折,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,867評論 3 312
  • 文/蒙蒙 一胯府、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧腌乡,春花似錦盟劫、人聲如沸夜牡。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,734評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽塘装。三九已至急迂,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間蹦肴,已是汗流浹背僚碎。 一陣腳步聲響...
    開封第一講書人閱讀 31,961評論 1 265
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留阴幌,地道東北人勺阐。 一個月前我還...
    沈念sama閱讀 46,297評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像矛双,于是被迫代替她去往敵國和親渊抽。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,472評論 2 348

推薦閱讀更多精彩內(nèi)容