iOS圖像最佳實踐總結

1. 前言

2018 WWDC 蘋果官方給出了關於iOS圖像處理的最佳實踐,本文主要是就官方文檔進行分析總結以及較爲全面的拓展延伸。面試

官方文檔:Image and Graphics Best Practices緩存

2. 基礎預備知識

本地圖片顯示到屏幕中,經歷了哪些過程

代碼很easy呀,兩行搞定性能優化

UIImage *image = [UIImage imageNamed:@"xxxxx"];
    imageView.image = image;
複製代碼

可是這中間的圖片加載真實過程以下bash

圖片加載過程

  1. 從磁盤讀取原始壓縮的圖片數據(png/jpeg格式等等)緩存到內存
  2. CPU解壓成未壓縮的圖片數據 (imageBuffer)
  3. 渲染圖片(會生成frameBuffer,幀緩存,最終顯示到手機屏幕)

按照經典的MVC架構,UIImage扮演model角色,負責承載圖片數據,UIImageView充當View的角色,負責渲染和展現圖片。系統提供接口很是的簡單,這中間隱藏瞭解碼的過程。微信

Buffers

Buffer是一段連續的內存區域,下面咱們看下圖片處理相關的Buffer網絡

Data Buffer

Data Buffer

Data Buffer存儲了圖片的元數據,咱們常見的圖片格式,jpeg,png等都是壓縮圖片格式。Data Buffer的內存大小就是源圖片在磁盤中的大小。架構

Image Buffer

Image Buffer

Image Buffer存儲的就是圖片解碼後的像素數據,也就是咱們常說的位圖。 Buffer中每個元素描述的一個像素的顏色信息,buffer的size和圖片的size成正相關關係。app

Frame Buffer

Frame Buffer

Frame Buffer 存儲了app每幀的實際輸出框架

和OpenGL中FrameBuffer相似,蘋果不容許咱們直接渲染操做屏幕顯示,而是把渲染數據放入幀緩存中,由系統按照60hz-120hz的頻率掃描顯示。iphone

當app視圖層級發生變化時,UIKit 會結合 UIWindow 和 Subviews,渲染出一個 frame buffer,而後按60hz的頻率掃描(ipad最高能夠達到120hz)顯示到屏幕上。

解碼操做

UIImage負責解壓Data Buffer內容並申請buffer(Image Buffer)存儲解壓後的圖片信息。UIImageView負責將Image Buffer 拷貝至 framebuffer,用於顯示屏幕展現。

解壓過程會大量佔用cpu,因此UIImage會持有解壓後的圖片數據,以便給須要渲染的地方複用數據。

渲染流程

渲染流程

綜上咱們能夠看到渲染的全過程。這裏須要注意的是,解碼後的ImageBuffer大小理論上只和圖片尺寸相關。

ImageBuffer按照每一個像素RGBA四個字節大小,一張1080p的圖片解碼後的位圖大小是1920 * 1080 * 4 / 1024 / 1024,約7.9mb,而原圖假設是jpg,壓縮比1比20,大約350kb,可看法碼後的內存佔用是至關大的。

3. 官方最佳實踐

內存的佔用會致使咱們app的CPU佔用高,直接致使耗電大,APP響應慢

Memory & CPU

DownSampling(下降採樣)

在視圖比較小,圖片比較大的場景下,直接展現原圖片會形成沒必要要的內存和CPU消耗,這裏就可使用ImageIO的接口,DownSampling,也就是生成縮略圖

DownSampling

具體代碼以下,指定顯示區域大小

DownSampling Code

這裏有兩個注意事項

  • 設置kCGImageSourceShouldCache爲false,避免緩存解碼後的數據,64位設置上默認是開啓緩存的,(很好理解,由於下次使用該圖片的時候,可能場景不一樣,須要生成的縮略圖大小是不一樣的,顯然不能作緩存處理)
  • 設置kCGImageSourceShouldCacheImmediately爲true,避免在須要渲染的時候才作解碼,默認選項是false

這樣的縮略圖方式能夠省去大量的內存和CPU消耗,官方Case給出的先後內存對比

DownSampling內存對比

Prefetching && Background decoding

解碼過程是很是佔用CPU資源的,放在主線程必定會形成阻塞,因此這個操做應該放在異步線程。代碼以下

code

Prefetching:預加載,也就是提早爲以後的cell預加載數據(基本上主流的app都有這麼作滴,iOS10以後,系統引入的tableView(_:prefetchRowsAt:) 能夠更加方便的實現預加載。)

小tips: 這裏使用串行隊列能夠很好地避免Thread Explosion,線程切換的代價是很是昂貴的,因此在咱們app中應該使用GCD串行隊列建立一個解碼線程。

官方實現UI實例

咱們如今須要實現下面的live按鈕

Live按鈕

先看一種不合理的實現方式

不合理繪製

咱們先來分析這種方案的問題所在,

系統draw實現

