文檔更新說(shuō)明
- 最后更新 2020年03月22日
- 首次更新 2020年03月27日
前言
現(xiàn)在的iPhone性能越來(lái)越好, 正常開(kāi)發(fā)一個(gè)界面都很少會(huì)遇到影響體驗(yàn)的卡頓. 但是如果把APP放到比較老的型號(hào)上, 卡頓就非常常見(jiàn)了. 利用這篇文章, 結(jié)合一下實(shí)際的案例QQ音樂(lè)首頁(yè), 聊一聊解決卡頓的基本思想和方法論.
這是QQ音樂(lè)的界面
這是Demo的界面, 部分素材找不到就臨時(shí)用別的代替一下, 效果基本一致
演示的機(jī)器是iPhone 6 Plus, iOS 10.2, Xcode 11.3
源碼下載
首頁(yè)的實(shí)現(xiàn)思路
整體UI結(jié)構(gòu)
先用一個(gè)UITableView實(shí)現(xiàn)界面的整體, 而每一個(gè)能夠進(jìn)行左右滾動(dòng)的UITableViewCell, 都嵌套一個(gè)UICollectionView來(lái)做.
雖然說(shuō)UICollectionView比較重量級(jí), 不過(guò)我在老古董iPhone6 Plus上看, CPU占有率只有10%左右, 完全可以接受的.
至于其他的能支持橫向滾動(dòng)并且復(fù)用視圖的組件, 這東西我個(gè)人認(rèn)為, 只有系統(tǒng)提供的視圖無(wú)法優(yōu)化到滿意的情況下, 再去造輪子或者用新輪子, 要看看額外做的工作和得到的收益是不是值得.
布局方式
先用Auto Layout + XIB文件的形式開(kāi)發(fā)視圖. 自動(dòng)布局相比手動(dòng)布局, 好處就是速度快一些, 現(xiàn)在第一個(gè)版本用的是自動(dòng)布局, 假如后面優(yōu)化之后還有明顯卡頓的話, 再考慮代碼布局.
首頁(yè)類型劃分
頂部搜索框
QQ音樂(lè)的搜索框會(huì)隨著頁(yè)面向上移動(dòng)而移動(dòng), 但是頁(yè)面向下移動(dòng)的時(shí)候, 搜索框則固定不動(dòng). 所以這里采用一個(gè)獨(dú)立的UIView, 存放搜索框也左邊的音樂(lè)館
label, 以及右邊的Logo
并且監(jiān)聽(tīng)了TableView的contentOffset
屬性, 根據(jù)滾動(dòng)的偏移量來(lái)設(shè)置搜索視圖的位置. 這里用到了我之前做過(guò)的一個(gè)支持自動(dòng)釋放的便捷觀察者類庫(kù) "NSObject+CCEasyKVO.h"
, 有興趣可以看代碼.
Banner
搜索框下面是一個(gè)可以左右滾動(dòng)的Banner, 網(wǎng)上輪子很多, 這里就不重新做了.
固定內(nèi)容的視圖
這部分界面有5個(gè)圖標(biāo), 因?yàn)槭枪潭ú蛔円膊豢梢詽L動(dòng)的, 所以可以直接用普通的UIView或者UIStackView來(lái)做, 這里我直接用UICollectionView實(shí)現(xiàn).
再用另一個(gè)UITableVIewCell存放下方的歌單新碟
, 數(shù)字專輯
兩個(gè)普通的UIView.
橫向瀑布流
#話題部分
Topic是一個(gè)橫向瀑布流視圖, 采用自定義UICollectionViewFlowLayout實(shí)現(xiàn).
創(chuàng)建TopicWalterfallFlowLayout類, 繼承自UICollectionViewFlowLayout, 重寫(xiě)prepareLayout
方法, 算好每一個(gè)Topic的文本寬度并且緩存起來(lái), 這樣TopicWalterfallFlowLayout就可以算出全部CollectionCell的位置了. 效果如上圖.
各種不同的CollectionViewCell
往下的可以橫向滾動(dòng)的視圖都用UICollectionView實(shí)現(xiàn), 其中分為多種不同的Cell. QQ音樂(lè)首頁(yè)的Cell種類是后臺(tái)配置的, 我這里只挑選其中幾種實(shí)現(xiàn), 其他的都是一樣的道理.
其中歌單的Cell, 因?yàn)橐趫D片上顯示白色的文本, 所以我在圖片上加了一個(gè)灰色漸變蒙版, 這樣底部的數(shù)字看起來(lái)才會(huì)清晰. 不然遇到白色圖片文字就看不清了. 另外兩個(gè)Cell也是同理 不過(guò)截圖沒(méi)體現(xiàn)出來(lái). 這些圓角都使用下面兩行代碼搞定
self.maskV.layer.masksToBounds = YES;
self.maskV.layer.cornerRadius = 10.f;
到這里基本就把首頁(yè)的UI結(jié)構(gòu)介紹完畢了. 這個(gè)版本的代碼可以從tag v1
獲取.
代碼優(yōu)化
興匆匆地運(yùn)行了一下tag v1
代碼, 在iPhone xs max上挺流暢的, 有點(diǎn)失望, 這不是沒(méi)得優(yōu)化嗎??.
換個(gè)手機(jī), 在iPhone6p上跑了一下, 問(wèn)題來(lái)了. 好卡, 略興奮, 卡頓還挺嚴(yán)重的, 這種會(huì)影響到用戶體驗(yàn), 沒(méi)優(yōu)化好肯定不能上線的.
不過(guò)有一點(diǎn)讓我覺(jué)得奇怪的是, 屏幕上顯示的FPS一直是60, CPU占有率只有15%, 這種卡頓很容易讓人猜出來(lái)是GPU處理不過(guò)來(lái). 因?yàn)镕PS指示器用的是CADisplayLink
加一個(gè)整形變量實(shí)現(xiàn)的, 計(jì)算出CADisplayLink
每秒調(diào)用的次數(shù)就是幀率. 既然這個(gè)FPS一直是60, 那么意味著CPU還是能處理的過(guò)來(lái)的.
借助性能調(diào)試工具Instruments中的Core animation, 可以看到真實(shí)的幀率.
幀率在40幀左右, GPU使用率高達(dá)90%, 說(shuō)明我猜的沒(méi)錯(cuò), 下面要做的事情就是平衡GPU和CPU的工作量.
下面分別從這兩個(gè)角度來(lái)談代碼優(yōu)化問(wèn)題.
GPU 優(yōu)化
一說(shuō)到優(yōu)化, 很多人都知道圓角這些會(huì)影響性能, 可以用帶圓角的圖片啊, 用CGContext畫(huà)帶圓角圖片之類的來(lái)取代對(duì)視圖圓角的設(shè)置, 但是并不知道為什么要這么解決. 這會(huì)導(dǎo)致無(wú)法對(duì)出現(xiàn)的卡頓現(xiàn)象做比較深入的分析, 無(wú)法精準(zhǔn)解決問(wèn)題.
比如一開(kāi)始我就對(duì)Topic部分帶圓角的視圖設(shè)置了masksToBounds=YES
, 然后胡亂打開(kāi)了光柵化等, 沒(méi)有指導(dǎo)思想碰運(yùn)氣式地解決問(wèn)題, 效率并不高.
GPU使用率過(guò)高, 常見(jiàn)的原因有下面幾個(gè)
- 太多紋理(texture)要處理, 比如一個(gè)View有太多子Layer.
- 渲染的視圖有陰影, 圓角.
- Layer上有Mask.
- View采用模糊顯示, 比如用了UIVisualEffectView.
- 柵格化(shouldRasterize)圖層緩存命中率過(guò)低.
上面這幾個(gè)比較常見(jiàn).
其中陰影,圓角, Mask, Effect, shouldRasterize這幾個(gè)會(huì)觸發(fā)GPU離屏渲染, 優(yōu)化GPU的大部分方式, 就是如何處理好離屏渲染. 離屏渲染是GPU的性能殺手, 這里有必要去了解一下.
iOS的渲染過(guò)程
從CPU計(jì)算好視圖內(nèi)容, 到顯示在屏幕上給用戶觀看, iOS的UI渲染一共經(jīng)歷了下面幾個(gè)過(guò)程.
我們的代碼運(yùn)行在Application層, CPU計(jì)算好視圖信息(座標(biāo)尺寸, 視圖文本信息, 圖層關(guān)系等), 會(huì)把數(shù)據(jù)提交到Render Server
層, 接著進(jìn)入GPU渲染, 再顯示到屏幕上.
實(shí)際過(guò)程比這個(gè)復(fù)雜, 可以找一下資料看看這個(gè)具體過(guò)程
什么是渲染? 光柵化?
一定要先搞明白什么叫渲染, 不然對(duì)這個(gè)渲染知識(shí)點(diǎn)只會(huì)是似懂非懂. 這里只討論2D領(lǐng)域.
所謂的渲染, 粗魯?shù)卣f(shuō), 就是把幾何圖形, 圖片數(shù)據(jù), 文本等一大堆用來(lái)表達(dá)視圖內(nèi)容的東西, 計(jì)算成像素圖(位圖), 并且把像素圖放到frame buffer中, 這個(gè)過(guò)程就叫渲染! 顯示器就可以讀取frame buffer的數(shù)據(jù), 顯示到屏幕上.
渲染里面經(jīng)常看到光柵化這個(gè)詞, 它指的是把幾何圖形像素化, 粗淺理解, 光柵化可以等同于渲染.
這部分知識(shí)點(diǎn)應(yīng)該足夠我們做UI性能優(yōu)化了...
看到一個(gè)很有意思的比喻, 如果把渲染比作做菜, 那么你起鍋擺盤(pán)就是光柵化毛嫉。
什么是GPU渲染, 什么是CPU渲染?
上面說(shuō)的視圖信息其實(shí)就是用的CALayer來(lái)表示, 由Core Animation
這個(gè)框架負(fù)責(zé)傳給GPU渲染(硬件渲染), 這就是為啥說(shuō)用CALayer及其子類(CAShapeLayer等)來(lái)展示視圖信息效率高, 因?yàn)樗詈髸?huì)由GPU渲染.
而平時(shí)我們可能會(huì)自己用CoreGraphics
這個(gè)框架, 創(chuàng)建一個(gè)圖形上下文CGContext, 畫(huà)啊畫(huà), 再得到一個(gè)UIImage, 賦值給layer.contents, 這個(gè)步驟其實(shí)就是我們自己手動(dòng)用CPU渲染(軟件渲染)出像素?cái)?shù)據(jù), 這樣Core Animation
就會(huì)直接把這個(gè)contents的內(nèi)容放入到frame buffer中, 顯示器直接讀取frame buffer, 就可以把它里面一個(gè)一個(gè)像素打到屏幕上了.
當(dāng)然渲染并不是一次完成, 比如一個(gè)視圖有很多個(gè)子視圖, 渲染的時(shí)候就要從最下層開(kāi)始, 一層一層把視圖內(nèi)容渲染到frame buffer中, 這種方式, 稱為畫(huà)家算法.
PS. 有關(guān)資料顯示, iOS采用雙緩沖技術(shù), 實(shí)際上是有兩個(gè)frame buffer, 用來(lái)加快渲染效率, 不管它有多少個(gè), 原理都是一樣. frame buffer(緩沖區(qū)), 就是一塊內(nèi)存區(qū)域, 用來(lái)存放即將顯示到屏幕上的像素?cái)?shù)據(jù).
為什么會(huì)出現(xiàn)離屏渲染?
上面說(shuō)到渲染就像在畫(huà)畫(huà)一樣, 一層一層畫(huà), 前面畫(huà)上去的東西就不能修改了.
這就導(dǎo)致有些視圖是無(wú)法直接渲染到frame buffer中, 比如有圓角, Mask, 陰影這些.
帶圓角需要裁剪的視圖, 它的所有子視圖也需要跟著裁剪, 要提高裁剪效率, 最好的做法就是把全部圖層依次畫(huà)到frame buffer中, 然后再裁剪. 不過(guò)前面已經(jīng)說(shuō)了, 畫(huà)進(jìn)去的東西就不能改了, 所以GPU只能在另一個(gè)地方開(kāi)辟一個(gè)新的frame buffer用來(lái)存放臨時(shí)的渲染結(jié)果, 然后再把最終結(jié)果復(fù)制到frame buffer. 這塊新的frame buffer也叫離屏緩沖區(qū), 自然這個(gè)過(guò)程就叫做離屏渲染了.
可以看到, 離屏渲染需要GPU不停地切換工作環(huán)境, 從一個(gè)frame buffer切換到另一個(gè)frame buffer, GPU的工作環(huán)境稱為上下文, 不停切換上下文, 會(huì)嚴(yán)重降低GPU的工作效率. 這塊涉及到GPU的工作原理, 不是我的專業(yè)范圍就不多說(shuō)了.
Mask和陰影這些也是同個(gè)道理, 只有把全部視圖都畫(huà)好了, 才能知道裁剪的形狀或者陰影的路徑, 所以這個(gè)渲染的方式會(huì)轉(zhuǎn)化成離屏渲染.
CALayer有個(gè)shadowPath
, 設(shè)置好它GPU就可以事先知道陰影路徑, 就不需要離屏渲染了. 可以看上圖, 紅色陰影就是用shadowPath
實(shí)現(xiàn)的; 而圓角的設(shè)置, 如果不需要裁剪子視圖的話, 把masksToBounds
設(shè)置成NO, 也不會(huì)造成離屏渲染. 下文會(huì)講到這個(gè).
注意, 不同版本iOS系統(tǒng)對(duì)渲染的處理會(huì)有差異, 如果能找到一次性渲染好視圖的算法, 就不需要離屏了, 所以判斷是不是離屏必須用專門(mén)的工具, 而不能單憑直覺(jué)
開(kāi)始優(yōu)化 tag v1
上面這部分知識(shí)點(diǎn), 是優(yōu)化的核心指導(dǎo)思想.
開(kāi)啟離屏檢測(cè), 看看首頁(yè)的渲染情況
Debug->View Debugging->Rendering->Color Offscreen-Rendered Yellow
和預(yù)期的一樣, 所有圓角區(qū)域都是離屏渲染.
嘗試開(kāi)啟光柵化, 設(shè)置CALayer.shouldRasterize=YES
, 這樣視圖只需要離屏渲染一次, 就會(huì)把
內(nèi)容緩存起來(lái)供下次使用, 提升性能.
開(kāi)啟光柵化后CollectionView里面的視圖都是紅色的, 說(shuō)明光柵化后無(wú)法得到有效緩存, 這樣實(shí)際上機(jī)器性能消耗, 并沒(méi)有好處.
UITableView和UICollectionView這些視圖, 都是在反復(fù)利用那幾個(gè)Cell, 同時(shí)刷新Cell的內(nèi)容, 這種會(huì)復(fù)用視圖的, 就會(huì)不停更新Layer內(nèi)容導(dǎo)致緩存命中率超低, 不適合開(kāi)啟光柵化.
所以通過(guò)光柵化并沒(méi)法解決問(wèn)題, 反而界面的幀率只剩下30了.
代碼改回去, 繼續(xù)優(yōu)化, 先針對(duì)Topic Collection View優(yōu)化.
觀察一下, Topic 的每一個(gè)cell里面雖然也有圓角, 但是只包含了文本, 并沒(méi)有圖片, 顯示圓角的背景色并不需要設(shè)置masksToBounds.
官方說(shuō)了這個(gè)問(wèn)題, 指針對(duì)contents的圓角, 才需要設(shè)置masksToBounds.
Setting the radius to a value greater than 0.0 causes the layer to begin drawing rounded corners on its background. By default, the corner radius does not apply to the image in the layer’s contents property; it applies only to the background color and border of the layer. However, setting the masksToBounds property to true causes the content to be clipped to the rounded corners.
把Topic視圖相關(guān)的masksToBounds =YES
代碼移除掉, 重新運(yùn)行一下, 滾動(dòng)到topic這個(gè)區(qū)間幀數(shù)明顯提升, 代碼可以看tag v1.1
開(kāi)始優(yōu)化 tag v1.1
開(kāi)始優(yōu)化各種帶圓角圖片的Cell. 這里的指導(dǎo)思想, 就是平衡CPU和GPU的使用率.
GPU不夠, CPU來(lái)湊.
iPhone6 Plus的GPU確實(shí)不怎么好, 圓角一多占有率飆升到90%了. 結(jié)合上面的知識(shí)點(diǎn), 要做的事情就是把部分GPU的工作交給CPU處理.
利用CoreGraphics
框架, 使用CPU渲染帶圓角的圖片, 在設(shè)置給layer.contents, 同時(shí)關(guān)閉masksToBounds
, 這樣即可減輕GPU的工作量.
這里我利用YYAsyncLayer
來(lái)實(shí)現(xiàn)CPU異步渲染, YYAsyncLayer
的原理很簡(jiǎn)單, 當(dāng)layer需要display的時(shí)候, 開(kāi)啟一個(gè)異步線程, 創(chuàng)建CGContextRef畫(huà)布, 用戶可以在這個(gè)異步線程里把視圖內(nèi)容畫(huà)到CGContextRef里, 然后YYAsyncLayer
會(huì)在主線程幫你把渲染好的內(nèi)容賦值給layer.contents.
YYAsyncLayer
內(nèi)部根據(jù)CPU核數(shù)定義了若干串行隊(duì)列, 放到隊(duì)列池里, 每次要渲染的時(shí)候就從池里一次按順序取出一個(gè)串行隊(duì)列, 異步執(zhí)行CoreGraphics
渲染代碼, 這樣做的好處就是能控制并發(fā)線程數(shù). 不過(guò)我覺(jué)得用NSOperationQueue來(lái)實(shí)現(xiàn)就可以了, 沒(méi)必要搞這么復(fù)雜.
這個(gè)庫(kù)還提供了一個(gè)事務(wù)類YYTransaction
, 這個(gè)類在Runloop上注冊(cè)了觀察者, 當(dāng)Runloop處于kCFRunLoopBeforeWaiting
狀態(tài)時(shí)觸發(fā), 優(yōu)先級(jí)非常低, 適合在程序有空閑的時(shí)候處理業(yè)務(wù)邏輯, 后面CPU優(yōu)化部分會(huì)用到.
這里我封裝了一個(gè)支持異步CPU渲染圓角圖片和灰色漸變的類AsyncImageView , 核心代碼如下
- (YYAsyncLayerDisplayTask *)newAsyncDisplayTask {
// 在主線程訪問(wèn)bounds屬性
CGRect bounds = self.bounds;
YYAsyncLayerDisplayTask *task = [YYAsyncLayerDisplayTask new];
task.willDisplay = ^(CALayer *layer) {};
task.display = ^(CGContextRef context, CGSize size, BOOL (^isCancelled)(void)) {
if (isCancelled()) return;
CGContextAddPath(context, [UIBezierPath bezierPathWithRoundedRect:bounds cornerRadius:self.asyncCornerRadius].CGPath);
// 注意必須在把圖片繪制到上下文之前就切割好繪制區(qū)域. 否則切割只對(duì)后續(xù)的繪制生效, 對(duì)已經(jīng)繪制好的圖片不生效.
CGContextClip(context);
CGContextSaveGState(context);
CGContextTranslateCTM(context, 0, bounds.size.height);
CGContextScaleCTM(context, 1.0, -1.0);
CGContextDrawImage(context, bounds, self.image.CGImage);
CGContextRestoreGState(context);
if (self.drawMask) {
CGColorSpaceRef rgb = CGColorSpaceCreateDeviceRGB();
CGGradientRef gradient = CGGradientCreateWithColorComponents(rgb, self->_colors, NULL, self.maskColors.count);
CGContextDrawLinearGradient(context, gradient, CGPointMake(size.width / 2, 0), CGPointMake(size.width / 2, size.height), 0);
CGGradientRelease(gradient);
CGColorSpaceRelease(rgb);
}
};
task.didDisplay = ^(CALayer *layer, BOOL finished) {};
return task;
}
前面提到說(shuō)QQ音樂(lè)首頁(yè)會(huì)在圖片上放一些白色的文本, 一開(kāi)始的做法是添加一個(gè)灰色漸變的視圖蓋在圖片上, 然后文本放灰色視圖上.
這里我順便給AsyncImageView類增加了繪制漸變蒙版的功能, 這樣就不需要額外疊加灰色圖層了, 能提高效率.
利用AsyncImageView替換掉UICollectionViewCell上的UIImageView.
現(xiàn)在豎向滾動(dòng)的時(shí)候, GPU從45幀提高到55~59幀了, 肉眼只能偶爾看到輕微卡頓, 完全可以接受的. 已經(jīng)到達(dá)上線標(biāo)準(zhǔn)了.
滾動(dòng)時(shí)的CPU使用率從之前的15%提升30~50%, GPU從90%下降到28%左右.
由此可見(jiàn), 通過(guò)正確的指導(dǎo)思想, 確實(shí)讓CPU核GPU的使用率更加平衡, 用戶體驗(yàn)也會(huì)更好. 這個(gè)版本的代碼可以在tag v2.0
獲得.
GPU的其他優(yōu)化
上面的優(yōu)化主要是處理離屏渲染, 我覺(jué)得離屏渲染是GPU優(yōu)化的重點(diǎn), 工作量最少, 提升最大. 其他的優(yōu)化, 可以從減少紋理的角度出發(fā).
比如減少透明圖層的使用. 能合并的圖層, 可以先合并到一起. 比如下面這個(gè)界面, 是可以從圖片的角度上, 直接提供一張圖片即可
但是這種操作工作量比較大, 首先要修改UICollectionViewCell的視圖結(jié)構(gòu), 然后還要讓服務(wù)器把兩個(gè)圖片合成一張, 或者在APP里, 找個(gè)主線程空閑時(shí)間把兩個(gè)圖合成一張?jiān)俦4嫫饋?lái), 篇幅有限我就不做了, 如果你的程序優(yōu)化了離屏渲染還是很卡, 那就有必要做了.
下面開(kāi)始著手CPU優(yōu)化, CPU優(yōu)化也有很多指導(dǎo)思想.
CPU 優(yōu)化
CPU的幾種常見(jiàn)優(yōu)化思路
在優(yōu)化之前, 可以用Instruments工具里的Time Profiler時(shí)間分析工具, 很方便查看各行代碼的CPU執(zhí)行時(shí)間.
常見(jiàn)的CPU優(yōu)化指導(dǎo)思想, 總結(jié)起來(lái)大概就是這兩點(diǎn),
- 時(shí)間不闊綽, 任務(wù)提前做, 就是預(yù)處理
- 大事化小, 小事化了, 就是拆分任務(wù)
具體到編碼上, 有下面幾種方法
- 文件資源提前加載, 就是預(yù)加載
- 取消自動(dòng)布局, 提前計(jì)算視圖frame, 就是預(yù)排版
- 提前緩存像素圖, 供下次直接使用, 就是預(yù)渲染
- 空間換時(shí)間, 就是緩存, 預(yù)渲染也屬于緩存的一種.
- 限制線程數(shù), 就是并發(fā)控制
- 代碼布局, 放棄xib, StoryBoard, 就是很麻煩
保持界面流暢, 還要時(shí)刻注意不要在主線程上做太多事情, 主線程每一幀只有16.67ms.
此外應(yīng)該還有其他, 比如對(duì)象的釋放放到后臺(tái)隊(duì)列(這個(gè)在YYAsyncLayer里面可以看到YYAsyncLayerGetReleaseQueue), 其他的暫時(shí)想不到了.
開(kāi)始優(yōu)化 v2.0
有了指導(dǎo)思想, 下面開(kāi)始用Time Profiler找一下哪些任務(wù)占用較多CPU資源, 如果能預(yù)處理的, 就先預(yù)處理.
預(yù)處理的時(shí)候, 多利用runloop提供的觀察者模式, 盡量把預(yù)處理的代碼放到runloop即將休眠的時(shí)候處理, 而且每次只處理一個(gè)任務(wù), 把任務(wù)拆分成多個(gè)子任務(wù)處理, 盡量避免在一幀的時(shí)間內(nèi)做太多事情.
先關(guān)閉FPS視圖, 避免Timer干擾分析.
運(yùn)行程序
從程序啟動(dòng)后了1秒左右的時(shí)間里, 可以看到消耗CPU的地方幾種在下面幾個(gè).
- 主線程主要工作量在TableViewCell和CollectionViewCell的加載
- 非主線程主要工作量是AsyncImageView的CPU渲染, SDImageCache的圖片加載
通過(guò)Time Profiler的代碼分析功能, 可以輕松看到具體的代碼細(xì)節(jié), 其中SongListCell
中的UICollectionViewCell的xib文件的加載消耗26ms.
一個(gè)線程中的AsyncImageView的圖片繪制占用了149ms, 陰影繪制占用了36ms.
一個(gè)線程中的SDImageCache緩存加載主要是在圖片解碼的地方, 消耗了119ms.
可以看出來(lái), 首頁(yè)消耗CPU的地方就是加載xib文件和圖片解碼, CPU渲染圖片, 因?yàn)镈ome比較簡(jiǎn)單, 所以這個(gè)情況是符合預(yù)期的.
圖片解碼這塊已經(jīng)使用了SDWebImage這個(gè)框架, 他把解碼操作放到非主線程了, 如果要加載的圖片是在磁盤(pán)中有的, 加載后在一個(gè)串行IO隊(duì)列中解碼, 這個(gè)不會(huì)有線程爆炸問(wèn)題. 如果圖片在磁盤(pán)沒(méi)有的, 需要聯(lián)網(wǎng)下載的, 下載和解碼的邏輯被放到下載隊(duì)列中執(zhí)行, 最大并發(fā)數(shù)是6, 所以SDWebImage針對(duì)當(dāng)前這個(gè)Demo來(lái)說(shuō), 不會(huì)有線程問(wèn)題.
_ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);
_downloadQueue = [NSOperationQueue new];
_downloadQueue.maxConcurrentOperationCount = _config.maxConcurrentDownloads; // 默認(rèn)是6
_downloadQueue.name = @"com.hackemist.SDWebImageDownloader";
上線滾動(dòng)和左右滾動(dòng), 都可以看到性能消耗的地方主要是對(duì)AsyncImageView的渲染. 滾動(dòng)時(shí)加載Cell后會(huì)設(shè)置URL, 而AsyncImageView只要被設(shè)置了URL, 馬上開(kāi)始圓角Image的渲染, 同一個(gè)圖片來(lái)回滾動(dòng)會(huì)被重復(fù)渲染, 所以這個(gè)地方可以優(yōu)化一下.
思路就是把URL作為key, 渲染出圓角的圖片作為value, 一起保存到內(nèi)存中. 同時(shí)在得到圓角的value后, 還要把SDWebImage從內(nèi)存緩存里的相同URL的圖片刪除, 避免原圖和圓角圖同時(shí)存在內(nèi)容, 浪費(fèi)內(nèi)存空間.
關(guān)于緩存類的選擇, 我在NSCache, YYCache, SDMemoryCache中, 選擇了SDMemoryCache. 原因就是這里暫時(shí)不需要追求極致性能, SDMemoryCache比較適合緩存圖片.
創(chuàng)建內(nèi)存緩存對(duì)象AsyncImageCache, 繼承自SDMemoryCache, SDMemoryCache繼承自NSCache, 它除了提供系統(tǒng)的緩存功能之外, 還特別適合緩存圖片.
這是因?yàn)?strong>SDMemoryCache內(nèi)部定義了一個(gè)NSMapTable類型的weakCache, MapTable支持對(duì)值弱引用, 這樣做的好處就是如果系統(tǒng)發(fā)起內(nèi)存警告時(shí), 父類NSCache會(huì)把緩存釋放掉, 這樣用戶從緩存里獲取圖片的時(shí)候, 如果在weakCache里還存在圖片的話, 說(shuō)明圖片還顯示在屏幕上, 這時(shí)候直接把屏幕上的圖片寫(xiě)入緩存并返回即可, 效率更高.
- (void)setImageURL:(NSURL *)imageURL {
_imageURL = imageURL;
UIImage *cacheImage = [AsyncImageCache.shareCache objectForKey:imageURL.absoluteString];
if (cacheImage) {
self.layer.contents = (id)cacheImage.CGImage;
} else {
__weak __typeof(self) wself = self;
[SDWebImageManager.sharedManager loadImageWithURL:imageURL options:0 progress:nil completed:^(UIImage *_Nullable image, NSData *_Nullable data, NSError *_Nullable error, SDImageCacheType cacheType, BOOL finished, NSURL *_Nullable imageURL) {
if (!error) {
wself.image = image;
[wself.layer setNeedsDisplay];
}
}];
}
}
- (YYAsyncLayerDisplayTask *)newAsyncDisplayTask {
// 在主線程訪問(wèn)bounds屬性
CGRect bounds = self.bounds;
YYAsyncLayerDisplayTask *task = [YYAsyncLayerDisplayTask new];
task.willDisplay = ^(CALayer *layer) {};
task.display = 略
task.didDisplay = ^(CALayer *layer, BOOL finished) {
if (finished) {
UIImage *image = [UIImage imageWithCGImage:(__bridge CGImageRef)layer.contents];
[AsyncImageCache.shareCache setObject:image forKey:self.imageURL.absoluteString];
[SDWebImageManager.sharedManager.imageCache removeImageForKey:self.imageURL.absoluteString cacheType:SDImageCacheTypeMemory completion:nil];
}
};
return task;
}
緩存好已渲染的圖片后, 現(xiàn)在滾動(dòng)時(shí)CPU基本保持在20%以下了. 界面相比之前更流暢了, 偶爾會(huì)有輕微卡頓.
這個(gè)版本的代碼, 可以看 tag v2.1
CPU的其他優(yōu)化
其他優(yōu)化, 比如把xib布局換成代碼布局, 而且取消自動(dòng)布局, 直接手動(dòng)計(jì)算frame的大小, 再緩存好, 這樣就不用在滾動(dòng)的時(shí)候讓CPU去計(jì)算了.
另外也可以考慮一下提前拉取首頁(yè)圖片的數(shù)據(jù), 先渲染好并緩存起來(lái), 這樣滾動(dòng)的時(shí)候就不需要再去計(jì)算了.
上面說(shuō)的這些預(yù)處理預(yù)渲染, 可以使用YYTransaction這個(gè)對(duì)象, 里面封裝好了runloop觀察者, 在runloop快要休眠的時(shí)候, 一次性處理已經(jīng)提交到靜態(tài)transactionSet
集合的YYTransaction對(duì)象.
當(dāng)然也可以自己注冊(cè)觀察者, 然后弄一個(gè)隊(duì)列, 每次runloop要休眠的時(shí)候就執(zhí)行一下隊(duì)頭一個(gè)任務(wù)即可.
這些篇幅有限精力優(yōu)先, 我就不做了, 本文如有錯(cuò)誤, 還請(qǐng)指正謝謝.