iOS多线程--RunLoop

1 RunLoop简介

神秘的RunLoop。一个应用开始运行以后放在那里,如果不对它进行任何操作,这个应用就像静止了一样,不会自发的有任何动作发生,但是如果我们点击界面上的一个按钮,这个时候就会有对应的按钮响应事件发生。给我们的感觉就像应用一直处于随时待命的状态,在没人操作的时候它一直在休息,在让它干活的时候,它就能立刻响应。其实,这就是RunLoop的功劳。
RunLoop实际上是一个对象,这个对象在循环中用来处理程序运行过程中出现的各种事件(比如说触摸事件、UI刷新事件、定时器事件、Selector事件),从而保持程序的持续运行;而且在没有事件处理的时候,会进入睡眠模式,从而节省CPU资源,提高程序性能。

2 RunLoop和线程

说说线程,有些线程执行的任务是一条直线,起点到终点;而另一些线程要干的活则是一个圆,不断循环,直到通过某种方式将它终止。在iOS中,圆型的线程就是通过RunLoop不停的循环实现的。RunLoop和线程是紧密相连的,可以这样说RunLoop是为了线程而生,没有线程,它就没有存在的必要。

  • 一条线程对应一个RunLoop对象,每条线程(包括主线程)都有唯一一个与之对应的RunLoop对象。
  • 我们只能在当前线程中操作当前线程的RunLoop,而不能去操作其他线程的RunLoop。
  • RunLoop对象在第一次获取RunLoop时创建,销毁则是在线程结束的时候。
  • 主线程的RunLoop对象系统自动帮助我们创建好了,而子线程的RunLoop对象需要我们主动创建。

RunLoop的伪代码表现方式为如下:

int main(int argc, char * argv[]) {        
    BOOL running = YES;
    do {
        // 执行各种任务,处理各种事件
        // ......
    } while (running);
    return 0;
}

2.1 主线程的RunLoop对象默认创建及启动

iOS的应用程序里面,程序启动后会有一个如下的main() 函数:

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

重点是UIApplicationMain() 函数,这个方法会为main thread 设置一个NSRunLoop 对象,这就解释了本文开始说的为什么我们的应用可以在无人操作的时候休息,需要让它干活的时候又能立马响应。

2.2 子线程的RunLoop对象需手动创建及启动

需要更多的线程交互则可以手动配置和启动,如果线程只是去执行一个长时间的已确定的任务则不需要。

//获取(创建)当前线程的RunLoop
NSRunLoop  *runloop = [NSRunLoop currentRunLoop];
//启动
[runloop run];

(有待后续例证,先把概念贴上)Cocoa中的NSRunLoop类并不是线程安全的,我们不能在一个线程中去操作另外一个线程的run loop对象,那很可能会造成意想不到的后果。不过幸运的是CoreFundation中的不透明类CFRunLoopRef是线程安全的,而且两种类型的run loop完全可以混合使用。Cocoa中的NSRunLoop类可以通过实例方法:- (CFRunLoopRef)getCFRunLoop;获取对应的CFRunLoopRef类,来达到线程安全的目的。

3 RunLoop相关类和方法

下面我们来了解一下Core Foundation框架下关于RunLoop的5个类,只有弄懂这几个类的含义,我们才能深入了解RunLoop运行机制。

CFRunLoopRef:代表RunLoop的对象
CFRunLoopModeRef:RunLoop的运行模式
CFRunLoopSourceRef:就是RunLoop模型图中提到的输入源/事件源
CFRunLoopTimerRef:就是RunLoop模型图中提到的定时源
CFRunLoopObserverRef:观察者,能够监听RunLoop的状态改变

NSRunLoop关系类图.png

一个RunLoop对象(CFRunLoopRef)中包含若干个运行模式(CFRunLoopModeRef),简称Mode。而每一个运行模式下又包含若干个输入源(CFRunLoopSourceRef)、定时源(CFRunLoopTimerRef)、观察者(CFRunLoopObserverRef),这三个统称为Mode Item。

  • 每次RunLoop启动时,只能指定其中一个运行模式(CFRunLoopModeRef),这个运行模式(CFRunLoopModeRef)被称作CurrentMode。如果需要切换运行模式(CFRunLoopModeRef),只能退出Loop,再重新指定一个运行模式(CFRunLoopModeRef)进入。这样做主要是为了分隔开不同组的Mode Item,不同mode下的mode item互不影响。
  • 一个 Item可被加入不同的Mode。但一个 Item 被重复加入同一个 Mode 时是不会有效果的。如果一个 Mode 中一个 Item 都没有,RunLoop退出。(不过如果仅仅依赖没有Mode Item来让RunLoop退出,这做法是不可靠的)

3.1 CFRunLoopRef RunLoop对象

CFRunLoopRef就是Core Foundation框架下RunLoop对象类。我们可通过以下方式来获取RunLoop对象:

CFRunLoopGetCurrent(); // 获得当前线程的RunLoop对象
CFRunLoopGetMain(); // 获得主线程的RunLoop对象

在Foundation框架下获取RunLoop对象类的方法如下:

[NSRunLoop currentRunLoop]; // 获得当前线程的RunLoop对象
[NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象

由苹果源码(参看下图),这两个函数的内部实现看出:
(1)线程和RunLoop是一一对应的,这种对应关系用一个字典保存起来,key是pthread,value是CFRunLoopRef。
(2) RunLoop在第一次获取时创建,然后在线程结束时销毁。
(3)子线程如果不手动获取RunLoop,它是一直都不会有的。

image.png
image.png

3.2 CFRunLoopModeRef 运行模式

CFRunLoopMode 的结构大致如下:

struct __CFRunLoopMode {
    CFStringRef _name;            // Mode Name, 例如 @"kCFRunLoopDefaultMode"
    CFMutableSetRef _sources0;    // Set
    CFMutableSetRef _sources1;    // Set
    CFMutableArrayRef _observers; // Array
    CFMutableArrayRef _timers;    // Array
    ...
};

系统默认定义了多种运行模式(CFRunLoopModeRef),如下:

  1. kCFRunLoopDefaultMode:App的默认运行模式,通常主线程是在这个运行模式下运行
  2. UITrackingRunLoopMode:跟踪用户交互事件(用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他Mode影响)
  3. UIInitializationRunLoopMode:在刚启动App时第进入的第一个 Mode,启动完成后就不再使用
  4. GSEventReceiveRunLoopMode:接受系统内部事件,通常用不到
  5. kCFRunLoopCommonModes:是一种常用的模式集合,包含 NSDefaultRunLoopMode 、UITrackingRunLoopMode。这个模式存在的好处是,如果现在异步线程有个timer启动,不需要再所有的 RunLoop Mode 中都去加一遍,只需要直接在 NSRunLoopCommonModes 加一次即可。

其中kCFRunLoopDefaultMode、UITrackingRunLoopMode、kCFRunLoopCommonModes是我们开发中需要用到的模式。

RunLoop 其实内部就是do-while循环,在这个循环内部不断地处理各种任务(比如Source、Timer、Observer),通过判断result的值实现的。所以 可以看成是一个死循环。如果没有RunLoop,UIApplicationMain 函数执行完毕之后将直接返回,就是说程序一启动然后就结束。

//CFRunLoopRun
void CFRunLoopRun(void) {   /* DOES CALLOUT */
    int32_t result;
    do {
        result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
        CHECK_FOR_FORK();
    } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}

//CFRunLoopRunInMode
SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
    CHECK_FOR_FORK();
    return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}

CFRunLoopRunInMode方法可以设置runLoop运行在哪个mode下modeName,超时时间seconds,以及是否处理完事件就返回returnAfterSourceHandled。
但这两个方法实际调用的是同一个方法CFRunLoopRunSpecific,其返回是一个SInt32类型的值,根据返回值,来决定runLoop的运行状况。

3.2.1 Mode 及操作接口

CFRunLoopModeRef 类并没有对外暴露,只是通过 CFRunLoopRef 的接口进行了封装。CFRunLoopRef 获取 Mode 的接口:

CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);
CFRunLoopRunInMode(CFStringRef modeName, ...);

我们没有办法直接创建一个CFRunLoopMode对象,但是我们可以调用CFRunLoopAddCommonMode 传入一个字符串向 RunLoop 中添加 Mode,传入的字符串即为 Mode 的名字,Mode对象应该是此时在RunLoop内部创建的。

这里看一下CFRunLoopAddCommonMode源码。

void CFRunLoopAddCommonMode(CFRunLoopRef rl, CFStringRef modeName) {
    CHECK_FOR_FORK();
    if (__CFRunLoopIsDeallocating(rl)) return;
    __CFRunLoopLock(rl);
    //看rl中是否已经有这个mode,如果有就什么都不做
    if (!CFSetContainsValue(rl->_commonModes, modeName)) {
        CFSetRef set = rl->_commonModeItems ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModeItems) : NULL;
        //把modeName添加到RunLoop的_commonModes中
        CFSetAddValue(rl->_commonModes, modeName);
        if (NULL != set) {
            CFTypeRef context[2] = {rl, modeName};
            /* add all common-modes items to new mode */
            //这里调用CFRunLoopAddSource/CFRunLoopAddObserver/CFRunLoopAddTimer的时候会调用
            //__CFRunLoopFindMode(rl, modeName, true),CFRunLoopMode对象在这个时候被创建
            CFSetApplyFunction(set, (__CFRunLoopAddItemsToCommonMode), (void *)context);
            CFRelease(set);
        }
    } else {
    }
    __CFRunLoopUnlock(rl);
}

可以看得出:

  • modeName不能重复,modeName是mode的唯一标识符
  • RunLoop的_commonModes数组存放所有被标记为common的mode的名称
  • 添加commonMode会把commonModeItems数组中的所有source同步到新添加的mode中
  • CFRunLoopMode对象在CFRunLoopAddItemsToCommonMode函数中调用CFRunLoopFindMode时被创建

3.2.2 mode item 及操作接口

Source/Timer/Observer 被统称为 mode item,一个 item 可以被同时加入多个 mode。但一个 item 被重复加入同一个 mode 时是不会有效果的。如果一个 mode 中一个 item 都没有,则 RunLoop 会直接退出,不进入循环。

Mode 暴露的管理 mode item 的接口有下面几个,通过他们我们可以为RunLoop 添加 Source(ModeItem)。

void CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef mode)
void CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef mode)
void CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef mode)
void CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef mode)
void CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode)
void CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode)

你只能通过 mode name 来操作内部的 mode,当你传入一个新的 mode name 但 RunLoop 内部没有对应 mode 时,RunLoop会自动帮你创建对应的 CFRunLoopModeRef。对于一个 RunLoop 来说,其内部的 mode 只能增加不能删除。

这里只分析其中 CFRunLoopAddSource 的源码

