返回 登录
0

再读苹果《Threading Programming Guide》笔记(三):Run Loop操作配置实践

本文为再读苹果《Threading Programming Guide》笔记第三篇,在对线程技术实践以及Run Loop的相关知识点进行讲解后,作者付宇轩开启了如何具体创建、配置和操作Run Loop的实践分享。

系列阅读

  1. 初识线程
  2. 线程配置与Run Loop
  3. Run Loop操作配置实践

何时使用Run Loop

前文中多次提到过,在主线程中Run Loop是随着应用程序一起启动的,也就是说当我们打开一个应用时,主线程中的Run Loop就已经启动了,尤其现在我们都使用Xcode中的项目模版创建项目,更是不用考虑主线程中Run Loop的状体。所以只有在二级线程中,也就是我们自己创建的线程中才有机会手动的创建的Run Loop,并对其进行配置的操作。

在前文中还提到过,Run Loop在线程中的主要作用就是帮助线程常驻在进程中,并且不会过多消耗资源。所以说Run Loop在二级线程中也不是必须需要的,要根据该线程执行的任务类型以及在整个应用中担任何作用而决定是否需要使用Run Loop。比如说,如果你创建一个二级线程只是为了执行一个不会频繁执行的一次性任务,或者需要执行很长时间的任务,那么可能就不需要使用Run Loop了。如果你需要一个线程执行周期性的定时任务,或者需要较为频繁的与主线程之间进行交互,那么就需要使用Run Loop。归纳一下需要使用Run Loop的情况大概有以下四点:

  • 通过基于端口或自定义的数据源与其他线程进行交互。
  • 在线程中执行定时事件源的任务。
  • 使用Cocoa框架提供的performSelector…系列方法。
  • 在线程中执行较为频繁的,具有周期性的任务。

光说不练假把式,下面就让我们来看看如何具体创建、配置、操作Run Loop。

Run Loop对象

要想操作配置Run Loop,那自然需要通过Run Loop对象来完成,它提供了一系列接口,可帮助我们便捷的添加Input sources、timers以及观察者。较高级别的Cocoa框架提供了NSRunLoop类,较底层级别的Core Foundation框架提供了指向CFRunloopRef的指针。

获取Run Loop对象

前文中提到过,在Cocoa和Core Foundation框架中都没有提供创建Run Loop的方法,只有从当前线程获取Run Loop的方法:

  • 在Cocoa框架中,NSRunLoop类提供了类方法currentRunLoop()获取NSRunLoop对象。> 该方法是获取当前线程中已存在的Run Loop,如果不存在,那其实还是会创建一个Run Loop对象返回,只是Cocoa框架没有向我们暴露该接口。

  • 在Core Foundation框架中提供了CFRunLoopGetCurrent()函数获取CFRunLoop对象。

虽然这两个Run Loop对象并不完全等价,它们之间还是可以转换的,我们可以通过NSRunLoop对象提供的getCFRunLoop()方法获取CFRunLoop对象。因为NSRunLoopCFRunLoop指向的都是当前线程中同一个Run Loop,所以在使用时它们可以混用,比如说要给Run Loop添加观察者时就必须得用CFRunLoop了。

配置Run Loop观察者

前文中提到过,可以向Run Loop中添加各种事件源和观察者,这里事件源是必填项,也就是说Run Loop中至少要有一种事件源,不论是Input source还是timer,如果Run Loop中没有事件源的话,那么在启动Run Loop后就会立即退出。而观察者是可选项,如果没有监控Run Loop各运行状态的需求,可以不配置观察者,这一节先看看如何向Run Loop中添加观察者。

在Cocoa框架中,并没有提供创建配置Run Loop观察者的相关接口,所以我们只能通过Core Foundation框架中提供的对象和方法创建并配置Run Loop观察者,下面我们看看示例代码:

import Foundation

class TestThread: NSObject {

    func launch() {

        print("First event in Main Thread.")

        NSThread.detachNewThreadSelector("createAndConfigObserverInSecondaryThread", toTarget: self, withObject: nil)

        print(NSThread.isMultiThreaded())

        sleep(3)

        print("Second event in Main Thread.")

    }

