RunLoop(从源码分析到Demo分析到mainLoop log分析)

RunLoop的概念

首先我们通过Xcode创建一个Command Line Tool project。发现Xcode给我们自动生成了main.m,包含如下代码:

#import 
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        NSLog(@"Hello, World!");
    }
    return 0;
}

运行后,控制台输出Hello,World!然后程序就退出了。

然而,我们在创建的iOS程序,main.m中包含的代码如下:

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

运行后,程序并不会退出,而是一直处于运行状态,等待用户响应。当我们把main函数稍作修改,如下:

int main(int argc, char * argv[]) {
    @autoreleasepool {
        int i = UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
        return i;
    }
}

return i;这一行加上断点。发现并不会被断点断住。说明并没有执行到这一步,即上一步还没有结束。为什么会这样呢,那就引出了今天的主角:RunLoop!因为UIApplicationMain函数内部帮我创建了一个RunLoop “运行循环”,来保证线程不会退出,能随时处理事件和消息。大致的代码逻辑:

function loop() {
    initialize();
    do {
        var message = get_next_message();
        process_message(message);
    } while (message != quit);
}

会有一个do while循环来等待message,并处理message,只有当while条件不满足时(比如传入 quit 的消息),才会退出循环,让函数返回。而RunLoop内对其进行了进一步的优化:它能很好的管理事件和消息,并且让线程在没有处理消息时休眠以避免资源占用、在有消息到来时立刻被唤醒。下文会对其进行讲解。

在继续之前,我们可以先下载CFRunLoopRef源码,来进行更好的分析和学习。下载地址:https://opensource.apple.com/tarballs/CF/。

通过CoreFoundation框架和Foundation框架,我们发现OSX/iOS 系统中,提供了两个这样的对象:CFRunLoopRef 和 NSRunLoop。

CFRunLoopRef提供了纯C语言的api,所以是线程安全。而NSRunLoop是基于CFRunLoopRef的封装,符合了面向对象的特点,但是不是线程安全的。

RunLoop的创建过程

CFRunLoop.c文件的部分源代如下:

static CFMutableDictionaryRef __CFRunLoops = NULL;
static pthread_t kNilPthreadT = (pthread_t)0;


// t==0 is a synonym for "main thread" that always works //当t==0时代表主线程
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
    if (pthread_equal(t, kNilPthreadT)) {
    t = pthread_main_thread_np();
    }
    //进行加锁操作
    __CFLock(&loopsLock);
    if (!__CFRunLoops) {
        __CFUnlock(&loopsLock);
    // 第一次进入时,初始化全局dict,并先为主线程创建一个 RunLoop。并将mainLoop添加到dict中
    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);
    }
    //通过线程直接从dict中获取loop
    CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    __CFUnlock(&loopsLock);
    if (!loop) {
    //如果获取失败,通过线程创建一个loop,
    CFRunLoopRef newLoop = __CFRunLoopCreate(t);
        __CFLock(&loopsLock);
    loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    if (!loop) {
        //再次确认没有loop,就添加到dict中。
        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())) {
        // 注册一个回调,当线程销毁时,顺便也销毁其对应的 RunLoop。
        _CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
        if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
            _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
        }
    }
    return loop;
}

//获取当前线程的RunLoop
CFRunLoopRef CFRunLoopGetCurrent(void) {
    CHECK_FOR_FORK();
    CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
    if (rl) return rl;
    return _CFRunLoopGet0(pthread_self());
}

//获取主线程的RunLoop
CFRunLoopRef CFRunLoopGetMain(void) {
    CHECK_FOR_FORK();
    static CFRunLoopRef __main = NULL; // no retain needed
    if (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np()); // no CAS needed
    return __main;
}

整体的流程可以概括为以下几步:

  • 通过_CFRunLoopGet0函数传入一条线程。
  • 判断线程是否为主线程并且判断是否已经存在__CFRunLoops(全局CFMutableDictionaryRef)。
  • 如果不存在,说明第一次进入,初始化全局dict,并先为主线程创建一个 RunLoop。并将mainLoop添加到dict中。
  • 如果__CFRunLoops存在,会通过对应线程在全局的__CFRunLoops中查找对应的RunLoop。
  • 如果对应RunLoop不存在,会创建一个新的RunLoop,并添加到__CFRunLoops中。
  • 注册一个回调,当线程销毁时,顺便也销毁其对应的 RunLoop。
  • 返回RunLoop。

