iOS 圖片加載速度優化


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

一、優化點

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

  1. 從磁盤拷貝數據到內核緩沖區
  2. 從內核緩沖區復制數據到用戶空間
  3. 生成 UIImageView,把圖像數據賦值給 UIImageView
  4. 如果圖像數據為未解碼的 PNG/JPG,解碼為位圖數據
  5. CATransaction 捕獲到 UIImageView layer 樹的變化
  6. 主線程 Runloop 提交 CATransaction,開始進行圖像渲染
    • 如果數據沒有字節對齊,Core Animation 會再拷貝一份數據,進行字節對齊。
    • GPU 處理位圖數據,進行渲染。

FastImageCache 分別優化了 2、4、6(1) 三個步驟:

  1. 使用 mmap 內存映射,省去了上述第 2 步數據從內核空間拷貝到用戶空間的操作。
  2. 緩存解碼后的位圖數據到磁盤,下次從磁盤讀取時省去第 4 步解碼的操作。
  3. 生成字節對齊的數據,防止上述第 6(1) 步 CoreAnimation 在渲染時再拷貝一份數據。

接下來具體介紹這三個優化點以及它的實現。

二、內存映射

平常我們讀取磁盤上的一個文件,上層 API 調用到最后會使用系統方法 read() 讀取數據,內核把磁盤數據讀入內核緩沖區,用戶再從內核緩沖區讀取數據復制到用戶內存空間,這里有一次內存拷貝的時間消耗,並且讀取后整個文件數據就已經存在於用戶內存中,占用了進程的內存空間。

FastImageCache 采用了另一種讀寫文件的方法,就是用 mmap 把文件映射到用戶空間里的虛擬內存,文件中的位置在虛擬內存中有了對應的地址,可以像操作內存一樣操作這個文件,相當於已經把整個文件放入內存,但在真正使用到這些數據前卻不會消耗物理內存,也不會有讀寫磁盤的操作,只有真正使用這些數據時,也就是圖像准備渲染在屏幕上時,虛擬內存管理系統 VMS 才根據缺頁加載的機制從磁盤加載對應的數據塊到物理內存,再進行渲染。這樣的文件讀寫文件方式少了數據從內核緩存到用戶空間的拷貝,效率很高。

三、解碼圖像

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

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

四、字節對齊

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 做的字節對齊就是這個事情。

五、實現

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


一些補充和說明:

5.1 ImageTable

一個 ImageFormat 對應一個 ImageTable,ImageFormat 指定了 ImageTable 里圖像渲染格式/大小等信息,ImageTable 里的圖像數據都由 ImageFormat 規定了統一的尺寸,每張圖像大小都是一樣的。

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

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

5.2 ImageTableEntry

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

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

圖像數據做了字節對齊處理,CoreAnimation使用時無需再處理拷貝。具體做法是CGBitmapContextCreate創建位圖畫布時bytesPerRow參數傳64倍數。

5.3 Chunk

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

5.4 緩存管理

用戶可以定義整個ImageTable里最大緩存的圖像數量,在有新圖像需要緩存時,如果緩存沒有超過限制,會以chunk為單位擴展文件大小,順序寫下去。如果已超過最大緩存限制,會把最少使用的緩存替換掉,實現方法是每次使用圖像都會把UUID插入到MRUEntries數組的開頭,MRUEntries按最近使用順序排列了圖像UUID,數組里最后一個圖像就是最少使用的。被替換掉的圖片下次需要再使用時,再走一次取原圖—解壓—存儲的流程。

六、使用

FastImageCache 適合用於 tableView 里緩存每個 cell 上同樣規格的圖像,優點是能極大加快第一次從磁盤加載這些圖像的速度。但它有兩個明顯的缺點:

  1. 占空間大。因為緩存了解碼后的位圖到磁盤,位圖是很大的,寬高 100*100 的圖像在 2x 的高清屏設備下就需要 200*200*4byte/pixel = 156KB,這也是為什么 FastImageCache 要大費周章限制緩存大小。
  2. 接口不友好,需預定義好緩存的圖像尺寸。FastImageCache 無法像 SDWebImage 那樣無縫接入UIImageView,使用它需要配置 ImageTable,定義好尺寸,手動提供的原圖,每種實體圖像要定義一個 FICEntity 模型,使邏輯變復雜。

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

文章

iOS圖片加載速度極限優化—FastImageCache解析


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM