iOS圖片加載速度極限優(yōu)化—FastImageCache解析

FastImageCache是Path團(tuán)隊開發(fā)的一個開源庫,用于提升圖片的加載和渲染速度,讓基于圖片的列表滑動起來更順暢,來看看它是怎么做的。


優(yōu)化點


iOS從磁盤加載一張圖片,使用UIImageVIew顯示在屏幕上,需要經(jīng)過以下步驟:


從磁盤拷貝數(shù)據(jù)到內(nèi)核緩沖區(qū)

從內(nèi)核緩沖區(qū)復(fù)制數(shù)據(jù)到用戶空間

生成UIImageView,把圖像數(shù)據(jù)賦值給UIImageView

如果圖像數(shù)據(jù)為未解碼的PNG/JPG,解碼為位圖數(shù)據(jù)

CATransaction捕獲到UIImageView layer樹的變化

主線程Runloop提交CATransaction,開始進(jìn)行圖像渲染

? ?6.1 如果數(shù)據(jù)沒有字節(jié)對齊,Core Animation會再拷貝一份數(shù)據(jù),進(jìn)行字節(jié)對齊。


? ?6.2 GPU處理位圖數(shù)據(jù),進(jìn)行渲染。


FastImageCache分別優(yōu)化了2,4,6.1三個步驟:


使用mmap內(nèi)存映射,省去了上述第2步數(shù)據(jù)從內(nèi)核空間拷貝到用戶空間的操作。

緩存解碼后的位圖數(shù)據(jù)到磁盤,下次從磁盤讀取時省去第4步解碼的操作。

生成字節(jié)對齊的數(shù)據(jù),防止上述第6.1步CoreAnimation在渲染時再拷貝一份數(shù)據(jù)。

接下來具體介紹這三個優(yōu)化點以及它的實現(xiàn)。


內(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)核緩存到用戶空間的拷貝,效率很高。


解碼圖像


一般我們使用的圖像是JPG/PNG,這些圖像數(shù)據(jù)不是位圖,而是是經(jīng)過編碼壓縮后的數(shù)據(jù),使用它渲染到屏幕之前需要進(jìn)行解碼轉(zhuǎn)成位圖數(shù)據(jù),這個解碼操作是比較耗時的,并且沒有GPU硬解碼,只能通過CPU,iOS默認(rèn)會在主線程對圖像進(jìn)行解碼。很多庫都解決了圖像解碼的問題,不過由于解碼后的圖像太大,一般不會緩存到磁盤,SDWebImage的做法是把解碼操作從主線程移到子線程,讓耗時的解碼操作不占用主線程的時間。


FastImageCache也是在子線程解碼圖像,不同的是它會緩存解碼后的圖像到磁盤。因為解碼后的圖像體積很大,F(xiàn)astImageCache對這些圖像數(shù)據(jù)做了系列緩存管理,詳見下文實現(xiàn)部分。另外緩存的圖像體積大也是使用內(nèi)存映射讀取文件的原因,小文件使用內(nèi)存映射無優(yōu)勢,內(nèi)存拷貝的量少,拷貝后占用用戶內(nèi)存也不高,文件越大內(nèi)存映射優(yōu)勢越大。


字節(jié)對齊


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


fastImageCache1.png


那什么是字節(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ù))


fastImageCache2.png


塊的大小應(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é)對齊就是這個事情。


實現(xiàn)


FastImageCache把同個類型和尺寸的圖像都放在一個文件里,根據(jù)文件偏移取單張圖片,類似web的css雪碧圖,這里稱為ImageTable。這樣做主要是為了方便統(tǒng)一管理圖片緩存,控制緩存的大小,整個FastImageCache就是在管理一個個ImageTable的數(shù)據(jù)。整體實現(xiàn)的數(shù)據(jù)結(jié)構(gòu)如圖:


fastImageCache3.png


一些補(bǔ)充和說明:


ImageTable


一個ImageFormat對應(yīng)一個ImageTable,ImageFormat指定了ImageTable里圖像渲染格式/大小等信息,ImageTable里的圖像數(shù)據(jù)都由ImageFormat規(guī)定了統(tǒng)一的尺寸,每張圖像大小都是一樣的。

一個ImageTable一個實體文件,并有另一個文件保存這個ImageTable的meta信息。