我们只能通过CFRunLoopGetMain函数或者CFRunLoopGetCurrent函数来获取RunLoop,通过上面的源代码我们发现,无论是CFRunLoopGetMain函数还是CFRunLoopGetCurrent函数,都是通过对应的线程获取对应的RunLoop,线程和RunLoop是一一对应的,不会重复创建。在主线程,系统会帮我们创建RunLoop,来处理事件。而子线程RunLoop并不会默认开启。所有,子线程操作完成后,线程就被销毁了,如果我们想线程不被销毁,得主动获取一个RunLoop,并且在RunLoop中添加Timer/Source/Observer其中的一个。

RunLoop的内部结构

在 CoreFoundation 里面关于 RunLoop 有5个类:

  • CFRunLoopRef
  • CFRunLoopModeRef
  • CFRunLoopObserverRef
  • CFRunLoopSourceRef
  • CFRunLoopTimerRef

CFRunLoopMode 和 CFRunLoop 的源码结构大致如下:

struct __CFRunLoopMode {
    CFStringRef _name;            // Mode Name, 例如 @"kCFRunLoopDefaultMode"
    CFMutableSetRef _sources0;    // Set
    CFMutableSetRef _sources1;    // Set
    CFMutableArrayRef _observers; // Array
    CFMutableArrayRef _timers;    // Array
    ...
};
 
struct __CFRunLoop {
    CFMutableSetRef _commonModes;     // Set
    CFMutableSetRef _commonModeItems; // Set
    CFRunLoopModeRef _currentMode;    // Current Runloop Mode
    CFMutableSetRef _modes;           // Set
    ...
};

可以看出CFRunLoop和CFRunLoopMode有如下关系:

RunLoop(从源码分析到Demo分析到mainLoop log分析)_第1张图片
RunLoop内部结构图

每个RunLoop中有若干个Mode,每个Mode中又存在着若干个Observer、Source和Timer,每次调用 RunLoop 的主函数时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode。如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。

CFRunLoopObserverRef:是观察者,我们可以通过CFRunLoopObserverCreateWithHandler函数来创建一个观察者(函数会有一个block回调),来对RunLoop进行观察,当RunLoop状态变化时,会触发block回调,回调会返回对应的状态,我们可以在回调里做相应做的操作。可以观察到的状态有:

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry             = (1UL << 0),// 1 即将进入Loop
    kCFRunLoopBeforeTimers      = (1UL << 1),// 2 即将处理 Timer
    kCFRunLoopBeforeSources     = (1UL << 2),// 4 即将处理 Source
    kCFRunLoopBeforeWaiting     = (1UL << 5),// 32 即将进入休眠
    kCFRunLoopAfterWaiting      = (1UL << 6),// 64 刚从休眠中唤醒
    kCFRunLoopExit              = (1UL << 7),// 128 即将退出Loop
    kCFRunLoopAllActivities     = 0x0FFFFFFFU// 所有状态类型
};

我们来看下具体的举例(Demo地址)大家不妨下载看看:

- (void)observer {
    // 创建observer
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        switch (activity) {
            case kCFRunLoopEntry:
                NSLog(@"监听到即将进入RunLoop------%zd----%@",activity,[[NSRunLoop currentRunLoop] currentMode]);
                break;
            case kCFRunLoopBeforeTimers:
                NSLog(@"监听到即将处理Timer------%zd----%@",activity,[[NSRunLoop currentRunLoop] currentMode]);
                break;
            case kCFRunLoopBeforeSources:
                NSLog(@"监听到即将处理Source------%zd----%@",activity,[[NSRunLoop currentRunLoop] currentMode]);
                break;
            case kCFRunLoopBeforeWaiting:
                NSLog(@"监听到即将进入睡眠------%zd----%@",activity,[[NSRunLoop currentRunLoop] currentMode]);
                break;
            case kCFRunLoopAfterWaiting:
                NSLog(@"监听到即将从睡眠中醒来------%zd----%@",activity,[[NSRunLoop currentRunLoop] currentMode]);
                break;
            case kCFRunLoopExit:
                NSLog(@"监听到即将从退出RunLoop------%zd----%@",activity,[[NSRunLoop currentRunLoop] currentMode]);
                break;
            default:
                break;
        }
    });
    
    /*
    CFRunLoopAddObserver函数有三个参数:
     * 第一个:传入一个RunLoop,CFRunLoopGetCurrent()获取当前的RunLoop
     * 第二个:传入一个观察者,observer就是新创建的观察者
     * 第三个:传入Mode,kCFRunLoopCommonModes指定要监听的Mode
    */
    
    // 添加观察者到RunLoop:来监听RunLoop的状态
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopCommonModes);
    // 释放Observer
    CFRelease(observer);
}

除了在viewDidLoad里面调用了[self observer];方法,还在Main.storyboard中添加了UITextView。下面我们拿打印结果阐述。

运行后的模拟器截图:

RunLoop(从源码分析到Demo分析到mainLoop log分析)_第2张图片
模拟器截图

刚刚触发监听的截图:

RunLoop(从源码分析到Demo分析到mainLoop log分析)_第3张图片
刚刚触发监听的截图

我们发现RunLoop在不停的处理Timer事件和Source事件。虽然我们没有主动添加Timer和Source事件,但是系统会添加Timer和Source,比如:模拟器的时间发生变化时就会触发kCFRunLoopBeforeTimers回调(每分钟),如下图:

当视图显示完成稳定后无事件的截图(看具体的时间点发现是在整分钟的时候触发的回调):


RunLoop(从源码分析到Demo分析到mainLoop log分析)_第4张图片
稳定后无事件的截图

当无其他点击等事件时,会进入睡眠状态,有事件时会立刻苏醒过来处理事件。

拖拽textView时的截图:

RunLoop(从源码分析到Demo分析到mainLoop log分析)_第5张图片
拖拽textView时的截图

发现在拖拽textView的时候,当前RunLoop的Mode[[NSRunLoop currentRunLoop] currentMode]变成了从kCFRunLoopDefaultMode变成了UITrachingRunloopMode,RunLoop也触发了kCFRunLoopExit(即将退出RunLoop)的回调和kCFRunLoopEntry(即将进入RunLoop)的回调,也就是:退出了当前RunLoop,进入了一个新的RunLoop,也就验证了上面所说的:每次调用 RunLoop 的主函数时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode。如果需要切换 Mode,只能退出 当前RunLoop,再重新指定一个 Mode 进入。

如果到这里看不懂,没有关系,后面会对RunLoop 的内部逻辑进行分析。看了后面的分析在回过头来看就能明白了。

CFRunLoopSourceRef:是事件产生的地方。Source分为Source0 和 Source1。

  • Source0 只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 。
  • Source1 包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。这种 Source 能主动唤醒 RunLoop 的线程。

CFRunLoopTimerRef:基于时间的触发器,CFRunLoopTimerRef是Core Foundation提供的基础定时器,NSTimer则是建立在CFRunLoopTimerRef之上的高层组件。当Timer被加入到RunLoop时,RunLoop会注册对应的时间点,当达到时间时,RunLoop会被唤醒,执行创建Timer时的回调。

RunLoop 的 Mode

我们在viewDidLoad中通过下面代码打印当前RunLoop

    NSRunLoop *runloop = [NSRunLoop currentRunLoop];
    NSLog(@"%@",runloop);

我们通过打印结果和分析结合来看(可以先到下面看分析,再回到上面找对应的打印结果或者下载demo,在控制栏对照来看。)
打印结果如下:

2018-03-13 23:14:28.671838+0800 runloop[30490:55545179] {wakeup port = 0x1b03, stopped = false, ignoreWakeUps = false, 
current mode = kCFRunLoopDefaultMode,
common modes = {type = mutable set, count = 2,
entries =>
    0 : {contents = "UITrackingRunLoopMode"}
    2 : {contents = "kCFRunLoopDefaultMode"}
}
,
common mode items = {type = mutable set, count = 16,
entries =>
    0 : {signalled = No, valid = Yes, order = -1, context = {version = 0, info = 0x0, callout = PurpleEventSignalCallback (0x1153c16c6)}}
    3 : {valid = Yes, activities = 0xa0, repeats = Yes, order = 2000000, callout = _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv (0x1161d3648), context = }
    4 : {signalled = No, valid = Yes, order = 0, context =  {port = 23555, subsystem = 0x1119cb088, context = 0x600000224660}}
    6 : {signalled = No, valid = Yes, order = -1, context = {version = 1, info = 0x2f03, callout = PurpleEventCallback (0x1153c3bef)}}
    7 : {signalled = Yes, valid = Yes, order = 0, context = {version = 0, info = 0x6000000bf260, callout = FBSSerialQueueRunLoopSourceHandler (0x114b2d821)}}
    8 : {signalled = No, valid = Yes, order = 0, context =  {port = 4e1b, callback = 0x127cb11c0}}
    9 : {valid = Yes, activities = 0xa0, repeats = Yes, order = 2001000, callout = _afterCACommitHandler (0x110854057), context = }
    10 : {valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x110824276), context = {type = mutable-small, count = 1, values = (
    0 : <0x7f874a802048>
)}}
    11 : {valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x110824276), context = {type = mutable-small, count = 1, values = (
    0 : <0x7f874a802048>
)}}
    12 : {valid = Yes, activities = 0x20, repeats = Yes, order = 0, callout = _UIGestureRecognizerUpdateObserver (0x110e121a9), context = }
    13 : {valid = Yes, activities = 0x4, repeats = No, order = 0, callout = _runLoopObserverWithBlockContext (0x11034eeb0), context = }
    16 : {signalled = No, valid = Yes, order = 0, context =  {port = 22027, subsystem = 0x1119b0ce8, context = 0x0}}
    18 : {signalled = No, valid = Yes, order = 0, context =  {port = 5527, callback = 0x0}}
    20 : {signalled = No, valid = Yes, order = -1, context = {version = 0, info = 0x604000159860, callout = __handleEventQueue (0x11118cdcc)}}
    21 : {valid = Yes, activities = 0xa0, repeats = Yes, order = 1999000, callout = _beforeCACommitHandler (0x110853fdc), context = }
    22 : {signalled = No, valid = Yes, order = -2, context = {version = 0, info = 0x60c000256260, callout = __handleHIDEventFetcherDrain (0x11118cdd8)}}
}
,
modes = {type = mutable set, count = 4,
entries =>
    2 : {name = UITrackingRunLoopMode, port set = 0x2603, queue = 0x604000159910, source = 0x60400019e100 (not fired), timer port = 0x2a03, 
    sources0 = {type = mutable set, count = 4,
entries =>
    0 : {signalled = No, valid = Yes, order = -1, context = {version = 0, info = 0x0, callout = PurpleEventSignalCallback (0x1153c16c6)}}
    3 : {signalled = No, valid = Yes, order = -2, context = {version = 0, info = 0x60c000256260, callout = __handleHIDEventFetcherDrain (0x11118cdd8)}}
    4 : {signalled = Yes, valid = Yes, order = 0, context = {version = 0, info = 0x6000000bf260, callout = FBSSerialQueueRunLoopSourceHandler (0x114b2d821)}}
    5 : {signalled = No, valid = Yes, order = -1, context = {version = 0, info = 0x604000159860, callout = __handleEventQueue (0x11118cdcc)}}
}
,
    sources1 = {type = mutable set, count = 5,
entries =>
    0 : {signalled = No, valid = Yes, order = 0, context =  {port = 23555, subsystem = 0x1119cb088, context = 0x600000224660}}
    1 : {signalled = No, valid = Yes, order = 0, context =  {port = 5527, callback = 0x0}}
    2 : {signalled = No, valid = Yes, order = -1, context = {version = 1, info = 0x2f03, callout = PurpleEventCallback (0x1153c3bef)}}
    3 : {signalled = No, valid = Yes, order = 0, context =  {port = 4e1b, callback = 0x127cb11c0}}
    5 : {signalled = No, valid = Yes, order = 0, context =  {port = 22027, subsystem = 0x1119b0ce8, context = 0x0}}
}
,
    observers = (
    "{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x110824276), context = {type = mutable-small, count = 1, values = (\n\t0 : <0x7f874a802048>\n)}}",
    "{valid = Yes, activities = 0x20, repeats = Yes, order = 0, callout = _UIGestureRecognizerUpdateObserver (0x110e121a9), context = }",
    "{valid = Yes, activities = 0x4, repeats = No, order = 0, callout = _runLoopObserverWithBlockContext (0x11034eeb0), context = }",
    "{valid = Yes, activities = 0xa0, repeats = Yes, order = 1999000, callout = _beforeCACommitHandler (0x110853fdc), context = }",
    "{valid = Yes, activities = 0xa0, repeats = Yes, order = 2000000, callout = _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv (0x1161d3648), context = }",
    "{valid = Yes, activities = 0xa0, repeats = Yes, order = 2001000, callout = _afterCACommitHandler (0x110854057), context = }",
    "{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x110824276), context = {type = mutable-small, count = 1, values = (\n\t0 : <0x7f874a802048>\n)}}"
),
    timers = (null),
    currently 542646869 (1754787729427038) / soft deadline in: 1.84449893e+10 sec (@ -1) / hard deadline in: 1.84449893e+10 sec (@ -1)
},

    3 : {name = GSEventReceiveRunLoopMode, port set = 0x2c03, queue = 0x6040001599c0, source = 0x60400019e2a0 (not fired), timer port = 0x2e03, 
    sources0 = {type = mutable set, count = 1,
entries =>
    0 : {signalled = No, valid = Yes, order = -1, context = {version = 0, info = 0x0, callout = PurpleEventSignalCallback (0x1153c16c6)}}
}
,
    sources1 = {type = mutable set, count = 1,
entries =>
    2 : {signalled = No, valid = Yes, order = -1, context = {version = 1, info = 0x2f03, callout = PurpleEventCallback (0x1153c3bef)}}
}
,
    observers = (null),
    timers = (null),
    currently 542646869 (1754787749292029) / soft deadline in: 1.84449893e+10 sec (@ -1) / hard deadline in: 1.84449893e+10 sec (@ -1)
},

    4 : {name = kCFRunLoopDefaultMode, port set = 0x1c03, queue = 0x6000001595a0, source = 0x60000019e440 (not fired), timer port = 0x1e03, 
    sources0 = {type = mutable set, count = 4,
entries =>
    0 : {signalled = No, valid = Yes, order = -1, context = {version = 0, info = 0x0, callout = PurpleEventSignalCallback (0x1153c16c6)}}
    3 : {signalled = No, valid = Yes, order = -2, context = {version = 0, info = 0x60c000256260, callout = __handleHIDEventFetcherDrain (0x11118cdd8)}}
    4 : {signalled = Yes, valid = Yes, order = 0, context = {version = 0, info = 0x6000000bf260, callout = FBSSerialQueueRunLoopSourceHandler (0x114b2d821)}}
    5 : {signalled = No, valid = Yes, order = -1, context = {version = 0, info = 0x604000159860, callout = __handleEventQueue (0x11118cdcc)}}
}
,
    sources1 = {type = mutable set, count = 5,
entries =>
    0 : {signalled = No, valid = Yes, order = 0, context =  {port = 23555, subsystem = 0x1119cb088, context = 0x600000224660}}
    1 : {signalled = No, valid = Yes, order = 0, context =  {port = 5527, callback = 0x0}}
    2 : {signalled = No, valid = Yes, order = -1, context = {version = 1, info = 0x2f03, callout = PurpleEventCallback (0x1153c3bef)}}
    3 : {signalled = No, valid = Yes, order = 0, context =  {port = 4e1b, callback = 0x127cb11c0}}
    5 : {signalled = No, valid = Yes, order = 0, context =  {port = 22027, subsystem = 0x1119b0ce8, context = 0x0}}
}
,
    observers = (
    "{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x110824276), context = {type = mutable-small, count = 1, values = (\n\t0 : <0x7f874a802048>\n)}}",
    "{valid = Yes, activities = 0x20, repeats = Yes, order = 0, callout = _UIGestureRecognizerUpdateObserver (0x110e121a9), context = }",
    "{valid = Yes, activities = 0x4, repeats = No, order = 0, callout = _runLoopObserverWithBlockContext (0x11034eeb0), context = }",
    "{valid = Yes, activities = 0xa0, repeats = Yes, order = 1999000, callout = _beforeCACommitHandler (0x110853fdc), context = }",
    "{valid = Yes, activities = 0xa0, repeats = Yes, order = 2000000, callout = _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv (0x1161d3648), context = }",
    "{valid = Yes, activities = 0xa0, repeats = Yes, order = 2001000, callout = _afterCACommitHandler (0x110854057), context = }",
    "{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x110824276), context = {type = mutable-small, count = 1, values = (\n\t0 : <0x7f874a802048>\n)}}"
),
    timers = {type = mutable-small, count = 2, values = (
    0 : {valid = Yes, firing = No, interval = 0, tolerance = 0, next fire date = 542646863 (-5.80513895 @ 1754781946507642), callout = (Delayed Perform) UIApplication _accessibilitySetUpQuickSpeak (0x10f493d9c / 0x110d1c92d) (/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks/UIKit.framework/UIKit), context = }
    1 : {valid = Yes, firing = No, interval = 0.5, tolerance = 0, next fire date = 542646863 (-5.657637 @ 1754782094433008), callout = (NSTimer) [UITextSelectionView caretBlinkTimerFired:] (0x10f4a8acb / 0x10f1d6536) (/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks/UIKit.framework/UIKit), context = }
)},
    currently 542646869 (1754787749365050) / soft deadline in: 1.84467441e+10 sec (@ 1754781946507642) / hard deadline in: 1.84467441e+10 sec (@ 1754781946507642)
},

    5 : {name = kCFRunLoopCommonModes, port set = 0x3f0f, queue = 0x604000159bd0, source = 0x60400019eac0 (not fired), timer port = 0x570f, 
    sources0 = (null),
    sources1 = (null),
    observers = (null),
    timers = (null),
    currently 542646869 (1754787752303309) / soft deadline in: 1.84449893e+10 sec (@ -1) / hard deadline in: 1.84449893e+10 sec (@ -1)
},

}
}

