卡頓的原理
想要進(jìn)行界面優(yōu)化,首先就要了解怎么產(chǎn)生卡頓?
通常來(lái)說(shuō)計(jì)算機(jī)中的顯示過(guò)程是下面這樣的,通過(guò)CPU、GPU、顯示器協(xié)同工作來(lái)將圖片顯示到屏幕上

-
CPU計(jì)算好顯示內(nèi)容,提交至GPU -
GPU經(jīng)過(guò)渲染完成后將渲染的結(jié)果放入FrameBuffer(幀緩存區(qū)) - 隨后視頻控制器會(huì)按照
VSync垂直信號(hào)逐行讀取FrameBuffer的數(shù)據(jù) - 經(jīng)過(guò)可能的
數(shù)模轉(zhuǎn)換傳遞給顯示器進(jìn)行顯示
最開(kāi)始時(shí)FrameBuffer只有一個(gè),這種情況下FrameBuffer的讀取和刷新有很大的效率問(wèn)題,為了解決這個(gè)問(wèn)題,引入了雙緩存區(qū)即雙緩沖機(jī)制。在這種情況下,GPU會(huì)預(yù)先渲染好一幀放入FrameBuffer,讓視頻控制器讀取。當(dāng)下一幀渲染好后,GPU會(huì)直接將視頻控制器的指針指向第二個(gè)FrameBuffer。
雙緩存機(jī)制解決了效率問(wèn)題,但隨之而來(lái)的是新的問(wèn)題。比如當(dāng)前這一幀處理比較慢,GPU會(huì)將視頻控制器的指針指向第二個(gè)FrameBuffer,那么上一幀的圖像處理就會(huì)丟掉即掉幀。現(xiàn)象就是屏幕出現(xiàn)跳屏即卡頓。
屏幕卡頓原因
在VSync信號(hào)到來(lái)后,系統(tǒng)圖形服務(wù)會(huì)通過(guò)CADisplayLink等機(jī)制通知App。App主線程開(kāi)始在CPU中計(jì)算顯示內(nèi)容,隨后CPU 會(huì)將計(jì)算好的內(nèi)容提交到GPU,由GPU進(jìn)行變換、合成、渲染。隨后GPU會(huì)把渲染結(jié)果提交到幀緩沖區(qū),等待下一次VSync信號(hào)到來(lái)時(shí)顯示到屏幕上。由于垂直同步的機(jī)制,如果在一個(gè)VSync時(shí)間內(nèi),CPU 或者 GPU 沒(méi)有完成內(nèi)容提交,則那一幀就會(huì)被丟棄,等待下一次機(jī)會(huì)再顯示,而這時(shí)顯示屏?xí)A糁暗膬?nèi)容不變。
如下圖顯示過(guò)程,第1幀在VSync到來(lái)前,處理完成,正常顯示,第2幀在VSync到來(lái)后,仍在處理中,此時(shí)屏幕不刷新依舊顯示第1幀,此時(shí)就出現(xiàn)了掉幀情況,渲染時(shí)就會(huì)出現(xiàn)明顯的卡頓現(xiàn)象。