UIView是經過CALayer建立FrameBuffer最後顯示的。重寫了drawRect方法,Calayer會建立一個Backing Store,而後在Backing Store上執行draw函數,最後將內容傳遞給frameBuffer最終顯示。

Backing Store的默認大小和View的大小成正比,以iphone6爲例,750 * 1134 * 4 字節 ≈ 3.4 Mb。

iOS 12,對 backing store 有作優化,它的大小會根據圖片的色彩空間,動態改變。 在此以前,若是你使用 sRGB 格式,可是實際繪製的內容,只使用了單通道,那麼大小會比實際要的大,形成沒必要要開銷。iOS 12 會自動優化這部分。

總結下這種使用drawRect繪製方案的問題

    1. Backing Store的建立形成了沒必要要的內存開銷
    1. UIImage先繪製到Backing Store,再渲染到frameBuffer,中間多了一層內存拷貝
    1. 背景顏色不須要繪製到Backing Store,直接使用BackGroundColor繪製到FrameBuffer

因此,正確的實現姿式是將這個大的view拆分紅小的subview逐個實現。

背景顏色實現

這裏有一個圓角的處理

UIView的maskView 及CALayer.maskLayer都會將圖層渲染到臨時的image buffer中,也就是咱們常說的離屏渲染,而CALayer.cornerRadius不會形成離屏渲染,真正形成離屏渲染的是設置MaskToBounds這樣的屬性。因此背景圖直接使用UIView設置BackGroudColor便可。

這裏拓展下圓角的處理,先看一種不正確的作法

override func drawRect(rect: CGRect) {
    let maskPath = UIBezierPath(roundedRect: rect,
                                byRoundingCorners: .AllCorners,
                                cornerRadii: CGSize(width: 5, height: 5))
    let maskLayer = CAShapeLayer()
    maskLayer.frame = self.bounds
    maskLayer.path = maskPath.CGPath
    self.layer.mask = maskLayer
}

複製代碼

首先同理,重寫drawRect會形成沒必要要的backing store內存開銷,而且這種作法的本質是建立遮罩mask,再進行圖層混合,一樣會離屏渲染。

正確的姿式, 對於UIView直接使用CornerRadius,CoreAnimation能夠爲咱們在不額外建立內存開銷的狀況下繪製出圓角。

對於UIImageView可使用CoreGraphics本身裁剪出帶圓角的Image,實例代碼以下

extension UIImage {
    func drawRectWithRoundedCorner(radius radius: CGFloat, _ sizetoFit: CGSize) -> UIImage {
        let rect = CGRect(origin: CGPoint(x: 0, y: 0), size: sizetoFit)
        
        UIGraphicsBeginImageContextWithOptions(rect.size, false, UIScreen.mainScreen().scale)
        CGContextAddPath(UIGraphicsGetCurrentContext(),
                         UIBezierPath(roundedRect: rect, byRoundingCorners: UIRectCorner.AllCorners,
                                      cornerRadii: CGSize(width: radius, height: radius)).CGPath)
        CGContextClip(UIGraphicsGetCurrentContext())
        
        self.drawInRect(rect)
        CGContextDrawPath(UIGraphicsGetCurrentContext(), .FillStroke)
        let output = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();
        
        return output
    }
}

複製代碼

Live圖片實現

直接使用UIImageView,這裏有個技巧,若是是純色圖片,想要展現不一樣顏色的同一張圖片,可使用UIImageView的tintColor屬性平鋪顏色,來達到複用圖片的目的。

代碼以下:

UIImage.withRenderingMode(_:)
UIImageView.tintColor

複製代碼

文本實現

文本使用UILabel能夠減小百分之75的Backing Store開銷,系統針對UILabel作了優化,而且自動更新Backing Store的size,針對emoji和富文本內容。

最終實現

最終Live按鈕的正確實現方案以下圖

推薦使用Image Assets

  • 基於名稱和特效優化了查找效率,更快的查找圖片
  • 運行時,對內存的管理也有優化
  • App Slicing,app安裝包瘦身。iOS 9 後會從 Image Assets 中保留設備支持的圖片 (2x 或者 3x)
  • iOS 11 後的 Preserve Vector Data。支持矢量圖的功能,放大也不會失真

Advanced Image Effects

對於圖片的實時處理推薦使用CoreImage框架。 例如將一張圖片的灰度值進行調整這樣的操做,有滴小夥伴可能使用CoreGraphics獲取圖像的每一個像素點數據,而後改變灰度值,最終生成目標圖標,這種作法將大量gpu擅長的工做放在了cpu上處理,合理的作法是: 使用CoreImage的濾鏡filter或者metal,OpenGL的shader,讓圖像處理的工做交給GPU去作。

Drawing Off-Screen

對於須要離屏渲染的場景推薦使用UIGraphicsImageRenderer替代UIGraphicsBeginImageContext,性能更好,而且支持廣色域。

4. 拓展與思考

用提問的方式來拓展一下,針對每一個問題進行深刻的思考

