iOS RunLoop中你应该知道的那些事、图文代码三茂

阅读本篇文章需要有一定的runloop基础、runloop的基础认知还请先自行搜索

RunLoop运行流程

static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
     // 通知即将进入runloop
     __CFRunLoopDoObservers(KCFRunLoopEntry);
     
     do {
         // 通知将要处理timer和source
         __CFRunLoopDoObservers(kCFRunLoopBeforeTimers);
         __CFRunLoopDoObservers(kCFRunLoopBeforeSources);
         
         // 处理非延迟的主线程调用 (Perform blocks queued by CFRunLoopPerformBlock;)
         __CFRunLoopDoBlocks(rl, rlm);
 
         // 处理Source0事件
         Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
         // sourceHandledThisLoop的值等于__CFIsValid(rls)的返回值, 在CFRunLoopSourceInvalidate函数中设置了"__CFUnsetValid(rls);" & 在 CFRunLoopObserverInvalidate函数中设置了"__CFUnsetValid(rlo);" & 在CFRunLoopTimerInvalidate函数中设置了"__CFUnsetValid(rlt);"
         在这几个方法的初始化方法里设置了"CFRunLoopSourceCreate -> __CFSetValid(CFRunLoopSourceRef)" & "CFRunLoopObserverCreate -> __CFSetValid(CFRunLoopObserverRef)" & "CFRunLoopTimerCreate -> __CFSetValid(CFRunLoopTimerRef)"
         if (sourceHandledThisLoop) {
             __CFRunLoopDoBlocks(rl, rlm); // 处理非延迟的主线程调用 (Perform blocks queued by CFRunLoopPerformBlock;)
         }
 
         // 如果有 Source1 (基于port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息。
         if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
             goto handle_msg;
         }
         
         // 即将进入休眠
         __CFRunLoopDoObservers(kCFRunLoopBeforeWaiting);
         __CFRunLoopSetSleeping(rl);
         
         // 等待内核mach_msg事件
         __CFPortSetInsert(dispatchPort, waitSet);
         do {
                __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
 
                if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {
                    // Drain the internal queue. If one of the callout blocks sets the timerFired flag, break out and service the timer.
                    while (_dispatch_runloop_root_queue_perform_4CF(rlm->_queue));
                    if (rlm->_timerFired) {
                        // Leave livePort as the queue port, and service timers below
                        rlm->_timerFired = false;
                        break;
                    } else {
                        if (msg && msg != (mach_msg_header_t *)msg_buffer) free(msg);
                    }
                } else {
                    // Go ahead and leave the inner loop.
                    break;
                }
         } while(1);
         
         // 从等待中被唤醒
         __CFPortSetRemove(dispatchPort, waitSet);
         __CFRunLoopUnsetSleeping(rl);
         __CFRunLoopDoObservers(kCFRunLoopAfterWaiting);
         
  handle_msg:
         // 处理来自于"timerPort"的唤醒
         if (livePort == rlm->_timerPort)
             CFRUNLOOP_WAKEUP_FOR_TIMER();
             if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
                 // Re-arm the next timer
                 __CFArmNextTimerInMode(rlm, rl);
             }
         
         // 处理来自于"dispatchPort"的唤醒
         else if (livePort == dispatchPort)
             CFRUNLOOP_WAKEUP_FOR_DISPATCH();
             __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
             
         // 处理Source1
         else
             CFRUNLOOP_WAKEUP_FOR_SOURCE();
             __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply);
         
         // 再次确保是否有同步的方法需要调用 (Perform blocks queued by CFRunLoopPerformBlock;)
         __CFRunLoopDoBlocks(rl, rlm);
         
     } while (!stop && !timeout);
     
     // 通知即将退出runloop
     __CFRunLoopDoObservers(CFRunLoopExit);
 }


代码运行过程中、runloop的状态是如何变化的?

示例代码1如下:

- (void)testMethod_tmp {    // button-taped-action
    {
        [self testMethod_1];
        [self performSelector:@selector(testMethod_2) withObject:nil afterDelay:32];
        
        [self testBlock:^{
            NSLog(@"异步执行15秒...");
            dispatch_async(dispatch_get_main_queue(), ^{
                NSLog(@" testBlock-get_main_queue:");
                [self PrintCallstack];
            });
        }];

        NSLog(@"开始for循环");
        for (long i = 0; i < 999999999; i++) {
            [self testMethod_0];
        }
        NSLog(@"for循环结束");

        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            NSLog(@"异步执行20秒...");
            [NSThread sleepForTimeInterval:20];
            dispatch_async(dispatch_get_main_queue(), ^{
                [self testMethod_4];
            });
        });

    }
}
- (void)testBlock:(void(^)(void))completion {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [NSThread sleepForTimeInterval:15];
        NSLog(@" testBlock:");
        [self PrintCallstack];
        if (completion) {
            completion();
        }
    });
}

如上图中的"testMethod_tmp"方法、runloop的状态变化如下所示:


runloop的状态变化



示例代码2如下:

- (void)dispatch_action {  // (通过"afterdelay:"调用) __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__
    int timeDuration = 5;
    
    dispatch_async(dispatch_get_main_queue(), ^{  // __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE_
        NSLog(@"outer task milestone 1");   // +++ step 1
        
        dispatch_async(dispatch_get_main_queue(), ^{  // __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__
            NSLog(@"inner task");           // +++ step 4
        });
        
        for (int j = 0; j < 100; j++) {     // +++ step 2
            NSLog(@"j = %d",j);
        }
        
        [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:timeDuration]];
        NSLog(@"outer task milestone 2");   // +++ step 3  (第3步与第2步之间的时间差为timeDuration秒)
    });
}

如上的"dispatch_action"方法、runloop的状态变化如下所示:

runloop的状态变化



示例代码3如下:

- (void)cfRunLoopPerformBlock_action {   // (通过"afterdelay:"调用) __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__
    int timeDuration = 2;
    
    CFRunLoopPerformBlock(CFRunLoopGetMain(), kCFRunLoopCommonModes, ^{  // __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__
        NSLog(@"outer task milestone 1");     // +++ step 1
        
        CFRunLoopPerformBlock(CFRunLoopGetMain(), kCFRunLoopCommonModes, ^{  // __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__
            NSLog(@"inner task");             // +++ step 3
        });
        
        for (int j = 0; j < 100; j++) {       // +++ step 2
            NSLog(@"j = %d",j);
        }
        
        [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:timeDuration]];
        NSLog(@"outer task milestone 2");     // +++ step 4  (第4步与第3步之间的时间差为timeDuration秒)
    });
}

如上的"cfRunLoopPerformBlock_action"方法、runloop的状态变化如下所示:


runloop的状态变化



RunLoop主体是一个死循环,保证程序一直能够运行,这个循环中,程序大部分时间停留在mach_msg中等待消息。并且在每次循环过程中处理当前mode的source、observer、timers。在开发过程中几乎所有的操作都是通过Call Out进行回调的(无论是Observer的状态通知还是Timer、Source的处理),而系统在回调时通常使用如下几个函数进行回调(换句话说你的代码其实最终都是通过下面几个函数来负责调用的,即使你自己监听Observer也会先调用下面的函数然后间接通知你,所以在调用堆栈中经常看到这些函数):
static void CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION();
static void CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK();
static void CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE();
static void CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION();
static void CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION();
static void CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION();
对于Event Loop而言RunLoop最核心的事情就是保证线程在没有消息时休眠以避免占用系统资源,有消息时能够及时唤醒。
RunLoop的这个机制完全依靠系统内核来完成,具体来说是苹果操作系统核心组件Darwin中的Mach来完成的。
Mach是Darwin的核心,可以说是内核的核心,提供了进程间通信、处理器调度等基础服务。在Mach中进程&线程间的通信是以消息的方式来完成的,消息在两个Port之间进行传递 (这也正是Source1之所以称之为Port-based Source的原因,因为它就是依靠系统发送消息到指定的Port来触发的)。消息的发送和接收使用<mach/message.h>中的mach_msg()函数。

app启动后哪些线程开启了runloop?

