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

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

由上圖可知CPU和GPU無論哪個阻礙了顯示流程,都會造成掉幀現(xiàn)象。為了給用戶提供更好的體驗,我們需要進行卡頓檢測以及相應的優(yōu)化。
卡頓的檢測
卡頓監(jiān)控的方案一般有兩種
-
FPS監(jiān)控:為了保持流程的UI交互,App的刷新頻率應該保持在60fps左右,其原因是iOS設備默認的刷新頻率是60次/秒,而1次刷新(即VSync信號發(fā)出)的間隔是1000ms/60 = 16.67ms。如果在16.67ms內(nèi)沒有準備好下一幀數(shù)據(jù),就會產(chǎn)生卡頓 -
主線程卡頓監(jiān)控:通過子線程監(jiān)測主線程的RunLoop,判斷兩個狀態(tài)(kCFRunLoopBeforeSources和kCFRunLoopAfterWaiting)之間的耗時是否達到一定閾值
FPS監(jiān)控
- 方案一:參考
YYKit中的YYFPSLabel通過CADisplayLink實現(xiàn)。借助link的時間差,來計算一次刷新所需的時間,然后通過刷新次數(shù) / 時間差得到刷新頻次,并判斷是否符合范圍,通過顯示不同的文字顏色來表示卡頓嚴重程度。
<!-- 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)控
- 方案二:通過
RunLoop來監(jiān)控,因為卡頓的是事務,而事務是交由主線程的RunLoop處理的。
實現(xiàn)原理:檢測主線程每次執(zhí)行消息循環(huán)的時間,當這個時間大于規(guī)定的閾值時,就記為發(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ā)送信號
dispatch_semaphore_t semaphore = monitor->_semaphore;
dispatch_semaphore_signal(semaphore);
}
- (void)registerObserver{
CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
//NSIntegerMax : 優(yōu)先級最小
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES,
NSIntegerMax,
&CallBack,
&context);
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
}
- (void)startMonitor{
// 創(chuàng)建信號
_semaphore = dispatch_semaphore_create(0);
// 在子線程監(jiān)控時長
dispatch_async(dispatch_get_global_queue(0, 0), ^{
while (YES)
{
// 超時時間是 1 秒,沒有等到信號量,st 就不等于 0, RunLoop 所有的任務
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ù)來 避免大規(guī)模打印!
NSLog(@"檢測到超過兩次連續(xù)卡頓");
}
}
self->_timeoutCount = 0;
}
});
}
@end
使用方式:
[[LGBlockMonitor sharedInstance] start];
- 方案三:直接使用三方庫
-
Swift可以使用ANREye,其實現(xiàn)思路是:創(chuàng)建一個子線程通過信號量去ping主線程,因為ping的時候主線程肯定是在kCFRunLoopBeforeSources和kCFRunLoopAfterWaiting之間。每次檢測時設置標記位為YES,然后派發(fā)任務到主線程中將標記位設置為NO。接著子線程沉睡超過閾值時,判斷標志位是否成功設置成NO,如果沒有說明主線程發(fā)生了卡頓。ANREye是使用子線程Ping的方式監(jiān)測卡頓的。 -
OC可以使用 微信matrix、滴滴DoraemonKit
界面優(yōu)化-預排版
開發(fā)圖文混排頁面時,滑動頁面需要不停的計算和渲染,比如計算cell高度。案例代碼如下
// 頁面數(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;
}
其實在網(wǎng)絡請求的時候,我們已經(jīng)拿到了數(shù)據(jù)。有了這些數(shù)據(jù),我們就能知道cell的高度。這個時候可以對頁面進行預排版,而不需要等到tableView渲染的時候才去進行大量計算。我們可以在model中提前計算好cell行高,頁面frame,富文本等等。其主要思想是把耗時的操作放在頁面顯示前處理,這樣頁面滑動的時候就不需要計算很多遍,只是在處理數(shù)據(jù)的時候計算一次,這就是對頁面做了優(yōu)化處理。
// 優(yōu)化后代碼
- (void)loadData{
//外面的異步線程:網(wǎ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行高,頁面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;
//文本信息高度計算
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中把所有頁面相關邏輯提前處理好,轉(zhuǎn)成layoutModel如上面所示。
界面優(yōu)化-預解碼
比如頁面加載一張圖片,其加載流程如下


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

-
image圖片來源于網(wǎng)絡請求didComplete

- 這里拿到的是二進制文件
imageData

- 對二進制文件進行解碼

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


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

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

- 查看
drawRect方法的堆棧信息

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