問題一:圖像展現有這麼多細節在裏面,但是爲何在日常開發中爲何沒有感受到,能夠從哪些地方對本身的工程進行優化。

答:咱們日常大部分會使用UIImage imageNamed這樣的API加載了本地圖片,而網絡圖片則使用了SDWebImage或者YYWebImage等框架來加載。因此沒有去細究。

進而引伸出

問題二: 使用imageNamed,系統什麼時候去解碼,有沒有緩存,緩存的大小是多少,有沒有性能問題,和imageWithContentsOfFile有什麼區別

答: 一一來解答這個問題

  1. 首先先說imageNamed和imageWithContentsOfFile有什麼區別,想必大部分小夥伴都很清楚,由於這也是面試老生常談的東西。imageNamed加載本地圖片會緩存圖片,也就是加載一千張相同的本地圖片,內存中也只會有一份,而imageWithContentsOfFile不會緩存,也就是重複加載相同圖片,在內存中會有多份圖片數據。
  2. imageNamed加載圖片會將圖片源數據和解碼後的數據加載入內存緩存中,只有收到內存警告的時候纔會釋放,有興趣的小夥伴能夠自行調試一下。
  3. 關於UIImage對象什麼時候去解碼,其實剛剛咱們在下降採樣的時候已經提到了,kCGImageSourceShouldCacheImmediately屬性系統默認是false,咱們能夠看ImageIO/CGImageSource.h文件中kCGImageSourceShouldCache的註釋

pecifies whether image decoding and caching should happen at image creation time. The value of this key must be a CFBooleanRef. The default value is kCFBooleanFalse (image decoding will happen at rendering time).

也就是說UIImage只有在屏幕上渲染時再去解碼的。而關於UIImageView的操做必定是在主線程,解碼操做是放在主線程的。因此若是在tableview滑動中頻繁的建立較大的UIImage渲染展現,會形成主線程阻塞。

總結: imageNamed默認帶緩存,緩存經過NSCache實現。適用於須要頻繁複用的圖片的加載,而imageWithContentsOfFile不會緩存,適用於不經常使用的較大圖片的加載,因爲系統默認主線程解碼UIImage,因此imageNamed僅僅適用於加載較小的例如APP各個tab的icon,須要在首屏展現的圖片。而不適用於滑動的下載好的大量網絡圖片的本地加載。會形成主線程阻塞。

5. 正確的網絡圖片加載方式

其實這裏SDWebImage或者YYWebImage等框架已經給出了正確的姿式,細節能夠挑其中一個閱讀源碼便可。

分享下優秀的源碼解析

YImage 設計思路,實現細節剖析

YYWebImage 源碼剖析:線程處理與緩存策略

下載圖片主要簡化流程以下

  1. 從網絡下載圖片源數據,默認放入內存和磁盤緩存中
  2. 異步解碼,解碼後的數據放入內存緩存中
  3. 回調主線程渲染圖片
  4. 內部維護磁盤和內存的cache,支持設置定時過時清理,內存cache的上限等

加載圖片的主要簡化流程以下

  1. 從內存中查找圖片數據,若是有而且已經解碼,直接返回數據,若是沒有解碼,異步解碼緩存內存後返回
  2. 內存中未查找到圖片數據,從磁盤查找,磁盤查找到後,加載圖片源數據到內存,異步解碼緩存內存後返回,若是沒有去網絡下載圖片。走上面的流程。

分析:

  • 這樣滴流程解決了UIImage imageNamed這種加載必定在主線程解碼圖片的問題,異步加載,避免了主線程阻塞。
  • 經過緩存內存方式,避開了頻繁的磁盤IO
  • 經過緩存解碼後的圖片數據,避開了頻繁解碼的CPU消耗。

6. 超大圖片的處理

以前咱們分析過1080p的圖片解碼後的內存大小,大約是7.9mb,若是是4k,8k圖,這個內存佔用將會很是的大,若是使用SDWebImage或者YYWebImage的默認解碼緩存技術方案去加載多張這樣的大圖,帶來的結果會是內存爆掉。閃退。

能夠設置SDWebImage或者YYWebImage的Option選項不解碼下載好的圖片

那麼大圖該怎麼處理呢,這裏有兩個場景

  1. 一張超大圖加載在一個小的view上

解決方法: 使用蘋果推薦的縮略圖DownSampling方案便可

  1. 像微信,微博長圖詳情那樣,全屏加載大圖,經過拖動來查看不一樣位置圖片細節

解決方法: 使用蘋果的CATiledLayer去加載。原理是分片渲染,滑動時經過指定目標位置,經過映射原圖指定位置的部分圖片數據解碼渲染。這裏再也不累述,有興趣的小夥伴能夠自行了解下官方API。

7. 總結

瞭解圖像加載的細節和全過程很是有必要,有助於咱們在日常開發中選擇合適的方案,作出合理的性能優化。

相關文章
相關標籤/搜索