让我们来添加一个符号断点CFRunLoopRunInMode:


图1

图2

图3

如上面这2张图所示:app运行之后系统依次会在"com.apple.uikit.eventfetch-thread" & "com.apple.main-thread"两个线程中开启runloop。

当RunLoop没有任务处理时就会进入到休眠状态,此时如果在XCode里点击暂停,你会看到主线程调用栈是停留在 mach_msg_trap() 这个地方(陷入mach_msg_trap状态)。
如下图所示:


图4


Apple基于runloop实现的功能

用户代码都是系统框架对RunLoop进行相应操作以后(如增加observer、source等),通过CALL_OUT系列函数回调回来的。通过打印 [NSRunLoop mainRunLoop] 可以查看主RunLoop的所有mode、observer、source、timer等。对应用程序来说唤醒程序的操作一般都是通过点击(tap)、时间(timer)、和延迟执行等操作。

1. NSObject (NSThreadPerformAdditions)

图5

示例代码:

// PermanentThreadManager中创建了一个常驻线程
self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadEntryPoint:) object:nil];
[self.thread start];

- (void)threadEntryPoint:(id)__unused object {
    // NSMachPort:
    @autoreleasepool {
        [[NSThread currentThread] setName:@"Avery-PermanentThread"];

        NSRunLoop *currentRunLoop = [NSRunLoop currentRunLoop];
        [currentRunLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [currentRunLoop run];
    }
}

TestObject *obj = [TestObject new];
_threadManager = [[PermanentThreadManager alloc] init];
[_threadManager start];
[obj performSelector:@selector(test) onThread:_threadManager.networkRequestThread withObject:nil waitUntilDone:NO];

通过打断点查看调用堆栈可以发现这个扩展里的所有方法都是通过 CALL_OUT_SOURCE0 调起的。
因此可以推测出在performAdditions方法中其实就是在指定线程的runloop中注册一个runloop source0、然后在回调中调用执行代码。
需要注意的是在 waitUntilDone为YES时调用有不一样,这时分为两种情况:
如果指定的线程为当前线程这时是正常的函数调用与runloop无关;如果指定线程和当前线程不同,则会在目标线程的runloop中注册source0,为了实现等待的效果内部可能是使用信号量等方法进行当前线程的阻塞。另外如果子线程中没有runloop,那么指定一个没有runloop的线程去执行performAdditions方法是不生效的。


图6

图7


2. NSObject (NSDelayedPerforming)

图8

示例代码如下:

TestObject *obj = [TestObject new];
[obj performSelector:@selector(test) withObject:nil afterDelay:1];

这一系列的函数都是通过 CALL_OUT_TIMER 调起的,同样的也可以推测delayedPerforming方法内部是通过增加runloop timer实现的。与上面一样在一个没有runloop的线程中使用delayedPerforming方法是不生效的。


图9

图10

想要在gcd block中调用"performSelector:withObject:afterDelay:"、应该怎么做?


图11

图12

图12-1

如以上图11、图12、图12-1所示: dalayAction方法被成功执行后runloop即会退出; runloop退出后"[[NSRunLoop currentRunLoop] run];"方法则会返回,RunLoop内部的CFRunLoopRunSpecific方法返回kCFRunLoopRunFinished (因为此时runloopMode中已没有items了)、随后会运行子线程方法底部的NSLog方法并打印出log:"end"。
在block内部使用"[[NSRunLoop currentRunLoop] run];"会导致该block的内存泄漏吗?让我们来添加符号断点"_Block_release"并追踪一下weakBlock来监测该blcok的释放时机:

__weak dispatch_block_t _weakBlock;

dispatch_block_t block = ^{
    NSLog(@" start");
    [self performSelector:@selector(delayAction) withObject:nil afterDelay:3];
    [[NSRunLoop currentRunLoop] run];
    NSLog(@" end");
};
_weakBlock = block;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), block);
    
for (int i = 0; i < 120; i++) {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [NSThread sleepForTimeInterval:0.3];
    });
}
图12-2