    func createAndConfigObserverInSecondaryThread() {

        autoreleasepool{

            // 1
            let runloop = NSRunLoop.currentRunLoop()

            // 2
            var _self = self

            // 3
            var observerContext = CFRunLoopObserverContext(version: 0, info: &_self, retain: nil, release: nil, copyDescription: nil)

            // 4
            let observer = CFRunLoopObserverCreate(kCFAllocatorDefault, CFRunLoopActivity.AllActivities.rawValue, true, 0, self.observerCallbackFunc(), &observerContext)

            if(observer != nil) {

                // 5
                let cfRunloop = runloop.getCFRunLoop()

                // 6
                CFRunLoopAddObserver(cfRunloop, observer, kCFRunLoopDefaultMode)

            }

            // 7
            NSTimer.scheduledTimerWithTimeInterval(0.5, target: self, selector: "fireTimer", userInfo: nil, repeats: true)

            var loopCount = 10

            repeat {

                // 8
                runloop.runUntilDate(NSDate(timeIntervalSinceNow: 1))

                loopCount--

            } while(loopCount > 0)

        }

    }

    func observerCallbackFunc() -> CFRunLoopObserverCallBack {

        return {(observer, activity, context) -> Void in

            switch(activity) {

            case CFRunLoopActivity.Entry:
                print("Run Loop已经启动")
                break
            case CFRunLoopActivity.BeforeTimers:
                print("Run Loop分配定时任务前")
                break
            case CFRunLoopActivity.BeforeSources:
                print("Run Loop分配输入事件源前")
                break
            case CFRunLoopActivity.BeforeWaiting:
                print("Run Loop休眠前")
                break
            case CFRunLoopActivity.AfterWaiting:
                print("Run Loop休眠后")
                break
            case CFRunLoopActivity.Exit:
                print("Run Loop退出后")
                break
            default:
                break

            }

        }

    }

    func fireTimer() {

    }

}

let testThread = TestThread()
testThread.launch()

下面解读一下上述代码示例,launch()方法在主线程中,通过NSThread类的类方法detachNewThreadSelector:toTarget:withObject:创建并启动一个二级线程,将createAndConfigObserverInSecondaryThread()方法作为事件消息传入该二级线程,这个方法的主要作用就是在二级线程中创建配置Run Loop观察者并启动Run Loop,然后让主线程持续3秒,以便二级线程有足够的时间执行任务。

createAndConfigObserverInSecondaryThread()中共有8个关键步骤,下面一一进行说明:

  • 第一步:通过NSRunLoop类的类方法currentRunLoop()获取当前线程的Run Loop,这里获取到的Run Loop对象是NSRunLoop对象。
  • 第二步:申明当前对象的变量,至于为什么要这么做,在下一步中会有说明。
  • 第三步:通过Core Foundation框架的CFRunLoopObserverContext结构体构造Run Loop观察者上下文,大家需要注意前两个参数,我们先看看这个结构体:
public struct CFRunLoopObserverContext {
    public var version: CFIndex
    public var info: UnsafeMutablePointer<Void>
    public var retain: (@convention(c) (UnsafePointer<Void>) -> UnsafePointer<Void>)!
    public var release: (@convention(c) (UnsafePointer<Void>) -> Void)!
    public var copyDescription: (@convention(c) (UnsafePointer<Void>) -> Unmanaged<CFString>!)!
    public init()
    public init(version: CFIndex, info: UnsafeMutablePointer<Void>, retain: (@convention(c) (UnsafePointer<Void>) -> UnsafePointer<Void>)!, release: (@convention(c) (UnsafePointer<Void>) -> Void)!, copyDescription: (@convention(c) (UnsafePointer<Void>) -> Unmanaged<CFString>!)!)
}
  1. version:结构体版本号,必须设置为0。
  2. info:上下文中retainreleasecopyDescription三个回调函数以及Run Loop观察者的回调函数所有者对象的指针。在Swift中,UnsafePointer结构体代表C系语言中申明为常量的指针,UnsafeMutablePoinger结构体代表C系语言中申明为非常量的指针,比如说:
C:
void functionWithConstArg(const int *constIntPointer);

Swift:
func functionWithConstArg(constIntPointer: UnsafePointer<Int32>)

C:
void functionWithNotConstArg(unsigned int *unsignedIntPointer);

Swift:
func functionWithNotConstArg(unsignedIntPointer: UnsafeMutablePointer<UInt32>)

C:
void functionWithNoReturnArg(void *voidPointer);

