iOS從磁盤加載一張圖片,使用UIImageVIew顯示在屏幕上,須要通過如下步驟:css
- 從磁盤拷貝數據到內核緩衝區
- 從內核緩衝區複製數據到用戶空間生成UIImageView,
- 把圖像數據賦值給UIImageView
- 若是圖像數據爲未解碼的PNG/JPG,解碼爲位圖數據
- CATransaction捕獲到UIImageView layer樹的變化
- 主線程Runloop提交CATransaction,
- 開始進行圖像渲染
- 若是數據沒有字節對齊,Core Animation會再拷貝一份數據,進行字節對齊。
- GPU處理位圖數據,進行渲染
2,4,6三個步驟的優化策略:web
a.mmap內存映射,省去了上述第2步數據從內核空間拷貝到用戶空間的操做。數組
b.緩存位圖數據,緩存解碼後的位圖數據到磁盤,下次從磁盤讀取時省去第4步解碼的操做。緩存
6.字節對齊,防止上述第6步CoreAnimation在渲染時再拷貝一份數據。接下來具體介紹這三個優化點以及它的實現。數據結構
1.內存映射 日常咱們讀取磁盤上的一個文件,上層API調用到最後會使用系統方法read()讀取數據,內核把磁盤數據讀入內核緩衝區,用戶再從內核緩衝區讀取數據複製到用戶內存空間,這裏有一次內存拷貝的時間消耗,而且讀取後整個文件數據就已經存在於用戶內存中,佔用了進程的內存空間。 FastImageCache採用了另外一種讀寫文件的方法,就是用mmap把文件映射到用戶空間裏的虛擬內存,文件中的位置在虛擬內存中有了對應的地址,能夠像操做內存同樣操做這個文件,至關於已經把整個文件放入內存,但在真正使用到這些數據前卻不會消耗物理內存,也不會有讀寫磁盤的操做,只有真正使用這些數據時,也就是圖像準備渲染在屏幕上時,虛擬內存管理系統VMS才根據缺頁加載的機制從磁盤加載對應的數據塊到物理內存,再進行渲染。這樣的文件讀寫文件方式少了數據從內核緩存到用戶空間的拷貝,效率很高。oop
2.解碼圖像 通常咱們使用的圖像是JPG/PNG,這些圖像數據不是位圖,而是是通過編碼壓縮後的數據,使用它渲染到屏幕以前須要進行解碼轉成位圖數據,這個解碼操做是比較耗時的,而且沒有GPU硬解碼,只能經過CPU,iOS默認會在主線程對圖像進行解碼。不少庫都解決了圖像解碼的問題,不過因爲解碼後的圖像太大,通常不會緩存到磁盤,SDWebImage的作法是把解碼操做從主線程移到子線程,讓耗時的解碼操做不佔用主線程的時間。 FastImageCache也是在子線程解碼圖像,不一樣的是它會緩存解碼後的圖像到磁盤。由於解碼後的圖像體積很大,FastImageCache對這些圖像數據作了系列緩存管理,詳見下文實現部分。另外緩存的圖像體積大也是使用內存映射讀取文件的緣由,小文件使用內存映射無優點,內存拷貝的量少,拷貝後佔用用戶內存也不高,文件越大內存映射優點越大。 字節對齊 Core Animation在圖像數據非字節對齊的狀況下渲染前會先拷貝一份圖像數據,官方文檔沒有對此次拷貝行爲做說明,模擬器和Instrument裏有高亮顯示「copied images」的功能,但彷佛它有bug,即便某張圖片沒有被高亮顯示出渲染時被copy,從調用堆棧上也仍是能看到調用了CA::Render::copy_image方法: 性能
3.字節對齊,按個人理解,爲了性能,底層渲染圖像時不是一個像素一個像素渲染,而是一塊一塊渲染,數據是一塊塊地取,就可能遇到這一塊連續的內存數據裏結尾的數據不是圖像的內容,是內存裏其餘的數據,可能越界讀取致使一些奇怪的東西混入,因此在渲染以前CoreAnimation要把數據拷貝一份進行處理,確保每一塊都是圖像數據,對於不足一塊的數據置空。大體圖示:(pixel是圖像像素數據,data是內存裏其餘數據) 塊的大小應該是跟CPU cache line有關,ARMv7是32byte,A9是64byte,在A9下CoreAnimation應該是按64byte做爲一塊數據去讀取和渲染,讓圖像數據對齊64byte就能夠避免CoreAnimation再拷貝一份數據進行修補。FastImageCache作的字節對齊就是這個事情。優化
- 實現 FastImageCache把同個類型和尺寸的圖像都放在一個文件裏,根據文件偏移取單張圖片,相似web的css雪碧圖,這裏稱爲ImageTable。這樣作主要是爲了方便統一管理圖片緩存,控制緩存的大小,整個FastImageCache就是在管理一個個ImageTable的數據。總體實現的數據結構如圖: 一些補充和說明: ImageTable
- 一個ImageFormat對應一個ImageTable,ImageFormat指定了ImageTable裏圖像渲染格式/大小等信息,ImageTable裏的圖像數據都由ImageFormat規定了統一的尺寸,每張圖像大小都是同樣的。一個ImageTable一個實體文件,並有另外一個文件保存這個ImageTable的meta信息。圖像使用entityUUID做爲惟一標示符,由用戶定義,一般是圖像url的hash值。ImageTable Meta的indexMap記錄了entityUUID->entryIndex的映射,經過indexMap就能夠用圖像的entityUUID找到緩存數據在ImageTable對應的位置。ImageTableEntry
- ImageTable的實體數據是ImageTableEntry,每一個entry有兩部分數據,一部分是對齊後的圖像數據,另外一部分是meta信息,meta保存這張圖像的UUID和原圖UUID,用於校驗圖像數據的正確性。Entry數據是按內存分頁大小對齊的,數據大小是內存分頁大小的整數倍,這樣能夠保證虛擬內存缺頁加載時使用最少的內存頁加載一張圖像。圖像數據作了字節對齊處理,CoreAnimation使用時無需再處理拷貝。具體作法是CGBitmapContextCreate建立位圖畫布時bytesPerRow參數傳64倍數。Chunk
- ImageTable和實體數據Entry間多了層Chunk,Chunk是邏輯上的數據劃分,N個Entry做爲一個Chunk,內存映射mmap操做是以chunk爲單位的,每個chunk執行一次mmap把這個chunk的內容映射到虛擬內存。爲何要多一層chunk呢,按個人理解,這樣作是爲了靈活控制mmap的大小和調用次數,若對整個ImageTable執行mmap,載入虛擬內存的文件過大,若對每一個Entry作mmap,調用次數會太多。
緩存管理
- 用戶能夠定義整個ImageTable裏最大緩存的圖像數量,在有新圖像須要緩存時,若是緩存沒有超過限制,會以chunk爲單位擴展文件大小,順序寫下去。若是已超過最大緩存限制,會把最少使用的緩存替換掉,實現方法是每次使用圖像都會把UUID插入到MRUEntries數組的開頭,MRUEntries按最近使用順序排列了圖像UUID,數組裏最後一個圖像就是最少使用的。被替換掉的圖片下次須要再使用時,再走一次取原圖—解壓—存儲的流程。
- 使用 FastImageCache適合用於tableView裏緩存每一個cell上一樣規格的圖像,優勢是能極大加快第一次從磁盤加載這些圖像的速度。但它有兩個明顯的缺點:一是佔空間大。由於緩存瞭解碼後的位圖到磁盤,位圖是很大的,寬高100*100的圖像在2x的高清屏設備下就須要200*200*4byte/pixel=156KB,這也是爲何FastImageCache要大費周章限制緩存大小。二是接口不友好,需預約義好緩存的圖像尺寸。FastImageCache沒法像SDWebImage那樣無縫接入UIImageView,使用它須要配置ImageTable,定義好尺寸,手動提供的原圖,每種實體圖像要定義一個FICEntity模型,使邏輯變複雜。 FastImageCache已經屬於極限優化,作圖像加載/渲染優化時應該優先考慮一些低代價高回報的優化點,例如CALayer代替UIImageVIew,減小GPU計算(去透明/像素對齊),圖像子線程解碼,避免Offscreen-Render等。在其餘優化都作到位,圖像的渲染仍是有性能問題的前提下才考慮使用FastImageCache進一步提高首次加載的性能,不過字節對齊的優化卻是能夠脫離FastImageCache直接運用在項目上,只須要在解碼圖像時bitmap畫布的bytesPerRow設爲64的倍數便可