使用运行环路检测卡纸
前言
runloop不只是停留在面试的认知上,实际在开发中也可以利用其来处理一些特出情况,例如:通过runloop检测主线程卡顿情况,通过runloop加载较大任务等
本文主要介绍使用runloop检测主线程卡顿情况,并打印出卡顿代码的调用栈信息
源码demo
runloop简介
之前介绍到了runloop有多个mode,也就是有多个不同的运行模式,一次只能在一个模式下运行,且通过mode切换来达到切换状态的效果,其mode,如下所示
NSDefaultRunLoopMode
NSConnectionReplyMode
NSModalPanelRunLoopMode
NSEventTrackingRunLoopMode
NSRunLoopCommonModes
其中最后一个NSRunLoopCommonModes实际上是不属于基本运行mode,他是所有mode的集合,即设置了NSRunLoopCommonModes参数的代码,可以在各个mode模式下正常执行
如果想尽可能减少用户操作时的事件,可以将任务放到NSDefaultRunLoopMode模式下运行
此外在温习一下runloop运行的流程图,可以清楚的看到observer的调用步骤
然后查看一下runloop代码枚举给出的可以监听的observer类型
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即将进入Loop
kCFRunLoopBeforeTimers = (1UL << 1), //即将处理Timer
kCFRunLoopBeforeSources = (1UL << 2), //即将处理Source
kCFRunLoopBeforeWaiting = (1UL << 5), //即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), //刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7), //即将退出Loop
kCFRunLoopAllActivities = 0x0FFFFFFFU //所有状态改变
};
因此当runloop长时间停在kCFRunLoopBeforeSources或者kCFRunLoopAfterWaiting状态,可以认为runloop卡在了任务执行之前,为了避免减少误差,一定次数可以认为是一次有效卡顿,这也是我们小工具的核心逻辑
runloop卡顿检测工具
前面介绍了我们的卡顿是通过观测runloop是否长时间停在kCFRunLoopBeforeSources或者kCFRunLoopAfterWaiting状态来检测卡顿,下面介绍下实现逻辑
创建observer监听主线程状态
设置runloopObserver来监听主队列runloop的状态变化,并设置回调方法,最后添加到commonMode上,以保证所有模式都能监听到状态变化
//注册observer监听runloop
CFRunLoopObserverContext context = {0, (__bridge void*)self, NULL, NULL};
_observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities, YES, 0, &runloopCallback, &context);
//添加observer到主队列的commonMode上
CFRunLoopAddObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);
设置runloop的observer改变后的回调方法
void runloopCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
RunloopMonitor *monitor = [RunloopMonitor sharedInstance];
//使用工具类保存主线程的状态activity
monitor->_activity = activity;
//释放信号量(信号值+1,如果信号量值低于0则阻塞)
dispatch_semaphore_signal(monitor->_semaphore);
}
子线程监听主线程状态activity
在子线程中,配合信号量实现卡顿检测
开始检测功能之前,先开启信号量机制,设置默认信号量为0,当信号量值小于0时,会阻塞当前队列,使用wait方法会使信号量-1,使用signal会使信号量+1
子队列开启循环,每次通过wait方法阻塞子队列,并设置超时时间,一旦超过超时时间则会自动解除阻塞继续执行代码,当子队列收到主队列收到的消息后也会解除阻塞,实现正常功能
可以看到,正常没有卡顿情况下,主队列会在子队列超时之前切换runloopMode从而signal来释放信号,进而解除子线程阻塞,因此wait方法会返回一个为0的参数,表示没有卡顿;
当主线程长时间不调用signal,子线程会等待超时,因此会通过wait方法返回一个不为0的result,来继续执行代码,此时可以认为是主线程存在卡顿可能,因此查看主线程状态是否是kCFRunLoopBeforeSources、kCFRunLoopAfterWaiting,如果是可以认为主线程卡在了此方法中(为了操作巧合,使用了一定次数来矫正为一次卡顿),如果不是处于此状态,表示没有卡在处理方法哪里,可以认为不卡顿
检测到一定次数的卡顿后,认为一次有效卡顿,则可以回调对应的方法,或者打印调用栈信息等
代码如下所示:
//初始化信号量等相关参数
_semaphore = dispatch_semaphore_create(0);
_semaphoreCount = 0;
_cardCount = 0;
__weak typeof(self) wself = self;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
__strong typeof(self) sself = wself;
if (!sself) return;
while (sself->_isMonitor) {
//使用信号量阻塞当前线程(信号量值-1,如果信号量值低于0则阻塞),设置一个超时时间400ms
//如果超时时间到了,则会停止阻塞,信号量恢复,并返回一个非零的resut
//如果是正常signal唤醒,则result返回0
intptr_t result = dispatch_semaphore_wait(sself->_semaphore,
dispatch_time(DISPATCH_TIME_NOW, sself->_minInterval * NSEC_PER_MSEC));
//如果等待超时,runloop仍然在等到sources处理或者刚刚唤醒状态,被认为一次卡顿
if (result != 0 && (sself->_activity == kCFRunLoopBeforeSources
|| sself->_activity == kCFRunLoopAfterWaiting)) {
if (++sself->_cardCount >= sself->_maxCount) {
//大于或者等于为一次有效卡顿,回调卡顿提示block
if (sself.runloopMonitorCardCallback) sself.runloopMonitorCardCallback(sself);
if (sself.isPrintStackSymbols) __printStackSymbols(sself);
sself->_cardCount = 0;
}
}else {
//没有超时,则重置卡顿此时
sself->_cardCount = 0;
}
};
});
此外,还加入了调用栈信息打印的方法,且去除了重复的调用栈打印信息(具体的打印方法是以前看来的,忘了哪里看的),注意仅仅支持在debug下打印,且xcode有时打印会有问题,可以重新尝试
代码如下所示
//打印堆栈信息
void __printStackSymbols(RunloopMonitor *self) {
NSString *callStackSymbols = [LSCallStack ls_backtraceOfMainThread];
//仅仅显示2s之外的重复卡顿信息,为了方便调试
if (!self->_lastCallStackSymbols ||
![self->_lastCallStackSymbols isEqualToString:callStackSymbols] ||
(self->_lastInterval && CACurrentMediaTime() - self->_lastInterval > 2) ) {
NSLog(@"检测到了卡顿\n 堆栈信息---callStackSymbols:\n%@\n", callStackSymbols);
}
self->_lastCallStackSymbols = callStackSymbols;
self->_lastInterval = CACurrentMediaTime();
}
测试效果
接下来我们开启检测功能
[[RunloopMonitor sharedInstance] startMonitor];
加入测试案例:
在tableView中sleep,设置了一个tap事件,方便后面单次点击测试
- (void)initTableView {
UITableView *tableView = [[UITableView alloc] initWithFrame:self.view.frame
style:UITableViewStylePlain];
tableView.dataSource = self;
tableView.delegate = self;
[tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"identifier"];
[self.view addSubview:tableView];
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc]
initWithTarget:self action:@selector(onTapTableView)];
[tableView addGestureRecognizer:tap];
}
- (void)onTapTableView {
[NSThread sleepForTimeInterval:3];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return 1000;
}
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView
dequeueReusableCellWithIdentifier:@"identifier" forIndexPath:indexPath];
cell.textLabel.text = @"我就测试一下";
if (indexPath.row % 10 == 0) {
[NSThread sleepForTimeInterval:1];
}
return cell;
}
打印效果图如下所示,可以看到效果非常nice
最后
可以参考着源码查看理解,这里代码粘了一部分,这边是runloop的应用之一了,下一章介绍通过runloop加载任务(最多的是大图)
推荐阅读
-
使用运行环路检测卡纸
-
@Validated和@Valid区别-1.分组 @Validated:提供了一个分组功能,可以在入参验证时,根据不同的分组采用不同的验证机制。没有添加分组属性时,默认验证没有分组的验证属性。 伪代码如下: public interface First{ } public interface Second{ } public class UserModel { @NotNull(message = "{id.empty}", groups = { First.class }) private int id; @NotNull(message = "{username.empty}", groups = { First.class, Second.class }) private String username; @NotNull(message = "{content.empty}", groups = { First.class, Second.class }) private String content; } public String save(@Validated( { Second.class }) UserModel userModel, BindingResult result) { if (result.hasErrors) { return "validate/error"; } return "redirect:/success"; } 对一个参数需要多种验证方式时,也可通过分配不同的组达到目的。例: @NotEmpty(groups = { First.class }) @Size(min = 3, max = 8, groups = { Second.class }) private String name; 分组还支持组序列 默认情况下,不同组别的约束验证是无序的,然而在某些情况下,约束验证的顺序却很重要,如下面两个例子:(1)第二个组中的约束验证依赖于一个稳定状态来运行,而这个稳定状态是由第一个组来进行验证的。(2)某个组的验证比较耗时,CPU 和内存的使用率相对比较大,最优的选择是将其放在最后进行验证。因此,在进行组验证的时候尚需提供一种有序的验证方式,这就提出了组序列的概念。 一个组可以定义为其他组的序列,使用它进行验证的时候必须符合该序列规定的顺序。在使用组序列验证的时候,如果序列前边的组验证失败,则后面的组将不再给予验证。 public interface GroupA { } public interface GroupB { } @GroupSequence( { GroupA.class, GroupB.class }) public interface Group { } public @ResponseBody String addPeople(@Validated({Group.class}) People p,BindingResult result) { if(result.hasErrors) { return "0"; } return "1"; } @Valid:作为标准JSR-303规范,还没有吸收分组的功能。 2.注解地方 @Validated:可以用在类型、方法和方法参数上。但是不能用在成员属性(字段)上 @Valid:可以用在方法、构造函数、方法参数和成员属性(字段)上 两者是否能用于成员属性(字段)上直接影响能否提供嵌套验证的功能。 3.嵌套验证 在比较两者嵌套验证时,先说明下什么叫做嵌套验证。 比如我们现在有个实体叫做Item: public class Item { @NotNull(message = "id不能为空") @Min(value = 1, message = "id必须为正整数") private Long id; @NotNull(message = "props不能为空") @Size(min = 1, message = "至少要有一个属性") private List<Prop> props; } Item带有很多属性,属性里面有:pid、vid、pidName和vidName,如下所示: public class Prop { @NotNull(message = "pid不能为空") @Min(value = 1, message = "pid必须为正整数") private Long pid; @NotNull(message = "vid不能为空") @Min(value = 1, message = "vid必须为正整数") private Long vid; @NotBlank(message = "pidName不能为空") private String pidName; @NotBlank(message = "vidName不能为空") private String vidName; } 属性这个实体也有自己的验证机制,比如pid和vid不能为空,pidName和vidName不能为空等。 现在我们有个ItemController接受一个Item的入参,想要对Item进行验证,如下所示: @RestController public class ItemController { @RequestMapping("/item/add") public void addItem(@Validated Item item, BindingResult bindingResult) { doSomething; } } 在上图中,如果Item实体的props属性不额外加注释,只有@NotNull和@Size,无论入参采用@Validated还是@Valid验证,Spring Validation框架只会对Item的id和props做非空和数量验证,不会对props字段里的Prop实体进行字段验证,也就是@Validated和@Valid加在方法参数前,都不会自动对参数进行嵌套验证。也就是说如果传的List中有Prop的pid为空或者是负数,入参验证不会检测出来。 为了能够进行嵌套验证,必须手动在Item实体的props字段上明确指出这个字段里面的实体也要进行验证。由于@Validated不能用在成员属性(字段)上,但是@Valid能加在成员属性(字段)上,而且@Valid类注解上也说明了它支持嵌套验证功能,那么我们能够推断出:@Valid加在方法参数时并不能够自动进行嵌套验证,而是用在需要嵌套验证类的相应字段上,来配合方法参数上@Validated或@Valid来进行嵌套验证。 我们修改Item类如下所示: public class Item { @NotNull(message = "id不能为空") @Min(value = 1, message = "id必须为正整数") private Long id; @Valid // 嵌套验证必须用@Valid @NotNull(message = "props不能为空") @Size(min = 1, message = "props至少要有一个自定义属性") private List<Prop> props; } 然后我们在ItemController的addItem函数上再使用@Validated或者@Valid,就能对Item的入参进行嵌套验证。此时Item里面的props如果含有Prop的相应字段为空的情况,Spring Validation框架就会检测出来,bindingResult就会记录相应的错误。 总结一下@Validated和@Valid在嵌套验证功能上的区别:
-
在Linux中使用asan进行内存检测:从编译到运行的详细步骤