综上可知在block{}内部使用runloop不会创建常驻线程也不会引起内存泄漏、当block{}所处的线程中的runloop退出后、该线程就会在合适的时机被系统回收。


3. Button的Touch事件

iOS的事件响应:(1)用户触发事件 (2)系统将事件转交到对应APP的事件队列 (3)app从消息队列头取出事件 (4)交由Main Window进行消息分发 (5)找到合适的Responder进行处理,如果没找到则会沿着Responder chain返回到APP层,丢弃不响应该事件。
app启动后查看主线程中的runloop:


图13

发现在kCFRunLoopDefaultMode、UITrackingRunLoopMode下面均注册了的source0事件源:


图14

图15

图16

他们的回调函数都是__handleEventQueue,app就是通过这个回调函数来处理事件队列的。但是__handleEventQueue 所对应的source类型是source0,source0本身是不可能唤醒休眠中的mainRunLoop的、当主线程自身在休眠状态中的时候也不可能自己去唤醒自己对吧。那么系统中肯定还存在有另外的一个子线程,用来接收事件并唤醒main-thread、并将相应的事件传递给main-thread。

那么会是哪个线程呢?
让我们添加一个符号断点“CFRunLoopRunInMode”来找找看:


图17

经测试发现:添加的CFRunLoopRunInMode断点会依次在"com.apple.uikit.eventfetch-thread"子线程& "com.apple.main-thread"主线程这两个线程中断住;
首先是创建"com.apple.uikit.eventfetch-thread"子线程的runloop:


图18

然后创建"com.apple.main-thread"主线程的runloop:


图19

让我们研究下"com.apple.uikit.eventfetch-thread"子线程的RunLoop, "com.apple.uikit.eventfetch-thread"子线程是UIKit所创建的用于接收event的线程, 查看该子线程里的runloop:如下图所示该子线程中只包含一个RunLoop Mode:default mode。
图20

如下图所示: 在这个default mode中注册了一个source1、其回调函数是:__IOHIDEventSystemClientQueueCallback。这个source1类型则是可以被系统通过mach port唤醒"com.apple.uikit.eventfetch-thread"子线程的RunLoop,并执行__IOHIDEventSystemClientQueueCallback回调的。
图21

我们再添加符号断点__IOHIDEventSystemClientQueueCallback & __handleEventQueue,然后再进行测试执行流程:可以发现,会依次调用__IOHIDEventSystemClientQueueCallback & __handleEventQueue来处理事件。


图22

图23

综上可得以下结论:
用户触发事件时 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收,SpringBoard会利用mach port产生的source1来唤醒目标的com.apple.uikit.eventfetch-thread子线程的RunLoop。eventfetch-thread线程会将main runloop 中__handleEventQueue所对应的source0的signalled设置为Yes状态,同时并唤醒main RunLoop。mainRunLoop继而再调用__handleEventQueue进行事件队列的处理。__handleEventQueue会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发给UIWindow、其中包括识别 UIGesture/UIButton点击/处理屏幕旋转等。

4.手势

iOS的手势操作也依赖runloop、UIKit会在Main RunLoop中的trackingMode & defaultMode下注册一个observer:


图24

activities属性值为0x20即32;可知该observer监听main RunLoop的kCFRunLoopBeforeWaiting事件。每当main Loop即将休眠时该observer被触发,同时调用回调函数_UIGestureRecognizerUpdateObserver。
手势的调用栈: 64(由eventfetch-thread子线程的source1唤醒mainloop) -> 2 -> 4 -> 2 -> 4 -> 执行手势操作


图25

2 -> 4 -> 2 -> 4 ->32
可见手势操作并不依赖UIResponder的响应链(touchesBegan...)。