由上圖可知CPU和GPU無(wú)論哪個(gè)阻礙了顯示流程,都會(huì)造成掉幀現(xiàn)象。為了給用戶(hù)提供更好的體驗(yàn),我們需要進(jìn)行卡頓檢測(cè)以及相應(yīng)的優(yōu)化。
卡頓的檢測(cè)
卡頓監(jiān)控的方案一般有兩種
-
FPS監(jiān)控:為了保持流程的UI交互,App的刷新頻率應(yīng)該保持在60fps左右,其原因是iOS設(shè)備默認(rèn)的刷新頻率是60次/秒,而1次刷新(即VSync信號(hào)發(fā)出)的間隔是1000ms/60 = 16.67ms。如果在16.67ms內(nèi)沒(méi)有準(zhǔn)備好下一幀數(shù)據(jù),就會(huì)產(chǎn)生卡頓 -
主線程卡頓監(jiān)控:通過(guò)子線程監(jiān)測(cè)主線程的RunLoop,判斷兩個(gè)狀態(tài)(kCFRunLoopBeforeSources和kCFRunLoopAfterWaiting)之間的耗時(shí)是否達(dá)到一定閾值
FPS監(jiān)控
- 方案一:參考
YYKit中的YYFPSLabel通過(guò)CADisplayLink實(shí)現(xiàn)。借助link的時(shí)間差,來(lái)計(jì)算一次刷新所需的時(shí)間,然后通過(guò)刷新次數(shù) / 時(shí)間差得到刷新頻次,并判斷是否符合范圍,通過(guò)顯示不同的文字顏色來(lái)表示卡頓嚴(yán)重程度。
<!-- YYFPSLabel.h -->
#import <UIKit/UIKit.h>
/**
Show Screen FPS...
The maximum fps in OSX/iOS Simulator is 60.00.
The maximum fps on iPhone is 59.97.
The maxmium fps on iPad is 60.0.
*/
@interface YYFPSLabel : UILabel
@end
<!-- YYFPSLabel.m -->
#import "YYFPSLabel.h"
#import "YYKit.h"
#define kSize CGSizeMake(55, 20)
@implementation YYFPSLabel {
CADisplayLink *_link;
NSUInteger _count;
NSTimeInterval _lastTime;
UIFont *_font;
UIFont *_subFont;
NSTimeInterval _llll;
}
- (instancetype)initWithFrame:(CGRect)frame {
if (frame.size.width == 0 && frame.size.height == 0) {
frame.size = kSize;
}
self = [super initWithFrame:frame];
self.layer.cornerRadius = 5;
self.clipsToBounds = YES;
self.textAlignment = NSTextAlignmentCenter;
self.userInteractionEnabled = NO;
self.backgroundColor = [UIColor colorWithWhite:0.000 alpha:0.700];
_font = [UIFont fontWithName:@"Menlo" size:14];
if (_font) {
_subFont = [UIFont fontWithName:@"Menlo" size:4];
} else {
_font = [UIFont fontWithName:@"Courier" size:14];
_subFont = [UIFont fontWithName:@"Courier" size:4];
}
_link = [CADisplayLink displayLinkWithTarget:[YYWeakProxy proxyWithTarget:self] selector:@selector(tick:)];
[_link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
return self;
}
- (void)dealloc {
[_link invalidate];
}
- (CGSize)sizeThatFits:(CGSize)size {
return kSize;
}
// 60 vs 16.67ms
// 1/60 * 1000
- (void)tick:(CADisplayLink *)link {
if (_lastTime == 0) {
_lastTime = link.timestamp;
return;
}
_count++;
NSTimeInterval delta = link.timestamp - _lastTime;
if (delta < 1) return;
_lastTime = link.timestamp;
float fps = _count / delta;
_count = 0;
CGFloat progress = fps / 60.0;
UIColor *color = [UIColor colorWithHue:0.27 * (progress - 0.2) saturation:1 brightness:0.9 alpha:1];
NSMutableAttributedString *text = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"%d FPS",(int)round(fps)]];
[text setColor:color range:NSMakeRange(0, text.length - 3)];
[text setColor:[UIColor whiteColor] range:NSMakeRange(text.length - 3, 3)];
text.font = _font;
[text setFont:_subFont range:NSMakeRange(text.length - 4, 1)];
self.attributedText = text;
}
@end
主線程卡頓監(jiān)控
- 方案二:通過(guò)
RunLoop來(lái)監(jiān)控,因?yàn)榭D的是事務(wù),而事務(wù)是交由主線程的RunLoop處理的。
實(shí)現(xiàn)原理:檢測(cè)主線程每次執(zhí)行消息循環(huán)的時(shí)間,當(dāng)這個(gè)時(shí)間大于規(guī)定的閾值時(shí),就記為發(fā)生了一次卡頓。
<!-- LGBlockMonitor.h -->
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface LGBlockMonitor : NSObject
+ (instancetype)sharedInstance;
- (void)start;
@end
NS_ASSUME_NONNULL_END
<!-- LGBlockMonitor.m -->
#import "LGBlockMonitor.h"
@interface LGBlockMonitor (){
CFRunLoopActivity activity;
}
@property (nonatomic, strong) dispatch_semaphore_t semaphore;
@property (nonatomic, assign) NSUInteger timeoutCount;
@end
@implementation LGBlockMonitor
+ (instancetype)sharedInstance {
static id instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[self alloc] init];
});
return instance;
}
- (void)start{
[self registerObserver];
[self startMonitor];
}
static void CallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
LGBlockMonitor *monitor = (__bridge LGBlockMonitor *)info;
monitor->activity = activity;
// 發(fā)送信號(hào)
dispatch_semaphore_t semaphore = monitor->_semaphore;
dispatch_semaphore_signal(semaphore);
}
- (void)registerObserver{
CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
//NSIntegerMax : 優(yōu)先級(jí)最小
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES,
NSIntegerMax,
&CallBack,
&context);
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
}
- (void)startMonitor{
// 創(chuàng)建信號(hào)
_semaphore = dispatch_semaphore_create(0);
// 在子線程監(jiān)控時(shí)長(zhǎng)
dispatch_async(dispatch_get_global_queue(0, 0), ^{
while (YES)
{
// 超時(shí)時(shí)間是 1 秒,沒(méi)有等到信號(hào)量,st 就不等于 0, RunLoop 所有的任務(wù)
long st = dispatch_semaphore_wait(self->_semaphore, dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC));
if (st != 0)
{
if (self->activity == kCFRunLoopBeforeSources || self->activity == kCFRunLoopAfterWaiting)
{
if (++self->_timeoutCount < 2){
NSLog(@"timeoutCount==%lu",(unsigned long)self->_timeoutCount);
continue;
}
// 一秒左右的衡量尺度 很大可能性連續(xù)來(lái) 避免大規(guī)模打印!
NSLog(@"檢測(cè)到超過(guò)兩次連續(xù)卡頓");
}
}
self->_timeoutCount = 0;
}
});
}
@end
使用方式:
[[LGBlockMonitor sharedInstance] start];
- 方案三:直接使用三方庫(kù)
-
Swift可以使用ANREye,其實(shí)現(xiàn)思路是:創(chuàng)建一個(gè)子線程通過(guò)信號(hào)量去ping主線程,因?yàn)閜ing的時(shí)候主線程肯定是在kCFRunLoopBeforeSources和kCFRunLoopAfterWaiting之間。每次檢測(cè)時(shí)設(shè)置標(biāo)記位為YES,然后派發(fā)任務(wù)到主線程中將標(biāo)記位設(shè)置為NO。接著子線程沉睡超過(guò)閾值時(shí),判斷標(biāo)志位是否成功設(shè)置成NO,如果沒(méi)有說(shuō)明主線程發(fā)生了卡頓。ANREye是使用子線程Ping的方式監(jiān)測(cè)卡頓的。 -
OC可以使用 微信matrix、滴滴DoraemonKit
界面優(yōu)化-預(yù)排版
開(kāi)發(fā)圖文混排頁(yè)面時(shí),滑動(dòng)頁(yè)面需要不停的計(jì)算和渲染,比如計(jì)算cell高度。案例代碼如下
// 頁(yè)面數(shù)據(jù)源
- (void)loadData{
NSString *path = [[NSBundle mainBundle] pathForResource:@"timeLine" ofType:@"json"];
NSData *data = [[NSData alloc] initWithContentsOfFile:path];
NSDictionary *dicJson=[NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:nil];
for (id json in dicJson[@"data"]) {
LGTimeLineModel *timeLineModel = [LGTimeLineModel yy_modelWithJSON:json];
[self.timeLineModels addObject:timeLineModel];
}
[self.timeLineTableView reloadData];
}
#pragma mark -- UITableViewDelegate
// 返回cell高度
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
LGTimeLineModel *timeLineModel = self.timeLineModels[indexPath.row];
timeLineModel.cacheId = indexPath.row + 1;
NSString *stateKey = nil;
if (timeLineModel.isExpand) {
stateKey = @"expanded";
} else {
stateKey = @"unexpanded";
}
LGTimeLineCell *cell = [[LGTimeLineCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:nil];
[cell configureTimeLineCell:timeLineModel];
[cell setNeedsUpdateConstraints];
[cell updateConstraintsIfNeeded];
[cell setNeedsLayout];
[cell layoutIfNeeded];
CGFloat rowHeight = 0;
for (UIView *bottomView in cell.contentView.subviews) {
if (rowHeight < CGRectGetMaxY(bottomView.frame)) {
rowHeight = CGRectGetMaxY(bottomView.frame);
}
}
return rowHeight;
}
其實(shí)在網(wǎng)絡(luò)請(qǐng)求的時(shí)候,我們已經(jīng)拿到了數(shù)據(jù)。有了這些數(shù)據(jù),我們就能知道cell的高度。這個(gè)時(shí)候可以對(duì)頁(yè)面進(jìn)行預(yù)排版,而不需要等到tableView渲染的時(shí)候才去進(jìn)行大量計(jì)算。我們可以在model中提前計(jì)算好cell行高,頁(yè)面frame,富文本等等。其主要思想是把耗時(shí)的操作放在頁(yè)面顯示前處理,這樣頁(yè)面滑動(dòng)的時(shí)候就不需要計(jì)算很多遍,只是在處理數(shù)據(jù)的時(shí)候計(jì)算一次,這就是對(duì)頁(yè)面做了優(yōu)化處理。
// 優(yōu)化后代碼
- (void)loadData{
//外面的異步線程:網(wǎng)絡(luò)請(qǐng)求的線程
dispatch_async(dispatch_get_global_queue(0, 0), ^{
//加載`JSON 文件`
NSString *path = [[NSBundle mainBundle] pathForResource:@"timeLine" ofType:@"json"];
NSData *data = [[NSData alloc] initWithContentsOfFile:path];
NSDictionary *dicJson=[NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:nil];
for (id json in dicJson[@"data"]) {
LGTimeLineModel *timeLineModel = [LGTimeLineModel yy_modelWithJSON:json];
[self.timeLineModels addObject:timeLineModel];
}
for (LGTimeLineModel *timeLineModel in self.timeLineModels) {
LGTimeLineCellLayout *cellLayout = [[LGTimeLineCellLayout alloc] initWithModel:timeLineModel];
[self.layouts addObject:cellLayout];
}
dispatch_async(dispatch_get_main_queue(), ^{
[self.timeLineTableView reloadData];
});
});
}
#pragma mark -- UITableViewDelegate
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return self.layouts[indexPath.row].height;
}
<!-- LGTimeLineCellLayout.m文件 -->
// 把cell行高,頁(yè)面frame,富文本等等提前處理好
- (instancetype)initWithModel:(LGTimeLineModel *)timeLineModel{
if (!timeLineModel) return nil;
self = [super init];
if (self) {
_timeLineModel = timeLineModel;
[self layout];
}
return self;
}
- (void)setTimeLineModel:(LGTimeLineModel *)timeLineModel{
_timeLineModel = timeLineModel;
[self layout];
}
- (void)layout{
CGFloat sWidth = [UIScreen mainScreen].bounds.size.width;
self.iconRect = CGRectMake(10, 10, 45, 45);
CGFloat nameWidth = [self calcWidthWithTitle:_timeLineModel.name font:titleFont];
CGFloat nameHeight = [self calcLabelHeight:_timeLineModel.name fontSize:titleFont width:nameWidth];
self.nameRect = CGRectMake(CGRectGetMaxX(self.iconRect) + nameLeftSpaceToHeadIcon, 17, nameWidth, nameHeight);
CGFloat msgWidth = sWidth - 10 - 16;
CGFloat msgHeight = 0;
//文本信息高度計(jì)算
NSMutableParagraphStyle * paragraphStyle = [[NSMutableParagraphStyle alloc] init];
[paragraphStyle setLineSpacing:5];
NSDictionary *attributes = @{NSFontAttributeName: [UIFont systemFontOfSize:msgFont],
NSForegroundColorAttributeName: [UIColor colorWithRed:26/255.0 green:26/255.0 blue:26/255.0 alpha:1]
,NSParagraphStyleAttributeName: paragraphStyle
,NSKernAttributeName:@0
};
NSMutableAttributedString *attrStr = [[NSMutableAttributedString alloc] initWithString:_timeLineModel.msgContent attributes:attributes];
msgHeight = [self caculateAttributeLabelHeightWithString:attrStr width:msgWidth];
if (attrStr.length > msgExpandLimitHeight) {
if (_timeLineModel.isExpand) {
self.contentRect = CGRectMake(10, CGRectGetMaxY(self.iconRect) + 10, msgWidth, msgHeight);
} else {
attrStr = [[NSMutableAttributedString alloc] initWithString:[_timeLineModel.msgContent substringToIndex:msgExpandLimitHeight] attributes:attributes];
msgHeight = [self caculateAttributeLabelHeightWithString:attrStr width:msgWidth];
self.contentRect = CGRectMake(10, CGRectGetMaxY(self.iconRect) + 10, msgWidth, msgHeight);
}
} else {
self.contentRect = CGRectMake(10, CGRectGetMaxY(self.iconRect) + 10, msgWidth, msgHeight);
}
if (attrStr.length < msgExpandLimitHeight) {
self.expandHidden = YES;
self.expandRect = CGRectMake(10, CGRectGetMaxY(self.contentRect) - 20, 30, 20);
} else {
self.expandHidden = NO;
self.expandRect = CGRectMake(10, CGRectGetMaxY(self.contentRect) + 10, 30, 20);
}
CGFloat timeWidth = [self calcWidthWithTitle:_timeLineModel.time font:timeAndLocationFont];
CGFloat timeHeight = [self calcLabelHeight:_timeLineModel.time fontSize:timeAndLocationFont width:timeWidth];
self.imageRects = [NSMutableArray array];
if (_timeLineModel.contentImages.count == 0) {
// self.timeRect = CGRectMake(10, CGRectGetMaxY(self.expandRect) + 10, timeWidth, timeHeight);
} else {
if (_timeLineModel.contentImages.count == 1) {
CGRect imageRect = CGRectMake(11, CGRectGetMaxY(self.expandRect) + 10, 250, 150);
[self.imageRects addObject:@(imageRect)];
} else if (_timeLineModel.contentImages.count == 2 || _timeLineModel.contentImages.count == 3) {
for (int i = 0; i < _timeLineModel.contentImages.count; i++) {
CGRect imageRect = CGRectMake(11 + i * (10 + 90), CGRectGetMaxY(self.expandRect) + 10, 90, 90);
[self.imageRects addObject:@(imageRect)];
}
} else if (_timeLineModel.contentImages.count == 4) {
for (int i = 0; i < 2; i++) {
for (int j = 0; j < 2; j++) {
CGRect imageRect = CGRectMake(11 + j * (10 + 90), CGRectGetMaxY(self.expandRect) + 10 + i * (10 + 90), 90, 90);
[self.imageRects addObject:@(imageRect)];
}
}
} else if (_timeLineModel.contentImages.count == 5 || _timeLineModel.contentImages.count == 6 || _timeLineModel.contentImages.count == 7 || _timeLineModel.contentImages.count == 8 || _timeLineModel.contentImages.count == 9) {
for (int i = 0; i < _timeLineModel.contentImages.count; i++) {
CGRect imageRect = CGRectMake(11 + (i % 3) * (10 + 90), CGRectGetMaxY(self.expandRect) + 10 + (i / 3) * (10 + 90), 90, 90);
[self.imageRects addObject:@(imageRect)];
}
}
}
if (self.imageRects.count > 0) {
CGRect lastRect = [self.imageRects[self.imageRects.count - 1] CGRectValue];
self.seperatorViewRect = CGRectMake(0, CGRectGetMaxY(lastRect) + 10, sWidth, 15);
}
self.height = CGRectGetMaxY(self.seperatorViewRect);
}
本質(zhì)是在model中把所有頁(yè)面相關(guān)邏輯提前處理好,轉(zhuǎn)成layoutModel如上面所示。
界面優(yōu)化-預(yù)解碼
比如頁(yè)面加載一張圖片,其加載流程如下


