界面優化無非就是解決卡頓問,優化界面流暢度,如下就經過先分析卡頓的緣由,而後再介紹具體的優化方案,來分析如何作界面優化微信
CPU
處理階段 GPU
處理階段和視頻控制器顯示階段。
CPU
主要是計算出須要渲染的模型數據GPU
主要是根據 CPU
提供的渲染模型數據渲染圖片而後存到幀緩衝區VSync
+ 雙緩衝區的形式,就是顯示器顯示完成一幀的渲染的時候會向 發送一個垂直信號 VSync
,收到這個這個垂直信號以後顯示器開始讀取另一個幀緩衝區中的數據而 App
接到垂直信號以後開始新一幀的渲染。CPU
和 GPU
的工做尚未完成,也就是另一個幀緩衝區仍是加鎖狀態沒有數據的時候,此時顯示器顯示的仍是上一幀的圖像那麼這種狀況就會一直等待下一幀繪製完成而後視頻控制器再讀取另一個幀緩衝區中的數據而後成像,中間這個等待的過程就形成了掉幀,也就是會卡頓。60Hz
,也就是每秒中刷新屏幕60次,也就是每秒中有60幀渲染完成,差很少每幀渲染的時間是1000/60 = 16.67
毫秒整個界面會比較流暢,通常刷新率低於45Hz
的就會出現明顯的卡頓現象。這裏能夠經過YYFPSLabel
來實現FPS
的監控,該原理主要是依靠 CADisplayLink
來實現的,經過CADisplayLink
來監聽每次屏幕刷新並獲取屏幕刷新的時間,而後使用次數(也就是1)除以每次刷新的時間間隔獲得FPS
,具體源碼以下: #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];
}
//YYWeakProxy 這裏使用了虛擬類來解決強引用問題
_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;
}
- (void)tick:(CADisplayLink *)link {
if (_lastTime == 0) {
_lastTime = link.timestamp;
NSLog(@"sdf");
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
複製代碼
FPS
只用在開發階段的輔助性的數值,由於他會頻繁喚醒 runloop
若是 runloop
在閒置的狀態被 CADisplayLink
喚醒則會消耗性能。Runloop
一次循環的時間來判斷是否卡頓,這裏須要配合使用 GCD
的信號量來實現,設置初始化信號量爲0,而後開一個子線程等待信號量的觸發,也是就是在子線程的方法裏面調用 dispatch_semaphore_wait
方法設置等待時間是1秒,而後主線程的 Runloop
的 Observer
回調方法中發送信號也就是調用 dispatch_semaphore_signal
方法,此時時間能夠置爲0了,若是是等待時間超時則看此時的 Runloop
的狀態是不是 kCFRunLoopBeforeSources
或者是 kCFRunLoopAfterWaiting
,若是在這兩個狀態下兩秒則說明有卡頓,詳細代碼以下:(代碼中也有相關的註釋) #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;
// 發送信號
dispatch_semaphore_t semaphore = monitor->_semaphore;
dispatch_semaphore_signal(semaphore);
}
- (void)registerObserver{
CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
//NSIntegerMax : 優先級最小
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES,
NSIntegerMax,
&CallBack,
&context);
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
}
- (void)startMonitor{
// 建立信號c
_semaphore = dispatch_semaphore_create(0);
// 在子線程監控時長
dispatch_async(dispatch_get_global_queue(0, 0), ^{
while (YES)
{
// 超時時間是 1 秒,沒有等到信號量,st 就不等於 0, RunLoop 全部的任務
// 沒有接收到信號底層會先對信號量進行減減操做,此時信號量就變成負數
// 因此開始進入等到,等達到了等待時間尚未收到信號則進行加加操做復原信號量
// 執行進入等待的方法dispatch_semaphore_wait會返回非0的數
// 收到信號的時候此時信號量是1 底層是減減操做,此時恰好等於0 因此直接返回0
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)
{
//若是一直處於處理source0或者接受mach_port的狀態則說明runloop的此次循環尚未完成
if (++self->_timeoutCount < 2){
NSLog(@"timeoutCount==%lu",(unsigned long)self->_timeoutCount);
continue;
}
// 若是超過兩秒則說明卡頓了
// 一秒左右的衡量尺度 很大可能性連續來 避免大規模打印!
NSLog(@"檢測到超過兩次連續卡頓");
}
}
self->_timeoutCount = 0;
}
});
}
@end
複製代碼
runloop
實現的大致流程和方案三相同,不過微信加入了堆棧分析,可以定位到耗時的方法調用堆棧,因此須要準確的分析卡頓緣由能夠藉助微信matrix來分析卡頓。固然也能夠在方案2中使用 PLCrashReporter
這個開源的第三方庫來獲取堆棧信息ping
主線程,在主線程卡頓的狀況下,會出現斷在的無響應的表現,進而檢測卡頓CPU
和 GPU
階段佔用時間太長致使了掉幀卡頓,因此界面優化主要工做就是給 CPU
和 GPU
減負
CPU
進行減負。TableView
其中須要根據每一個 cell
的內容來定 cell
的高度。咱們知道 TableView
有重用機制,若是複用池中有數據,即將滑入屏內的 cell
就會使用複用池內的 cell
,作到節省資源,可是仍是要根據新數據的內容來計算 cell
的高度,從新佈局新 cell
中內容的佈局 ,這樣反覆滑動 TableView
相同的 cell
就會反覆計算其 frame
,這樣也給 CPU
帶來了負擔。若是在獲得數據建立模型的時候就把 cell
frame
算出,TableView
返回模型中的 frame
這樣的話一樣的一條 cell
就算來回反覆滑動 TableView
,計算 frame
這個操做也就僅僅只會執行一次,因此也就作到了減負的功能,以下圖:一個 cell
的組成須要 modal
找到數據,也須要 layout
找到這個 cell
如何佈局: CPU
階段拿到圖片的頂點數據和紋理以後會進行解碼生產位圖,而後傳遞到 GPU
進行渲染主要流程圖以下 若是圖片不少很大的狀況下解碼工做就會佔用主線程 RunLoop
致使其餘工做沒法執行好比滑動,這樣就會形成卡頓現象,因此這裏就能夠將解碼的工做放到異步線程中不佔用主線程,可能有人會想只要將圖片加載放到異步線程中在異步線程中生成一個 UIImage
或者是 CGImage
而後再主線程中設置給 UIImageView
,此時能夠寫段代碼使用 instruments
的 Time Profiler
查看一下堆棧信息 發現圖片的編解碼仍是在主線程。 針對這種問題常見的作法是在子線程中先將圖片繪製到CGBitmapContext
,而後從Bitmap
直接建立圖片,例如SDWebImage
三方框架中對圖片編解碼的處理。這就是Image
的預解碼,代碼以下: dispatch_async(queue, ^{
CGImageRef cgImage = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:self]]].CGImage;
CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(cgImage) & kCGBitmapAlphaInfoMask;
BOOL hasAlpha = NO;
if (alphaInfo == kCGImageAlphaPremultipliedLast ||
alphaInfo == kCGImageAlphaPremultipliedFirst ||
alphaInfo == kCGImageAlphaLast ||
alphaInfo == kCGImageAlphaFirst) {
hasAlpha = YES;
}
CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
size_t width = CGImageGetWidth(cgImage);
size_t height = CGImageGetHeight(cgImage);
CGContextRef context = CGBitmapContextCreate(NULL, width, height, 8, 0, CGColorSpaceCreateDeviceRGB(), bitmapInfo);
CGContextDrawImage(context, CGRectMake(0, 0, width, height), cgImage);
cgImage = CGBitmapContextCreateImage(context);
UIImage * image = [[UIImage imageWithCGImage:cgImage] cornerRadius:width * 0.5];
CGContextRelease(context);
CGImageRelease(cgImage);
completion(image);
});
複製代碼
TableView
中的圖片滑動的時候不加載,在滑動中止的時候加載(可使用Runloop
,圖片繪製設置 defaultModal
就行)UIView
和 CALayer
的關係:
UIView
是基於 UIKit
框架的,可以接受點擊事件,處理用戶的觸摸事件,並管理子視圖CALayer
是基於 CoreAnimation
,而CoreAnimation
是基於QuartzCode
的。因此CALayer
只負責顯示,不能處理用戶的觸摸事件UIView
是直接繼承 UIResponder
的,CALayer
是繼承 NSObject
的UIVIew
的主要職責是負責接收並響應事件;而 CALayer
的主要職責是負責顯示 UI
。UIView
依賴於 CALayer
得以顯示UIView
主要負責時間處理,CALayer
主要是視圖顯示 異步渲染的原理其實也就是在子線程將全部的視圖繪製成一張位圖,而後回到主線程賦值給 layer
的 contents
,例如 Graver
框架的異步渲染流程以下:if (drawingFinished && targetDrawingCount == layer.drawingCount)
{
CGImageRef CGImage = context ? CGBitmapContextCreateImage(context) : NULL;
{
// 讓 UIImage 進行內存管理
// 最終生成的位圖
UIImage *image = CGImage ? [UIImage imageWithCGImage:CGImage] : nil;
void (^finishBlock)(void) = ^{
// 因爲block可能在下一runloop執行,再進行一次檢查
if (targetDrawingCount != layer.drawingCount)
{
failedBlock();
return;
}
//主線程中賦值完成顯示
layer.contents = (id)image.CGImage;
// ...
}
if (drawInBackground) dispatch_async(dispatch_get_main_queue(), finishBlock);
else finishBlock();
}
// 一些清理工做: release CGImageRef, Image context ending
}
複製代碼
最終效果圖以下:YYAsyncLayer
和addView
給cell
動態添加view
view
,由於使用透明view
,會致使在GPU
中計算像素時,會將透明view
下層圖層的像素也計算進來,即顏色混合處理(當有兩個圖層的時候一個是半透明一個是不透明若是半透明的層級更高的話此時就會觸發顏色混合,底層的混合並非僅僅的將兩個圖層疊加而是會將兩股顏色混合計算出新的色值顯示在屏幕中)