从上往下看发现结果和上面说的CFRunLoopMode 和 CFRunLoop 的源码结构是对应上的:

  • current mode = kCFRunLoopDefaultMode:现在处于kCFRunLoopDefaultMode下。
  • common modes:一个 Mode 可以将自己标记为”Common”属性(通过将其 ModeName 添加到 RunLoop 的 “commonModes” 中)。每当 RunLoop 的内容发生变化时,RunLoop 都会自动将 _commonModeItems 里的 Source/Observer/Timer 同步到具有 “Common” 标记的所有Mode里。 上面我们可以看到被标记为“Common”属性的有2种Mode(count = 2):
    • UITrackingRunLoopMode
    • kCFRunLoopDefaultMode
  • common mode items:count = 16 ,包含了所有被添加到Mode的Source/Observer/Timer。
  • modes:所有被添加到RunLoop中的RunLoopMode,count = 4,包含了四个Mode如下:
    • name = UITrackingRunLoopMode:scrollView滚动时候会切换到这种Mode,上文讲Observer的具体demo中,在打印Observer监听时,当拖拽textView的时候就会切换到这种Mode。从上面打印的RunLoop里面的UITrackingRunLoopMode,可以看出它有包含了sources0(count = 4包含了4个Source0)/sources1(count = 5包含了5个Source1)/observers(7个Observer)/timers(没有Timer)。
    • name = GSEventReceiveRunLoopMode: 苹果接受系统事件的内部 Mode,通常用不到。从上面打印的RunLoop里面的GSEventReceiveRunLoopMode,可以看出它仅仅包含了一个Source0和一个Source1。
    • name = kCFRunLoopDefaultMode:App的默认 Mode,通常主线程是在这个 Mode 下运行的,从上面打印的RunLoop里面的kCFRunLoopDefaultMode,可以看出它包含了4个Source0、5个Source1、7个Observer和2个Timer。
    • name = kCFRunLoopCommonModes:可以看到它里面sources0/sources1/observers/timers都是null,可见这是一个占位的 Mode,没有实际作用。
    • 其实还有一种Mode,这里没有被添加到当前的RunLoop UIInitializationRunLoopMode:它在刚启动 App 时进入的第一个 Mode,启动完成后就不再使用。所以在这里看不到这个Mode。