如果有手势被触发,在_UIGestureRecognizerUpdateObserver回调中会借助UIKit一个内部类UIGestureEnvironment 来进行一系列处理。其中会向APP的event queue中投递一个gesture event,这个gesture event的处理流程应该和上面的事件处理类似的,内部会调用__handleEventQueueInternal处理该gesture event,并通过UIKit内部类UIGestureEnvironment 来处理这个gesture event,并最终回调到我们自己所写的gesture回调中。手势操作和button的点击事件一样、在"com.apple.uikit.eventfetch-thread"子线程的default mode中注册了一个source1、其回调函数是: __IOHIDEventSystemClientQueueCallback。这个source1类型则是可以被系统通过mach port唤醒"com.apple.uikit.eventfetch-thread"子线程的RunLoop,并执行__IOHIDEventSystemClientQueueCallback回调的。Eventfetch-thread会将main runloop中__handleEventQueue所对应的source0设置为signalled == Yes状态,同时并唤醒main RunLoop。main RunLoop继而再调用__handleEventQueue进行事件队列处理。当__handleEventQueue识别了一个手势时,其首先会调用 Cancel、将当前的 touchesBegin/Move/End 系列回调打断,随后系统会将对应的 UIGestureRecognizer 标记为待处理。在runloop mode中注册的observer监听BeforeWaiting事件,这个Observer的回调函数是_UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的GestureRecognizer,并执行GestureRecognizer的回调。当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应的处理。


5. 界面刷新

当我们需要刷新界面时,如UIView/CALayer 调用了setNeedsLayout/setNeedsDisplay,或者更新了UIView的frame或UI层次。其实系统并不会立刻就开始刷新界面,而是先提交UI刷新请求,再等到下一次main RunLoop循环时,集中处理(集中处理的好处在于可以合并一些重复或矛盾的UI刷新)。而这种实现方式则是通过监听main RunLoop的before waitting和Exit通知来实现的。


图26

苹果注册了一个 Observer 监听事件,可以看到该回调函数其注册事件是activities = 0xa0(BeforeWaiting | Exit),它的优先级(order=2000000)比事件响应的优先级(order=0)要低(order的值越大优先级越低)。当在操作 UI 时,比如改变了 Frame、更新了 UIView/CALayer 的层次、或者手动调用了 UIView/CALayer 的setNeedsLayout/setNeedsDisplay方法后,这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去。_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()这个函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。
让我们来验证一下:


图27

图28

如上图所示:多次修改frame后只会调用1次"_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv"方法。


6. NSTimer

显示调用runloop添加timer的方式:

图29

其调用栈如下:
图30

scheduledTimerWithTimeInterval方式:

图31

如上图所示:通过scheduledTimerWithTimeInterval方式启动的timer、并不存在于当前runloop的common mode items中,也就是说当runloop将model切换到UITrackingRunLoopMode之后就不能触发timer事件了。
其调用栈如下:
图32


7. CADisplayLink

CADisplayLink 和 NSTimer有相同的功能,都是定时的不断调用同一块代码。CADisplayLink与NSTimer不同的是,它是通过CALL_OUT_SOURCE1调出来的。
CADisplayLink的触发是通过CoreAnimation注册了source1(基于mach_port),屏幕在刷新之前(基于硬件时钟, 每秒发送60次即60HZ)中间经过操作系统把消息发送给mach_msg,经过runloop把source1事件转发给CoreAnimation,CoreAnimation再进行用户代码回调。CADisplayLink和NSTimer都可能出现时间不精确的问题。但是NSTimer选择的做法是延时以后有机会就调用;CADisplayLink是如果错过本次调用时机就会放弃,让调用顺延到下个周期。NSTimer在iOS7以后增加了 tolerance 属性,如果延迟时间超过这个属性也会和CADisplayLink一样放弃本次调用。
但是还是不能让NSTimer代替CADisplayLink,原因是NSTimer的开始时间是自由的,而CADisplayLink是完全基于屏幕刷新频率开始的,能够充分发挥<垂直同步>机制。
添加断点并执行"po [NSRunLoop currentRunLoop]"可看到在DefaultModel& TrackingModel下均注册了source1事件源:


图33

图34


8. GCD dispatch to main queue

当调用 dispatch_async(dispatch_get_main_queue(), block) 时,libDispatch 会向主线程的 RunLoop 发送消息,mainRunLoop会被唤醒,并从消息中取得这个 block,并在回调CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE() 里执行这个 block。但这个逻辑仅限于 dispatch 到主线程,dispatch 到其他线程仍然是由 libDispatch 处理的。