//添加source事件
void CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef rls, CFStringRef modeName) {    /* DOES CALLOUT */
    CHECK_FOR_FORK();
    if (__CFRunLoopIsDeallocating(rl)) return;
    if (!__CFIsValid(rls)) return;
    Boolean doVer0Callout = false;
    __CFRunLoopLock(rl);
    //如果是kCFRunLoopCommonModes
    if (modeName == kCFRunLoopCommonModes) {
        //如果runloop的_commonModes存在,则copy一个新的复制给set
        CFSetRef set = rl->_commonModes ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModes) : NULL;
       //如果runl _commonModeItems为空
        if (NULL == rl->_commonModeItems) {
            //先初始化
            rl->_commonModeItems = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
        }
        //把传入的CFRunLoopSourceRef加入_commonModeItems
        CFSetAddValue(rl->_commonModeItems, rls);
        //如果刚才set copy到的数组里有数据
        if (NULL != set) {
            CFTypeRef context[2] = {rl, rls};
            /* add new item to all common-modes */
            //则把set里的所有mode都执行一遍__CFRunLoopAddItemToCommonModes函数
            CFSetApplyFunction(set, (__CFRunLoopAddItemToCommonModes), (void *)context);
            CFRelease(set);
        }
        //以上分支的逻辑就是,如果你往kCFRunLoopCommonModes里面添加一个source,那么所有_commonModes里的mode都会添加这个source
    } else {
        //根据modeName查找mode
        CFRunLoopModeRef rlm = __CFRunLoopFindMode(rl, modeName, true);
        //如果_sources0不存在,则初始化_sources0,_sources0和_portToV1SourceMap
        if (NULL != rlm && NULL == rlm->_sources0) {
            rlm->_sources0 = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
            rlm->_sources1 = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
            rlm->_portToV1SourceMap = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, NULL);
        }
        //如果_sources0和_sources1中都不包含传入的source
        if (NULL != rlm && !CFSetContainsValue(rlm->_sources0, rls) && !CFSetContainsValue(rlm->_sources1, rls)) {
            //如果version是0,则加到_sources0
            if (0 == rls->_context.version0.version) {
                CFSetAddValue(rlm->_sources0, rls);
                //如果version是1,则加到_sources1
            } else if (1 == rls->_context.version0.version) {
                CFSetAddValue(rlm->_sources1, rls);
                __CFPort src_port = rls->_context.version1.getPort(rls->_context.version1.info);
                if (CFPORT_NULL != src_port) {
                    //此处只有在加到source1的时候才会把souce和一个mach_port_t对应起来
                    //可以理解为,source1可以通过内核向其端口发送消息来主动唤醒runloop
                    CFDictionarySetValue(rlm->_portToV1SourceMap, (const void *)(uintptr_t)src_port, rls);
                    __CFPortSetInsert(src_port, rlm->_portSet);
                }
            }
            __CFRunLoopSourceLock(rls);
            //把runloop加入到source的_runLoops中
            if (NULL == rls->_runLoops) {
                rls->_runLoops = CFBagCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeBagCallBacks); // sources retain run loops!
            }
            CFBagAddValue(rls->_runLoops, rl);
            __CFRunLoopSourceUnlock(rls);
            if (0 == rls->_context.version0.version) {
                if (NULL != rls->_context.version0.schedule) {
                    doVer0Callout = true;
                }
            }
        }
        if (NULL != rlm) {
            __CFRunLoopModeUnlock(rlm);
        }
    }
    __CFRunLoopUnlock(rl);
    if (doVer0Callout) {
        // although it looses some protection for the source, we have no choice but
        // to do this after unlocking the run loop and mode locks, to avoid deadlocks
        // where the source wants to take a lock which is already held in another
        // thread which is itself waiting for a run loop/mode lock
        rls->_context.version0.schedule(rls->_context.version0.info, rl, modeName); /* CALLOUT */
    }
}

通过添加source的这段代码可以得出如下结论:

  • 如果modeName传入kCFRunLoopCommonModes,则该source会被保存到RunLoop的_commonModeItems中
  • 如果modeName传入kCFRunLoopCommonModes,则该source会被添加到所有commonMode中
  • 如果modeName传入的不是kCFRunLoopCommonModes,则会先查找该Mode,如果没有,会创建一个
  • 同一个source在一个mode中只能被添加一次

3.3 输入事件来源

RunLoop接收输入事件来自两种不同的来源:输入源(input source)和定时源(timer source),两种源都使用程序的某一特定的处理例程来处理到达的事件。下图是官方RunLoop模型图,显示了RunLoop的概念结构以及各种源。
RunLoop就是线程中的一个循环,RunLoop在循环中会不断检测,通过Input sources(输入源)和Timer sources(定时源)两种来源等待接受事件;然后对接受到的事件通知线程进行处理,并在没有事件的时候进行休息。


官方RunLoop模型图

在启动RunLoop之前,必须添加监听的输入源事件或者定时源事件,否则调用[runloop run]会直接返回,而不会进入循环让线程长驻。具体可参看基于端口的输入源和定时源的demo实例。