-
UIImageView的本質(zhì)是一個(gè)模型,里面包含了UIImage -
UIImage中包含了Data Buffer,圖片是通過(guò)Data Buffer二進(jìn)制流轉(zhuǎn)換過(guò)來(lái)的。 - 再通過(guò)
image Buffer緩存區(qū)進(jìn)行儲(chǔ)存。 - 最后通過(guò)
ViewController顯示到UIImageView上。
UIImageView的model屬性依賴(lài)于Data Buffer的解碼過(guò)程,解碼之后Image Buffer才能夠進(jìn)行緩存,緩存之后才能在幀緩存區(qū)Frame Buffer中進(jìn)行渲染。
我們?cè)诩虞d圖片的時(shí)候,一般使用SDWebImage,下面探索其原理......
- 查看
sd_setImageWithURL方法

-
image圖片來(lái)源于網(wǎng)絡(luò)請(qǐng)求didComplete

- 這里拿到的是二進(jìn)制文件
imageData

- 對(duì)二進(jìn)制文件進(jìn)行解碼

把圖片的所有二進(jìn)制流進(jìn)行解碼,比如對(duì)圖片的寬高、imageRef、大小、縮放因子maxPixelSize進(jìn)行解碼,最終形成了UIImage,最終就是顯示。
圖片為什么需要預(yù)解碼
SDWebImage在子線程對(duì)圖片的二進(jìn)制文件imageData做了解碼操作,那么圖片的展示為什么需要進(jìn)行上面的解碼呢?SDWebImage的解碼操作又放在了哪里?通過(guò)添加符號(hào)斷點(diǎn)、打印堆棧信息進(jìn)行調(diào)試查看......


