图像显示原理
- 关于CPU和GPU两个硬件都是通过事件总线连接起来的。
- 在CPU中输出的结果是一个位图。
- 再经由总线在合适的时机上传给GPU,GPU拿到位图之后会做相应位图的图层的渲染,包括纹理的合成。
- 之后把结果放到针缓冲区当中。
- 由视频控制器根据VSync信号在指定时间之前去提取帧缓冲区当中的屏幕显示内容,然后最终显示到手机屏幕上面。
CPU和GPU分别做了哪些事
- 创建一个UIView的控件之后,显示部分是由CALayer来负责的,CALayer中有一个contents属性最终要绘制到屏幕上的位图。比如创建的是一个UILabel,contents中最终结果放置的就是UILabel中Hello World文字的位图
- 系统会在合适的时机回调一个drawRect:方法,在此基础之上绘制一些想要自定义绘制的内容。绘制好的内容经由CoreAnimation框架。
- 提交给GPU部分的OpenGL渲染管线,进行最终的位图渲染,包括纹理的合成,最终显示到屏幕上。
渲染原理
- 任务是以总线的方式进行的,CPU负责计算,GPU负责渲染
- CPU UI布局、文本计算、绘制;图片编解码;提交位图
- GPU负责顶点着色、图元装配、光栅化、片段着色、片段处理
UI卡顿&掉帧原因
一般页面滑动的流畅性是60ps,每一秒钟是60针的画面,16.7ms产生一针的画面。
在规定的16.7ms内,在下一帧Vsync信号到来之前,并没有CPU和GPU完成下一帧画面的合成,于是就会导致卡顿和掉帧。
卡顿的分类及思考
卡顿分为线上、线下;主线程、子线程;偶现与必现。
- 线上的卡顿,我们该如何监控。
- 偶现的卡顿,该如何排查。
- 子线程严格意义上说并不能归类于卡顿,因为不会阻塞UI,但会导致UI展示出现异常。
线上卡顿的监控源码分析
一些第三方提供了卡顿检测的功能,如腾讯的bugly。腾讯也提供了开源代码:监控RunLoop状态。
源码
- (void)beginMonitor {
//监测卡顿
if (runLoopObserver) {
return;
}
dispatchSemaphore = dispatch_semaphore_create(0); //Dispatch Semaphore保证同步
//创建一个观察者
CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
runLoopObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES,
0,
&runLoopObserverCallBack,
&context);
//将观察者添加到主线程runloop的common模式下的观察中
CFRunLoopAddObserver(CFRunLoopGetMain(), runLoopObserver, kCFRunLoopCommonModes);
dispatch_queue_t serial = dispatch_queue_create("serial", DISPATCH_QUEUE_SERIAL);
//创建子线程监控
dispatch_async(serial, ^{
//子线程开启一个持续的loop用来进行监控
while (YES) {
long semaphoreWait = dispatch_semaphore_wait(dispatchSemaphore, dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC));
if (semaphoreWait != 0) {
if (!runLoopObserver) {
timeoutCount = 0;
dispatchSemaphore = 0;
runLoopActivity = 0;
return;
}
//两个runloop的状态,BeforeSources和AfterWaiting这两个状态区间时间能够检测到是否卡顿
if (runLoopActivity == kCFRunLoopBeforeSources || runLoopActivity == kCFRunLoopAfterWaiting) {
//出现三次出结果
// if (++timeoutCount < 3) {
// continue;
// }
NSLog(@"monitor trigger");
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[SMCallStack callStackWithType:SMCallStackTypeAll];
});
} //end activity
}// end semaphore wait
timeoutCount = 0;
}// end while
});
}
static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){
SMLagMonitor *lagMonitor = (__bridge SMLagMonitor*)info;
lagMonitor->runLoopActivity = activity;
dispatch_semaphore_t semaphore = lagMonitor->dispatchSemaphore;
dispatch_semaphore_signal(semaphore);
}
- CFRunLoopAddObserver为主线程添加runloop观察者。
- 子线程开启while循环。
- 如果主线程没有卡顿, RunLoop的状态会实时发生变化,调用runLoopObserverCallBack中的dispatch_semaphore_signal,while中的 dispatch_semaphore_wait不会超时。
- 如果主线程有卡顿,while中的dispatch_semaphore_wait会发生超时。
- 监听两个状态kCFRunLoopBeforeSources、kCFRunLoopAfterWaiting。
timeoutCount如果连续出现3次,认为主线程发生了卡顿。callStackWithType:抓取主线程中的堆栈信息,进行上传。
思考:
- 子线程开启while循环,会不会有性能损耗?
分析:如果主线程卡住了,子线程会挂起,等待信号量超时。不会一直死循环。
如果主线程没有卡主,主线程的循环跟着runloop的唤醒频率来刷新。 - 为什么是kCFRunLoopBeforeSources、kCFRunLoopAfterWaiting两个状态,其他状态行不行?
首先发生卡顿肯定是主线程的任务发生卡顿,做事情的时候发生卡顿。
kCFRunLoopBeforeTimers 触发timers回调,任务轻。
kCFRunLoopBeforeSources触发source0开始干活了,如果发生了卡顿,肯定无法进入下一个状态。
kCFRunLoopAfterWaiting 接受mach_port消息,如果发生了卡顿,肯定无法进入下一个状态。 - 为什么需要timeoutCount,如果不加行吗?
卡顿实战
- 如果app发生卡顿,能获抓取到主线程的堆栈信息,基本能解决一半。
- 堆栈信息还需要仔细分析,有些情况可能都没见过。
实战1: 使用YYThreadSafeArray造成的页面卡死
背景
在遍历数组时,可能会获取前一个或后一个obj
[conversation.messages enumerateObjectsUsingBlock:^(BLMessage *message, NSUInteger index, BOOL * _Nonnull stop) {
if (index == 0) {
...
} else {
BLMessage *compareMessage = [conversation.messages objectAtIndex:index - 1];
...
}];
原因
#define LOCK(...) dispatch_semaphore_wait(_lock, DISPATCH_TIME_FOREVER); \
__VA_ARGS__; \
dispatch_semaphore_signal(_lock);
- (void)enumerateObjectsUsingBlock:(void (^)(id obj, NSUInteger idx, BOOL *stop))block {
LOCK([_arr enumerateObjectsUsingBlock:block]);
}
- (id)objectAtIndex:(NSUInteger)index {
LOCK(id obj = [_arr objectAtIndex:index]); return obj;
}
YYThreadSafeArray使用信号量来保证线程安全; enumerateObjectsUsingBlock遍历时,信号量已经dispatch_semaphore_wait,而再调用objectAtIndex会再次调用dispatch_semaphore_wait,造成死锁。
解决方案
- 如果使用for循环进行遍历,不会存在死锁,但非线程安全。会造成偶现闪退。
- 自定义信号量,在数组操作时进行加信号量,注意方法中不能出现嵌套。
小提示
使用YYThreadSafeArray正常遍历遍历是没有问题的,但注意不能再进行其他数组操作。
实战2. socket快速发送消息会出现接受消息阻塞
场景:快速点击某个按钮,发送消息时,会出现socket响应消息丢失。
- 最后分析出现是-[NSInputStream read:maxLength:]卡主了。
- 下面是出现的解释:TCP Socket的一些行为、Does -[NSInputStream read:maxLength:] block?
- read:maxLength:会阻塞,直到至少有一个字节可用之后,或者发生错误或者流到达EOS。它也将阻塞,直到打开流为止。
解决方案
- 通过心跳来打破这种等待行为
- 使用超时的定时器,每秒触发一次,当read:maxLength阻塞时发送心跳。
实战3. 加密引起的界面卡顿问题沟通
- 加入视频会议,有时候会特别慢。也不是必现。
- 堆栈信息跟踪到声网的[self.agoraKit enableEncryption:YES encryptionConfig:config]这段加密方法卡住了。最慢能有8s。
反馈到声网,声网一度怀疑是我们调用SDK逻辑有问题。然后发了个demo,给我测试。 - 测试发现,快速加入退出会议,demo最慢也会卡2s。
- 分析问题:声网的AgoraRtcEngineKit是单例,加密对象只调用了config设置。
而我们音视频加密是一个会议一个秘钥,也就是退出会议,秘钥会进行释放。声网内部的资源竞争导致的耗时。
再次分析发现:有些方法不能在加密之前调用,如本地视频流的推送。个人理解在推视频流,然后又加密了,这个时候SDK内部会对推流视频做加密处理,导致卡顿。
总结
- 对于一些偶现的卡顿,可以观察线上统计的数据,如bugly平台。
- 卡顿问题也是千奇百怪。了解卡顿检测的原理。
- 你会遇到哪些有意思的卡顿问题,欢迎留言交流。
网友评论