图像的显示能够简单理解成先通过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)
}
}
}
复制代码