1. 前言
2018 WWDC 蘋果官方給出了關(guān)于iOS圖像處理的最佳實(shí)踐,本文主要是就官方文檔進(jìn)行分析總結(jié)以及較為全面的拓展延伸感局。
官方文檔:Image and Graphics Best Practices
2. 基礎(chǔ)預(yù)備知識(shí)
本地圖片顯示到屏幕中狗超,經(jīng)歷了哪些過(guò)程
代碼很easy呀,兩行搞定
UIImage *image = [UIImage imageNamed:@"xxxxx"];
imageView.image = image;
但是這中間的圖片加載真實(shí)過(guò)程如下
- 從磁盤讀取原始?jí)嚎s的圖片數(shù)據(jù)(png/jpeg格式等等)緩存到內(nèi)存
- CPU解壓成未壓縮的圖片數(shù)據(jù) (imageBuffer)
- 渲染圖片(會(huì)生成frameBuffer,幀緩存算芯,最終顯示到手機(jī)屏幕)
按照經(jīng)典的MVC架構(gòu),UIImage扮演model角色娄帖,負(fù)責(zé)承載圖片數(shù)據(jù)也祠,UIImageView充當(dāng)View的角色,負(fù)責(zé)渲染和展示圖片近速。系統(tǒng)提供接口非常的簡(jiǎn)單诈嘿,這中間隱藏了解碼的過(guò)程。
Buffers
Buffer是一段連續(xù)的內(nèi)存區(qū)域削葱,下面我們看下圖片處理相關(guān)的Buffer
Data Buffer
Data Buffer存儲(chǔ)了圖片的元數(shù)據(jù)奖亚,我們常見(jiàn)的圖片格式,jpeg析砸,png等都是壓縮圖片格式昔字。Data Buffer的內(nèi)存大小就是源圖片在磁盤中的大小。
Image Buffer
Image Buffer存儲(chǔ)的就是圖片解碼后的像素?cái)?shù)據(jù)首繁,也就是我們常說(shuō)的位圖作郭。
Buffer中每一個(gè)元素描述的一個(gè)像素的顏色信息,buffer的size和圖片的size成正相關(guān)關(guān)系弦疮。
Frame Buffer
Frame Buffer 存儲(chǔ)了app每幀的實(shí)際輸出
和OpenGL中FrameBuffer類似夹攒,蘋果不允許我們直接渲染操作屏幕顯示,而是把渲染數(shù)據(jù)放入幀緩存中胁塞,由系統(tǒng)按照60hz-120hz的頻率掃描顯示咏尝。
當(dāng)app視圖層級(jí)發(fā)生變化時(shí),UIKit 會(huì)結(jié)合 UIWindow 和 Subviews啸罢,渲染出一個(gè) frame buffer编检,然后按60hz的頻率掃描(ipad最高可以達(dá)到120hz)顯示到屏幕上。
解碼操作
UIImage負(fù)責(zé)解壓Data Buffer內(nèi)容并申請(qǐng)buffer(Image Buffer)存儲(chǔ)解壓后的圖片信息扰才。UIImageView負(fù)責(zé)將Image Buffer 拷貝至 framebuffer允懂,用于顯示屏幕展示。
解壓過(guò)程會(huì)大量占用cpu衩匣,所以UIImage會(huì)持有解壓后的圖片數(shù)據(jù)累驮,以便給需要渲染的地方復(fù)用數(shù)據(jù)酣倾。
渲染流程
綜上我們可以看到渲染的全過(guò)程。這里需要注意的是谤专,解碼后的ImageBuffer大小理論上只和圖片尺寸相關(guān)躁锡。
ImageBuffer按照每個(gè)像素RGBA四個(gè)字節(jié)大小,一張1080p的圖片解碼后的位圖大小是1920 * 1080 * 4 / 1024 / 1024置侍,約7.9mb映之,而原圖假設(shè)是jpg,壓縮比1比20蜡坊,大約350kb杠输,可見(jiàn)解碼后的內(nèi)存占用是相當(dāng)大的。
3. 官方最佳實(shí)踐
內(nèi)存的占用會(huì)導(dǎo)致我們app的CPU占用高秕衙,直接導(dǎo)致耗電大蠢甲,APP響應(yīng)慢
DownSampling(降低采樣)
在視圖比較小,圖片比較大的場(chǎng)景下据忘,直接展示原圖片會(huì)造成不必要的內(nèi)存和CPU消耗鹦牛,這里就可以使用ImageIO的接口,DownSampling勇吊,也就是生成縮略圖
具體代碼如下曼追,指定顯示區(qū)域大小
這里有兩個(gè)注意事項(xiàng)
- 設(shè)置kCGImageSourceShouldCache為false,避免緩存解碼后的數(shù)據(jù)汉规,64位設(shè)置上默認(rèn)是開(kāi)啟緩存的礼殊,(很好理解,因?yàn)橄麓问褂迷搱D片的時(shí)候针史,可能場(chǎng)景不同晶伦,需要生成的縮略圖大小是不同的,顯然不能做緩存處理)
- 設(shè)置kCGImageSourceShouldCacheImmediately為true啄枕,避免在需要渲染的時(shí)候才做解碼婚陪,默認(rèn)選項(xiàng)是false
這樣的縮略圖方式可以省去大量的內(nèi)存和CPU消耗,官方Case給出的前后內(nèi)存對(duì)比
Prefetching && Background decoding
解碼過(guò)程是非常占用CPU資源的射亏,放在主線程一定會(huì)造成阻塞近忙,所以這個(gè)操作應(yīng)該放在異步線程竭业。代碼如下
Prefetching:預(yù)加載智润,也就是提前為之后的cell預(yù)加載數(shù)據(jù)(基本上主流的app都有這么做滴,iOS10之后未辆,系統(tǒng)引入的tableView(_:prefetchRowsAt:) 可以更加方便的實(shí)現(xiàn)預(yù)加載窟绷。)
小tips: 這里使用串行隊(duì)列可以很好地避免Thread Explosion,線程切換的代價(jià)是非常昂貴的咐柜,所以在我們app中應(yīng)該使用GCD串行隊(duì)列創(chuàng)建一個(gè)解碼線程兼蜈。
官方實(shí)現(xiàn)UI實(shí)例
我們現(xiàn)在需要實(shí)現(xiàn)下面的live按鈕
先看一種不合理的實(shí)現(xiàn)方式
我們先來(lái)分析這種方案的問(wèn)題所在攘残,
UIView是通過(guò)CALayer創(chuàng)建FrameBuffer最后顯示的。重寫了drawRect方法为狸,Calayer會(huì)創(chuàng)建一個(gè)Backing Store歼郭,然后在Backing Store上執(zhí)行draw函數(shù),最后將內(nèi)容傳遞給frameBuffer最終顯示辐棒。
Backing Store的默認(rèn)大小和View的大小成正比病曾,以iphone6為例,750 * 1134 * 4 字節(jié) ≈ 3.4 Mb漾根。
iOS 12泰涂,對(duì) backing store 有做優(yōu)化,它的大小會(huì)根據(jù)圖片的色彩空間辐怕,動(dòng)態(tài)改變逼蒙。
在此之前,如果你使用 sRGB 格式寄疏,但是實(shí)際繪制的內(nèi)容是牢,只使用了單通道,那么大小會(huì)比實(shí)際要的大赁还,造成不必要開(kāi)銷妖泄。iOS 12 會(huì)自動(dòng)優(yōu)化這部分。
總結(jié)下這種使用drawRect繪制方案的問(wèn)題
- Backing Store的創(chuàng)建造成了不必要的內(nèi)存開(kāi)銷
- UIImage先繪制到Backing Store艘策,再渲染到frameBuffer蹈胡,中間多了一層內(nèi)存拷貝
- 背景顏色不需要繪制到Backing Store,直接使用BackGroundColor繪制到FrameBuffer
所以朋蔫,正確的實(shí)現(xiàn)姿勢(shì)是將這個(gè)大的view拆分成小的subview逐個(gè)實(shí)現(xiàn)罚渐。
背景顏色實(shí)現(xiàn)
這里有一個(gè)圓角的處理
UIView的maskView 及CALayer.maskLayer都會(huì)將圖層渲染到臨時(shí)的image buffer中,也就是我們常說(shuō)的離屏渲染驯妄,而CALayer.cornerRadius不會(huì)造成離屏渲染荷并,真正造成離屏渲染的是設(shè)置MaskToBounds這樣的屬性。所以背景圖直接使用UIView設(shè)置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會(huì)造成不必要的backing store內(nèi)存開(kāi)銷微猖,并且這種做法的本質(zhì)是創(chuàng)建遮罩mask谈息,再進(jìn)行圖層混合,同樣會(huì)離屏渲染凛剥。
正確的姿勢(shì)侠仇,
對(duì)于UIView直接使用CornerRadius,CoreAnimation可以為我們?cè)诓活~外創(chuàng)建內(nèi)存開(kāi)銷的情況下繪制出圓角。
對(duì)于UIImageView可以使用CoreGraphics自己裁剪出帶圓角的Image逻炊,實(shí)例代碼如下
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圖片實(shí)現(xiàn)
直接使用UIImageView互亮,這里有個(gè)技巧,如果是純色圖片余素,想要展示不同顏色的同一張圖片豹休,可以使用UIImageView的tintColor屬性平鋪顏色,來(lái)達(dá)到復(fù)用圖片的目的桨吊。
代碼如下:
UIImage.withRenderingMode(_:)
UIImageView.tintColor
文本實(shí)現(xiàn)
文本使用UILabel可以減少百分之75的Backing Store開(kāi)銷慕爬,系統(tǒng)針對(duì)UILabel做了優(yōu)化,并且自動(dòng)更新Backing Store的size,針對(duì)emoji和富文本內(nèi)容屏积。
最終實(shí)現(xiàn)
最終Live按鈕的正確實(shí)現(xiàn)方案如下圖
推薦使用Image Assets
- 基于名稱和特效優(yōu)化了查找效率医窿,更快的查找圖片
- 運(yùn)行時(shí),對(duì)內(nèi)存的管理也有優(yōu)化
- App Slicing炊林,app安裝包瘦身姥卢。iOS 9 后會(huì)從 Image Assets 中保留設(shè)備支持的圖片 (2x 或者 3x)
- iOS 11 后的 Preserve Vector Data。支持矢量圖的功能渣聚,放大也不會(huì)失真
Advanced Image Effects
對(duì)于圖片的實(shí)時(shí)處理推薦使用CoreImage框架独榴。
例如將一張圖片的灰度值進(jìn)行調(diào)整這樣的操作,有滴小伙伴可能使用CoreGraphics獲取圖像的每個(gè)像素點(diǎn)數(shù)據(jù)奕枝,然后改變灰度值棺榔,最終生成目標(biāo)圖標(biāo),這種做法將大量gpu擅長(zhǎng)的工作放在了cpu上處理隘道,合理的做法是: 使用CoreImage的濾鏡filter或者metal症歇,OpenGL的shader,讓圖像處理的工作交給GPU去做谭梗。
Drawing Off-Screen
對(duì)于需要離屏渲染的場(chǎng)景推薦使用UIGraphicsImageRenderer替代UIGraphicsBeginImageContext忘晤,性能更好,并且支持廣色域激捏。
4. 拓展與思考
用提問(wèn)的方式來(lái)拓展一下设塔,針對(duì)每個(gè)問(wèn)題進(jìn)行深入的思考
問(wèn)題一:圖像展示有這么多細(xì)節(jié)在里面,可是為什么在平常開(kāi)發(fā)中為什么沒(méi)有感覺(jué)到远舅,可以從哪些地方對(duì)自己的工程進(jìn)行優(yōu)化闰蛔。
答:我們平常大部分會(huì)使用UIImage imageNamed這樣的API加載了本地圖片,而網(wǎng)絡(luò)圖片則使用了SDWebImage或者YYWebImage等框架來(lái)加載图柏。所以沒(méi)有去細(xì)究序六。
進(jìn)而引申出
問(wèn)題二: 使用imageNamed,系統(tǒng)何時(shí)去解碼爆办,有沒(méi)有緩存难咕,緩存的大小是多少课梳,有沒(méi)有性能問(wèn)題距辆,和imageWithContentsOfFile有什么區(qū)別
答: 一一來(lái)解答這個(gè)問(wèn)題
- 首先先說(shuō)imageNamed和imageWithContentsOfFile有什么區(qū)別余佃,想必大部分小伙伴都很清楚,因?yàn)檫@也是面試?yán)仙U劦臇|西跨算。imageNamed加載本地圖片會(huì)緩存圖片爆土,也就是加載一千張相同的本地圖片,內(nèi)存中也只會(huì)有一份诸蚕,而imageWithContentsOfFile不會(huì)緩存步势,也就是重復(fù)加載相同圖片,在內(nèi)存中會(huì)有多份圖片數(shù)據(jù)背犯。
- imageNamed加載圖片會(huì)將圖片源數(shù)據(jù)和解碼后的數(shù)據(jù)加載入內(nèi)存緩存中坏瘩,只有收到內(nèi)存警告的時(shí)候才會(huì)釋放,有興趣的小伙伴可以自行調(diào)試一下漠魏。
- 關(guān)于UIImage對(duì)象何時(shí)去解碼倔矾,其實(shí)剛剛我們?cè)诮档筒蓸拥臅r(shí)候已經(jīng)提到了,kCGImageSourceShouldCacheImmediately屬性系統(tǒng)默認(rèn)是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).
也就是說(shuō)UIImage只有在屏幕上渲染時(shí)再去解碼的哪自。而關(guān)于UIImageView的操作一定是在主線程,解碼操作是放在主線程的禁熏。所以如果在tableview滑動(dòng)中頻繁的創(chuàng)建較大的UIImage渲染展示壤巷,會(huì)造成主線程阻塞。
總結(jié): imageNamed默認(rèn)帶緩存瞧毙,緩存通過(guò)NSCache實(shí)現(xiàn)胧华。適用于需要頻繁復(fù)用的圖片的加載,而imageWithContentsOfFile不會(huì)緩存宙彪,適用于不常用的較大圖片的加載撑柔,由于系統(tǒng)默認(rèn)主線程解碼UIImage,所以imageNamed僅僅適用于加載較小的例如APP各個(gè)tab的icon您访,需要在首屏展示的圖片铅忿。而不適用于滑動(dòng)的下載好的大量網(wǎng)絡(luò)圖片的本地加載。會(huì)造成主線程阻塞灵汪。
5. 正確的網(wǎng)絡(luò)圖片加載方式
其實(shí)這里SDWebImage或者YYWebImage等框架已經(jīng)給出了正確的姿勢(shì)檀训,細(xì)節(jié)可以挑其中一個(gè)閱讀源碼即可。
分享下優(yōu)秀的源碼解析
YImage 設(shè)計(jì)思路享言,實(shí)現(xiàn)細(xì)節(jié)剖析
下載圖片主要簡(jiǎn)化流程如下
- 從網(wǎng)絡(luò)下載圖片源數(shù)據(jù)峻凫,默認(rèn)放入內(nèi)存和磁盤緩存中
- 異步解碼,解碼后的數(shù)據(jù)放入內(nèi)存緩存中
- 回調(diào)主線程渲染圖片
- 內(nèi)部維護(hù)磁盤和內(nèi)存的cache览露,支持設(shè)置定時(shí)過(guò)期清理荧琼,內(nèi)存cache的上限等
加載圖片的主要簡(jiǎn)化流程如下
- 從內(nèi)存中查找圖片數(shù)據(jù),如果有并且已經(jīng)解碼,直接返回?cái)?shù)據(jù)命锄,如果沒(méi)有解碼堰乔,異步解碼緩存內(nèi)存后返回
- 內(nèi)存中未查找到圖片數(shù)據(jù),從磁盤查找脐恩,磁盤查找到后镐侯,加載圖片源數(shù)據(jù)到內(nèi)存,異步解碼緩存內(nèi)存后返回驶冒,如果沒(méi)有去網(wǎng)絡(luò)下載圖片苟翻。走上面的流程。
分析:
- 這樣滴流程解決了UIImage imageNamed這種加載一定在主線程解碼圖片的問(wèn)題骗污,異步加載崇猫,避免了主線程阻塞。
- 通過(guò)緩存內(nèi)存方式需忿,避開(kāi)了頻繁的磁盤IO
- 通過(guò)緩存解碼后的圖片數(shù)據(jù)邓尤,避開(kāi)了頻繁解碼的CPU消耗。
6. 超大圖片的處理
之前我們分析過(guò)1080p的圖片解碼后的內(nèi)存大小贴谎,大約是7.9mb汞扎,如果是4k,8k圖擅这,這個(gè)內(nèi)存占用將會(huì)非常的大澈魄,如果使用SDWebImage或者YYWebImage的默認(rèn)解碼緩存技術(shù)方案去加載多張這樣的大圖,帶來(lái)的結(jié)果會(huì)是內(nèi)存爆掉仲翎。閃退痹扇。
可以設(shè)置SDWebImage或者YYWebImage的Option選項(xiàng)不解碼下載好的圖片
那么大圖該怎么處理呢,這里有兩個(gè)場(chǎng)景
- 一張超大圖加載在一個(gè)小的view上
解決方法: 使用蘋果推薦的縮略圖DownSampling方案即可
- 像微信溯香,微博長(zhǎng)圖詳情那樣鲫构,全屏加載大圖,通過(guò)拖動(dòng)來(lái)查看不同位置圖片細(xì)節(jié)
解決方法: 使用蘋果的CATiledLayer去加載玫坛。原理是分片渲染结笨,滑動(dòng)時(shí)通過(guò)指定目標(biāo)位置,通過(guò)映射原圖指定位置的部分圖片數(shù)據(jù)解碼渲染湿镀。這里不再累述炕吸,有興趣的小伙伴可以自行了解下官方API。
7. 總結(jié)
了解圖像加載的細(xì)節(jié)和全過(guò)程非常有必要勉痴,有助于我們?cè)谄匠i_(kāi)發(fā)中選擇合適的方案赫模,做出合理的性能優(yōu)化。