2018 WWDC 蘋果官方給出了關於iOS圖像處理的最佳實踐,本文主要是就官方文檔進行分析總結以及較爲全面的拓展延伸。面試
官方文檔:Image and Graphics Best Practices緩存
代碼很easy呀,兩行搞定性能優化
UIImage *image = [UIImage imageNamed:@"xxxxx"];
imageView.image = image;
複製代碼
可是這中間的圖片加載真實過程以下bash
按照經典的MVC架構,UIImage扮演model角色,負責承載圖片數據,UIImageView充當View的角色,負責渲染和展現圖片。系統提供接口很是的簡單,這中間隱藏瞭解碼的過程。微信
Buffer是一段連續的內存區域,下面咱們看下圖片處理相關的Buffer網絡
Data Buffer存儲了圖片的元數據,咱們常見的圖片格式,jpeg,png等都是壓縮圖片格式。Data Buffer的內存大小就是源圖片在磁盤中的大小。架構
Image Buffer存儲的就是圖片解碼後的像素數據,也就是咱們常說的位圖。 Buffer中每個元素描述的一個像素的顏色信息,buffer的size和圖片的size成正相關關係。app
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,可看法碼後的內存佔用是至關大的。
內存的佔用會致使咱們app的CPU佔用高,直接致使耗電大,APP響應慢
在視圖比較小,圖片比較大的場景下,直接展現原圖片會形成沒必要要的內存和CPU消耗,這裏就可使用ImageIO的接口,DownSampling,也就是生成縮略圖
具體代碼以下,指定顯示區域大小
這裏有兩個注意事項
這樣的縮略圖方式能夠省去大量的內存和CPU消耗,官方Case給出的先後內存對比
解碼過程是很是佔用CPU資源的,放在主線程必定會形成阻塞,因此這個操做應該放在異步線程。代碼以下
Prefetching:預加載,也就是提早爲以後的cell預加載數據(基本上主流的app都有這麼作滴,iOS10以後,系統引入的tableView(_:prefetchRowsAt:) 能夠更加方便的實現預加載。)
小tips: 這裏使用串行隊列能夠很好地避免Thread Explosion,線程切換的代價是很是昂貴的,因此在咱們app中應該使用GCD串行隊列建立一個解碼線程。
咱們如今須要實現下面的live按鈕
先看一種不合理的實現方式
咱們先來分析這種方案的問題所在,
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繪製方案的問題
因此,正確的實現姿式是將這個大的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
}
}
複製代碼
直接使用UIImageView,這裏有個技巧,若是是純色圖片,想要展現不一樣顏色的同一張圖片,可使用UIImageView的tintColor屬性平鋪顏色,來達到複用圖片的目的。
代碼以下:
UIImage.withRenderingMode(_:)
UIImageView.tintColor
複製代碼
文本使用UILabel能夠減小百分之75的Backing Store開銷,系統針對UILabel作了優化,而且自動更新Backing Store的size,針對emoji和富文本內容。
最終Live按鈕的正確實現方案以下圖
對於圖片的實時處理推薦使用CoreImage框架。 例如將一張圖片的灰度值進行調整這樣的操做,有滴小夥伴可能使用CoreGraphics獲取圖像的每一個像素點數據,而後改變灰度值,最終生成目標圖標,這種作法將大量gpu擅長的工做放在了cpu上處理,合理的作法是: 使用CoreImage的濾鏡filter或者metal,OpenGL的shader,讓圖像處理的工做交給GPU去作。
對於須要離屏渲染的場景推薦使用UIGraphicsImageRenderer替代UIGraphicsBeginImageContext,性能更好,而且支持廣色域。
用提問的方式來拓展一下,針對每一個問題進行深刻的思考
問題一:圖像展現有這麼多細節在裏面,但是爲何在日常開發中爲何沒有感受到,能夠從哪些地方對本身的工程進行優化。
答:咱們日常大部分會使用UIImage imageNamed這樣的API加載了本地圖片,而網絡圖片則使用了SDWebImage或者YYWebImage等框架來加載。因此沒有去細究。
進而引伸出
問題二: 使用imageNamed,系統什麼時候去解碼,有沒有緩存,緩存的大小是多少,有沒有性能問題,和imageWithContentsOfFile有什麼區別
答: 一一來解答這個問題
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,須要在首屏展現的圖片。而不適用於滑動的下載好的大量網絡圖片的本地加載。會形成主線程阻塞。
其實這裏SDWebImage或者YYWebImage等框架已經給出了正確的姿式,細節能夠挑其中一個閱讀源碼便可。
分享下優秀的源碼解析
下載圖片主要簡化流程以下
加載圖片的主要簡化流程以下
分析:
以前咱們分析過1080p的圖片解碼後的內存大小,大約是7.9mb,若是是4k,8k圖,這個內存佔用將會很是的大,若是使用SDWebImage或者YYWebImage的默認解碼緩存技術方案去加載多張這樣的大圖,帶來的結果會是內存爆掉。閃退。
能夠設置SDWebImage或者YYWebImage的Option選項不解碼下載好的圖片
那麼大圖該怎麼處理呢,這裏有兩個場景
解決方法: 使用蘋果推薦的縮略圖DownSampling方案便可
解決方法: 使用蘋果的CATiledLayer去加載。原理是分片渲染,滑動時經過指定目標位置,經過映射原圖指定位置的部分圖片數據解碼渲染。這裏再也不累述,有興趣的小夥伴能夠自行了解下官方API。
瞭解圖像加載的細節和全過程很是有必要,有助於咱們在日常開發中選擇合適的方案,作出合理的性能優化。