图35


9. AutoReleasePool

当App启动后苹果会在主线程的 RunLoop 里注册两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler():
第1个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647优先级最高,保证创建释放池发生在其他所有回调之前。
第2个 Observer 监视了两个事件:(1) BeforeWaiting(将要进入休眠) 时调用_objc_autoreleasePoolPop() 和_objc_autoreleasePoolPush()释放旧的autoreleasePool并创建新的autoreleasePool;(2) Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放autoreleasePool。这个 Observer 的 order 是 2147483647优先级最低,保证其释放池子发生在其他所有回调之后。
在主线程执行的代码通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,开发者也不必显式创建 Pool 了。如下图所示:


图36

有一些autorelease对象会在事件处理中就被释放了, 这样开发者就不用担心处理事件回调时 autorelease的释放问题了:


图37

如上图所示: 在__handleEventQueue方法之后出现了autoreleasePoolPop方法、并且弱引用对象_object_weak的释放是发生在autoreleasePoolPop之后、可以断定在回调用户代码的函数中(堆栈信息中是函数"__handleEventQueueInternal")有一个 autoreleasepool 把用户代码包裹了。

如果把"__autoreleasing"修饰去掉、在回调用户代码的函数中弱引用对象_object_weak的释放就和auroreleasepool没有关系了:

图38

图39

由上图可知当object调用dealloc方法的时候并没有发现autoreleasePool的踪迹、此处临时变量object的释放是因为其出了作用域而触发的;随后会在sidetable_clearDeallocating方法中释放掉对其进行弱引用的_object_weak对象。

RunLoop与线程

一个线程有五个状态:新建、就绪、运行、阻塞、死亡。runLoop是用来支撑线程阻塞状态的,不让其进入死亡状态。
所以线程和RunLoop是一一对应的,在CFRunLoop源码中也可以看到是以pthread为key去存储的CFRunLoop对象。
runloop有两大作用:阻塞和激活。而激活的目的就是让当前线程开始工作。
从runloop的设计角度来看,等待线程去完成的工作有两种:用户任务和内核任务。用户任务是指source0和call runloop block,它们都是用户添加的任务;内核任务则是那些通过mach_msg调起的任务(source1 & timer),虽然timer是用户主动添加的但是其还是通过mach_msg唤醒的。
NSObject的NSThreadPerformAdditions这种方式本身也是使用runloop去完成的,这种方式简单易用,但是相对于runloop回调有几个不足:
(1) 如果指定的子线程已经是死亡状态,也就是说它本身就没有通过runloop进行阻塞,这个方法就失效了(临时子线程的情况);
(2) 这一系列的方法能够输入的调用参数有限,对于某些参数较多的方法不能调用,但是这一点可以通过一些方法避免比如封装自己的NSInvocation,然后通过[invocation performSelector:@selector(invoke) withThread:…]来完成;
(3) 由于这个方法是oc方法,不能调用c的方法。
使用dispatch_async(queue, block{}) 就能够避免这些问题。dispatch_queue_t内部在执行代码时所使用的线程是不固定的。它底层是一个线程池,每次要执行任务就会向线程池申请一个线程去执行。
如果要频繁地执行某一项任务(比如后台下载)还是在一个固定的常驻线程比较好,反复的切换线程同样会带来性能上的消耗。


RunLoop运行Mode的切换

主线程的runloop在平时的时候是 处在NSDefaultRunLoopMode模式下,当用户滑动滑动视图的时候就会被切换到UITrackingRunLoopMode模式 ,停止滑动以后会再次切换到DefaultRunLoopMode模式。
那么RunLoop的mode是怎样切换的呢?
RunLoop每次切换mode时,runloop都会被停止一次(kCFRunLoopExit)、如下图所示:


图40

当然我们也可以自定义mode的切换:

.h:
@interface TestThreadManager : NSObject
- (void)changeToDefaultMode;
- (void)changeToAveryMode;
@end