Swift:
func functionWithNoReturnArg(voidPointer: UnsafeMutablePointer<Void>)
  • 第四步:通过Core Foundation框架的CFRunLoopObserverCreate函数创建CFRunLoopObserver对象:
public func CFRunLoopObserverCreate(allocator: CFAllocator!, _ activities: CFOptionFlags, _ repeats: Bool, _ order: CFIndex, _ callout: CFRunLoopObserverCallBack!, _ context: UnsafeMutablePointer<CFRunLoopObserverContext>) -> CFRunLoopObserver!
  1. allocator:该参数为对象内存分配器,一般使用默认的分配器kCFAllocatorDefault
  2. activities:该参数配置观察者监听Run Loop的哪种运行状态。在示例中,我们让观察者监听Run Loop的所有运行状态。
  3. repeats:该参数标识观察者只监听一次还是每次Run Loop运行时都监听。
  4. order:观察者优先级,当Run Loop中有多个观察者监听同一个运行状态时,那么就根据该优先级判断,0为最高优先级别。
  5. callout:观察者的回调函数,在Core Foundation框架中用CFRunLoopObserverCallBack重定义了回调函数的闭包。
  6. context:观察者的上下文。

    • 第五步:因为NSRunLoop没有提供操作观察者的接口,所以我们需要getCFRunLoop()方法获取到CFRunLoop对象。
    • 第六步:通过CFRunLoopAddObserver函数向当前线程的Run Loop中添加创建好的观察者:
func CFRunLoopAddObserver(_ rl: CFRunLoop!, _ observer: CFRunLoopObserver!, _ mode: CFString!)
  1. rl:当前线程的CFRunLoop对象。
  2. observer:创建好的观察者。
  3. mode:设置将观察者添加到哪个Run Loop模式中。

    这里需要注意的是,一个观察者只能被添加到一个Run Loop中,但是可以被添加到Run Loop中的多个模式中。

    • 第七步:通过Timer事件源向当前线程发送重复执行的定时任务,时间间隔为0.5秒,因为只是为了测试观察者,所以fireTimer()是一个空任务。另外前文中提到过,如果Run Loop中没有任何数据源,那么Run Loop启动后会立即退出,所以大家可以把这行注释了运行看看会有什么效果。
    • 第八步:通过NSRunLoop对象的runUntilDate(limitDate: NSDate)方法启动Run Loop,设置Run Loop的运行时长为1秒。这里将其放在一个循环里,最大循环次数为10次,也就是说,如果不考虑主线程的运行时间,该二级线程的Run Loop可运行10次。

再来看看观察者的回调方法observerCallbackFunc(),上面在介绍CFRunLoopObserverCreate函数时提到观察者的回调函数是CFRunLoopObserverCallBack重定义的一个闭包,我们来看看这个闭包:

typealias CFRunLoopObserverCallBack = (CFRunLoopObserver!, CFRunLoopActivity, UnsafeMutablePointer<Void>) -> Void

这个闭包没有返回值,第一个参数是触发监听的观察者,第二个参数是观察者监听的Run Loop运行状态,第三个参数是观察者的运行上下文环境。所以在回调方法中,我们只需要根据第二个参数的值即可判断观察者监听到的Run Loop状态。大家可以拷贝上面的代码,建一个Command Application运行看看结果。

启动Run Loop

在启动Run Loop前务必要保证已添加一种类型的事件源,原因在前文中已提到多次。在Cocoa框架和Core Foundation框架中启动Run Loop大体有三种形式,分别是无条件启动、设置时间限制启动、指定特定模式启动。

无条件启动

NSRunLoop对象的run()方法和Core Foundation框架中的CFRunLoopRun()函数都是无条件启动Run Loop的方式。这种方式虽然是最简单的启动方式,但也是最不推荐使用的一个方式,因为这种方式将Run Loop置于一个永久运行并且不可控的状态,它使Run Loop只能在默认模式下运行,无法给Run Loop设置特定的或自定义的模式,而且以这种模式启动的Run Loop只能通过CFRunLoopStop(_ rl: CFRunLoop!)函数强制停止。

设置时间限制启动

该方式对应的方法是NSRunLoop对象的runUntilDate(_ limitDate: NSDate)方法,在启动Run Loop时设置超时时间,一旦超时那么Run Loop则自动退出。该方法的好处是可以在循环中反复启动Run Loop处理相关任务,而且可控制运行时长。

指定特定模式启动

