背景
一直以来都有博客说明以下代码能够获得列表reloadData完成的时机,但实际上我在工作中遇到了以下代码不生效的情况。另外,stack overflow中也有人遇到了与我有相同的问题。
[self.tableView reloadData];
dispatch_async(dispatch_get_main_queue(), ^{
//列表刷新完成
});
复制代码
因此,本篇文章通过分析runloop的源码和UI刷新时机来说明为什么以上代码有时生效有时又不生效。
分析
问题说明
代码1
代码如下:
[self.tableView reloadData];
NSLog(@"reloadData");
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"dispatch_get_main_queue");
});
复制代码
输出如下,在reloadData后紧接着输出了mainQueue的log,接着又经历了一个runloop循环,直到kCFRunLoopBeforeWaiting时触发了cellForRow。显然,可以发现这段代码是不能用于判断列表刷新完成的。
kCFRunLoopAfterWaiting
kCFRunLoopBeforeTimers
kCFRunLoopBeforeSources
reloadData
dispatch_get_main_queue
kCFRunLoopBeforeTimers
kCFRunLoopBeforeSources
kCFRunLoopBeforeWaiting
cellForRowAtIndexPath
复制代码
但有时候,通过点击一个按钮来调用reloadData,它的输出如下。可以发现,这段代码又很神奇的能判断列表刷新完成了。
kCFRunLoopAfterWaiting
kCFRunLoopBeforeTimers
kCFRunLoopBeforeSources
reloadData
cellForRowAtIndexPath
dispatch_get_main_queue
kCFRunLoopBeforeTimers
kCFRunLoopBeforeSources
kCFRunLoopBeforeWaiting
复制代码
代码2
接着再看一段代码如下,为了方便,我们简称外层的block为block1,内层block为block2。
//block1
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"dispatch_get_main_queue_1");
[self.tableView reloadData];
NSLog(@"reloadData");
//block2
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"dispatch_get_main_queue_2");
});
});
复制代码
输出如下,block1中调用了reloadData,接着循环runloop,走到了beforeWaitding阶段开始触发列表cellForRow。之后runloop进入休眠,在休眠过后执行派发到主队列的block2。从log中可以得到一个结论,那就是在block2中列表确实刷新完成了。
kCFRunLoopAfterWaiting
kCFRunLoopBeforeTimers
kCFRunLoopBeforeSources
dispatch_get_main_queue_1
reloadData
kCFRunLoopBeforeTimers
kCFRunLoopBeforeSources
kCFRunLoopBeforeWaiting
cellForRowAtIndexPath
kCFRunLoopAfterWaiting
dispatch_get_main_queue_2
复制代码
Runloop源码分析
在解释上述问题前,先需要回顾一遍runloop的源码,其伪代码如下。
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
do {
//通知监听kCFRunLoopBeforeTimers的observer
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
//通知监听kCFRunLoopBeforeSources的observer
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
//调用添加到runloop的block
__CFRunLoopDoBlocks(rl, rlm);
//调用source0
Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
if (sourceHandledThisLoop) {
__CFRunLoopDoBlocks(rl, rlm);
}
if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) {
//如果有CGD派发到主队列的任务可以消费,goto到handle_msg来跳过runloop休眠
if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
goto handle_msg;
}
}
didDispatchPortLastTime = false;
//通知监听kCFRunLoopBeforeWaiting的observer
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
//runloop休眠
__CFRunLoopSetSleeping(rl);
...
//被唤醒
//唤醒后,通知监听kCFRunLoopAfterWaiting的observer
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
//刚刚的goto定义在这里
handle_msg:;
if (MACH_PORT_NULL == livePort) {
//啥也不做
} else if (livePort == rl->_wakeUpPort) {
//啥也不做
} else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort) {
//处理Timer事件
if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
__CFArmNextTimerInMode(rlm, rl);
}
} else if (livePort == dispatchPort) {
//处理dispatch到主队列的事件
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
didDispatchPortLastTime = true;
} else {
//处理source1
__CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply)
}
} while (...)
}
复制代码
runloop源码最重要的逻辑在于do-while循环,主要逻辑描述如下:
- 通知kCFRunLoopBeforeTimers
- 通知kCFRunLoopBeforeSources
- 执行Sources0
- 若主队列有任务且上一次循环没有处理过派发到主队列的任务,则跳转到9
- 通知kCFRunLoopBeforeWaiting
- 休眠
- 被唤醒
- 通知kCFRunLoopAfterWaiting
- 条件判断
- 若因timer唤醒,处理timer任务
- 若因dispatch唤醒,处理派发到主队列的任务
- 若因source1唤醒,处理source1事件
太长不看版:
CellForRow调用时机
CATransaction注册了runloop的observer监听kCFRunLoopBeforeWaiting时机,会在该阶段调用commit触发UI更新。列表的cellForRow代理会在CATransaction的commit方法中触发。
另外,runloop由source1唤醒后会继续在source0中处理任务,比如说处理手势任务。如下Trace,在这个过程中,UIKit有些情况下会调用CATransaction的flush方法来触发UI刷新,因此source0中也有可能调用cellForRow。
通过查看反汇编UIKitCore代码,可以看出来在某种情况下会触发CATransaction的flush方法。
问题解答
代码1
在代码1中,reloadData会在source0事件中触发,紧接着runloop会判断是否有GCD的主队列任务可以处理,若可以处理则会直接goto去处理,这样就跳过了beforeWaiting和休眠,主队列任务也就在cellForRow之前执行。
处理GCD主队列任务后有设置标识符didDispatchPortLastTime为true,下次处理完source0后会判断标识符,若为true则不能直接跳过再次去处理GCD任务了。这个逻辑很好理解,可以避免当CGD主队列一直有任务时,runloop循环会一直去处理。
在代码1中,若reloadData调用后,由于某种原因UIKit触发了CATransaction的flush方法,那么会同步调用cellForRow,此时GCD主队列任务一定会在列表刷新完成后触发。
代码2
在代码2中,runloop处理完source0,检测到GCD主队列有任务,因此直接goto到休眠后处理GCD主队列任务,在block1中触发reloadData。之后由于本次loop唤醒后处理过GCD主队列,因此在处理完source0后不能继续goto来跳过休眠,而是走到了beforeWaiting,触发了列表的cellForRow,之后runloop休眠。在休眠后,会继续处理GCD主队列任务,此时block2肯定在cellForRow之后,因此可以判断列表刷新完成。
结论
列表存在同步刷新和runloop的beforeWaiting时机刷新两种情况,GCD主队列任务存在跳过beforeWaiting时机直接处理和等待休眠后处理两种情况,因此在实际开发中用GCD判断列表刷新完成有时生效有时失效。