在上文讲Observer的具体demo中,有如下代码:

// 添加观察者到RunLoop:来监听RunLoop的状态
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopCommonModes);

CFRunLoopAddObserver函数有三个参数:

  • 第一个:传入一个RunLoop,CFRunLoopGetCurrent()获取当前的RunLoop
  • 第二个:传入一个观察者,observer就是新创建的观察者
  • 第三个:传入Mode,kCFRunLoopCommonModes指定要监听的Mode

其中有个kCFRunLoopCommonModes就是指被标记成“Common”的Mode,上面打印和分析发现,其实就是指UITrackingRunLoopModekCFRunLoopDefaultMode这两种Mode。所以在拖拽textView的时候和不拖拽的时候都能收到回调,如果将kCFRunLoopCommonModes改为kCFRunLoopDefaultMode,会发现,当拖拽的时候收不到回调。大家可以自己试试。

和将NSTimer添加到RunLoop时,指定Mode回调是一样的道理,这里就不再阐述了。已经够啰嗦了。。。

AutoreleasePool的创建和释放时机

在上面对RunLoop的打印中,我们发现系统添加了两个Observer

  • :activities = 0x1, repeats = Yes, order = -2147483647 它的activities = 0x1,而0x1转换为10进制1,就是表示监听的kCFRunLoopEntry
  • :activities = 0xa0, repeats = Yes, order = 2147483647 它的activities = 0xa0,而0xa0转换为10进制160,发现没有160这个值的状态,其实它是kCFRunLoopBeforeWaiting | kCFRunLoopExit,也就是32+128