.m:
static NSRunLoopMode __RunLoopMode__;
static NSRunLoop *_currentRunloop;
static NSString *AveryModeName = @"_Avery_Test_Mode";  // 自定义Mode的名称

@interface TestThreadManager ()
@property (nonatomic) NSThread *thread;
@end

@implementation TestThreadManager
- (void)changeToAveryMode {
    if (![self.thread.name isEqualToString:[NSThread currentThread].name]) {
        [self performSelector:@selector(changeToAveryMode) onThread:self.thread withObject:nil waitUntilDone:NO modes:@[NSDefaultRunLoopMode]];
    }
    else {
        if (__RunLoopMode__ != AveryModeName) {
            __RunLoopMode__ =  AveryModeName;
            CFRunLoopStop([_currentRunloop getCFRunLoop]);
        }
    }
}
- (void)changeToDefaultMode {
    if (![self.thread.name isEqualToString:[NSThread currentThread].name]) {
        [self performSelector:@selector(changeToDefaultMode) onThread:self.thread withObject:nil waitUntilDone:NO modes:@[AveryModeName]];
    }
    else {
        if (__RunLoopMode__ != NSDefaultRunLoopMode) {
            __RunLoopMode__ = NSDefaultRunLoopMode;
            CFRunLoopStop([_currentRunloop getCFRunLoop]);
        }
    }
}
static void QARunLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
    NSLog(@"activity: %ld",activity);
    NSLog(@"currentMode: %@", [NSRunLoop currentRunLoop].currentMode);
    NSLog(@" ");
}
static void TestThreadRunLoopObserverSetup() {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        CFRunLoopRef cfRunloopRef = CFRunLoopGetCurrent();   // 获取当前线程的cfRunloopRef
        CFRunLoopObserverRef observer;
        observer = CFRunLoopObserverCreate(CFAllocatorGetDefault(),
                                           kCFRunLoopAllActivities,
                                           true,        // is repeat
                                           (2000000-1), // 设定观察者的优先级  CATransaction(2000000)
                                           QARunLoopObserverCallBack,
                                           NULL);
        CFRunLoopAddObserver(cfRunloopRef, observer, kCFRunLoopCommonModes);
        CFRelease(observer);
    });
}

- (instancetype)init {
    if (self = [super init]) {
        self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadEntryPoint:) object:nil];
        [self.thread start];
    }
    return self;
}
- (void)threadEntryPoint:(id)__unused object {
    _currentRunloop = [NSRunLoop currentRunLoop];
    [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSRunLoopCommonModes];
    
    // 将自定义的Mode加入到CommonMode中:
    NSRunLoopMode diy_RunLoopMode = AveryModeName;
    // CFRunLoopAddCommonMode(CFRunLoopRef rl, CFRunLoopMode mode);
    CFRunLoopAddCommonMode(CFRunLoopGetCurrent(), CFBridgingRetain(diy_RunLoopMode));
    
    // 开启runloop监听:
    TestThreadRunLoopObserverSetup();
    
    do {
        if (__RunLoopMode__) {
            [[NSRunLoop currentRunLoop] runMode:__RunLoopMode__ beforeDate:[NSDate distantFuture]];
        }
        else {
            [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
        }
    } while(1);
}
@end


我们能用runloop做什么

创建常驻线程;
解决NSTimer在trackingMode下不能正确执行的问题;
只在某个(某些)特定mode下做某件事比如imageView推迟image的显示 (详见:NSRunLoop (NSOrderedPerform) & NSObject (NSThreadPerformAdditions));
监听runloop的一些状态的变化kCFRunLoopBeforeWaiting & kCFRunLoopExit:在这两个状态你可以做一些预缓存、或者计算的任务,也可以通过其来判断线程是否卡顿。

【 请勿直接转载 - 节约能源从你我做起 】

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 221,576评论 6 515
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,515评论 3 399
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 168,017评论 0 360
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,626评论 1 296
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,625评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,255评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,825评论 3 421
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,729评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,271评论 1 320
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,363评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,498评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,183评论 5 350
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,867评论 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,338评论 0 24
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,458评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,906评论 3 376
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,507评论 2 359

推荐阅读更多精彩内容