该方式对应的方法是NSRunLoop对象的runMode(_ mode: String, beforeDate limitDate: NSDate)方法和Core Foundation框架的CFRunLoopRunInMode(_ mode: CFString!, _ seconds: CFTimeInterval, _ returnAfterSourceHandled: Bool)函数。前者有两个参数,第一个参数是Run Loop模式,第二个参数仍然是超时时间,该方法使Run Loop只处理指定模式中的事件源事件,当处理完事件或超时Run Loop会退出,该方法的返回值类型是Bool,如果返回true则表示Run Loop启动成功,并分派执行了任务或者达到超时时间,若返回false则表示Run Loop启动失败。后者有三个参数,前两个参数的作用一样,第三个参数的意思是Run Loop是否在执行完任务后就退出,如果设置为false,那么代表Run Loop在执行完任务后不退出,而是一直等到超时后才退出。该方法返回Run Loop的退出状态:

  • CFRunLoopRunResult.Finished:表示Run Loop已分派执行完任务,并且再无任务执行的情况下退出。
  • CFRunLoopRunResult.Stopped:表示Run Loop通过CFRunLoopStop(_ rl: CFRunLoop!)函数强制退出。
  • CFRunLoopRunResult.TimedOut:表示Run Loop因为超时时间到而退出。
  • CFRunLoopRunResult.HandledSource:表示Run Loop已执行完任务而退出,改状态只有在returnAfterSourceHandled设置为true时才会出现。

退出Run Loop

退出Run Loop的方式总体来说有三种:

  • 启动Run Loop时设置超时时间。
  • 强制退出Run Loop。
  • 移除Run Loop中的事件源,从而使Run Loop退出。

第一种方式是推荐使用的方式,因为可以给Run Loop设置可控的运行时间,让它执行完所有的任务以及给观察者发送通知。第二种强制退出Run Loop主要是应对无条件启动Run Loop的情况。第三种方式是最不推荐的方式,虽然在理论上说当Run Loop中没有任何数据源时会立即退出,但是在实际情况中我们创建的二级线程除了执行我们指定的任务外,有可能系统还会让其执行一些系统层面的任务,而且这些任务我们一般无法知晓,所以用这种方式退出Run Loop往往会存在延迟退出。

Run Loop对象的线程安全性

Run Loop对象的线程安全性取决于我们使用哪种API去操作。Core Foundation框架中的CFRunLoop对象是线程安全的,我们可以在任何线程中使用。Cocoa框架的NSRunLoop对象是线程不安全的,我们必须在拥有Run Loop的当前线程中操作Run Loop,如果操作了不属于当前线程的Run loop,会导致异常和各种潜在的问题发生。

自定义Run Loop事件源

Cocoa框架因为是较为高层的框架,所以没有提供操作较为底层的Run Loop事件源相关的接口和对象,所以我们只能使用Core Foundation框架中的对象和函数创建事件源并给Run Loop设置事件源。

创建Run Loop事件源对象

我们定义自己的Run Loop事件源首先就是需要创建事件源,我们来看看创建事件源的方法:

func CFRunLoopSourceCreate(_ allocator: CFAllocator!, _ order: CFIndex, _ context: UnsafeMutablePointer<CFRunLoopSourceContext>) -> CFRunLoopSource!
  1. allocator:该参数为对象内存分配器,一般使用默认的分配器kCFAllocatorDefault
  2. order:事件源优先级,当Run Loop中有多个接收相同事件的事件源被标记为待执行时,那么就根据该优先级判断,0为最高优先级别。
  3. context:事件源上下文。

Run Loop事件源上下文很重要,我们来看看它的结构:

struct CFRunLoopSourceContext { 
    var version: CFIndex 
    var info: UnsafeMutablePointer<Void> 
    var retain: ((UnsafePointer<Void>) -> UnsafePointer<Void>)! 
    var release: ((UnsafePointer<Void>) -> Void)! 
    var copyDescription: ((UnsafePointer<Void>) -> Unmanaged<CFString>!)! 
    var equal: ((UnsafePointer<Void>, UnsafePointer<Void>) -> DarwinBoolean)! 
    var hash: ((UnsafePointer<Void>) -> CFHashCode)! 
    var schedule: ((UnsafeMutablePointer<Void>, CFRunLoop!, CFString!) -> Void)! 
    var cancel: ((UnsafeMutablePointer<Void>, CFRunLoop!, CFString!) -> Void)! 
    var perform: ((UnsafeMutablePointer<Void>) -> Void)! 
    init() 
    init(version version: CFIndex, info info: UnsafeMutablePointer<Void>, retain retain: ((UnsafePointer<Void>) -> UnsafePointer<Void>)!, release release: ((UnsafePointer<Void>) -> Void)!, copyDescription copyDescription: ((UnsafePointer<Void>) -> Unmanaged<CFString>!)!, equal equal: ((UnsafePointer<Void>, UnsafePointer<Void>) -> DarwinBoolean)!, hash hash: ((UnsafePointer<Void>) -> CFHashCode)!, schedule schedule: ((UnsafeMutablePointer<Void>, CFRunLoop!, CFString!) -> Void)!, cancel cancel: ((UnsafeMutablePointer<Void>, CFRunLoop!, CFString!) -> Void)!, perform perform: ((UnsafeMutablePointer<Void>) -> Void)!) 
}

该结构体中我们需要关注的是前两个和后三个属性:

  1. version:事件源上下文的版本,必须设置为0。
  2. info:上下文中retainreleasecopyDescriptionequalhashschedulecancelperform这八个回调函数所有者对象的指针。
  3. schedule:该回调函数的作用是将该事件源与给它发送事件消息的线程进行关联,也就是说如果主线程想要给该事件源发送事件消息,那么首先主线程得能获取到该事件源。
  4. cancel:该回调函数的作用是使该事件源失效。
  5. perform:该回调函数的作用是执行其他线程或当前线程给该事件源发来的事件消息。

将事件源添加至Run Loop

事件源创建好之后,接下来就是将其添加到指定某个模式的Run Loop中,我们来看看这个方法:

func CFRunLoopAddSource(_ rl: CFRunLoop!, _ source: CFRunLoopSource!, _ mode: CFString!)
  1. rl:希望添加事件源的Run Loop对象,类型是CFRunLoop
  2. source:我们创建好的事件源。
  3. mode:Run Loop的模式。(可以回顾之前文章)

我们再来看看这个方法都干了些什么:

void CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef rls, CFStringRef modeName) {

    .....

    __CFRunLoopSourceSchedule(rls, rl, rlm);

    .....

}

static void __CFRunLoopSourceSchedule(CFRunLoopSourceRef rls, CFRunLoopRef rl, CFRunLoopModeRef rlm) {  

    .....

    if (0 == rls->_context.version0.version) {
         if (NULL != rls->_context.version0.schedule) {
             rls->_context.version0.schedule(rls->_context.version0.info, rl, rlm->_name);
         }
    } 

    .....

}

从上述的代码片段可以看出,在CFRunLoopAddSource中调用了__CFRunLoopSourceSchedule内部函数,而该函数中正是执行了Run Loop事件源上下文中的schedule回调函数。也就是说当把事件源添加到Run Loop中后就会将事件源与给它发送事件消息的线程进行关联。

标记事件源及唤醒Run Loop

前面的文章中说过,srouce0类型,也就是非port类型的事件源都需要进行手动标记,标记完还需要手动唤醒Run Loop,下面我们来看看这两个方法:

func CFRunLoopSourceSignal(_ source: CFRunLoopSource!)
func CFRunLoopWakeUp(_ rl: CFRunLoop!)

这里需要注意的是唤醒Run Loop并不等价与启动Run Loop,因为启动Run Loop时需要对Run Loop进行模式、时限的设置,而唤醒Run Loop只是当已启动的Run Loop休眠时重新让其运行。

执行Run Loop事件源的任务

唤醒Run Loop意味着让休眠的Run Loop重新运行,那么我们就从启动Run Loop,让其开始运行的方法看起:

extension NSRunLoop {

    .....

    public func runUntilDate(limitDate: NSDate) {
        while runMode(NSDefaultRunLoopMode, beforeDate: limitDate) && limitDate.timeIntervalSinceReferenceDate > CFAbsoluteTimeGetCurrent() { }
    }

    public func runMode(mode: String, beforeDate limitDate: NSDate) -> Bool {

        .....

        let limitTime = limitDate.timeIntervalSinceReferenceDate
        let ti = limitTime - CFAbsoluteTimeGetCurrent()
        CFRunLoopRunInMode(modeArg, ti, true)
        return true
    }

}

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

SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     

    .....

    result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, false);

    .....

    return result;
}

static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, Boolean waitIfEmpty) {

    .....

    __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);

    .....

}

static Boolean __CFRunLoopDoSources0(CFRunLoopRef rl, CFRunLoopModeRef rlm, Boolean stopAfterHandle) {

    CFTypeRef sources = NULL;

    .....

    if (__CFRunLoopSourceIsSignaled(rls)) {

        .....

        rls->_context.version0.perform(rls->_context.version0.info);

        .....

    }

    .....

}

从上述代码片段中可以看出,当Run Loop运行后会调用内部函数__CFRunLoopDoSources0执行自定义事件源的任务,在执行之前会通过内部函数__CFRunLoopSourceIsSignaled(rls)判断事件源是否已被标记为待执行,然后执行Run Loop事件上下文中的perform回调函数。

移除Run Loop事件源

当我们自定义的事件源完成使命后就可以将其从Run Loop中移除,我们来看看对应的方法:

func CFRunLoopRemoveSource(_ rl: CFRunLoop!, _ source: CFRunLoopSource!, _ mode: CFString!)

void CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef rls, CFStringRef modeName) {

    .....

    __CFRunLoopSourceCancel(rls, rl, rlm);

    .....

}

static void __CFRunLoopSourceCancel(CFRunLoopSourceRef rls, CFRunLoopRef rl, CFRunLoopModeRef rlm) {
    if (0 == rls->_context.version0.version) {
          if (NULL != rls->_context.version0.cancel) {
              rls->_context.version0.cancel(rls->_context.version0.info, rl, rlm->_name);
          }
    }

    .....

}

从上述代码片段可以看出,当我们调用了CFRunLoopRemoveSource方法后,其实是执行了Run Loop事件源上下文中的cancel回调函数。

自定义Run Loop事件源的实际运用

在讲解示例之前,我们先来看看示例Demo的效果:

LearnThread-5

在这个示例中,创建了两个自定义事件源,一个添加到主线程中,另一个添加到二级线程中。主线程给二级线程中的自定义事件源发送事件消息,目的是让其改变所有UICollectionViewCell的透明度,当二级线程收到事件消息后执行计算每个UICollectionViewCell透明度的任务,然后再给主线程的自定义事件源发送事件消息,让其更新UICollectionViewCell的透明度并显示。下面来看看类图:

LearnThread-6

整个工程一共就这六个类:

  • MainCollectionViewController:程序主控制器,启动程序、展示UI及计算UICollectionViewCell透明度的相关方法。
  • MainThreadRunLoopSource:主线程自定义事件源管理对象,负责初始化事件源,将事件源添加至指定线程,标记事件源并唤醒指定Run Loop以及包含上文中说过的事件源最主要的三个回调方法。
  • MainThreadRunLoopSourceContext:主线程自定义事件源上下文,可获取到对应的事件源及添加了该事件源的Run Loop。
  • SecondaryThreadRunLoopSource:二级线程自定义事件源管理对象,负责初始化事件源,将事件源添加至指定线程,标记事件源并唤醒指定Run Loop以及包含上文中说过的事件源最主要的三个回调方法。
  • SecondaryThreadRunLoopSourceContext:二级线程自定义事件源上下文,可获取到对应的事件源及添加了该事件源的Run Loop。
  • AppDelegate:应用程序代理类,这里零时充当为各自定义事件源回调方法执行内容的管理类。

下面我按照程序的运行顺序一一对这些类及属性和方法进行简单说明。

程序开始运行

MainCollectionViewController类中与UI展示相关的方法在这里就不再累赘了。点击Start按钮,调用start()方法,初始化MainThreadRunLoopSource对象,在这个过程中初始化了CFRunLoopSourceContext对象并且创建CFRunLoopSource对象以及初始化该事件源的指令池:

let mainThreadRunLoopSource = MainThreadRunLoopSource()

mainThreadRunLoopSource.addToCurrentRunLoop()
var runloopSourceContext = CFRunLoopSourceContext(version: 0, info: unsafeBitCast(self, UnsafeMutablePointer<Void>.self), retain: nil, release: nil, copyDescription: nil, equal: nil, hash: nil, schedule: runloopSourceScheduleRoutine(), cancel: runloopSourceCancelRoutine(), perform: runloopSourcePerformRoutine())

runloopSource = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &runloopSourceContext)