3.3.1 CFRunLoopSourceRef 输入源

输入源常见的有:基于端口的输入源、自定义输入源、Cocoa上的Selector源。

3.3.1.1 基于端口的输入源

Cocoa和Core Foundation内置支持使用端口相关的对象和函数来创建的基于端口的源。例如,在Cocoa里面你从来不需要直接创建输入源。你只要简单的创建端口对象,并使用NSPort的方法把该端口添加到RunLoop。端口对象会自己处理创建和配置输入源。

- (void)showDemo4
{
    // 创建线程,并调用run1方法执行任务
    self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(run1) object:nil];
    [self.thread start];
}

- (void) run1
{
    // 这里写任务
    NSLog(@"----run1-----%@",[NSThread currentThread]);
    //端口添加到RunLoop
    [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
    //启动RunLoop(在启动之前一定是添加了输入源或者定时源的,[runloop run]会直接返回)
    [[NSRunLoop currentRunLoop] run];
    // 测试是否开启了RunLoop,如果开启RunLoop,则来不了这里,因为RunLoop开启了循环。
    NSLog(@"-------------");
}

#这段也可以只作为了解#在Core Foundation,你必须人工创建端口和它的RunLoop源。我们可以使用端口相关的函数(CFMachPortRef,CFMessagePortRef,CFSocketRef)来创建合适的对象。下面的例子展示了如何创建一个基于端口的输入源,将其添加到RunLoop并启动:

void createPortSource()
{
    CFMessagePortRef port = CFMessagePortCreateLocal(kCFAllocatorDefault, CFSTR("com.someport"),myCallbackFunc, NULL, NULL);
    CFRunLoopSourceRef source =  CFMessagePortCreateRunLoopSource(kCFAllocatorDefault, port, 0);
    CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopCommonModes);
    while (pageStillLoading) {
        NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
        CFRunLoopRun();
        [pool release];
    }
    CFRunLoopRemoveSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
    CFRelease(source);
}

3.3.1.2 自定义输入源(只作为了解即可,具体还没有实践)

在Core Foundation程序中,必须使用CFRunLoopSourceRef类型相关的函数来创建自定义输入源,接着使用回调函数来配置输入源。Core Fundation会在恰当的时候调用回调函数,处理输入事件以及清理源。常见的触摸、滚动事件等就是该类源,由系统内部实现。一般我们不会使用该种源,第三种情况已经满足我们的需求。

除了定义在事件到达时自定义输入源的行为,你也必须定义消息传递机制。源的这部分运行在单独的线程里面,并负责在数据等待处理的时候传递数据给源并通知它处理数据。消息传递机制的定义取决于你,但最好不要过于复杂。创建并启动自定义输入源的示例如下:

void createCustomSource()
{
    CFRunLoopSourceContext context = {0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL};
    CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
    CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
    while (pageStillLoading) {
        NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
        CFRunLoopRun();
        [pool release];
    }
    CFRunLoopRemoveSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
    CFRelease(source);
}

用button的点击事件来举个例子,看点击时程序的调用栈,可以看到点击事件是由source0来处理的。


button点击是source0输入源.png

3.3.1.3 Cocoa上的Selector源

Cocoa允许你在任何线程执行selector方法。和基于端口的源一样,执行selector请求会在目标线程上序列化,减缓许多在线程上允许多个方法容易引起的同步问题。不像基于端口的源,一个selector执行完后会自动从RunLoop里面移除。

当在其他线程上面执行selector时,目标线程须有一个活动的RunLoop。对于你创建的线程,这意味着线程在你显式的启动RunLoop之前是不会执行selector方法的,而是一直处于休眠状态。
NSObject类提供了类似如下的selector方法:

/// 主线程
performSelectorOnMainThread:withObject:waitUntilDone:
performSelectorOnMainThread:withObject:waitUntilDone:modes:

/// 指定线程
performSelector:onThread:withObject:waitUntilDone:
performSelector:onThread:withObject:waitUntilDone:modes:

/// 针对当前线程
performSelector:withObject:afterDelay:         
performSelector:withObject:afterDelay:inModes:

/// 取消,在当前线程,和上面两个方法对应
cancelPreviousPerformRequestsWithTarget:
cancelPreviousPerformRequestsWithTarget:selector:object:

还要注意以下这些selector方法,它们是同步执行的,和线程无关,主线程子线程都可以用。不会添加到runloop,而是直接执行,相当于是[self xxx]这样调用,只不过是编译期、运行期处理的不同。

- (id)performSelector:(SEL)aSelector;
- (id)performSelector:(SEL)aSelector withObject:(id)object;
- (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;

3.3.2 CFRunLoopTimerRef 定时源

CFRunLoopTimerRef 是基于时间的触发器,它和 NSTimer 是 Toll-Free Bridged 的,可以混用。其包含一个时间长度和一个回调(函数指针)。当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。

需要注意的是,尽管定时器可以产生基于时间的通知,但它并不是实时机制。和输入源一样,定时器也和你的RunLoop的特定模式相关。如果定时器所在的模式当前未被RunLoop监视,那么定时器将不会开始直到RunLoop运行在相应的模式下。类似的,如果定时器在RunLoop处理某一事件期间开始,定时器会一直等待直到下次RunLoop开始相应的处理程序。如果RunLoop不再运行,那定时器也将永远不启动。

创建定时器源有两种方法,
方法一:

NSTimer *timer = [NSTimer timerWithTimeInterval:4.0
                                                   target:self
                                                   selector:@selector(backgroundThreadFire:) 
                                                   userInfo:nil
                                                   repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timerforMode:NSDefaultRunLoopMode];

方法二:

//scheduledTimer方式下,NSTimer会自动被加入到了RunLoop的NSDefaultRunLoopMode模式下
[NSTimer scheduledTimerWithTimeInterval:10
                                       target:self
                                       selector:@selector(backgroundThreadFire:)
                                       userInfo:nil
                                       repeats:YES];

具体使用举例如下:

/**
 * 用来展示CFRunLoopModeRef和CFRunLoopTimerRef的结合使用
 */
- (void)showDemo1
{
    // 定义一个定时器,约定两秒之后调用self的run方法
    NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
    
    // 将定时器添加到当前RunLoop的NSDefaultRunLoopMode下,一旦RunLoop进入其他模式,定时器timer就不工作了
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    
    // 将定时器添加到当前RunLoop的UITrackingRunLoopMode下,只在拖动情况下工作
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];

    // 将定时器添加到当前RunLoop的NSRunLoopCommonModes下,定时器就会跑在被标记为Common Modes的模式下
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
    
    // 调用了scheduledTimer返回的定时器,已经自动被加入到了RunLoop的NSDefaultRunLoopMode模式下。
    [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
}

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

3.4 CFRunLoopSourceRef的数据结构source0、source1

虽然按3.3介绍的输入源分好几种,但数据结构只有两类(source0、source1)。
数据结构(source0/source1):

// source0 (manual): order(优先级),callout(回调函数)
CFRunLoopSource {order =..., {callout =... }}

// source1 (mach port):order(优先级),port:(端口), callout(回调函数)
CFRunLoopSource {order = ..., {port = ..., callout =...}

(1) source0 非基于port的:是app内部的消息机制,负责App内部事件,由App负责管理触发,例如UIEvent、UITouch事件。包含了一个回调,不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop)来唤醒 RunLoop,让其处理这个事件。

  • -performSelector:onThread:withObject:waitUntilDone: inModes:创建的是source0任务。

(2) source1 基于port的:是基于 mach_ports 的,用于通过内核和其他线程互相发送消息。包含一个 mach_port 和一个回调,可监听系统端口和通过内核和其他线程发送的消息,能主动唤醒runloop,接收分发系统事件。

  • iOS / OSX 都是基于 Mach 内核,Mach 的对象间的通信是通过消息在两个端口(port)之间传递来完成。
  • 很多时候我们的 app 都是处于什么事都不干的状态,在空闲前指定用于唤醒的 mach port 端口,然后在空闲时被 mach_msg() 函数阻塞着并监听唤醒端口, mach_msg() 又会调用 mach_msg_trap() 函数从用户态切换到内核态,这样系统内核就将这个线程挂起,一直停留在 mac_msg_trap 状态。直到另一个线程向内核发送这个端口的 msg 后, trap 状态被唤醒, RunLoop 继续开始干活。

Source1和Timer都属于端口事件源,不同的是所有的Timer都共用一个端口(Timer Port),而每个Source1都有不同的对应端口。

Source0属于Input Source中的一部分,Input Source还包括custom自定义源,由其他线程手动发出。

3.5 CFRunLoopObserverRef 观察者

CFRunLoopObserverRef是观察者,用来监听RunLoop的状态改变,CFRunLoopObserverRef可以监听的状态改变有以下几种:

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

具体使用举例如下:

/**
 * 用来展示CFRunLoopObserverRef使用
 */
- (void)showDemo2
{
    // 创建观察者
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        NSLog(@"监听到RunLoop发生改变---%zd",activity);
    });
    
    // 添加观察者到当前RunLoop中
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
    
    // 释放observer
    CFRelease(observer);
}

