卡顿原因
计算机通过CPU
、GPU
、显示器
三者协同工作将试图显示到屏幕上
- 1、CPU将需要显示的内容计算出来,提交到GPU
- 2、GPU将内容渲染完成后将渲染后的内容存放到
FrameBuffer
(帧缓冲区) - 3、视频控制器根据
VSync
(垂直同步)信号来读取FrameBuffer
中的数据 - 4、将转换的数模传递给显示器显示
iOS设备中采用双缓存区+VSync
在收到VSync
信号后,系统的图形服务通过CADisplayLink
等机制通知App,在主程序中调度CPU计算显示的内容,随后将计算好的内容提交到GPU变换、合成、渲染,GPU将渲染结果提交帧缓冲区
,等待下一个VSync信号到来时显示到屏幕上。由于垂直同步机制的原因
,如果再一个VSync时间内,CPU或者GPU没有完成内容的处理,就会导致当前处理的帧丢弃,此时屏幕会保持上一帧的显示,造成掉帧
卡顿检测
-
FPS监控
:因为iOS设备屏幕的刷新时间是60次/秒
,一次刷新就是一次VSync信号,时间间隔是1000ms/60 = 16.67ms
,所有如果咋16.67ms内下一帧数据没有准备好,就会产生掉帧 -
RunLoop监控
:通过子线程检测主线程的RunLoop的状态,kCFRunLoopBeforeSources
和kCFRunLoopAfterWaiting
两个状态之间的耗时是否达到一定的阈值
FPS监控
参照YYKit
中的YYFPSLabel
,其中通过CADisplayLink
来实现,通过刷新次数/时间差
得到刷新频率
class YPFPSLabel: UILabel {
fileprivate var link: CADisplayLink = {
let link = CADisplayLink.init()
return link
}()
fileprivate var count: Int = 0
fileprivate var lastTime: TimeInterval = 0.0
fileprivate var fpsColor: UIColor = {
return UIColor.green
}()
fileprivate var fps: Double = 0.0
override init(frame: CGRect) {
var f = frame
if f.size == CGSize.zero {
f.size = CGSize(width: 80.0, height: 22.0)
}
super.init(frame: f)
self.textColor = UIColor.white
self.textAlignment = .center
self.font = UIFont.init(name: "Menlo", size: 12)
self.backgroundColor = UIColor.lightGray
//通过虚拟类
link = CADisplayLink.init(target: CJLWeakProxy(target:self), selector: #selector(tick(_:)))
link.add(to: RunLoop.current, forMode: RunLoop.Mode.common)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
link.invalidate()
}
@objc func tick(_ link: CADisplayLink){
guard lastTime != 0 else {
lastTime = link.timestamp
return
}
count += 1
//时间差
let detla = link.timestamp - lastTime
guard detla >= 1.0 else {
return
}
lastTime = link.timestamp
//刷新次数 / 时间差 = 刷新频次
fps = Double(count) / detla
let fpsText = "\(String.init(format: "%.2f", fps)) FPS"
count = 0
let attrMStr = NSMutableAttributedString(attributedString: NSAttributedString(string: fpsText))
if fps > 55.0 {
//流畅
fpsColor = UIColor.green
}else if (fps >= 50.0 && fps <= 55.0){
//一般
fpsColor = UIColor.yellow
}else{
//卡顿
fpsColor = UIColor.red
}
attrMStr.setAttributes([NSAttributedString.Key.foregroundColor: fpsColor], range: NSMakeRange(0, attrMStr.length - 3))
attrMStr.setAttributes([NSAttributedString.Key.foregroundColor: UIColor.white], range: NSMakeRange(attrMStr.length - 3, 3))
DispatchQueue.main.async {
self.attributedText = attrMStr
}
}
}
RunLoop监控
参考 微信的matrix,滴滴的DoraemonKit
开辟子线程,通过监听主线程的kCFRunLoopBeforeSources
和kCFRunLoopAfterWaiting
两个Activity之间的差值
#import "YPBlockMonitor.h"
@interface YPBlockMonitor (){
CFRunLoopActivity activity;
}
@property (nonatomic, strong) dispatch_semaphore_t semaphore;
@property (nonatomic, assign) NSUInteger timeoutCount;
@end
@implementation YPBlockMonitor
+ (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{
// 创建信号
_semaphore = dispatch_semaphore_create(0);
// 在子线程监控时长
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;
}
// 一秒左右的衡量尺度 很大可能性连续来 避免大规模打印!
NSLog(@"检测到超过两次连续卡顿");
}
}
self->_timeoutCount = 0;
}
});
}
@end
界面优化
UIView和CALayer的关系
- UIView是基于
UIKit
框架,继承自UIResponder
,可以处理事件,管理子视图 - CALayer是基于
CoreAnimation
的,继承自NSObject
,只负责显示,不能处理事件 - UIKit组件最终都会分解为layer,存储到
图层树
中 - UIView中的部分属性,frame、bounds、transform等,来自CALayer的
映射
- CALayer内部没有属性,在调用属性时,内部通过运行时
resolveInstanceMethod
方法为对象临时添加一个方法,并将对应属性值保存到内部的Dictionary
,同时通知delegate、创建动画等
CPU层面的优化
-
1、对于不需要触摸的控件使用
CALayer
代替UIView
-
2、减少
UIView
和CALayer
的属性修改 -
3、大量对象释放时,移动到后台线程释放
-
4、
预排版
:在异步子线程中提前计算好视图的大小 -
5、
Autolayout
在简单页面情况下们可以很好的提升开发效率,但是对于复杂视图而言,会产生严重的性能问题,随着视图数量的增长,Autolayout带来的CPU消耗是呈指数上升的。所以尽量使用代码布局
-
6、文本处理
- 对于文本没有特殊要求的,可以使用UILabel内部实现方法,放在子线程中执行
- 计算文本宽高:
[NSAttributedString boundingRectWithSize:options:context:]
- 文本绘制:
[NSAttributedString drawWithRect:options:context:]
- 计算文本宽高:
- 使用自定义文本控件,通过
TextKit
或者CoreText
进行异步文本绘制。CoreText
对象创建后,可以直接获取文本宽高等信息。CoreText直接使用了CoreGraphics占用内存小,效率高
- 对于文本没有特殊要求的,可以使用UILabel内部实现方法,放在子线程中执行
-
7、图片优化
在使用UIImage
或者CGImageSource
方法创建图片时,图片数据不会立即解码,而是在设置到UIImageView/CALayer.contents
中,然后由CALayer提交到GPU渲染前才在主线程
进行解码,可以参考SDWebImage
中对图片的处理,在子线程中先将图片绘制到CGBitmapContext
,然后从Bitmap
直接创建图片- 使用
PNG
图片,而非JPGE
图片 - 在
子线程中解码,主线程渲染
,即通过Bitmap
创建图片,在子线程赋值image - 优化图片大小,避免动态缩放
- 多图合成一张图片显示
- 使用
-
8、避免使用
透明View
,会导致GPU在计算像素时,会将下层图层的像素也计算进来,颜色混合
处理 -
9、
按需加载
:例如通过RunLoop分发任务,ScrollView滚动时不加载 -
10、少使用
addView
给cell动态
添加view
GPU层面优化
GPU主要是接收CPU提交的纹理+顶点,经过一系列transform,最终混合并渲染,输出到屏幕上
1、避免短时间显示大量图片
,可以将多张图片合成一张
2、控制图片尺寸不超过4096x4096
,因为图片超过这个尺寸,CPU会先进行预处理再提交给GPU
3、减少视图层级和数量
4、避免离屏渲染
5、异步渲染
,例如可以将cell中的所有控件、视图合成一张位图进行显示,参考Graver
网友评论