commandBuffer = Array<SecondaryThreadRunLoopSourceContext>()

这里需要注意的是CFRunLoopSourceContextinit方法中的第二个参数和CFRunLoopSourceCreate方法的第三个参数都是指针,那么在Swift中,将对象转换为指针的方法有两种:

  • 使用unsafeBitCast方法,该方法会将第一个参数的内容按照第二个参数的类型进行转换。一般当需要对象与指针来回转换时使用该方法。
  • 在对象前面加&amp;符号,表示传入指针地址。

当主线程的自定义事件源初始化完成之后,调用addToCurrentRunLoop()方法,将事件源添加至当前Run Loop中,即主线程的Run Loop:

let cfrunloop = CFRunLoopGetCurrent()      
if let rls = runloopSource {

    CFRunLoopAddSource(cfrunloop, rls, kCFRunLoopDefaultMode)           
}

接下来创建二级线程,并且让其执行二级线程的配置任务:

let secondaryThread = NSThread(target: self, selector: "startThreadWithRunloop", object: nil)    
secondaryThread.start()

在二级线程中同样初始化自定义事件源,并将将其添加至二级线程的Run Loop中,然后启动Run Loop:

func startThreadWithRunloop() {
    autoreleasepool{  
        var done = false  
        let secondaryThreadRunLoopSource = SecondaryThreadRunLoopSource() 
        secondaryThreadRunLoopSource.addToCurrentRunLoop()  
        repeat {    
            let result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, 5, true)        
            if ((result == CFRunLoopRunResult.Stopped) || (result == CFRunLoopRunResult.Finished)) {            
                done = true;           
            }        
        } while(!done)      
    }   
}

执行事件源的schedule回调函数

前文中说过将事件源添加至Run Loop后会触发事件源的schedule回调函数,所以当执行完mainThreadRunLoopSource.addToCurrentRunLoop()这句代码后,便会触发主线程自定义事件源的schedule回调函数:

func runloopSourceScheduleRoutine() -> @convention(c) (UnsafeMutablePointer<Void>, CFRunLoop!, CFString!) -> Void {

    return { (info, runloop, runloopMode) -> Void in

        let mainThreadRunloopSource = unsafeBitCast(info, MainThreadRunLoopSource.self)

        let mainThreadRunloopSourceContext = MainThreadRunLoopSourceContext(runloop: runloop, runloopSource: mainThreadRunloopSource) 

    appDelegate.performSelector("registerMainThreadRunLoopSource:", withObject: mainThreadRunloopSourceContext)  
    }  
}

这里还需注意的是在Swift2.0中,如果一个作为回调函数方法的返回类型是指向函数的指针,这类指针可以转换为闭包,并且要在闭包前面加上@convention(c)标注。在runloopSourceScheduleRoutine()方法中,获取到主线程事件源对象并初始化事件源上下文对象,然后将该事件源上下文对象传给AppDelegate的对应方法注册该事件源上下文对象:

func registerMainThreadRunLoopSource(runloopSourceContext: MainThreadRunLoopSourceContext) {    
    mainThreadRunloopSourceContext = runloopSourceContext      
}

自然当在二级线程中执行完secondaryThreadRunLoopSource.addToCurrentRunLoop()这句代码后,也会触发二级线程自定义事件源的schedule回调函数:

func runloopSourceScheduleRoutine() -> @convention(c) (UnsafeMutablePointer<Void>, CFRunLoop!, CFString!) -> Void {

    return { (info, runloop, runloopMode) -> Void in

        let secondaryThreadRunloopSource = unsafeBitCast(info, SecondaryThreadRunLoopSource.self)

        let secondaryThreadRunloopSourceContext = SecondaryThreadRunLoopSourceContext(runloop: runloop, runloopSource: secondaryThreadRunloopSource)

        let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate

        appDelegate.performSelectorOnMainThread("registerSecondaryThreadRunLoopSource:", withObject: secondaryThreadRunloopSourceContext, waitUntilDone: true)

    }

}

这里要注意的是,在该方法中同样是将二级线程事件源上下文对象传给了AppDelegate的对应方法,但是这里用了performSelectorOnMainThread方法,让其在主线程中执行,目的在于注册完上下文对象后就接着从主线程给二级线程发送事件消息了,其实我将这里作为了主线程触发二级线程执行任务的触发点:

func registerSecondaryThreadRunLoopSource(runloopSourceContext: SecondaryThreadRunLoopSourceContext) {

    secondaryThreadRunloopSourceContext = runloopSourceContext

    sendCommandToSecondaryThread()

}

func sendCommandToSecondaryThread() {

    secondaryThreadRunloopSourceContext?.runloopSource?.commandBuffer?.append(mainThreadRunloopSourceContext!)

    secondaryThreadRunloopSourceContext?.runloopSource?.signalSourceAndWakeUpRunloop(secondaryThreadRunloopSourceContext!.runloop!)

}

从上述代码中可以看到在sendCommandToSecondaryThread()方法中,将主线程的事件源上下文放入了二级线程事件源的指令池中,这里我设计的是只要指令池中有内容就代表事件源需要执行后续任务了。然后执行了二级线程事件源的signalSourceAndWakeUpRunloop()方法,给其标记为待执行,并唤醒二级线程的Run Loop:

func signalSourceAndWakeUpRunloop(runloop: CFRunLoopRef) {   
    CFRunLoopSourceSignal(runloopSource) 
    CFRunLoopWakeUp(runloop)      
}

执行事件源的perform回调函数

当二级线程事件源被标记并且二级线程Run Loop被唤醒后,就会触发事件源的perform回调函数:

func runloopSourcePerformRoutine() -> @convention(c) (UnsafeMutablePointer<Void>) -> Void {

    return { info -> Void in   

        let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate  
         appDelegate.performSelector("performSecondaryThreadRunLoopSourceTask"    
    } 
}

二级线程事件源的perform回调函数会在当前线程,也就是二级线程中执行AppDelegate中的对应方法:

func performSecondaryThreadRunLoopSourceTask() {

    if secondaryThreadRunloopSourceContext!.runloopSource!.commandBuffer!.count > 0 {

        mainCollectionViewController!.generateRandomAlpha()

        let mainThreadRunloopSourceContext = secondaryThreadRunloopSourceContext!.runloopSource!.commandBuffer![0]

        secondaryThreadRunloopSourceContext!.runloopSource!.commandBuffer!.removeAll()

        mainThreadRunloopSourceContext.runloopSource?.commandBuffer?.append(secondaryThreadRunloopSourceContext!)

        mainThreadRunloopSourceContext.runloopSource?.signalSourceAndWakeUpRunloop(mainThreadRunloopSourceContext.runloop!)

    }

}

从上述代码中可以看到,先会判断二级线程事件源的指令池中有没有内容,如果有的话,那么执行计算UICollectionViewCell透明度的任务,然后从指令池中获取到主线程事件源上下文对象,将二级线程事件源上下文对象放入主线程事件源的指令池中,并将主线程事件源标记为待执行,然后唤醒主线程Run Loop。之后便会触发主线程事件源的perform回调函数:

func runloopSourcePerformRoutine() -> @convention(c) (UnsafeMutablePointer<Void>) -> Void {

    return { info -> Void in

        let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate

        appDelegate.performSelector("performMainThreadRunLoopSourceTask")

    }

}
func performMainThreadRunLoopSourceTask() {

    if mainThreadRunloopSourceContext!.runloopSource!.commandBuffer!.count > 0 {

        mainThreadRunloopSourceContext!.runloopSource!.commandBuffer!.removeAll()

        mainCollectionViewController!.collectionView.reloadData()

        let timer = NSTimer(timeInterval: 1, target: self, selector: "sendCommandToSecondaryThread", userInfo: nil, repeats: false)

        NSRunLoop.mainRunLoop().addTimer(timer, forMode: NSDefaultRunLoopMode)

    }

}

performMainThreadRunLoopSourceTask()方法中同样会先判断主线程事件源的指令池是否有内容,然后执行MainCollectionViewController中的刷新UI的方法,最后再次给二级线程发送事件消息,以此循环。大家可以去Github下载该示例的源码,编译环境是Xcode7.2,然后可以自己试着在界面中添加一个Stop按钮,让事件源执行cancel回调函数。

作者简介: 付宇轩(@DevTalking),从事Java中间件开发、iOS开发。主要主持开发企业级ETL中间件、BPM中间件、企业级移动应用,个人博客地址:http://www.devtalking.com

第一时间掌握最新移动开发相关信息和技术,请关注mobilehub公众微信号(ID: mobilehub)。

mobilehub

评论