runloop的理解借助了MJ视频以及开源文档
https://github.com/apple/swift-corelibs-foundation/
https://blog.csdn.net/u014795020/article/details/72084735
GNUstep源码下载地址:http://www.gnustep.org/resources/downloads.php
参考 https://www.jianshu.com/p/4c38d16a29f1/
CFRunLoopRef 的代码开源,且是NSRunLoopRef的封装基础,所以down它吧
staticCFMutableDictionaryRef __CFRunLoops =NULL;
staticCFLock_t loopsLock = CFLockInit;
// should only be called by Foundation
// t==0 is a synonym for "main thread" that always works
CF_EXPORT CFRunLoopRef_CFRunLoopGet0(pthread_t t) {
if(pthread_equal(t, kNilPthreadT)) {
t = pthread_main_thread_np();
}
__CFLock(&loopsLock);
if(!__CFRunLoops) {
__CFUnlock(&loopsLock);
CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault,0,NULL, &kCFTypeDictionaryValueCallBacks);
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
if(!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void*volatile*)&__CFRunLoops)) {
CFRelease(dict);
}CFRelease(mainLoop);
__CFLock(&loopsLock);
}
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFUnlock(&loopsLock);
if(!loop) {
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
__CFLock(&loopsLock);
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
if(!loop) {
CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
loop = newLoop;
}
// don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
__CFUnlock(&loopsLock);
CFRelease(newLoop);
}
if(pthread_equal(t, pthread_self())) {
_CFSetTSD(__CFTSDKeyRunLoop, (void*)loop,NULL);
if(0== _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
_CFSetTSD(__CFTSDKeyRunLoopCntr, (void*)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void(*)(void*))__CFFinalizeRunLoop);
}
}
returnloop;
}
主要看它有用逻辑信息CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);全局字典存储着runloop,其中所在线程为key、 RunLoop为value;线程刚创建时并没有 RunLoop,RunLoop 的创建是发生在第一次获取时,销毁是发生在线程结束。你只能在线程内部获取 RunLoop。
runloop运行以model为基础
__CFRunLoopMode :
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
__CFRunLoop :
CFMutableSetRef _commonModes; // Set
CFMutableSetRef _commonModeItems; //
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
这些统称为事件源,可以被同时加入多个 mode。如果没有事件源则 RunLoop 会直接退出。
runloop操作model 目前我们熟悉且公开看到的commonModel、defaultModel、trackingModel
//modeName为操作model的路径、当你没有的时候,会为你创建新的。。
voidCFRunLoopAddCommonMode(CFRunLoopRef rl, CFStringRef modeName) {
CHECK_FOR_FORK();
if(__CFRunLoopIsDeallocating(rl))return;
__CFRunLoopLock(rl);
if(!CFSetContainsValue(rl->_commonModes, modeName)) {
CFSetRef set = rl->_commonModeItems ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModeItems) :NULL;
CFSetAddValue(rl->_commonModes, modeName);
if(NULL!= set) {
CFTypeRef context[2] = {rl, modeName};
/* add all common-modes items to new mode commonmodel之所以会执行。因为commonmodel的item会同步到其它model中 */
CFSetApplyFunction(set, (__CFRunLoopAddItemsToCommonMode), (void*)context);
CFRelease(set);
}
}else{} __CFRunLoopUnlock(rl);}
CFRunLoopRunInMode(CFStringRef modeName, ...);
Mode 暴露的管理 mode item 的接口有下面几个:
CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
item的说明:
CFRunLoopSourceRef 是事件产生的地方。Source有两个版本:Source0 和 Source1。
• Source0 只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。
• Source1 包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。这种 Source 能主动唤醒 RunLoop 的线程,其原理在下面会讲到。
CFRunLoopTimerRef 是基于时间的触发器,它和 NSTimer 是toll-free bridged 的,可以混用。其包含一个时间长度和一个回调(函数指针)。当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。
CFRunLoopObserverRef 是观察者,每个 Observer 都包含了一个回调(函数指针),当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化。可以观测的时间点有以下几个:
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
};
借用mj的图来看执行流程
int32_t __CFRunLoopRun()
{
// 通知即将进入runloop
__CFRunLoopDoObservers(KCFRunLoopEntry);
do
{
// 通知将要处理timer
__CFRunLoopDoObservers(kCFRunLoopBeforeTimers);
// 通知将要处理source0
__CFRunLoopDoObservers(kCFRunLoopBeforeSources);
// 处理非延迟的主线程调用
__CFRunLoopDoBlocks();
// 处理Source0事件
__CFRunLoopDoSource0();
if (sourceHandledThisLoop) {
__CFRunLoopDoBlocks();
}
/// 如果有 Source1 (基于port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息。
if (__Source0DidDispatchPortLastTime) {
Boolean hasMsg = __CFRunLoopServiceMachPort();
if (hasMsg) goto handle_msg;
}
/// 通知 Observers: RunLoop 的线程即将进入休眠(sleep)。
if (!sourceHandledThisLoop) {
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
}
// GCD dispatch main queue
CheckIfExistMessagesInMainDispatchQueue();
// 即将进入休眠
__CFRunLoopDoObservers(kCFRunLoopBeforeWaiting);
// 等待内核mach_msg事件
mach_port_t wakeUpPort = SleepAndWaitForWakingUpPorts();
// 等待。。。
// 从等待中醒来
__CFRunLoopDoObservers(kCFRunLoopAfterWaiting);
// 处理因timer的唤醒
if (wakeUpPort == timerPort)
__CFRunLoopDoTimers();
// 处理异步方法唤醒,如dispatch_async
else if (wakeUpPort == mainDispatchQueuePort)
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__()
// 处理Source1
else __CFRunLoopDoSource1();
// 再次确保是否有同步的方法需要调用
__CFRunLoopDoBlocks();
} while (!stop && !timeout);
// 通知即将退出runloop
__CFRunLoopDoObservers(CFRunLoopExit);
}
__CFRunLoopRun内部其实是一个do while循环,只是不同于我们自己写的循环它在休眠时几乎不会占用系统资源,当然这是由于系统内核负责实现的,也是Runloop精华所在。
可以看到runloop进入休眠后需要item事件唤醒它 :
/// • 一个基于 port 的Source 的事件。
/// • 一个 Timer 到时间了
/// • RunLoop 自身的超时时间到了
/// • 被其他什么调用者手动唤醒
基于mach_msg()通信。苹果系统层级结构。没有深入研究。。链接内可以看到更详细的。。。
摘个图
如图所示,首先我要明确一个知识点:runloop跑一圈,只能执行一个事件。
timer和source0进入runloop中,都只是通知Observer我要处理,但是还是会有6、7、8睡眠唤醒这一步。但是source1如果有,就会直接跳到第9步执行。
我们前面也讲过第7步,这里再提一下。它一直阻塞再这一列,直到:
a.source1来了。b.定时器启动。c.runloop超时。d.runloop被显式唤醒CFRunLoopWakeUp(runloop)(也就是source0)来了。
实际中的app启动
系统一次性默认注册了5个Mode:
1. kCFRunLoopDefaultMode: App的默认 Mode,通常主线程是在这个 Mode 下运行的。
2. UITrackingRunLoopMode: 界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。
3. UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。
4: GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到。
5: kCFRunLoopCommonModes: 这是一个占位的 Mode,没有实际作用。
Runloop和autoreleasePool
{
/// 通知Observers,即将进入RunLoop
/// 此处有Observer会创建AutoreleasePool: _objc_autoreleasePoolPush();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry);
do {
/// 通知 Observers: 即将触发 Timer 回调。 __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers);
/// 通知 Observers: 即将触发 Source (非基于port的,Source0) 回调。 __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources); __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
/// 触发 Source0 (非基于port的) 回调。 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0); __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
/// 通知Observers,即将进入休眠 /// 此处有Observer释放并新建AutoreleasePool: _objc_autoreleasePoolPop(); _objc_autoreleasePoolPush(); __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeWaiting);
/// sleep to wait msg. mach_msg() -> mach_msg_trap();
/// 通知Observers,线程被唤醒 __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopAfterWaiting);
/// 如果是被Timer唤醒的,回调Timer __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(timer);
/// 如果是被dispatch唤醒的,执行所有调用 dispatch_async 等方法放入main queue 的 block __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block);
/// 如果如果Runloop是被 Source1 (基于port的) 的事件唤醒了,处理这个事件 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1); }
while (...);
/// 执行加入到Loop的block __CFRunLoopDoBlocks(runloop, currentMode);
if (sourceHandledThisLoop && stopAfterHandle) {
/// 进入loop时参数说处理完事件就返回。
retVal = kCFRunLoopRunHandledSource; }
else if (timeout) {
/// 超出传入参数标记的超时时间了
retVal = kCFRunLoopRunTimedOut; }
else if (__CFRunLoopIsStopped(runloop)) {
/// 被外部调用者强制停止了 retVal = kCFRunLoopRunStopped; }
else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
/// source/timer/observer一个都没有了
retVal = kCFRunLoopRunFinished; }
/// 如果没超时,mode里没空,loop也没被停止,那继续loop。 }
while (retVal == 0); }
通知 Observers: RunLoop 即将退出。
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
AutoreleasePool
App启动后,苹果在主线程 RunLoop 里注册了两个 Observer 即将进入、即将休眠。。
其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()。
第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。
第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;
Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。
在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,开发者也不必显示创建 Pool 了。
Runloop中的一些案例处理
事件响应
苹果注册了一个 Source1 (基于 mach port 的) 用来接收系统事件,其回调函数为 __IOHIDEventSystemClientQueueCallback()。
当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收。这个过程的详细情况可以参考这里。SpringBoard 只接收按键(锁屏/静音等),触摸,加速,接近传感器等几种 Event,随后用 mach port 转发给需要的App进程。随后苹果注册的那个 Source1 就会触发回调,并调用 _UIApplicationHandleEventQueue() 进行应用内部的分发。_UIApplicationHandleEventQueue() 会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发,其中包括识别 UIGesture/处理屏幕旋转/发送给 UIWindow 等。通常事件比如 UIButton 点击、touchesBegin/Move/End/Cancel 事件都是在这个回调中完成的。
手势识别
当上面的 _UIApplicationHandleEventQueue() 识别了一个手势时,其首先会调用 Cancel 将当前的 touchesBegin/Move/End 系列回调打断。随后系统将对应的 UIGestureRecognizer 标记为待处理。苹果注册了一个 Observer 监测 BeforeWaiting (Loop即将进入休眠) 事件,这个Observer的回调函数是 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行GestureRecognizer的回调。当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。
界面更新
当在操作 UI 时,比如改变了 Frame、更新了 UIView/CALayer 的层次时,或者手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去。
苹果注册了一个 Observer 监听 BeforeWaiting(即将进入休眠) 和 Exit (即将退出Loop) 事件,回调去执行一个很长的函数:
_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。这个函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。
定时器
NSTimer 其实就是 CFRunLoopTimerRef,他们之间是 toll-free bridged 的。一个 NSTimer 注册到 RunLoop 后,RunLoop 会为其重复的时间点注册好事件。例如 10:00, 10:10, 10:20 这几个时间点。RunLoop为了节省资源,并不会在非常准确的时间点回调这个Timer。Timer 有个属性叫做 Tolerance (宽容度),标示了当时间点到后,容许有多少最大误差。
如果某个时间点被错过了,例如执行了一个很长的任务,则那个时间点的回调也会跳过去,不会延后执行。就比如等公交,如果 10:10 时我忙着玩手机错过了那个点的公交,那我只能等 10:20 这一趟了。
CADisplayLink 是一个和屏幕刷新率一致的定时器(但实际实现原理更复杂,和 NSTimer 并不一样,其内部实际是操作了一个 Source)。如果在两次屏幕刷新之间执行了一个长任务,那其中就会有一帧被跳过去(和 NSTimer 相似),造成界面卡顿的感觉。在快速滑动TableView时,即使一帧的卡顿也会让用户有所察觉。Facebook 开源的 AsyncDisplayLink 就是为了解决界面卡顿的问题,其内部也用到了 RunLoop,这个稍后我会再单独写一页博客来分析。
PerformSelecter
当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。
当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。
(performSelecter这里说的太泛泛了,其中指定线程执行的方法调用实际产生了source事件。。。)
关于GCD
当调用 dispatch_async(dispatch_get_main_queue(), block) 时,libDispatch 会向主线程的 RunLoop 发送消息,RunLoop会被唤醒,并从消息中取得这个 block,并在回调 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__() 里执行这个 block。但这个逻辑仅限于 dispatch 到主线程,dispatch 到其他线程仍然是由 libDispatch 处理的。
Runloop实际调用
对于NSRunloop来说:
// 运行NSRunLoop,运行模式为默认的NSDefaultRunLoopMode模式,没有超时限制
- (void)run;一般来说不建议使用。因为停止困难。。cfrunloopstop也不能关停。。
// 运行NSRunLoop:参数为时间期限,运行模式为默认的NSDefaultRunLoopMode模式
- (void)runUntilDate:(NSDate *)limitDate; 可以设置一个超时时间,过期自动停止
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:3.0]];//3秒后超时
// 运行NSRunLoop:参数为运行模式、时间期限,返回值为YES表示处理事件后返回的,NO表示是超时或者停止运行导致返回的。
//这种方式的定义除了超时停止,还可以利用cfrunloopstop来停止,因为此方法就是cf第二种SInt32 CFRunLoopRunInMode的封装。。其次是利用返回值。出发source信号
- (BOOL)runMode:(NSString *)mode beforeDate:(NSDtate *)limitDate;
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];//持续运行。。
Rac中应用
do {
[NSRunloop.mainRunloop runMode:NSDefaultRunLoopMode beforeDate: [NSDate dateWithTimeIntervalSinceNow: 0.1]];
} while(!done);
对于CFRunLoopRef来说
void CFRunLoopRun();
// 运行CFRunLoopRef:参数为运行模式、时间和是否在处理Input Source后退出标志,返回值是exit原因
SInt32 CFRunLoopRunInMode(mode, second, returnAfterSourceHandled);
// 停止运行CFRunLoop
void CFRunLoopStop(CFRunLoopRef rl);
// 唤醒CFRunLoopRef
void CFRunLoopWakeUp(CFRunLoopRef rl);
测试- (BOOL)runMode:(NSString *)mode beforeDate:(NSDtate *)limitDate退出状态
void)testDemo1{
dispatch_async(dispatch_get_global_queue(0,0), ^ {
NSLog(@"线程开始");
// 获取当前线程
self.thread = [NSThread currentThread];
NSRunLoop *runloop = [NSRunLoop currentRunLoop];
// 添加一个Port,同理为了防止runloop没事干直接退出
[runloop addPort: [NSMachPort port] forMode: NSDefaultRunLoopMode];
// 运行一个runloop, [NSDate distantFuture]:很久很久以后才让它失效
[runloop runMode:NSDefaultRunloopMode beforeDate: [NSDate distantFuture]];
NSLog(@"线程结束");
});
dispatch_after(dispatch_time(DISPATCH_TIME_NOW,(int64_t)(2*NSEC_PER_SEC)), dispatch_get_main_queue(), ^ {
// 在我们开启的异步线程调用方法
[self performSelector:@selector(recieveMsg) onThread: self.thread withObject: nil waitUntilDone: NO];
});
}
- (void)recieveMsg {
NSLog(@"收到消息了,在这个线程:%@", [NSThread currentThread]);
}
cf中的方法 SInt32 CFRunLoopRunInMode(mode, seconds, returnAfterSourceHandled);
第三个参数是是否在处理事件后让Run Loop退出返回,NSRunLoop的第三种开启runloop的方法,综上所述,我们知道,实际上就是设置stopAfterHande这个参数为YES
关于返回值,我们知道调用runloop运行,代码是停在这一行不返回的,当返回的时候runloop就结束了,所有这个返回值就是runloop结束原因的返回,为一个枚举值,具体原因如下:
enum {
kCFRunLoopRunFinished = 1, // Run Loop结束,没有Timer或者其他Input Source
kCFRunLoopRunStopped = 2, // Run Loop被停止,使用CFRunLoopStop停止Run Loop
kCFRunLoopRunTimedOut = 3, // Run Loop超时
kCFRunLoopRunHandledSource = 4, // Run Loop处理完事件,注意Timer事件的触发是不会让Run Loop退出返回的,即使CFRunLoopRunInMode的第三个参数是YES也不行..
}
(void)testDemo2
{
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"starting thread.......");
NSTimer *timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(doTimerTask1:) userInfo:remotePort repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
//最后一个参数,是否处理完事件返回,结束runLoop
SInt32 result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, 100, YES);
/*
kCFRunLoopRunFinished = 1, //Run Loop结束,没有Timer或者其他Input Source
kCFRunLoopRunStopped = 2, //Run Loop被停止,使用CFRunLoopStop停止Run Loop
kCFRunLoopRunTimedOut = 3, //Run Loop超时
kCFRunLoopRunHandledSource = 4 Run Loop处理完事件,注意Timer事件的触发是不会让Run Loop退出返回的,即使CFRunLoopRunInMode的第三个参数是YES也不行
*/
switch (result) {
case kCFRunLoopRunFinished:
NSLog(@"kCFRunLoopRunFinished");
break;
case kCFRunLoopRunStopped:
NSLog(@"kCFRunLoopRunStopped");
case kCFRunLoopRunTimedOut:
NSLog(@"kCFRunLoopRunTimedOut");
case kCFRunLoopRunHandledSource:
NSLog(@"kCFRunLoopRunHandledSource");
default:
break;
}
NSLog(@"end thread.......");
});
}
- (void)doTimerTask1:(NSTimer *)timer
{
count++;
if (count == 2) {
[timer invalidate];
}
NSLog(@"do timer task count:%d",count);
starting thread…….、、 do timer task count:1 do timer task count:2 kCFRunLoopRunFinished end thread…….
此时只有timer无效的情况下才退出runloop。。
AsyncDisplayKit应用监听卡顿机制
AsyncDisplayKit 是 Facebook 推出的用于保持界面流畅性的框架,其原理大致如下:
UI 线程中一旦出现繁重的任务就会导致界面卡顿,这类任务通常分为3类:排版,绘制,UI对象操作。
排版通常包括计算视图大小、计算文本高度、重新计算子式图的排版等操作。
绘制一般有文本绘制 (例如 CoreText)、图片绘制 (例如预先解压)、元素绘制 (Quartz)等操作。
UI对象操作通常包括 UIView/CALayer 等 UI 对象的创建、设置属性和销毁。
其中前两类操作可以通过各种方法扔到后台线程执行,而最后一类操作只能在主线程完成,并且有时后面的操作需要依赖前面操作的结果 (例如TextView创建时可能需要提前计算出文本的大小)。ASDK 所做的,就是尽量将能放入后台的任务放入后台,不能的则尽量推迟 (例如视图的创建、属性的调整)。
为此,ASDK 创建了一个名为 ASDisplayNode 的对象,并在内部封装了 UIView/CALayer,它具有和 UIView/CALayer 相似的属性,例如 frame、backgroundColor等。所有这些属性都可以在后台线程更改,开发者可以只通过 Node 来操作其内部的 UIView/CALayer,这样就可以将排版和绘制放入了后台线程。但是无论怎么操作,这些属性总需要在某个时刻同步到主线程的 UIView/CALayer 去。
ASDK 仿照 QuartzCore/UIKit 框架的模式,实现了一套类似的界面更新的机制:即在主线程的 RunLoop 中添加一个 Observer,监听了 kCFRunLoopBeforeWaiting 和 kCFRunLoopExit 事件,在收到回调时,遍历所有之前放入队列的待处理的任务,然后一一执行。
具体的代码可以看这里:_ASAsyncTransactionGroup。
利用runloop监听主线程卡顿:
其中UI主要集中在__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0);
和__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1);之前。
获取kCFRunLoopBeforeSources到kCFRunLoopBeforeWaiting再到kCFRunLoopAfterWaiting的状态就可以知道是否有卡顿的情况。
kCFRunLoopBeforeSources 持续处于这个状态,表明再处理事情
kCFRunLoopBeforeWaiting 表明处理完成
kCFRunLoopAfterWaiting 即将醒来处理事情
// 创建信号
semaphore = dispatch_semaphore_create(0);
NSLog(@"dispatch_semaphore_create:%@",[BGPerformanceMonitor getCurTime]);
// 注册RunLoop状态观察
CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
//创建Run loop observer对象
//第一个参数用于分配observer对象的内存
//第二个参数用以设置observer所要关注的事件,详见回调函数myRunLoopObserver中注释
//第三个参数用于标识该observer是在第一次进入run loop时执行还是每次进入run loop处理时均执行
//第四个参数用于设置该observer的优先级
//第五个参数用于设置该observer的回调函数
//第六个参数用于设置该observer的运行环境
observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES,
0,
&runLoopObserverCallBack,
&context);
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
// 在子线程监控时长
dispatch_async(dispatch_get_global_queue(0, 0), ^{
while (YES) { // 有信号的话 就查询当前runloop的状态
// 假定连续5次超时50ms认为卡顿(当然也包含了单次超时250ms)
// 因为下面 runloop 状态改变回调方法runLoopObserverCallBack中会将信号量递增 1,所以每次 runloop 状态改变后,下面的语句都会执行一次
// dispatch_semaphore_wait:Returns zero on success, or non-zero if the timeout occurred.
long st = dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 50*NSEC_PER_MSEC));
NSLog(@"dispatch_semaphore_wait:st=%ld,time:%@",st,[self getCurTime]);
if (st != 0) { // 信号量超时了 - 即 runloop 的状态长时间没有发生变更,长期处于某一个状态下
if (!observer) {
timeoutCount = 0;
semaphore = 0;
activity = 0;
return;
}
NSLog(@"st = %ld,activity = %lu,timeoutCount = %d,time:%@",st,activity,timeoutCount,[self getCurTime]);
// kCFRunLoopBeforeSources - 即将处理source kCFRunLoopAfterWaiting - 刚从休眠中唤醒
// 获取kCFRunLoopBeforeSources到kCFRunLoopBeforeWaiting再到kCFRunLoopAfterWaiting的状态就可以知道是否有卡顿的情况。
// kCFRunLoopBeforeSources:停留在这个状态,表示在做很多事情
if (activity == kCFRunLoopBeforeSources || activity == kCFRunLoopAfterWaiting) { // 发生卡顿,记录卡顿次数
if (++timeoutCount < 5) {
continue; // 不足 5 次,直接 continue 当次循环,不将timeoutCount置为0
}
// 收集Crash信息也可用于实时获取各线程的调用堆栈
PLCrashReporterConfig *config = [[PLCrashReporterConfig alloc] initWithSignalHandlerType:PLCrashReporterSignalHandlerTypeBSD symbolicationStrategy:PLCrashReporterSymbolicationStrategyAll];
PLCrashReporter *crashReporter = [[PLCrashReporter alloc] initWithConfiguration:config];
NSData *data = [crashReporter generateLiveReport];
PLCrashReport *reporter = [[PLCrashReport alloc] initWithData:data error:NULL];
NSString *report = [PLCrashReportTextFormatter stringValueForCrashReport:reporter withTextFormat:PLCrashReportTextFormatiOS];
NSLog(@"---------卡顿信息\n%@\n--------------",report);
}
}
NSLog(@"dispatch_semaphore_wait timeoutCount = 0,time:%@",[self getCurTime]);
timeoutCount = 0;
}
});
}
除此之外,还有tableviewcell的大图加载。。放个监听器监听runloop执行状态。。。一次加载一个图片。。。
参考https://github.com/edsum/RunLoop