它们都触发了_wrapRunLoopWithAutoreleasePoolHandler回调。

第一个Observer监听kCFRunLoopEntry(即将进入RunLoop)其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池,其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。

第二个 Observer 监视了kCFRunLoopBeforeWaiting() 其回调内会调用_objc_autoreleasePoolPop()_objc_autoreleasePoolPush() 释放旧的AutoreleasePool并创建新AutoreleasePool;kCFRunLoopExit(即将退出RunLoop) 其回调内会调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。

前几天有写一篇iOS内存管理的文章,里面有讲AutoreleasePool的内部实现。

RunLoop 的内部逻辑

根据CoreFoundation框架RunLoop源码,RunLoop 内部的逻辑官方文档如下:

RunLoop(从源码分析到Demo分析到mainLoop log分析)_第6张图片
苹果官方文档RunLoop内部逻辑

郭大神描述的内部逻辑如下:

RunLoop(从源码分析到Demo分析到mainLoop log分析)_第7张图片
RunLoop内部逻辑

大体上他们是一致的。

其内部代码整理如下:

/// 用DefaultMode启动
void CFRunLoopRun(void) {
    CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
}
 
/// 用指定的Mode启动,允许设置RunLoop超时时间
int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) {
    return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}
 
/// RunLoop的实现
int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) {
    
    /// 首先根据modeName找到对应mode
    CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);
    /// 如果mode里没有source/timer/observer, 直接返回。
    if (__CFRunLoopModeIsEmpty(currentMode)) return;
    
    /// 1. 通知 Observers: RunLoop 即将进入 loop。
    __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);
    
    /// 内部函数,进入loop
    __CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {
        
        Boolean sourceHandledThisLoop = NO;
        int retVal = 0;
        do {
 
            /// 2. 通知 Observers: RunLoop 即将触发 Timer 回调。
            __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
            /// 3. 通知 Observers: RunLoop 即将触发 Source0 (非port) 回调。
            __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
            /// 执行被加入的block
            __CFRunLoopDoBlocks(runloop, currentMode);
            
            /// 4. RunLoop 触发 Source0 (非port) 回调。
            sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);
            /// 执行被加入的block
            __CFRunLoopDoBlocks(runloop, currentMode);
 
            /// 5. 如果有 Source1 (基于port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息。
            if (__Source0DidDispatchPortLastTime) {
                Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)
                if (hasMsg) goto handle_msg;
            }
            
            /// 通知 Observers: RunLoop 的线程即将进入休眠(sleep)。
            if (!sourceHandledThisLoop) {
                __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
            }
            
            /// 7. 调用 mach_msg 等待接受 mach_port 的消息。线程将进入休眠, 直到被下面某一个事件唤醒。
            /// • 一个基于 port 的Source 的事件。
            /// • 一个 Timer 到时间了
            /// • RunLoop 自身的超时时间到了
            /// • 被其他什么调用者手动唤醒
            __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {
                mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg
            }
 
            /// 8. 通知 Observers: RunLoop 的线程刚刚被唤醒了。
            __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);
            
            /// 收到消息,处理消息。
            handle_msg:
 
            /// 9.1 如果一个 Timer 到时间了,触发这个Timer的回调。
            if (msg_is_timer) {
                __CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
            } 
 
            /// 9.2 如果有dispatch到main_queue的block,执行block。
            else if (msg_is_dispatch) {
                __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
            } 
 
            /// 9.3 如果一个 Source1 (基于port) 发出事件了,处理这个事件
            else {
                CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);
                sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);
                if (sourceHandledThisLoop) {
                    mach_msg(reply, MACH_SEND_MSG, reply);
                }
            }
            
            /// 执行加入到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);
    }
    
    /// 10. 通知 Observers: RunLoop 即将退出。
    __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
}