最終發(fā)現(xiàn)SDWebImage在這一層面先做了預(yù)解碼操作,原因是頁(yè)面的卡頓大都是來(lái)自于圖片展示。
圖片加載流程
- 網(wǎng)絡(luò)請(qǐng)求中獲取到了
Data Buffer即ImageData -
ImageData交給子線程進(jìn)行解碼,解碼完成之后進(jìn)行回調(diào),回調(diào)回來(lái)的就是Image Buffer即像素緩存區(qū) - 最后交給
Frame Buffer去顯示。
最終優(yōu)化的就是Data Buffer解碼成Image Buffer的過(guò)程,所以大部分的三方框架都是在這一過(guò)程做了大量處理。
蘋(píng)果在底層提供了一圖形編解碼插件,比如原生音視頻框架AVFoundation、FFmpeg。其中FFmpeg中最好的點(diǎn)就是對(duì)視頻的編解碼過(guò)程。
異步渲染
按需加載
只有需要了才去加載。例如TableView滑動(dòng)時(shí),滑動(dòng)的越快也就意味著計(jì)算、渲染的頻率越高。這樣就有可能導(dǎo)致頁(yè)面卡頓......
- 優(yōu)化思路一:比如滑動(dòng)時(shí)使用
默認(rèn)占位圖,當(dāng)滑動(dòng)了10條cell,我們只處理可視范圍內(nèi)的3條cell - 優(yōu)化思路二:滑動(dòng)時(shí)使用
默認(rèn)占位圖,而是在滑動(dòng)停止時(shí)處理加載圖片的數(shù)據(jù)
異步渲染
關(guān)于UIView和Layer之間的關(guān)系?
- UIView主要是用于
頁(yè)面交互,比如頁(yè)面點(diǎn)擊等等 - Layer主要用于
頁(yè)面的渲染
真正的頁(yè)面展示并不是UIView去做,而是Layer層做的。

