圖像的顯示能夠簡單理解成先通過CPU的計算/排版/編解碼等操做,而後交由GPU去完成渲染放入緩衝中,當視頻控制器接受到vSync時會從緩衝中讀取已經渲染完成的幀並顯示到屏幕上。ios
一些概念:
CPU:負責對象的建立和銷燬、對象屬性的調整、佈局計算、文本的計算和排版、圖片的格式轉換和解碼、圖像的繪製(Core Graphics)
GPU:負責紋理的渲染(將數據渲染到屏幕) 垂直同步技術:讓CPU和GPU在收到vSync信號後再開始準備數據,防止撕裂感和跳幀,通俗來說就是保證每秒輸出的幀數不高於屏幕顯示的幀數。
雙緩衝技術:iOS是雙緩衝機制,前幀緩存和後幀緩存,cpu計算完GPU渲染後放入緩衝區中,當gpu下一幀已經渲染完放入緩衝區,且視頻控制器已經讀完前幀,GPU會等待vSync(垂直同步信號)信號發出後,瞬間切換先後幀緩存,並讓cpu開始準備下一幀數據
安卓4.0後採用三重緩衝,多了一個後幀緩衝,可下降連續丟幀的可能性,但會佔用更多的CPU和GPUgit
SDWebImage的使用:
CGImageRef imageRef = image.CGImage;
// device color space
CGColorSpaceRef colorspaceRef = SDCGColorSpaceGetDeviceRGB();
BOOL hasAlpha = SDCGImageRefContainsAlpha(imageRef);
// iOS display alpha info (BRGA8888/BGRX8888)
CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
size_t width = CGImageGetWidth(imageRef);
size_t height = CGImageGetHeight(imageRef);
// kCGImageAlphaNone is not supported in CGBitmapContextCreate.
// Since the original image here has no alpha info, use kCGImageAlphaNoneSkipLast
// to create bitmap graphics contexts without alpha info.
CGContextRef context = CGBitmapContextCreate(NULL,
width,
height,
kBitsPerComponent,
0,
colorspaceRef,
bitmapInfo);
if (context == NULL) {
return image;
}
// Draw the image into the context and retrieve the new bitmap image without alpha
CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef);
CGImageRef imageRefWithoutAlpha = CGBitmapContextCreateImage(context);
UIImage *imageWithoutAlpha = [[UIImage alloc] initWithCGImage:imageRefWithoutAlpha scale:image.scale orientation:image.imageOrientation];
CGContextRelease(context);
CGImageRelease(imageRefWithoutAlpha);
return imageWithoutAlpha;
複製代碼
離屏渲染
在OpenGL中,GPU有2種渲染方式 On-Screen Rendering:當前屏幕渲染,在當前用於顯示的屏幕緩衝區進行渲染操做 Off-Screen Rendering:離屏渲染,在當前屏幕緩衝區之外新開闢一個緩衝區進行渲染操做github離屏渲染消耗性能的緣由 須要建立新的緩衝區 離屏渲染的整個過程,須要屢次切換上下文環境,先是從當前屏幕(On-Screen)切換到離屏(Off-Screen);等到離屏渲染結束之後,將離屏緩衝區的渲染結果顯示到屏幕上,又須要將上下文環境從離屏切換到當前屏幕api
哪些操做會觸發離屏渲染?緩存
光柵化,layer.shouldRasterize = YES性能優化
遮罩,layer.maskbash
圓角,同時設置layer.masksToBounds = YES、layer.cornerRadius大於0 考慮經過CoreGraphics繪製裁剪圓角,或者叫美工提供圓角圖片服務器
陰影,layer.shadowXXX 若是設置了layer.shadowPath就不會產生離屏渲染併發
在開發階段,能夠直接使用Instrument來檢測性能問題,Time Profiler查看與CPU相關的耗時操做,Core Animation查看與GPU相關的渲染操做。app
正常狀況下,App的FPS只要保持在50~60之間,用戶就不會感到界面卡頓。經過向主線程添加CADisplayLink咱們能夠接收到每次屏幕刷新的回調,從而統計出每秒屏幕刷新次數。這種方案最多見,例如YYFPSLabel,且只用了CADisplayLink,實現成本較低,但因爲只能在CPU空閒時纔去回調,沒法精確採集到卡頓時調用棧信息,能夠在開發階段做爲輔助手段使用。
//
// YYFPSLabel.m
// YYKitExample
//
// Created by ibireme on 15/9/3.
// Copyright (c) 2015 ibireme. All rights reserved.
//
#import "YYFPSLabel.h"
//#import <YYKit/YYKit.h>
#import "YYText.h"
#import "YYWeakProxy.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];
}
// 建立CADisplayLink並添加到主線程的RunLoop中
_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;
}
//刷新回調時去計算fps
- (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 yy_setColor:color range:NSMakeRange(0, text.length - 3)];
[text yy_setColor:[UIColor whiteColor] range:NSMakeRange(text.length - 3, 3)];
text.yy_font = _font;
[text yy_setFont:_subFont range:NSMakeRange(text.length - 4, 1)];
self.attributedText = text;
}
@end
複製代碼
關於RunLoop,推薦參考深刻理解RunLoop,這裏只列出其簡化版的狀態。
// 1.進入loop
__CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled)
// 2.RunLoop 即將觸發 Timer 回調。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
// 3.RunLoop 即將觸發 Source0 (非port) 回調。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
// 4.RunLoop 觸發 Source0 (非port) 回調。
sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle)
// 5.執行被加入的block等Source1事件
__CFRunLoopDoBlocks(runloop, currentMode);
// 6.RunLoop 的線程即將進入休眠(sleep)。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
// 7.調用 mach_msg 等待接受 mach_port 的消息。線程將進入休眠, 直到被下面某一個事件喚醒。
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort)
// 進入休眠
// 8.RunLoop 的線程剛剛被喚醒了。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting
// 9.1.若是一個 Timer 到時間了,觸發這個Timer的回調
__CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
// 9.2.若是有dispatch到main_queue的block,執行bloc
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
// 9.3.若是一個 Source1 (基於port) 發出事件了,處理這個事件
__CFRunLoopDoSource1(runloop, currentMode, source1, msg);
// 10.RunLoop 即將退出
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
複製代碼
因爲source0處理的是app內部事件,包括UI事件,因此可知處理事件主要是在kCFRunLoopBeforeSources和kCFRunLoopAfterWaiting之間。咱們能夠建立一個子線程去監聽主線程狀態變化,經過dispatch_semaphore在主線程進入狀態時發送信號量,子線程設置超時時間循環等待信號量,若超過期間後還未接收到主線程發出的信號量則可判斷爲卡頓,保存響應的調用棧信息去進行分析。線上卡頓的收集多采用這種方式,可將卡頓信息上傳至服務器且用戶無感知。
#pragma mark - 註冊RunLoop觀察者
//在主線程註冊RunLoop觀察者
- (void)registerMainRunLoopObserver
{
//監聽每一個步湊的回調
CFRunLoopObserverContext context = {0, (__bridge void*)self, NULL, NULL};
self.runLoopObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES,
0,
&runLoopObserverCallBack,
&context);
CFRunLoopAddObserver(CFRunLoopGetMain(), self.runLoopObserver, kCFRunLoopCommonModes);
}
//觀察者方法
static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
self.runLoopActivity = activity;
//觸發信號,說明開始執行下一個步驟。
if (self.semaphore != nil)
{
dispatch_semaphore_signal(self.semaphore);
}
}
#pragma mark - RunLoop狀態監測
//建立一個子線程去監聽主線程RunLoop狀態
- (void)createRunLoopStatusMonitor
{
//建立信號
self.semaphore = dispatch_semaphore_create(0);
if (self.semaphore == nil)
{
return;
}
//建立一個子線程,監測Runloop狀態時長
dispatch_async(dispatch_get_global_queue(0, 0), ^
{
while (YES)
{
//若是觀察者已經移除,則中止進行狀態監測
if (self.runLoopObserver == nil)
{
self.runLoopActivity = 0;
self.semaphore = nil;
return;
}
//信號量等待。狀態不等於0,說明狀態等待超時
//方案一->設置單次超時時間爲500毫秒
long status = dispatch_semaphore_wait(self.semaphore, dispatch_time(DISPATCH_TIME_NOW, 500 * NSEC_PER_MSEC));
if (status != 0)
{
if (self.runLoopActivity == kCFRunLoopBeforeSources || self.runLoopActivity == kCFRunLoopAfterWaiting)
{
...
//發生超過500毫秒的卡頓,此時去記錄調用棧信息
}
}
/*
//方案二->連續5次卡頓50ms上報
long status = dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 50*NSEC_PER_MSEC));
if (status != 0)
{
if (!observer)
{
timeoutCount = 0;
semaphore = 0;
activity = 0;
return;
}
if (activity==kCFRunLoopBeforeSources || activity==kCFRunLoopAfterWaiting)
{
if (++timeoutCount < 5)
continue;
//保存調用棧信息
}
}
timeoutCount = 0;
*/
}
});
}
複製代碼
根據卡頓發生時,主線程無響應的原理,建立一個子線程循環去Ping主線程,Ping以前先設卡頓置標誌爲True,再派發到主線程執行設置標誌爲False,最後子線程在設定的閥值時間內休眠結束後判斷標誌來判斷主線程有無響應。該方法的監控準確性和性能損耗與ping頻率成正比。
代碼部分來源於ANREye
private class AppPingThread: Thread {
private let semaphore = DispatchSemaphore(value: 0)
//判斷主線程是否卡頓的標識
private var isMainThreadBlock = false
private var threshold: Double = 0.4
fileprivate var handler: (() -> Void)?
func start(threshold:Double, handler: @escaping AppPingThreadCallBack) {
self.handler = handler
self.threshold = threshold
self.start()
}
override func main() {
while self.isCancelled == false {
self.isMainThreadBlock = true
//主線程去重置標識
DispatchQueue.main.async {
self.isMainThreadBlock = false
self.semaphore.signal()
}
Thread.sleep(forTimeInterval: self.threshold)
//若標識未重置成功則說明再設置的閥值時間內主線程未響應,此時去作響應處理
if self.isMainThreadBlock {
//採集卡頓調用棧信息
self.handler?()
}
_ = self.semaphore.wait(timeout: DispatchTime.distantFuture)
}
}
}
複製代碼