可以看到,实际上 RunLoop 就是这样一个函数,其内部是一个 do-while 循环。当你调用 CFRunLoopRun() 时,线程就会一直停留在这个循环里;直到超时或被手动停止,该函数才会返回。

PerformSelecter

当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。我们可以在没有获取RunLoop的子线程进行测试验证。viewDidLoad中调用performSelectorTest方法。

- (void)performSelectorTest {
    NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
    self.thread = thread;
    [thread start];
}

- (void)run {
//    [self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:YES modes:@[NSDefaultRunLoopMode]];

    [self performSelector:@selector(test) withObject:nil afterDelay:2.0];

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

- (void)test {
    NSLog(@"-----------------");
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    [self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
}

当我们把下面代码注释就不会触发test方法了。

    NSRunLoop *runloop = [NSRunLoop currentRunLoop];
    [runloop addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
    [runloop run];

这里有个疑问:
看郭大神的博客说:performSelector:onThread:也是需要RunLoop,但是我在子线程执行的run 方法中执行[self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:YES modes:@[NSDefaultRunLoopMode]];而且没有主动获取RunLoop的情况下,也是调用test方法。知道的大神指导下。

RunLoop其他举例

UIImageView在NSDefaultRunLoopMode下展示图片

    // 只在NSDefaultRunLoopMode模式下显示图片
    [self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"image"] afterDelay:0 inModes:@[NSDefaultRunLoopMode]];

创建常驻子线程

    //在子线程中获取并启动添加了Source的RunLoop
    [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop] run];

除了获取当前线程的RunLoop外,还得添加Source或者Timer。并且调用run方法。要不然RunLoop中没有Source和Timer,会直接退出当前RunLoop不会进入循环。

欢迎指导沟通!


参考文献:
官方文档
深入理解RunLoop
RunLoop问题集

你可能感兴趣的:(RunLoop(从源码分析到Demo分析到mainLoop log分析))