1. 網(wǎng)絡(luò)圖片顯示大體步驟:
- 下載圖片
- 圖片處理(裁剪疚颊,邊框等)
- 寫入磁盤
- 從磁盤讀取數(shù)據(jù)到內(nèi)核緩沖區(qū)
- 從內(nèi)核緩沖區(qū)復(fù)制到用戶空間(內(nèi)存級別拷貝)
- 解壓縮為位圖(耗cpu較高)
- 如果位圖數(shù)據(jù)不是字節(jié)對齊的盹沈,
CoreAnimation
會copy
一份位圖數(shù)據(jù)并進(jìn)行字節(jié)對齊 -
CoreAnimation
渲染解壓縮過的位圖
以上4赃磨,5瞬浓,6惠遏,7舀武,8步是在
UIImageView
的setImage
時進(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 |
接口易用性 | *** | *** | * |
參考資料
- 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圖片加載速度極限優(yōu)化—FastImageCache解析
轉(zhuǎn)載請注明出處哦,我的博客: luoyibu