前言
造成iOS卡頓有很多因素轰异,而造成這個問題大多是阻塞主線程導(dǎo)致用戶的交互反饋出現(xiàn)可以感知的延遲铝穷。原因主要有一下三種情形:
1.UI 渲染需要時間較長皂甘,無法按時提交結(jié)果斧散;
2.一些需要密集計算的處理放在了主線程中執(zhí)行,導(dǎo)致主線程被阻塞摊聋,無法渲染 UI 界面鸡捐;
3.網(wǎng)絡(luò)請求由于網(wǎng)絡(luò)狀態(tài)的問題響應(yīng)較慢,UI 層由于沒有模型返回?zé)o法渲染麻裁。
上面的這些問題都會影響應(yīng)用的性能箍镜,最常見的表現(xiàn)就是 UITableView 在滑動時沒有達(dá)到 60 FPS,用戶能感受到明顯的卡頓煎源。
什么是FPS
FPS是圖像領(lǐng)域中的定義色迂,是指畫面每秒傳輸幀數(shù),通俗來講就是指動畫或視頻的畫面數(shù)手销。FPS是測量用于保存歇僧、顯示動態(tài)視頻的信息數(shù)量。每秒鐘幀數(shù)愈多,所顯示的動作就會愈流暢诈悍。通常祸轮,要避免動作不流暢的最低是30。來自百度文庫
屏幕是如何渲染的侥钳?
iPhone圖像顯示工作原理
iOS 的顯示系統(tǒng)是由 VSync脈沖信號驅(qū)動的适袜,VSync脈沖信號由硬件時鐘生成,大約每秒鐘發(fā)出 60 次(這個值取決設(shè)備硬件舷夺,比如 iPhone 真機(jī)上通常是 59.97)
iPhone采用的雙緩沖苦酱,安卓是三緩沖,各有優(yōu)缺點(diǎn)给猾,但都是各自的最優(yōu)解決方案
通常來說疫萤,計算機(jī)系統(tǒng)中 CPU、GPU耙册、顯示器是以上面這種方式協(xié)同工作的给僵。
1.CPU 計算好顯示內(nèi)容提交到 GPU,
2.GPU 渲染完成后將渲染結(jié)果放入第一緩沖區(qū)详拙,
3.然后硬件 Vsync到來帝际,第二緩沖會copy第一緩沖待顯示內(nèi)容,因?yàn)榻粨Q內(nèi)存地址饶辙,可認(rèn)為是瞬間完成
4.屏幕硬件經(jīng)過數(shù)模轉(zhuǎn)換傳遞而顯示出來
卡頓為什么會產(chǎn)生呢蹲诀?
所以我們做性能優(yōu)化,要從減輕cpu和gpu負(fù)擔(dān)的角度去思考
性能優(yōu)化之AutoLayout VS Frame(減輕CPU負(fù)擔(dān))
Autolayout 是自iOS 6之后 蘋果引入一種“自動布局”技術(shù)弃揽,
蘋果官方也大力推薦開發(fā)者使用此來進(jìn)行UI布局
AutoLayout其實(shí)最終會轉(zhuǎn)化為對 UIView.frame/bounds/center 等屬性的調(diào)整脯爪。
隨著視圖數(shù)量的增加, Autolayout 帶來的 CPU 消耗會呈指數(shù)級上升
所以建議大家矿微,在需要優(yōu)先考慮性能的界面痕慢,使用frame。
優(yōu)化性能之GUP
大多數(shù)的 CALayer 的屬性都是由 GPU 來繪制的涌矢,比如圖片的圓角掖举、變換、應(yīng)用紋理娜庇;但是過多的幾何結(jié)構(gòu)塔次、重繪、離屏繪制(Offscrren)以及過大的圖片都會導(dǎo)致 GPU 的性能明顯降低名秀。
Texture
Texture
字面上是紋理的意思励负,以前叫做AsyncDisplayKit
,是由 Facebook 開源的一個 iOS 框架匕得,能夠幫助最復(fù)雜的 UI 界面保持流暢和快速響應(yīng)继榆。下文簡稱為ASDK。
ASDK的作者是Scott Goodson,他本來是在蘋果工作裕照,開發(fā)了很多蘋果內(nèi)置的應(yīng)用攒发。后來到了Facebook,開發(fā)了ASDK這款產(chǎn)品〗希現(xiàn)在他就職于youtube惠猿。
ASDK 從開發(fā)到開源大約經(jīng)歷了一年多的時間,它其實(shí)并不是一個簡單的框架负间,更像是對 UIKit 的重新實(shí)現(xiàn)偶妖,把整個 UIKit
以及 CALayer
層封裝成一個一個 Node
,將昂貴的渲染政溃、圖片解碼趾访、布局以及其它 UI 操作移出主線程,這樣主線程就可以對用戶的操作及時做出反應(yīng)董虱。
在 ASDK 中最基本的單位就是 ASDisplayNode
扼鞋,每一個 node 都是對UIView
以及CALayer
的抽象。但是與UIView
不同的是愤诱,ASDisplayNode
是線程安全的云头,它可以在后臺線程中完成初始化以及配置工作。
如果按照 60 FPS 的刷新頻率來計算淫半,每一幀的渲染時間只有 16ms溃槐,在 16ms 的時間內(nèi)要完成對 UIView 的創(chuàng)建、布局科吭、繪制以及渲染昏滴,CPU 和 GPU 面臨著巨大的壓力。
從 A5 處理器之后对人,多核的設(shè)備成為了主流谣殊,原有的將所有操作放入主線程的實(shí)踐已經(jīng)不能適應(yīng)復(fù)雜的 UI 界面,所以 ASDK 將耗時的 CPU 操作以及 GPU 渲染紋理(Texture)的過程全部放入后臺進(jìn)程牺弄,使主線程能夠快速響應(yīng)用戶操作姻几。
ASTableNode
ASTableNode
類似于 UIKit
中的 UITableView
,但是又有著很大不同猖闪。ASTableNode
的cell使用ASCellNode
,ASCellNode
通過FlexBox進(jìn)行布局肌厨,性能基本和Frame相持平培慌。
ASTableNode
并沒有像 UITableView 一樣提供一個-tableView:heightForRowAtIndexPath:協(xié)議方法來決定每個 Cell 的高度,而是由 ASCellNode 本身決定柑爸。這樣帶來的另外一個好處是吵护,動態(tài)高度的實(shí)現(xiàn)可謂是易如反掌。
實(shí)現(xiàn)代碼:
ASTableNode
初始化和 UITableView
是一樣的:
ASImageNode *imageNode = [[ASImageNode alloc] init];
imageNode.image = [UIImage imageNamed:@"cat.jpg"];
imageNode.frame = CGRectMake(0, 100, 100, 100);
[imageNode addTarget:self action:@selector(imageAction) forControlEvents:ASControlNodeEventTouchUpInside];
[self.view addSubnode:imageNode];
ASNetworkImageNode *node = [[ASNetworkImageNode alloc] init];
node.defaultImage = [UIImage imageNamed:@"cat.jpg"];
node.URL = [NSURL URLWithString:@"https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1506530505693&di=68df3b2b8d7dad413af8c92a065712dc&imgtype=0&src=http%3A%2F%2Fpic.k73.com%2Fup%2Fsoft%2F2017%2F0907%2F153257_88565218.png"];
node.frame = CGRectMake(0, 300, 100, 100);
[node addTarget:self action:@selector(imageAction2) forControlEvents:ASControlNodeEventTouchUpInside];
[self.view addSubnode:node];
ASTableNode
的dataSource協(xié)議方法和UITableView
一一對應(yīng):
/**
返回rows數(shù)量
*/
- (NSInteger)tableNode:(ASTableNode *)tableNode numberOfRowsInSection:(NSInteger)section {
// 1
return 500;
}
/**
相當(dāng)于UITableView的cellForIndexPath
ASCellNode 是 UITableViewCell 的對應(yīng)封裝。本來我們可以直接返回 ASCellNode(ASDK 有一個返回這個類型的數(shù)據(jù)源方法)馅而,但是官方推薦我們使用返回塊的版本祥诽。因此這里定義了一個 ASCellNodeBlock 對象,這是一個特殊的塊瓮恭,會返回一個 ASCellNode雄坪,不需要提供任何參數(shù)。在這個塊中屯蹦,我們使用了一個自定義的 ASCellNode维哈,名為 StatusNode(后面我們會實(shí)現(xiàn)它),并調(diào)用它的初始化方法把模型傳遞進(jìn)去登澜,然后在塊的最后返回這個 ASCellNode阔挠。
*/
- (ASCellNodeBlock)tableNode:(ASTableNode *)tableNode nodeBlockForRowAtIndexPath:(NSIndexPath *)indexPath
{
ASCellNode *(^ASCellNodeBlock)() = ^ASCellNode *() {
StatusNode *cellNode = [[StatusNode alloc] init];
[cellNode setData];
return cellNode;
};
return ASCellNodeBlock;
}
- (NSInteger)numberOfSectionsInTableNode:(ASTableNode *)tableNode{
// 4
return 1;
}
ASTableNode
的delegate協(xié)議方法和UITableView
有著很大的不同:
/**
這個方法用于告訴 ASTableNode,用戶的一次下拉動作是否需要觸發(fā)異步抓取脑蠕,這里我們返回了 YES购撼,也就是不管什么情況都進(jìn)行異步抓取。我們這樣做的原因谴仙,是現(xiàn)在的后臺服務(wù)從來不告訴前端什么時候數(shù)據(jù)才會”完”,反正有數(shù)據(jù)的話服務(wù)器會返回數(shù)據(jù)迂求,沒數(shù)據(jù)的話則返回錯誤(比如“ 404 沒有數(shù)據(jù)” 之類)或者返回空結(jié)果集。所以我們根本無法事先知道數(shù)據(jù)什么時候數(shù)據(jù)已經(jīng)加載完狞甚。所以不管數(shù)據(jù)有沒有完锁摔,我們都當(dāng)做沒有完來進(jìn)行抓取,并通過服務(wù)器返回的結(jié)果來判斷哼审。這樣這個方法就沒有必要進(jìn)行任何計算了谐腰,直接返回 YES。
*/
- (BOOL)shouldBatchFetchForTableNode:(ASTableNode *)tableNode {
return YES;
}
/**
這個方法用于進(jìn)行一次抓取涩盾。loadPageWithContext: 方法是我們自定義的十气,它會加載一頁數(shù)據(jù),同時頁數(shù)會累加春霍,這樣每次都會加載“下一頁”砸西,除非服務(wù)器沒有數(shù)據(jù)返回。context 參數(shù)是必須的址儒,用于抓取完后通知 ASTableNode 抓取完成芹枷。
*/
- (void)tableNode:(ASTableNode *)tableNode willBeginBatchFetchWithContext:(ASBatchContext *)context
{
[context beginBatchFetching];
// 進(jìn)行數(shù)據(jù)拉取
// [self loadPageWithContext:context];
}
- (void)tableNode:(ASTableNode *)tableNode didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
[tableNode deselectRowAtIndexPath:indexPath animated:YES];
// 你自己的代碼
// ......
}
ASCellNode
ASCellNode
類似于 UIKit
中的 UITableViewCell
,使用方式和UITableViewCell
類似莲趣,這里主要講ASCellNode
對控件進(jìn)行FlexBox的布局方式鸳慈。
/**
布局的時候需要對下面四種方法實(shí)現(xiàn)其一:
*提供 layoutSpecBlock
*覆寫 - layoutSpecThatFits: 方法
*覆寫 - calculateSizeThatFits: 方法
*覆寫 - calculateLayoutThatFits: 方法
layoutSpecThatFits類似于layoutSpecBlock,其實(shí)和layoutSpecBlock沒什么不同
*/
- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize
{
/**
ASStackLayoutDirectionHorizontal為水平方式
justify-content屬性定義了項(xiàng)目在主軸上的對齊方式喧伞。
align-items屬性定義項(xiàng)目在交叉軸上如何對齊走芋。
*/
ASStackLayoutSpec *avatarStack = [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal
spacing:5
justifyContent:ASStackLayoutJustifyContentStart
alignItems:ASStackLayoutAlignItemsCenter
children:@[_avatarNode,_nameNode,_timeNode]];
ASStackLayoutSpec *likeStack = [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal
spacing:5
justifyContent:ASStackLayoutJustifyContentStart
alignItems:ASStackLayoutAlignItemsCenter
children:@[_likeNode,_viewsNode]];
/**
這里是垂直約束
將avatarStack和likeStack以及標(biāo)題看做一個整體
*/
ASStackLayoutSpec *contentStack = [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionVertical
spacing:5
justifyContent:ASStackLayoutJustifyContentSpaceBetween
alignItems:ASStackLayoutAlignItemsStretch
children:@[avatarStack,_titleNode,likeStack]];
/**
外層加上整體邊框
*/
return [ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsMake(15, 15, 15, 15)
child:contentStack];
}
我們分別通過使用ASDK和Masonry對同樣的界面進(jìn)行性能對比绩郎,Demo 地址。
通過ASDK
可以看到還是比較流暢的翁逞,及時暴力滑動肋杖,也可以達(dá)到60FPS或者接近60FPS。
下面是通過使用
Masonry
做的測試:
可以發(fā)現(xiàn)在快速滾動的時候有明顯卡頓挖函,F(xiàn)PS低至40以下状植,存在掉幀現(xiàn)象。