渲染的過(guò)程是非常耗時(shí)的,這個(gè)過(guò)程稱(chēng)之為事物。事務(wù)里面有如下環(huán)節(jié)
layout構(gòu)建視圖displayer繪制prepare關(guān)于coreAnimation動(dòng)畫(huà)的操作commit提交事務(wù) reader server去做事務(wù)相關(guān)的處理
drawRect的流程
-
drawRect是依賴(lài)于當(dāng)前UIView提供的一個(gè)UIViewRendering的功能

- 查看
drawRect方法的堆棧信息

- 將
繪制圖層的耗時(shí)操作放在子線程進(jìn)行,最后渲染的步驟放在主線程
<!-- 下面繪制的耗時(shí)操作放在子線程處理 -->
//繪制流程的發(fā)起函數(shù)
- (void)display{
// Graver 實(shí)現(xiàn)思路
CGContextRef context = (__bridge CGContextRef)([self.delegate performSelector:@selector(createContext)]);
// 渲染整個(gè)圖層
[self.delegate layerWillDraw:self];
[self drawInContext:context];
[self.delegate displayLayer:self];
[self.delegate performSelector:@selector(closeContext)];
}
<!-- 渲染的步驟放在主線程 -->
//layer.contents = (位圖)
- (void)displayLayer:(CALayer *)layer{
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
dispatch_async(dispatch_get_main_queue(), ^{
layer.contents = (__bridge id)(image.CGImage);
});
}
異步渲染框架Graver渲染流程