4 RunLoop事件队列原理

RunLoop运行逻辑图1
RunLoop运行逻辑图2

在每次运行开启RunLoop的时候,所在线程的RunLoop会自动处理之前未处理的事件,并且通知相关的观察者。

具体的顺序如下:

  1. 通知观察者RunLoop已经启动
  2. 通知观察者即将要开始的定时器
  3. 通知观察者任何即将启动的非基于端口的源
  4. 启动任何准备好的非基于端口的源
  5. 如果基于端口的源准备好并处于等待状态,立即启动;并进入步骤9
  6. 通知观察者线程进入休眠状态
  7. 将线程置于休眠直到任一下面的事件发生:
    • 某一事件到达基于端口的源
    • 定时器启动
    • RunLoop设置的时间已经超时
    • RunLoop被显式唤醒
  8. 通知观察者线程将被唤醒
  9. 处理未处理的事件
    • 如果用户定义的定时器启动,处理定时器事件并重启RunLoop。进入步骤2
    • 如果输入源启动,传递相应的消息
    • 如果RunLoop被显示唤醒而且时间还没超时,重启RunLoop。进入步骤2

10.通知观察者RunLoop结束。

5 什么时候使用RunLoop

仅当在为你的程序创建辅助线程的时候,你才需要显式运行一个RunLoop。RunLoop是程序主线程基础设施的关键部分。所以程序提供了代码运行主程序的循环并自动启动RunLoop。iOS程序中UIApplication的run方法(或Mac OS X中的NSApplication)作为程序启动步骤的一部分,它在程序正常启动的时候就会启动程序的主循环。

对于辅助线程,你需要判断一个RunLoop是否是必须的。如果是必须的,那么你要自己配置并启动它。你不需要在任何情况下都去启动一个线程的RunLoop。比如,你使用线程来处理一个预先定义的长时间运行的任务时,你应该避免启动RunLoop。RunLoop在你要和线程有更多的交互时才需要,比如以下情况:

  • 使用端口或自定义输入源来和其他线程通信
  • 使用线程的定时器
  • Cocoa中使用任何performSelector…的方法
  • 使线程周期性工作

如果你决定在程序中使用RunLoop,那么它的配置和启动都很简单。和所有线程编程一样,你需要计划好在辅助线程退出线程的情形。让线程自然退出往往比强制关闭它更好。

5.1 实战--Timer使用

实际应用开发中,会发现滑动事件会导致Timer暂停不执行,可以采用RunLoop的kCFRunLoopCommonModes模式解决此问题以及增加子线程来解决。