圖像使用entityUUID作為唯一標(biāo)示符,由用戶定義,通常是圖像url的hash值。ImageTable Meta的indexMap記錄了entityUUID->entryIndex的映射,通過indexMap就可以用圖像的entityUUID找到緩存數(shù)據(jù)在ImageTable對應(yīng)的位置。

ImageTableEntry


ImageTable的實體數(shù)據(jù)是ImageTableEntry,每個entry有兩部分?jǐn)?shù)據(jù),一部分是對齊后的圖像數(shù)據(jù),另一部分是meta信息,meta保存這張圖像的UUID和原圖UUID,用于校驗圖像數(shù)據(jù)的正確性。

Entry數(shù)據(jù)是按內(nèi)存分頁大小對齊的,數(shù)據(jù)大小是內(nèi)存分頁大小的整數(shù)倍,這樣可以保證虛擬內(nèi)存缺頁加載時使用最少的內(nèi)存頁加載一張圖像。

圖像數(shù)據(jù)做了字節(jié)對齊處理,CoreAnimation使用時無需再處理拷貝。具體做法是CGBitmapContextCreate創(chuàng)建位圖畫布時bytesPerRow參數(shù)傳64倍數(shù)。

Chunk


ImageTable和實體數(shù)據(jù)Entry間多了層Chunk,Chunk是邏輯上的數(shù)據(jù)劃分,N個Entry作為一個Chunk,內(nèi)存映射mmap操作是以chunk為單位的,每一個chunk執(zhí)行一次mmap把這個chunk的內(nèi)容映射到虛擬內(nèi)存。為什么要多一層chunk呢,按我的理解,這樣做是為了靈活控制mmap的大小和調(diào)用次數(shù),若對整個ImageTable執(zhí)行mmap,載入虛擬內(nèi)存的文件過大,若對每個Entry做mmap,調(diào)用次數(shù)會太多。


緩存管理


用戶可以定義整個ImageTable里最大緩存的圖像數(shù)量,在有新圖像需要緩存時,如果緩存沒有超過限制,會以chunk為單位擴(kuò)展文件大小,順序?qū)懴氯ァH绻殉^最大緩存限制,會把最少使用的緩存替換掉,實現(xiàn)方法是每次使用圖像都會把UUID插入到MRUEntries數(shù)組的開頭,MRUEntries按最近使用順序排列了圖像UUID,數(shù)組里最后一個圖像就是最少使用的。被替換掉的圖片下次需要再使用時,再走一次取原圖—解壓—存儲的流程。


使用


FastImageCache適合用于tableView里緩存每個cell上同樣規(guī)格的圖像,優(yōu)點是能極大加快第一次從磁盤加載這些圖像的速度。但它有兩個明顯的缺點:一是占空間大。因為緩存了解碼后的位圖到磁盤,位圖是很大的,寬高100*100的圖像在2x的高清屏設(shè)備下就需要200*200*4byte/pixel=156KB,這也是為什么FastImageCache要大費(fèi)周章限制緩存大小。二是接口不友好,需預(yù)定義好緩存的圖像尺寸。FastImageCache無法像SDWebImage那樣無縫接入UIImageView,使用它需要配置ImageTable,定義好尺寸,手動提供的原圖,每種實體圖像要定義一個FICEntity模型,使邏輯變復(fù)雜。


FastImageCache已經(jīng)屬于極限優(yōu)化,做圖像加載/渲染優(yōu)化時應(yīng)該優(yōu)先考慮一些低代價高回報的優(yōu)化點,例如CALayer代替UIImageVIew,減少GPU計算(去透明/像素對齊),圖像子線程解碼,避免Offscreen-Render等。在其他優(yōu)化都做到位,圖像的渲染還是有性能問題的前提下才考慮使用FastImageCache進(jìn)一步提升首次加載的性能,不過字節(jié)對齊的優(yōu)化倒是可以脫離FastImageCache直接運(yùn)用在項目上,只需要在解碼圖像時bitmap畫布的bytesPerRow設(shè)為64的倍數(shù)即可。


搜索CocoaChina微信公眾號:CocoaChina 微信掃一掃

訂閱每日移動開發(fā)及APP推廣熱點資訊

公眾號:CocoaChina

我要投稿 ? 收藏文章 分享到:

16

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容