界面優(yōu)化總結(jié)
CPU層面的優(yōu)化
- 盡量
用輕量級(jí)的對(duì)象代替重量級(jí)的對(duì)象,可以對(duì)性能有所優(yōu)化。例如不需要相應(yīng)觸摸事件的控件,用CALayer代替UIView - 盡量減少對(duì)
UIView和CALayer的屬性修改
- CALayer內(nèi)部并沒(méi)有屬性,當(dāng)調(diào)用屬性方法時(shí),其內(nèi)部是通過(guò)運(yùn)行時(shí)
resolveInstanceMethod為對(duì)象臨時(shí)添加一個(gè)方法,并將對(duì)應(yīng)屬性值保存在內(nèi)部的Dictionary中,同時(shí)還會(huì)通知delegate、創(chuàng)建動(dòng)畫(huà)等,非常耗時(shí) -
UIView相關(guān)的顯示屬性,例如frame、bounds、transform等,實(shí)際上都是從CALayer映射來(lái)的,對(duì)其進(jìn)行調(diào)整時(shí),消耗的資源比一般屬性要大
- 當(dāng)有大量對(duì)象釋放時(shí),也是非常耗時(shí)的,盡量挪到
后臺(tái)線程去釋放 - 盡量
提前計(jì)算視圖布局即預(yù)排版,例如計(jì)算cell的行高 -
Autolayout在簡(jiǎn)單頁(yè)面情況下們可以很好的提升開(kāi)發(fā)效率,但是對(duì)于復(fù)雜視圖而言,會(huì)產(chǎn)生嚴(yán)重的性能問(wèn)題,隨著視圖數(shù)量的增長(zhǎng),Autolayout帶來(lái)的CPU消耗是呈指數(shù)上升的,所以盡量使用代碼布局。如果不想手動(dòng)調(diào)整frame等,也可以借助三方庫(kù),例如Masonry(OC)、SnapKit(Swift)、ComponentKit、AsyncDisplayKit等 - 文本處理的優(yōu)化:當(dāng)一個(gè)界面有大量文本時(shí),其行高的計(jì)算、繪制也是非常耗時(shí)的
- 如果對(duì)文本沒(méi)有特殊要求,可以使用
UILabel內(nèi)部的實(shí)現(xiàn)方式,且需要放到子線程中進(jìn)行,避免阻塞主線程
計(jì)算文本寬高:[NSAttributedString boundingRectWithSize:options:context:]
文本繪制:[NSAttributedString drawWithRect:options:context:] - 自定義文本控件,利用
TextKit或最底層的CoreText對(duì)文本異步繪制。并且CoreText對(duì)象創(chuàng)建好后,能直接獲取文本的寬高等信息,避免了多次計(jì)算(調(diào)整和繪制都需要計(jì)算一次)。CoreText直接使用了CoreGraphics占用內(nèi)存小,效率高
- 圖片處理(解碼 + 繪制)
- 當(dāng)使用
UIImage或CGImageSource的方法創(chuàng)建圖片時(shí),圖片的數(shù)據(jù)不會(huì)立即解碼,而是在設(shè)置時(shí)解碼(即圖片設(shè)置到UIImageView/CALayer.contents中,然后在CALayer提交至GPU渲染前,CGImage中的數(shù)據(jù)才進(jìn)行解碼)。這一步是無(wú)可避免的,且是發(fā)生在主線程中的。想要繞開(kāi)這個(gè)機(jī)制,常見(jiàn)的做法是在子線程中先將圖片繪制到CGBitmapContext,然后從Bitmap直接創(chuàng)建圖片,例如SDWebImage三方框架中對(duì)圖片編解碼的處理。這就是Image的預(yù)解碼 - 當(dāng)使用CG開(kāi)頭的方法繪制圖像到畫(huà)布中,然后從畫(huà)布中創(chuàng)建圖片時(shí),可以將圖像的
繪制在子線程中進(jìn)行
- 圖片優(yōu)化
- 盡量使用
PNG圖片,不使用JPGE圖片 - 通過(guò)
子線程預(yù)解碼,主線程渲染,即通過(guò)Bitmap創(chuàng)建圖片,在子線程賦值image - 優(yōu)化圖片大小,盡量避免動(dòng)態(tài)縮放
- 盡量將多張圖合為一張進(jìn)行顯示
- 盡量
避免使用透明view,因?yàn)槭褂猛该鱲iew,會(huì)導(dǎo)致在GPU計(jì)算像素時(shí),會(huì)將透明view下層圖層的像素也計(jì)算進(jìn)來(lái)即顏色混合處理。 -
按需加載,例如在TableView中滑動(dòng)時(shí)不加載圖片,使用默認(rèn)占位圖,而是在滑動(dòng)停止時(shí)加載 - 少使用
addView給cell動(dòng)態(tài)添加view
GPU層面優(yōu)化
相對(duì)于CPU而言,GPU主要是接收CPU提交的紋理+頂點(diǎn),經(jīng)過(guò)一系列transform,最終混合并渲染輸出到屏幕上。
- 盡量
減少在短時(shí)間內(nèi)大量圖片的顯示,盡可能將多張圖片合為一張顯示,主要是因?yàn)楫?dāng)有大量圖片進(jìn)行顯示時(shí),無(wú)論是CPU的計(jì)算還是GPU的渲染,都是非常耗時(shí)的,很可能出現(xiàn)掉幀的情況 - 盡量避免圖片的尺寸超過(guò)
4096×4096,因?yàn)楫?dāng)圖片超過(guò)這個(gè)尺寸時(shí),會(huì)先由CPU進(jìn)行預(yù)處理,然后再提交給GPU處理,導(dǎo)致額外CPU資源消耗 -
盡量減少視圖數(shù)量和層次,主要是因?yàn)橐晥D過(guò)多且重疊時(shí),GPU會(huì)將其混合,混合的過(guò)程也是非常耗時(shí)的 - 盡量避免
離屏渲染 -
異步渲染,例如可以將cell中的所有控件、視圖合成一張圖片進(jìn)行顯示。參考Graver異步渲染框架