- (void)timerRunLoop:(UIButton *)btn {
    
//第一种方法:将timer加入到NSRunLoopCommonModes
        NSTimer *timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
            self.count --;
            self.countDown.text = [NSString stringWithFormat:@"%d",self.count];
        }];
        [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
        [timer fire];

- (void)timerRunLoop:(UIButton *)btn {
        //第二种方法:在子线程创建timer并将runloop run起来
        NSThread *sub = [[NSThread alloc] initWithTarget:self selector:@selector(subThreadRun) object:nil];
        [sub start];
}

- (void)subThreadRun {
    NSRunLoop *runloop = [NSRunLoop currentRunLoop];
    NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
        self.count --;
        dispatch_async(dispatch_get_main_queue(), ^{
            self.countDown.text = [NSString stringWithFormat:@"%d",self.count];
        });
    }];

    //必须让runloop 运行起来,否则timer仅执行一次
    [runloop run];
}

timer确实被添加到NSDefaultRunLoopMode中了,可是添加到子线程中的NSDefaultRunLoopMode里,无论如何滚动,timer都能够很正常的运转。这又是为啥呢?

这就是多线程与runloop的关系了,每一个线程都有一个与之关联的RunLoop,而每一个RunLoop可能会有多个Mode。CPU会在多个线程间切换来执行任务,呈现出多个线程同时执行的效果。执行的任务其实就是RunLoop去各个Mode里执行各个item。因为RunLoop是独立的两个,相互不会影响,所以在子线程添加timer,滑动视图时,timer能正常运行。

5.2 实战--ImageView延迟显示

实际应用开发中,会遇到滑动时加上图片加载会导致页面卡顿的情况,因此需要在滑动时延迟加载图片,具体代码示例如下:

  • 如程序的效果图,页面上布局了一个textView
  • 点击屏幕后,立即拖动textView,此时RunLoop进入UITrackingRunLoopMode,只要滑动不结束即不退出UITrackingRunLoopMode,则永远不会执行setImage方法
  • 滑动结束后加载图片
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [self showDemo3]; // 用来展示UIImageView的延迟显示
}

/**
 * 用来展示UIImageView的延迟显示
 */
- (void)showDemo3
{
    NSLog(@"showDemo3 begin");
    [self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"tupian"] afterDelay:4.0 inModes:@[NSDefaultRunLoopMode]];
}
图片延迟展示

5.3 实战--后台常驻线程

  • 创建一个后台常驻线程,执行run1开始
  • 并在屏幕点击方法中对该线程进行添加其他任务run2
- (void)showDemo4
{
    // 创建线程,并调用run1方法执行任务
    self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(run1) object:nil];
    [self.thread start];
}

- (void) run1
{
    // 这里写任务
    NSLog(@"----run1-----%@",[NSThread currentThread]);
    
    [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop] run];
    
    // 测试是否开启了RunLoop,如果开启RunLoop,则来不了这里,因为RunLoop开启了循环。
    NSLog(@"-------------");
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    // 利用performSelector调用常驻线程self.thread的run2方法
    [self performSelector:@selector(run2) onThread:self.thread withObject:nil waitUntilDone:NO]; // 用来展示常驻内存的方式
}

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

经过运行测试,除了之前打印的----run1-----,每当我们点击屏幕,都能调用----run2------。

5.4 PerformSelecter...

当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。

当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。

5.5 GCD

RunLoop 底层会用到 GCD 的东西,GCD 的某些 API 也用到了 RunLoop。如当调用了 dispatch_async(dispatch_get_main_queue(), block)时,主队列会把该 block 放到对应的线程(恰好是主线程)中,主线程的 RunLoop 会被唤醒,从消息中取得这个 block,回调 CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE() 来执行这个 block:

GCD.png

6 参考资料

iOS多线程--彻底学会多线程之『RunLoop』
关于Runloop的原理探究及基本使用
iOS开发-Runloop详解(简书)
iOS开发·RunLoop源码与用法完全解析(输入源,定时源,观察者,线程间通信,端口间通信,NSPort,NSMessagePort,NSMachPort,NSPortMessage)

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

推荐阅读更多精彩内容