0%

这是一篇对Run Loop开发文档《Threading Program Guide:Run Loops》的翻译,来源于苹果开发文档。

Run loops 是和线程相关的基础部分。一个run loop是一个用来调度工作和协调接受的事件的循环。一个run loop的目的是有任务的时候保持线程忙碌,没有任务的时候线程休眠。

Runloop的管理并不是完全自动的,你必须编写线程代码在合适的时间点启动runloop,并且响应接收的事件。Cocoa和Core框架都提供了runloop对象供开发者配置和管理线程的runloop。然而你的应用显示不需要创建这些对象,app的框架在程序启动的过程中已经自动设置并且运行了在主线程的runloop。

下面的章节提供了更多关于run loops和怎么在应用中配置run loops的信息,更多的关于runloop 对象的信息查看NSRunLoop Class Reference和CFRunLoop Reference

Run Loop解析

一个run loop和它的名字听起来非常相似,它是一个你的线程进入的循环,并且用户使用它运行事件处理程序来应答事件。 你的代码控制实现runloop的真正的循环部分。换句话说,你的代码提供了for或者while用来驱动run loop。在你的循环内,你使用一个run loop对象来启动事件处理代码—-这些代码能够接收事件并且调用已安装的事件处理程序。

runloop接收的事件来自两个不同类型的源,input source负责分发异步事件,消息通常来自其他的线程或者一个不同的应用程序。timer source 分发同步事件,这些事件发生在计划的时间点或者重复的时间间隔。两种类型的事件源都用一个应用程序特定的程序处理方式来处理到来的事件。

图标3-1展示了runloop和各种各样的事件源的概念结构,输入源异步地将事件发送给相应的处理程序,并且导致 runUntilDate:方法被在特定线程相关的run loop调用使得runloop终止,定时器源会给把事件传递给处理程序,但是不会导致runloop的终止。

图略,自己查看
除了处理输入源,run loops也会生成关于run loop的行为的通知,注册run-loop 观察者可以接收这些通知并且可以使用这些通知在线程上做额外的处理。你可以使用Core Foundation在线程上添加run loop观察者。

下面的节提供了更多关于run loop组成和run loop处理模式的信息,同样描述了runloop在处理事件的不同时刻获取到的通知。

Run Loop模式

一个run loop模式是一个将要被监听额输入源和定时器的集合,以及等待run loop通知的观察者集合。你每次启动run loop,你显示或者隐士的指定一个“模式”来运行,在run loop的运行过程中,只有和指定模式相关的源才会被监听和分发它们的事件(相似的,只有和指定模式关联的观察者才能获得run loop运行进度的通知),和其他模式相关的输入源会将任何接收到的事件保存起来,直到后来以合适的模式运行run loop。

在你的代码中,你可以通过名字识别运行模式,Cocoa和Core Foundation都定义了一个默认的模式和其他几个通用的模式,可以通过字符串在代码中指定。你可以通过简单为自定义的模式指定字符串名的方式实现自定义模式。虽然你在自定义模式下赋值的名字是任意的,但是这些模式的内容却不是随意的,你必须确保为你创建的模式添加一个或多个输入源、定时器或者run loop观察者,这样自定义的模式才会可用。

你使用模式可以在特定run loop运行中过滤掉不想要的源的事件。大多数情况下,你会在“default”模式运行代码。一个模态的面板,然而可能运行在“modal”模式下,因为在这种模式下,只有和模态面板相关的源才能够把事件传递到线程上。(这里是Mac开发的吧,不理解) ,对于次要的线程,你可以使用自定义的模式阻止低优先级的输入源在时间要求比较严格的操作期间传递事件。

注意:模式和事件的输入源要区别对待,模式不是事件的类型。比如:你不能使用模式去单独匹配鼠标按下事件或者单独匹配键盘事件。你可以使用模式来监听一组不同的端口(ports),暂时挂起定时器。也可以改变正在被监控的源和run loop的观察者。

表3-1列举了Cocoa和Core Foundationd的标准模式和使用的描述信息,name这一栏列举了在代码中指定模式所使用的常量。图略。。。

Default:大多数操作都使用的模式,大多数情况下你应该在这个模式下开启run loop,配置输入源。

Connection:Cocoa使用这个模式结合NSConnection对象来检查依赖。你自己几乎不会用到这种模式

Modal:Cocoa使用这个模式区分发送到模态面板的事件。

Event tracking:Cocoa用这个模式在鼠标拖拽和其他类型用户界面操作跟踪过程中限制输入的事件。

Common modes:这是一个可以通常使用的模式的课配置的组合,和这个模式相关的输入源同样会和组里面的任意一个模式关联。对于Cocoa application,这个组默认包含了default、modal、event tracking模式,Core Foundation初始化时仅仅包含了default模式,你可以使用CFRunLoopAddCommonMode 添加自定义的模式。

输入源

输入源异步的向你的线程分发事件,事件的来源取决于输入源的类型,通常是两种类型的一种,基于端口的输入源监控你的应用程序的Mach端口,自定义的输入源监控自定义事件源。就你的run loop而言,它不会关心一个输入源是自定义还是基于端口的。系统通常会实现两种输入源,你只管使用就可以了。两种输入源的唯一区别是他们的信号是怎么获得的。基于端口的源由内核发送信号,自定义的源必须手动的在其他线程发信号。

当你创建了一个输入源,你给它指定一种或者多种运行模式,模式决定了那些输入源在任意给定的时刻会被监视。大多数时间你在default模式下运行,但是也可以指定自定义的模式。如果一个输入源并不在当前模式的监视范围,它产生的任意事件都会被保存直到run loop运行在正确的模式。

基于端口的源

Cocoa和Core Foundation为使用端口相关对象和功能创建基于端口的输入源提供内置支持,比如在Cocoa里面,你从来不需要直接创建输入源,你只需要创建一个端口对象,调用NSPort的方法在run loop上添加端口,端口对象处理需要的输入源的创建和配置。

在Core Foundation,你必须手动的创建端口和run loop输入源。在创建端口和输入源的情况下,需要使用和对外不透透明的(开发文档没有描述)的类型(CFMachPortRef, CFMessagePortRef, or CFSocketRef)相关的函数创建合适的对象。

比如怎么创建一个和配置一个定制的基于端口的输入源,参考 7.7 配置基于端口的输入源

自定义输入源

创建一个定制的输入源,必须使用在Core Foundation中不透明类CFRunLoopSourceRef相关的函数,配置定制的输入源用到几个回调函数。Core Foundation会在不同的点调用这些函数配置源、处理到来的事件、在源从run loop移除的时候销毁源。

除了定义自定输入源在事件到来时的行为,你必须也定义事件的传递机制,输入源的这部分运行在一个单独的线程上,并且负责提供输入源的数据、在数据准备处理的时候发信号给输入源。事件的传递机制取决于你,但是不需要过于复杂。

有关如何创建自定义输入源的示例,请7.1 定义一个自定义的输入源。有关自定义输入源的参考信息,请参阅“CFRunLoopSource”。

Cocoa执行消息选择器源–(Cocoa Perform Selector Sources)

除了基于端口的输入源,Cocoa定义了一个自定义的输入源允许你在任意线程上执行selector的,就像基于端口的输入源,在目标线程上执行selector的请求被序列化了,减少了许多在多个方法同时执行在一个线程的情况下发生的同步问题。和基于端口不同的是,一个perform selector输入源在执行完selector后会自动把自己从run loop移除。

在10.5 之前的OS X上,perform selector 输入源主要给主线程发信息,在OS X10.5之后,可以给任意线程发消息。

当在线程上执行一个selector的时候,该线程必须有一个活跃的run loop,对于你创建的线程,这意味着一直等待到你的代码显示的开启run loop。因为主线程已经开启它的run loop了,所以程序一调用applicationDidFinishLaunching:就向该线程发出调用,run loop每进行一次循环就会处理队列化的perform selector的调用,而不是每次run loop循环处理队列中的选一个处理。

表3-2列举了定义在NSObject可以在其他线程上执行selecors的方法,因为这些方法定义在NSObject类里面,你可以在任何你可以访问到Objective-C对象的线程中使用,包括POSIX线程。这些方法实际上并不创建新的线程去执行selector。

表略。。。

1
2
performSelectorOnMainThread:withObject:waitUntilDone:
performSelectorOnMainThread:withObject:waitUntilDone:modes:

执行特定的selector在主线程的下一个run loop回路。这两个方法给你提供了选项来阻断当前线程直到selector被执行完毕。

1
2
performSelector:onThread:withObject:waitUntilDone:
performSelector:onThread:withObject:waitUntilDone:modes:

执行特定的selector在任意线程上,这些线程通过NSThread对象表示。同样提供了阻断当前线程直到selector被执行。

1
2
performSelector:withObject:afterDelay:
performSelector:withObject:afterDelay:inModes:

在当前线程上下一个run loop回路中执行selector,并附加了延迟选项。因为它等待下一个run loop回路到来才执行selector,这些方法从当前执行的代码中提供了一个自动的微小延迟。多个排队的selector会按照顺序一个一个的执行。

1
2
cancelPreviousPerformRequestsWithTarget:
cancelPreviousPerformRequestsWithTarget:selector:object:

让你取消一个通过performSelector:withObject:afterDelay: or performSelector:withObject:afterDelay:inModes: method方法发送到当前线程的消息。

每个方法更多详细信息见NSObject Class Reference.

定时器源

定时器源在一个未来预先设置的时间同步地传递事件给你的线程,定时器也是一种线程通知自己做某些事情的实现方式。比如一个搜索框可以使用一个定时器去初始化一个自动搜索,在用户用户连续输入关键字的时间间隔大于某个数时触发搜索。延时的使用给了用户一个在搜索开始之前尽可能多的去打印期望的关键字的机会。

虽然定时器产生了基于时间的通知,但是一个定时器并不是真正实时机制。就像输入源一样,定时器关联了你的run loop里的特定的模式。如果一个timer并不是处于run loop当前监控的模式,定时器在你以定时器支持的模式运行run loop之前就不会启动。

相似的,一个定时器如果在run loop执行处理代码的过程中开启了,定时器会等到下一次run loop调用它的处理程序。如果run loop没有运行,定时器永远不会启动。

你可以配置定时器一次或者重复的产生事件,一个重复的定时器自动的在一个预定的启动(fire)时间开始重复调度自己,并不是从真正的定时器fire的时间开始算。比如,一个定时器被设定在特定的时间点启动而且从那以后5秒钟一次。预定的fire时间将永远会落在于原来5s的时间间隔,如果真正的启动时间延迟。如果启动的时间延迟非常多以至于定时器错过了一次或多次预定的fire时间点,定时器只会在错过的时间片段内启动一次,在错过的时间段fire后,定时器会重新设定下次预设的fire时间。

配置定时器更多参考 7.6 配置定时器, NSTimer Class Reference or CFRunLoopTimer Reference.

run loop 观察者

与输入源相反,当一个合适的同步或者异步事件发生时输入源会fire.而run loop观察者在run loop本身自己执行的过程中会在一个特殊的地方fire。你可以用run loop观察者让你的线程去处理一个给定的事件或者为run loop将要进入睡眠准备线程。你同样可以将run loop观察者和run loop下面的事件关联起来。

run loop的入口

run loop将要处理一个定时器

run loop 将要处理一个输入源

run loop 将要进入睡眠

run loop 已经唤醒,但是还没有处理唤醒run loop的事件

退出run loop

你可以给app用 Core Foundation 添加run loop观察者,创建一个run loop观察者,你创建了一个CFRunLoopObserverRef的类型的对象,这个类型持续跟踪你自定义的回调和它关心的run loop活动部分。

和定时器相似,run loop观察者可以重复或者单次使用,一个单次使用的观察者会在它fire后在run loop中移除,一个重复的观察者依然依附在run loop上。单次还是重复可以在创建的时候指定。

有关如何创建run loop 观察者的示例,请参阅6.2 配置run loop。有关参考信息,请参阅CFRunLoopObserver。

run loop一些列的事件

每次你运行run loop,你的线程的run loop会处理挂起的事件,并且会给它的观察者发送通知。处理的顺序是非常特别的,就是下面顺序。

1.通知观察者run loop已经进入了循环。

2.通知观察者所有准备就绪的定时器将要 fire

3.通知观察者所有非基于端口的输入源将要 fire

4.fire所有非基于端口的准备fire的输入源

5.如果一个基于端口的输入源准备好了并且等待fire。立刻fire。到第9部。

6.通知观察者线程将要睡眠

7.将线程睡眠直到下面任意一个事件发生。

一个事件到达了基于端口的源

定时器fire

run loop设置了到期的超时事件

显示的指定run loop唤醒

8.通知观察者线程已经唤醒。

9.处理挂起的事件。

如果一个用户定义的定时器fire。处理定时器事件并且重新启动run loop。到步骤2.

如果一个输入源fire,传递事件。

如果run loop是被显示的被唤醒,但超时事件还没有到,重新启动run loop进入步骤2.

10.通知观察者run loop已经退出。

因为观察者从定时器和输入源来的通知会在那些事件实际发生之前被传递过来,可能在事件发生的时刻和收到通知的时刻之间有间隔,如果在事件上时效性是非常严格的,你可以使用睡眠和从睡眠中醒来的通知来帮助你关联事件之间的时间。

因为定时器和其他的周期性的事件会在你运行run loop的时候传递,所以要避免run loop对事件传递的打断。一个经典行为:每当你通过一个循环不断的从应用程序请求事件来实现一个鼠标的跟踪程序的时候。因为你的代码是直接捕获的事件,而不是让应用程序正常的分发这些事件,活跃的定时器将在你的鼠标跟踪程序退出并将控制权返回给应用程序后失效。

一个run loop可以用run loop对象显示的唤醒,其他的事件同样可以使run loop唤醒。比如添加其他的非基于端口的输入源可以唤醒run loop可以使得输入源可以立即被处理,而不是等到其他事件发生的时候。

什么时候会用一个run loop

唯一需要显示的运行一个run loop的场景是在应用程序中创建了辅助线程。应用程序主线程的run loop是基础设施的关键部分。所以app的框架提供了运行主线程run loop的代码并且自动开启。iOS的UIAppliaction的run方法(或者OS X 的NSApplication)开启一个应用程序的main loop作为一些列程序启动流程的一部分。如果你使用xcode模板工程创建应用,你应该从来不显示的调用这些例程。

对于辅助线程,你需要决定一个run loop是不是必要的,如果是,就配置并开启它。你并不需要在任意情况下都开启一个线程的run loop。比如:如果你使用一个线程执行某些长时间运行并且是事先确定的任务,你可以避免开启run loop。run loops的目的是为了应用在你想和线程有更多的交互的场合上的。比如:如果你想做下面的任何事情你就需要开启run loop。

使用端口或者自定义的输入源和其他线程通信

在线程上使用定时器

在cocoa应用中使用任意一个performSelector…方法

使得线程不被杀死去做周期性任务

如果你选择使用一个run loop,配置和创建是非常简单的。和所有的线程编程一样,你为在合适的场合下结束你的辅助线程指定计划。通常来说让线程以结束的退出(exit)的方式要比强制让线程终止的办法好。怎么配置和退出run loop的描述信息在 6. 使用run loop对象.

使用run loop对象

一个run loop对象提供了添加输入源,定时器,观察者和运行run loop的主要接口,每一个线程都单独有一个run loop对象和它关联。在 Cocoa中这个对象是NSRunLoop类的一个实例,在低层次的应用中,是一个CFRunLoopRef类型的指针。

6.1 获取一个run loop对象

获取当前线程的run loop只需要用下面的一种方法:

在Cocoa应用,使用NSRunLoop类的类方法currentRunLoop返回一个NSRunLoop对象

使用CFRunLoopGetCurrent函数

虽然这两个并不是可以自由的桥接类型,但是你在必要的时候可以从一个NSRunLoop对象中获取一个CFRunLoop类型。 通过NSRunLoop的getCFRunLoop方法获得,然后传递给Core Foundation的代码。因为两个对象引用了相同的run loop,你可以根据需要随意调用。

6.2 配置run loop

当你在一个辅助线程上开启run loop之前,必须给run loop添加至少一个输入源或者一个定时器。如果一个run loop没有任何源来监控,就会立刻退出。参考Configuring Run Loop Sources

除了添加输入源,你可以添加run loop观察者,并且使用他们监测run loop不同阶段的操作,添加观察者要创建一个 CFRunLoopObserverRef 类型的对象,用CFRunLoopAddObserve函数添加到run loop上。观察者必须用Core Foundation创建,即使在Cocoa应用中。

3-1是一个绑定了观察者的线程开启它的run loop的代码。这个案例主要展示怎么创建run loop观察者,所以代码只是简单的创建了一个观察者来监控run loop的所有的活动。基本的处理程序(没有展示)简单地在处理定时器请求的时候记录了run loop的活动。

Listing 3-1 Creating a run loop observer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
- (void)threadMain
{
// The application uses garbage collection, so no autorelease pool is needed.
NSRunLoop* myRunLoop = [NSRunLoop currentRunLoop];
// Create a run loop observer and attach it to the run loop.
CFRunLoopObserverContext context = {0, self, NULL, NULL, NULL};
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities, YES, 0, &myRunLoopObserver, &context);
if (observer)
{
CFRunLoopRef cfLoop = [myRunLoop getCFRunLoop];
CFRunLoopAddObserver(cfLoop, observer, kCFRunLoopDefaultMode);
}
// Create and schedule the timer.
[NSTimer scheduledTimerWithTimeInterval:0.1 target:self
selector:@selector(doFireTimer:) userInfo:nil repeats:YES];
NSInteger loopCount = 10;
do
{
// Run the run loop 10 times to let the timer fire.
[myRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]];
loopCount--;
}
while (loopCount);
}

当给长时间存在的线程配置run loop时,最好添加一个输入源来接收消息。即使你可以进入一个只有一个定时器的run loop,一旦定时器fire,就无效了。会导致run loop退出。绑定一个重复的定时器可以使得run loop在一个长的时间段运行。但是需要定期的触发定时器唤醒你的线程。这实际上是另一种形式的轮询。相反,一个输入源会等待事件的发生,在次之前会保持线程的休眠。

6.3 开启run loop

在应用中开启run loop仅仅对于辅助线程是必要的,run loop必须有至少一个输入源或者定时器去监控,如果一个都没有就会立刻结束。

下面是几种开启run loop的方法:

无条件的(Unconditionally)

带有时间限制设置的(With a set time limit)

在特定的模式下(In a particular mode)

无条件的进入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事件的输入源的类型。(详细信息1. Run Loop模式.)

3-2 是一个线程的主要代码结构,关键部分是这个案例展示了run loop的基本结构,实际上你可以给run loop添加自己的输入源和定时器然后重复的从多个程序例程中调用一个来启动run loop。每次run loop例程程序返回,你检查看看是否有任何可能导致线程结束的条件出现了。这个例子用了Core Foundation run loop程序,所以它可以检查返回结果并且知道为什么run loop退出了,如果你用Cocoa,同样可以用 NSRunLoop的方法以一个相似的方式运行run loop而且不用检查返回值,在3-14.

Listing 3-2 Running a run loop

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- (void)skeletonThreadMain
{
// Set up an autorelease pool here if not using garbage collection.
BOOL done = NO;
// Add your sources or timers to the run loop and do any other setup.
do
{
// Start the run loop but return after each source is handled.
SInt32 result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, YES);
// If a source explicitly stopped the run loop, or if there are no
// sources or timers, go ahead and exit.
if ((result == kCFRunLoopRunStopped) || (result == kCFRunLoopRunFinished))
done = YES;
// Check for any other exit conditions here and set the
// done variable as needed.
}
while (!done);

// Clean up code here. Be sure to release any allocated autorelease pools.
}

递归的运行一个run loop是有可能的,换句话说,你可以在输入源或者定时器的处理程序中调用CFRunLoopRun,CFRunLoopRunInMode,或者其他的任意的NSRunLoop方法。当这样做的时候,你可以使用任何你想要的模式运行嵌套的run loop,包括外层的run loop使用的模式。

6.4 退出 Run Loop

在使得一个run loop处理事件之前有两种办法结束run loop。

给run loop配置一个超时时间。

告诉run loop停止

使用超时时间当然是最好的,你可以管理它。指定超时时间让run loop结束它所有的在退出之前通常进行的操作,包括给观察者发通知。

用CFRunLoopStop函数显示的让run loop和通过设置超时时间产生的效果是相似的。run loop会发出所有剩下run loop相关的通知然后退出,区别在于你可以在无条件启动的run loop上使用这个技术

虽然移除run loop的输入源和定时器同样会导致run loop退出,但是这并不是一个可靠的停止run loop的方式。有些系统程序给run loop增加输入源处理必要的事件。因为你代码可能没有意识到这些输入源的存在,它不能移除掉这些输入源,这会阻止run loop的退出。

线程安全和Run Loop对象 7

线程安全的差异取决于你操作run loop所使用的API,在Core Foundation的函数通常是线程安全的,而且可以被任何线程调用。然而如果你在执行run loop配置的操作,尽可能的从该run loop对应的线程上操作依然是一个好的做法。

Cocoa的NSRunLoop类并不是像在Core Foundation中那样线程安全的,如果你使用NSRunLoop来修改你的run loop,你应该仅仅在run loop对应的那个线程上操作。添加一个输入源或者定时器给非当前线程的run loop会导致你的代码崩溃或者产生不可预测的行为。

8. 配置 Run Loop 资源

下面章节的代码是一些如何设置不同类型输入源的案例(Cocoa和Foundation)

  • 8.1 定义一个自定义的输入源

创建一个自定义的输入源包含如下定义选项

输入源希望处理的信息

一个调度程序让感兴趣的客户(client)知道怎么和你的输入源取得联系

一个处理程序负责执行客户(client)发来的请求

一个取消程序让你输入源无效

因为你自己创建一个自定义的输入源来处理自定义的信息,实际的配置的设计是灵活的。调度程序和取消程序是关键程序,你的自定义输入源几乎总是需要的,剩下的大部分输入源行为发生在这些程序之外。比如你可以定义传递数据给你的输入源的机制和将输入源的存在传递给其他线程。

图3-2是一个自定义输入源配置的案例。这案例中程序的主线程维护对输入源、自定义输入源的自定义命令缓冲区、输入源所在的run loop的引用。当主线程有一个任务要交给工作线程的时候,它会向命令缓冲区发送一个命令和工作线程需要的所有开始任务所需要的信息。(因为主线程和工作线程都有访问命令缓冲区的权限,访问必须是同步的)一旦受到唤醒的命令,run loop调用输入源的处理程序来处理在命令缓冲区的命令。

3-2 操作一个自定义的输入源.png

下面的章节解释了上面图标自定义输入源的实现,和关键要实现的代码

  • 7.2 定义输入源

自定义一个输入源需要用Core Foundation的代码来配置run loop资源,并且将它和run loop依附在一起。虽然基础的处理程序是C函数,但是并不排除你需要用OC或者C++来封装这些函数来实现你的代码主体。

图3-2中介绍的输入源使用了OC对象来管理一个命令行缓冲区,协调run loop。3-3展示的是这个对象的定义,RunLoopSource对象管理一个命令行缓冲区,用缓冲区接收其他线程的消息。3-3同样展示了RunLoopContext对象的定义,这是一个真正的用来传递一个RunLoopSource对象和run loop的引用到应用程序主线程的容器对象。

Listing 3-3 The custom input source object definition

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@interface RunLoopSource : NSObject
{
CFRunLoopSourceRef runLoopSource;
NSMutableArray* commands;
}

- (id)init;
- (void)addToCurrentRunLoop;
- (void)invalidate;

// Handler method
- (void)sourceFired;

// Client interface for registering commands to process
- (void)addCommand:(NSInteger)command withData:(id)data;
- (void)fireAllCommandsOnRunLoop:(CFRunLoopRef)runloop;

@end

// These are the CFRunLoopSourceRef callback functions.
void RunLoopSourceScheduleRoutine (void *info, CFRunLoopRef rl, CFStringRef mode);
void RunLoopSourcePerformRoutine (void *info);
void RunLoopSourceCancelRoutine (void *info, CFRunLoopRef rl, CFStringRef mode);

// RunLoopContext is a container object used during registration of the input source.
@interface RunLoopContext : NSObject
{
CFRunLoopRef runLoop;
RunLoopSource* source;
}
@property (readonly) CFRunLoopRef runLoop;
@property (readonly) RunLoopSource* source;

- (id)initWithSource:(RunLoopSource*)src andLoop:(CFRunLoopRef)loop;
@end

虽然输入源的自定义的数据是OC代码管理的,但是将输入源和run loop关联在一起的代码需要基于C的回调函数,这些函数的第一个会在你真正将run loop源和run loop绑定的时候调用,在3-4,因为输入源只有一个客户(主线程)它使用调度程序中的函数发送一个信息来将自己在那个线程的应用代理上注册自己。当代理想和输入源取得联系的时候,就会使用RunLoopContext对象来实现。

Listing 3-4 Scheduling a run loop source

1
2
3
4
5
6
7
8
void RunLoopSourceScheduleRoutine (void *info, CFRunLoopRef rl, CFStringRef mode)
{
RunLoopSource* obj = (RunLoopSource*)info;
AppDelegate* del = [AppDelegate sharedAppDelegate];
RunLoopContext* theContext = [[RunLoopContext alloc] initWithSource:obj andLoop:rl];
[del performSelectorOnMainThread:@selector(registerSource:)
withObject:theContext waitUntilDone:NO];
}

最重要的回调程序之一是用来在输入源收到到信号时处理自定义数据的,3-5展示了执行和RunLoopSource对象相关的回调代码。这个函数简单的转发了工作请求给sourceFired方法,这个方法会在以后处理命令缓冲区内出现的任何命令。

Listing 3-5 Performing work in the input source

1
2
3
4
5
void RunLoopSourcePerformRoutine (void *info)
{
RunLoopSource* obj = (RunLoopSource*)info;
[obj sourceFired];
}

如果你使用CFRunLoopSourceInvalidate函数将输入源移除,系统会调用输入源的取消代码。你可以用这个代码通知客户们你的输入源已经不再有效了,他们应该移除和它的所有的关联。3-6是RunLoopSource对象注册的取消回调代码。这个函数发送另一个RunLoopContext对象给应用代理,但是这次是请求代理移除run loop源的关联。

Listing 3-6 Invalidating an input source

1
2
3
4
5
6
7
8
9
void RunLoopSourceCancelRoutine (void *info, CFRunLoopRef rl, CFStringRef mode)
{
RunLoopSource* obj = (RunLoopSource*)info;
AppDelegate* del = [AppDelegate sharedAppDelegate];
RunLoopContext* theContext = [[RunLoopContext alloc] initWithSource:obj andLoop:rl];

[del performSelectorOnMainThread:@selector(removeSource:)
withObject:theContext waitUntilDone:YES];
}

备注:应用程序代理的registerSource: and removeSource:方法在Coordinating with Clients of the Input Source

  • 8.3 在run loop上添加输入源

3-7展示了RunLoopSource的init和addToCurrentRunLoop方法。init方法创建了必须依附到RunLoop上的CFRunLoopSourceRef非透明类型对象,它通过传递RunLoopSource对象本身作为上下文信息,所以回调程序会有指向该对象的指针。输入源的安装工作不会在工作线程调用addToCurrentRunLoop方法前进行,addToCurrentRunLoop调用时RunLoopSourceScheduleRoutine的回调函数就会被调用,一旦输入源添加到run loop,线程就可以运行它的run loop来等待事件。

Listing 3-7 Installing the run loop source

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (id)init
{
CFRunLoopSourceContext context = {0, self, NULL, NULL, NULL, NULL, NULL,
&RunLoopSourceScheduleRoutine,
RunLoopSourceCancelRoutine,
RunLoopSourcePerformRoutine};
runLoopSource = CFRunLoopSourceCreate(NULL, 0, &context);
commands = [[NSMutableArray alloc] init];

return self;
}
- (void)addToCurrentRunLoop
{
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFRunLoopAddSource(runLoop, runLoopSource, kCFRunLoopDefaultMode);
}
  • 8.4 协调输入源的客户

为了输入输入源起作用,你应该巧妙控制它并且在另一个线程给它发信号。输入源的要点让和它关联的线程睡眠直到有事可做。所以让其他的线程能够获得输入源的信息和并且和输入源进行通信是现实的需求。

一个通知输入源的客户的方式是当输入源第一次安装在run loop上的时候发送注册请求。可以为一个输入源注册多个客户,也可以简单的注册到一些中心机构,然后在把输入源给感兴趣的客户。3-8展示了应用程序代理的注册并在RunLoopSource对象的调度函数被调用时执行的注册方法,这个方法接收RunLoopSource对象提供的RunLoopContext对象,并且把它添加到源列表上,下面的代码也包含了在从run loop移除的时候如何反注册输入源。

Listing 3-8 Registering and removing an input source with the application delegate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- (void)registerSource:(RunLoopContext*)sourceInfo;
{
[sourcesToPing addObject:sourceInfo];
}

- (void)removeSource:(RunLoopContext*)sourceInfo
{
id objToRemove = nil;

for (RunLoopContext* context in sourcesToPing)
{
if ([context isEqual:sourceInfo])
{
objToRemove = context;
break;
}
}

if (objToRemove)
[sourcesToPing removeObject:objToRemove];
}

回调函数调用的方法在上面的3-4和3-6

  • 8.5 给输入源发信号

当一个客户把它的数据传递给输入源后,必须给输入源发信号唤醒它的run loop,给输入源发信号让run loop知道输入源已经准备好,等待处理。因为一个信号发生的时候线程可能正在休眠,你应该总是显示的唤醒run loop。如果不这样做可能会导致处理输入源的数据上产生延迟。

3-9展示了RunLoopSource 对象的fireCommandsOnRunLoop方法,客户在他们为输入源做好处理缓冲区数据的准备时调用这个方法。

Listing 3-9 Waking up the run loop

1
2
3
4
5
- (void)fireCommandsOnRunLoop:(CFRunLoopRef)runloop
{
CFRunLoopSourceSignal(runLoopSource);
CFRunLoopWakeUp(runloop);
}

备注:你不应该尝试通过发送自定义输入源来处理SIGHUP或其他类型的进程级信号,用于唤醒Run Loop的Core Foundation功能不是信号安全的,不应该在应用程序的信号处理程序中使用。 有关信号处理程序例程的更多信息,请参阅sigaction手册页。

  • 8.6 配置定时器

要创建定时器源,你只需创建一个定时器对象并在Run Loop中调度。 在Cocoa中,您可以使用NSTimer类来创建新的定时器对象,而在Core Foundation中,您可以使用CFRunLoopTimerRef类型。 在内部,NSTimer类只是Core Foundation的扩展,它提供了一些方便的功能,例如使用相同方法创建和计划定时器的能力。

在Cocoa中,您可以使用以下任一类方法一次创建和调度定时器器:

1
2
scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:
scheduledTimerWithTimeInterval:invocation:repeats:

这些方法创建定时器,并以默认模式(NSDefaultRunLoopMode)将其添加到当前线程的Run Loop中。 如果您想通过创建NSTimer对象然后使用NSRunLoop的addTimer:forMode:方法将其添加到运行循环中,也可以手动调度计时器。这两种技术基本上都是一样的,但是给你不同级别的控制定时器配置。 例如,如果创建定时器并手动将其添加到运行循环中,则可以使用除默认模式之外的模式来执行此操作。 清单3-10显示了如何使用这两种技术创建定时器。 第一个定时器的初始延迟为1秒,但随后每0.1秒钟定时fire。 第二个定时器在初始0.2秒延迟后开始首次fire,然后每0.2秒fire一次。

Listing 3-10 Creating and scheduling timers using NSTimer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
NSRunLoop* myRunLoop = [NSRunLoop currentRunLoop];

// Create and schedule the first timer.
NSDate* futureDate = [NSDate dateWithTimeIntervalSinceNow:1.0];
NSTimer* myTimer = [[NSTimer alloc] initWithFireDate:futureDate
interval:0.1
target:self
selector:@selector(myDoFireTimer1:)
userInfo:nil
repeats:YES];
[myRunLoop addTimer:myTimer forMode:NSDefaultRunLoopMode];

// Create and schedule the second timer.
[NSTimer scheduledTimerWithTimeInterval:0.2
target:self
selector:@selector(myDoFireTimer2:)
userInfo:nil
repeats:YES];

清单3-11显示了使用Core Foundation函数配置定时器所需的代码。 虽然此示例不会在上下文结构中传递任何用户定义的信息,但您可以使用此结构传递定时器所需的任何自定义数据。 有关此结构的内容的更多信息,请参阅CFRunLoopTimer参考中的描述。

1
2
3
4
5
6
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFRunLoopTimerContext context = {0, NULL, NULL, NULL, NULL};
CFRunLoopTimerRef timer = CFRunLoopTimerCreate(kCFAllocatorDefault, 0.1, 0.3, 0, 0,
&myCFTimerCallback, &context);

CFRunLoopAddTimer(runLoop, timer, kCFRunLoopCommonModes);
  • 8.7 配置基于端口的输入源

Cocoa和Core Foundation都提供基于端口的对象,用于线程之间或进程之间的通信。 以下部分将介绍如何使用几种不同类型的端口设置端口通信。

  • 8.7.1 配置NSMachPort对象

要建立与NSMachPort对象的本地连接,你将创建端口对象并将其添加到主线程的Run Loop中。 启动辅助线程时,将相同的对象传递给线程的入口点函数。 辅助线程可以使用相同的对象将消息发送回主线程。

  • 8.7.2 实现主线程代码

清单3-12显示了启动辅助工作线程的主线程代码。 因为Cocoa框架执行了许多用于配置端口和run loop的介入步骤,所以launchThread方法明显短于其Core Foundation中等效的配置(清单3-17);然而,两者的行为几乎相同。 一个区别是,该方法不是将本地端口的名称发送给工作线程,而是直接发送NSPort对象。

Listing 3-12 Main thread launch method

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (void)launchThread
{
NSPort* myPort = [NSMachPort port];
if (myPort)
{
// This class handles incoming port messages.
[myPort setDelegate:self];

// Install the port as an input source on the current run loop.
[[NSRunLoop currentRunLoop] addPort:myPort forMode:NSDefaultRunLoopMode];

// Detach the thread. Let the worker release the port.
[NSThread detachNewThreadSelector:@selector(LaunchThreadWithPort:)
toTarget:[MyWorkerClass class] withObject:myPort];
}
}

为了在线程之间建立一个双向通信通道,你可能希望工作线程在登录消息中将自己的本地端口发送到主线程。 接收签入消息让你的主线程知道在启动第二个线程时一切顺利,并且还可以向你发送更多消息到该线程。清单3-13显示了主线程的handlePortMessage:方法。 当数据到达线程自己的本地端口时调用此方法。 当一个签到消息到达时,该方法直接从端口消息中检索次要线程的端口,并保存以备以后使用。

Listing 3-13 Handling Mach port messages

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#define kCheckinMessage 100

// Handle responses from the worker thread.
- (void)handlePortMessage:(NSPortMessage *)portMessage
{
unsigned int message = [portMessage msgid];
NSPort* distantPort = nil;

if (message == kCheckinMessage)
{
// Get the worker thread’s communications port.
distantPort = [portMessage sendPort];

// Retain and save the worker port for later use.
[self storeDistantPort:distantPort];
}
else
{
// Handle other messages.
}
}
  • 8.7.3实现次要线程代码

对于辅助工作线程,你必须配置线程并使用指定的端口将信息传回主线程。

清单3-14显示了设置工作线程的代码。 为线程创建自动释放池后,该方法将创建一个工作对象来驱动线程执行。 工作对象的sendCheckinMessage:方法(如清单3-15所示)为工作线程创建一个本地端口,并将一个签入消息发送回主线程。

Listing 3-14 Launching the worker thread using Mach ports

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
+(void)LaunchThreadWithPort:(id)inData
{
NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];

// Set up the connection between this thread and the main thread.
NSPort* distantPort = (NSPort*)inData;

MyWorkerClass* workerObj = [[self alloc] init];
[workerObj sendCheckinMessage:distantPort];
[distantPort release];

// Let the run loop process things.
do
{
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
beforeDate:[NSDate distantFuture]];
}
while (![workerObj shouldExit]);

[workerObj release];
[pool release];
}

当使用NSMachPort时,本地和远程线程可以使用相同的端口对象进行线程之间的单向通信。 换句话说,由一个线程创建的本地端口对象将成为另一个线程的远程端口对象。

清单3-15显示了次要线程的签入例程。 该方法设置自己的本地端口用于将来的通信,然后发送一个检入消息回主线程。 该方法使用在LaunchThreadWithPort:方法中接收的端口对象作为消息的目标。

Listing 3-15 Sending the check-in message using Mach ports

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- (void)sendCheckinMessage:(NSPort*)outPort
{
// Retain and save the remote port for future use.
[self setRemotePort:outPort];

// Create and configure the worker thread port.
NSPort* myPort = [NSMachPort port];
[myPort setDelegate:self];
[[NSRunLoop currentRunLoop] addPort:myPort forMode:NSDefaultRunLoopMode];

// Create the check-in message.
NSPortMessage* messageObj = [[NSPortMessage alloc] initWithSendPort:outPort
receivePort:myPort components:nil];

if (messageObj)
{
// Finish configuring the message and send it immediately.
[messageObj setMsgId:setMsgid:kCheckinMessage];
[messageObj sendBeforeDate:[NSDate date]];
}
}
  • 8.7.4 配置一个NSMessagePort对象

要建立与NSMessagePort对象的本地连接,您不能简单地在线程之间传递端口对象。 远程消息端口必须以名称获取。 在Cocoa中可能需要使用特定的名称注册本地端口,然后将该名称传递给远程线程,以便它可以获取适当的端口对象进行通信。 清单3-16显示了要使用消息端口的端口创建和注册过程。

Listing 3-16 Registering a message port

1
2
3
4
5
6
7
8
9
10
NSPort* localPort = [[NSMessagePort alloc] init];

// Configure the object and add it to the current run loop.
[localPort setDelegate:self];
[[NSRunLoop currentRunLoop] addPort:localPort forMode:NSDefaultRunLoopMode];

// Register the port using a specific name. The name must be unique.
NSString* localPortName = [NSString stringWithFormat:@"MyPortName"];
[[NSMessagePortNameServer sharedInstance] registerPort:localPort
name:localPortName];
  • 8.7.6 Core Foundation中配置基于端口的输入源

本节介绍如何使用Core Foundation在应用程序的主线程和工作线程之间设置双向通信通道。清单3-17显示了应用程序主线程调用的代码,以启动工作线程。 代码的第一件事是设置一个CFMessagePortRef opaque类型来监听来自工作线程的消息。 工作线程需要进行连接的端口名称,以便将字符串值传递给工作线程的入口点函数。 端口名称通常在当前用户上下文中是唯一的; 否则,您可能会遇到冲突。

Listing 3-17 :将Core Foundation消息端口附加到新线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#define kThreadStackSize        (8 *4096)

OSStatus MySpawnThread()
{
// Create a local port for receiving responses.
CFStringRef myPortName;
CFMessagePortRef myPort;
CFRunLoopSourceRef rlSource;
CFMessagePortContext context = {0, NULL, NULL, NULL, NULL};
Boolean shouldFreeInfo;

// Create a string with the port name.
myPortName = CFStringCreateWithFormat(NULL, NULL, CFSTR("com.myapp.MainThread"));

// Create the port.
myPort = CFMessagePortCreateLocal(NULL,
myPortName,
&MainThreadResponseHandler,
&context,
&shouldFreeInfo);

if (myPort != NULL)
{
// The port was successfully created.
// Now create a run loop source for it.
rlSource = CFMessagePortCreateRunLoopSource(NULL, myPort, 0);

if (rlSource)
{
// Add the source to the current run loop.
CFRunLoopAddSource(CFRunLoopGetCurrent(), rlSource, kCFRunLoopDefaultMode);

// Once installed, these can be freed.
CFRelease(myPort);
CFRelease(rlSource);
}
}

// Create the thread and continue processing.
MPTaskID taskID;
return(MPCreateTask(&ServerThreadEntryPoint,
(void*)myPortName,
kThreadStackSize,
NULL,
NULL,
NULL,
0,
&taskID));
}

在安装端口并启动线程的情况下,主线程可以在等待线程检入时继续其正常执行。当检入消息到达时,它将被分派到主线程的MainThreadResponseHandler函数,如清单3- 18。 此函数提取工作线程的端口名称,并创建未来通信的管道。

Listing 3-18 Receiving the checkin message

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#define kCheckinMessage 100

// Main thread port message handler
CFDataRef MainThreadResponseHandler(CFMessagePortRef local,
SInt32 msgid,
CFDataRef data,
void* info)
{
if (msgid == kCheckinMessage)
{
CFMessagePortRef messagePort;
CFStringRef threadPortName;
CFIndex bufferLength = CFDataGetLength(data);
UInt8* buffer = CFAllocatorAllocate(NULL, bufferLength, 0);

CFDataGetBytes(data, CFRangeMake(0, bufferLength), buffer);
threadPortName = CFStringCreateWithBytes (NULL, buffer, bufferLength, kCFStringEncodingASCII, FALSE);

// You must obtain a remote message port by name.
messagePort = CFMessagePortCreateRemote(NULL, (CFStringRef)threadPortName);

if (messagePort)
{
// Retain and save the thread’s comm port for future reference.
AddPortToListOfActiveThreads(messagePort);

// Since the port is retained by the previous function, release
// it here.
CFRelease(messagePort);
}

// Clean up.
CFRelease(threadPortName);
CFAllocatorDeallocate(NULL, buffer);
}
else
{
// Process other messages.
}

return NULL;
}

在配置主线程之后,唯一剩下的就是新创建的工作线程创建自己的端口并签入。清单3-19显示了工作线程的入口点函数。 该函数提取主线程的端口名称,并使用它来创建一个远程连接回主线程。 该函数然后为其自身创建本地端口,将端口安装在线程的运行循环上,并向包含本地端口名称的主线程发送检入消息。

Listing 3-19 Setting up the thread structures

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
OSStatus ServerThreadEntryPoint(void* param)
{
// Create the remote port to the main thread.
CFMessagePortRef mainThreadPort;
CFStringRef portName = (CFStringRef)param;

mainThreadPort = CFMessagePortCreateRemote(NULL, portName);

// Free the string that was passed in param.
CFRelease(portName);

// Create a port for the worker thread.
CFStringRef myPortName = CFStringCreateWithFormat(NULL, NULL, CFSTR("com.MyApp.Thread-%d"), MPCurrentTaskID());

// Store the port in this thread’s context info for later reference.
CFMessagePortContext context = {0, mainThreadPort, NULL, NULL, NULL};
Boolean shouldFreeInfo;
Boolean shouldAbort = TRUE;

CFMessagePortRef myPort = CFMessagePortCreateLocal(NULL,
myPortName,
&ProcessClientRequest,
&context,
&shouldFreeInfo);

if (shouldFreeInfo)
{
// Couldn't create a local port, so kill the thread.
MPExit(0);
}

CFRunLoopSourceRef rlSource = CFMessagePortCreateRunLoopSource(NULL, myPort, 0);
if (!rlSource)
{
// Couldn't create a local port, so kill the thread.
MPExit(0);
}

// Add the source to the current run loop.
CFRunLoopAddSource(CFRunLoopGetCurrent(), rlSource, kCFRunLoopDefaultMode);

// Once installed, these can be freed.
CFRelease(myPort);
CFRelease(rlSource);

// Package up the port name and send the check-in message.
CFDataRef returnData = nil;
CFDataRef outData;
CFIndex stringLength = CFStringGetLength(myPortName);
UInt8* buffer = CFAllocatorAllocate(NULL, stringLength, 0);

CFStringGetBytes(myPortName,
CFRangeMake(0,stringLength),
kCFStringEncodingASCII,
0,
FALSE,
buffer,
stringLength,
NULL);

outData = CFDataCreate(NULL, buffer, stringLength);

CFMessagePortSendRequest(mainThreadPort, kCheckinMessage, outData, 0.1, 0.0, NULL, NULL);

// Clean up thread data structures.
CFRelease(outData);
CFAllocatorDeallocate(NULL, buffer);

// Enter the run loop.
CFRunLoopRun();
}

一旦进入其run loop,发送到线程端口的所有未来事件都将由ProcessClientRequest函数处理。 该功能的实现取决于线程工作的类型,此处未显示。

这里主要收集RunTime相关东西

为什么会有元类

如何运用 Runtime 进行模型的归解档

Runtime 遍历 ivar_list

如何运用 Runtime 字典转模型?

Runtime 遍历 ivar_list,结合 KVC 赋值。

如何给 Category 添加属性?关联对象以什么形式进行存储?

查看的是 关联对象 的知识点。

详细的说一下 关联对象
关联对象 以哈希表的格式,存储在一个全局的单例中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@interface NSObject (Extension)
@property (nonatomic,copy ) NSString *name;
@end


@implementation NSObject (Extension)

- (void)setName:(NSString *)name {

objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)name {

return objc_getAssociatedObject(self,@selector(name));
}

@end

什么时候会报unrecognized selector的异常?

objc在向一个对象发送消息时,runtime库会根据对象的isa指针找到该对象实际所属的类,然后在该类中的方法列表以及其父类方法列表中寻找方法运行,如果,在最顶层的父类中依然找不到相应的方法时,会进入消息转发阶段,如果消息三次转发流程仍未实现,则程序在运行时会挂掉并抛出异常unrecognized selector sent to XXX 。

runtime如何实现weak变量的自动置nil?知道SideTable吗?

runtime 对注册的类会进行布局,对于 weak 修饰的对象会放入一个 hash 表中。 用 weak 指向的对象内存地址作为key,当此对象的引用计数为0的时候会 dealloc,假如 weak 指向的对象内存地址是a,那么就会以a为键, 在这个 weak表中搜索,找到所有以a为键的 weak 对象,从而设置为 nil。

更细一点的回答:

1.初始化时:runtime会调用objc_initWeak函数,初始化一个新的weak指针指向对象的地址。
2.添加引用时:objc_initWeak函数会调用objc_storeWeak() 函数, objc_storeWeak()的作用是更新指针指向,创建对应的弱引用表。
3.释放时,调用clearDeallocating函数。clearDeallocating函数首先根据对象地址获取所有weak指针地址的数组,然后遍历这个数组把其中的数据设为nil,最后把这个entry从weak表中删除,最后清理对象的记录。

SideTable结构体是负责管理类的引用计数表和weak表,

详解:参考自《Objective-C高级编程》一书
1.初始化时:runtime会调用objc_initWeak函数,初始化一个新的weak指针指向对象的地址。

1
2
3
4
{
NSObject *obj = [[NSObject alloc] init];
id __weak obj1 = obj;
}

当我们初始化一个weak变量时,runtime会调用 NSObject.mm 中的objc_initWeak函数。

1
2
3
4
5
// 编译器的模拟代码
id obj1;
objc_initWeak(&obj1, obj);
/*obj引用计数变为0,变量作用域结束*/
objc_destroyWeak(&obj1);

通过objc_initWeak函数初始化“附有weak修饰符的变量(obj1)”,在变量作用域结束时通过objc_destoryWeak函数释放该变量(obj1)。

2.添加引用时:objc_initWeak函数会调用objc_storeWeak() 函数, objc_storeWeak()的作用是更新指针指向,创建对应的弱引用表。

objc_initWeak函数将“附有weak修饰符的变量(obj1)”初始化为0(nil)后,会将“赋值对象”(obj)作为参数,调用objc_storeWeak函数。

1
2
obj1 = 0
obj_storeWeak(&obj1, obj);

也就是说:

weak 修饰的指针默认值是 nil (在Objective-C中向nil发送消息是安全的)

然后obj_destroyWeak函数将0(nil)作为参数,调用objc_storeWeak函数。

1
objc_storeWeak(&obj1, 0);

前面的源代码与下列源代码相同。

1
2
3
4
5
6
// 编译器的模拟代码
id obj1;
obj1 = 0;
objc_storeWeak(&obj1, obj);
/* ... obj的引用计数变为0,被置nil ... */
objc_storeWeak(&obj1, 0);

objc_storeWeak函数把第二个参数的赋值对象(obj)的内存地址作为键值,将第一个参数__weak修饰的属性变量(obj1)的内存地址注册到 weak 表中。如果第二个参数(obj)为0(nil),那么把变量(obj1)的地址从weak表中删除。

由于一个对象可同时赋值给多个附有__weak修饰符的变量中,所以对于一个键值,可注册多个变量的地址。

可以把objc_storeWeak(&a, b)理解为:objc_storeWeak(value, key),并且当key变nil,将value置nil。在b非nil时,a和b指向同一个内存地址,在b变nil时,a变nil。此时向a发送消息不会崩溃:在Objective-C中向nil发送消息是安全的。

3.释放时,调用clearDeallocating函数。clearDeallocating函数首先根据对象地址获取所有weak指针地址的数组,然后遍历这个数组把其中的数据设为nil,最后把这个entry从weak表中删除,最后清理对象的记录。

当weak引用指向的对象被释放时,又是如何去处理weak指针的呢?当释放对象时,其基本流程如下:

1.调用objc_release
2.因为对象的引用计数为0,所以执行dealloc
3.在dealloc中,调用了_objc_rootDealloc函数
4.在_objc_rootDealloc中,调用了object_dispose函数
5.调用objc_destructInstance
6.最后调用objc_clear_deallocating

对象被释放时调用的objc_clear_deallocating函数:

1.从weak表中获取废弃对象的地址为键值的记录
2.将包含在记录中的所有附有 weak修饰符变量的地址,赋值为nil
3.将weak表中该记录删除
4.从引用计数表中删除废弃对象的地址为键值的记录

总结:

其实Weak表是一个hash(哈希)表,Key是weak所指对象的地址,Value是weak指针的地址(这个地址的值是所指对象指针的地址)数组。

解决iOS的scrollView属性directionalLockEnabled的问题修正

苹果官方文档说明

“If this property is NO, scrolling is permitted in both horizontal and vertical directions. If this property is YES and the user begins dragging in one general direction (horizontally or vertically), the scroll view disables scrolling in the other direction. If the drag direction is diagonal, then scrolling will not be locked and the user can drag in any direction
until the drag completes. The default value is NO”

问题出现了,当你斜着滑动的时候,这个属性就失效了,就会出现斜着滑动的情况

解决办法如下:

阅读全文 »

一、编程中的六大设计原则?

1.单一职责原则

通俗地讲就是一个类只做一件事

  • CALayer:动画和视图的显示。
  • UIView:只负责事件传递、事件响应。

2.开闭原则

对修改关闭,对扩展开放。
要考虑到后续的扩展性,而不是在原有的基础上来回修改

3.接口隔离原则

使用多个专门的协议、而不是一个庞大臃肿的协议

  • UITableviewDelegate
  • UITableViewDataSource

4.依赖倒置原则

抽象不应该依赖于具体实现、具体实现可以依赖于抽象。
调用接口感觉不到内部是如何操作的

5.里氏替换原则

父类可以被子类无缝替换,且原有的功能不受任何影响

例如 KVO

6.迪米特法则

一个对象应当对其他对象尽可能少的了解,实现高聚合、低耦合

推荐文章

面向对象设计的六大设计原则(附 Demo 及 UML 类图)- J_Knight_

App安装包(ipa文件)是由资源(图片+文档)和可执行文件(二进制文件)两部分组成,安装包瘦身也是从这两部分进行。

图片

图片基本来说在打包完成后,被压缩很少的,听从苹果的建议,使用 png,并且放到Assets.xcassets目录里面
删除无用的图片. LSUnusedResource软件

  • On Demand Resource
    苹果从iOS 9开始引入了On Demand Resource功能,即一部分图片可以被放置在苹果的服务器上,不随着app的下载而下载,直到用户真正进入到某个页面时才下载这些资源文件。
    我们考虑可以让某些业务仅在iOS 9及以后版本中可用,然后应用On Demand Resource来优化这些业务的资源。
    经过了一段时间的开发实验,一切都如同预期,当我们以为On Demand Resource是一个可行的思路时,我们却发现了一个Xcode巨坑的问题:当工程需要支持iOS9以下系统时,Xcode会在打包完成上传app store时失败。On Demand Resource的想法只能搁置。

  • 修复cocoapods带来的图片重复合并问题, 参考今日头条团队的文章(下文提到)

  • 使用tint color

资源文件

一定要注意,资源文件是否需要编译进入工程,特别是 readme 这种的,不要编译进项目

Xcode编译选项优化:

  • build setting 里 DEAD_CODE_STRIPPING = YES(好像默认就是YES)。 确定 dead code(代码被定义但从未被调用)被剥离,去掉冗余的代码,即使一点冗余代码,编译后体积也是很可观的。
  • Build Settings->Optimization Level有几个编译优化选项,release版应该选择Fastest, Smalllest[-Os],这个选项会开启那些不增加代码大小的全部优化,并让可执行文件尽可能小。
  • Strip Debug Symbols During Copy 和 Symbols Hidden by Default 在release版本应该设为yes,可以去除不必要的调试符号。Symbols Hidden by Default会把所有符号都定义成”private extern”,设了后会减小体积。
  • Strip Linked Product:DEBUG下设为NO,RELEASE下设为YES,用于RELEASE模式下缩减app的大小;

另外注意Xcode里面的Deployment选项,Deployment Postprocessing这个配置项如果使用xcode打包,xcode会默认把这个变量置为YES, 如果使用脚本打包,记得设置。Symbols Hidden by Default设置为YES Make Strings Read-Only 设置为YES

  • 编译选项:LTO,即Link Time Optimization。

苹果在2016年的WWDC What’s new in LLVM中详细介绍了这一功能。LTO能带来的优化有:
(1)将一些函数內联化
(2)去除了一些无用代码
(3)对程序有全局的优化作用
在build setting中开启Link-Time Optimization为Incremental,经测试可缩减安装包大小500KB左右。苹果还声称LTO对app的运行速度也有正向帮助。
但LTO也会带来一点副作用。LTO会降低编译链接的速度,因此只建议在打正式包时开启;开启了LTO之后,link map的可读性明显降低,多出了很多数字开头的“类”(LTO的全局优化导致的),导致我们还经常需要手动关闭LTO打包来阅读link map。

启动速度优化

main()调用之前的耗时我们可以优化的点有:

  • 减少不必要的framework,因为动态链接比较耗时
  • check framework应当设为 optional 和 required ,如果该framework在当前App支持的所有- iOS系统版本都存在,那么就设为required,否则就设为 optional,因为 optional 会有些额外的检查
  • 合并或者删减一些OC类,关于清理项目中没用到的类,使用工具AppCode代码检查功能
  • 删减一些无用的静态变量
  • 删减没有被调用到或者已经废弃的方法 -Wall -Wextra -Weverything Other Warning Flags
  • 将不必须在 +load 方法中做的事情延迟到 +initialize 中
  • 尽量不要用 C++ 虚函数(创建虚函数表有开销)

main()调用之后的加载时间

  • 分析
    在main()被调用之后,App的主要工作就是初始化必要的服务,显示首页内容等。而我们的优化也是围绕如何能够快速展现首页来开展。
    App通常在 AppDelegate 类中的didFinishLaunchingWithOptions: 方法中创建首页需要展示的view,然后在当前runloop的末尾,主动调用CA::Transaction::commit完成视图的渲染。

  • 而视图的渲染主要涉及三个阶段:

    1. 准备阶段 这里主要是图片的解码
    2. 布局阶段 首页所有UIView的 layoutSubViews 运行
    3. 绘制阶段 首页所有UIView的 drawRect: 运行
    4. 再加上启动之后必要服务的启动、必要数据的创建和读取,这些就是我们可以尝试优化的地方
  • main()函数调用之后可以优化的点:

    • 不使用是storyboard、xib,直接视用代码加载首页视图
    • NSUserDefaults实际上是在Library文件夹下会生产一个plist文件,如果文件太大的话一次能读取到内存中可能很耗时,这个影响需要评估,如果耗时很大的话需要拆分(需考虑老版本覆盖安装兼容问题)
    • 每次用NSLog方式打印会隐式的创建一个Calendar,因此需要删减启动时各业务方打的log,或者仅仅针对内测版输出log
    • 梳理应用启动时发送的所有网络请求,是否可以统一在异步线程请求

下面引用 今日头条团队优化

干货|今日头条iOS端安装包大小优化—思路与实践

link map是编译链接时可以生成的一个txt文件,它生成目的就是帮助程序员分析包大小。link map记录了每个方法在当前的二进制架构下占据的空间。通过分析link map,我们可以了解每个类甚至每个方法占据了多少安装包空间。
在编译时开启Xcode build setting中的Write Link Map File开关,Xcode就会生成一份link map文件。
目前已经有不少开源的分析link map的工具,可以输出每个类、每个静态库占用的空间,并进行排序。通过查看link map,我们可以对二进制代码占据的包大小空间有个直观了解,同时在引入第三方库时也可以使用link map作出评估。

如何进行二进制文件优化

技术手段排查冗余代码

没有被引用的类和方法是可以通过技术手段被筛选出来的。
MachO文件中有__DATA.__objc_classrefs和__DATA.__objc_selrefs段,分别近似于“被使用的类的集合”和“被使用的方法的集合”。通过取差集的方式可以筛选出未被使用的类和方法。

排查无用类

使用otool命令可查看__DATA.__objc_classrefs段和__DATA.__objc_classlist段,两者的差集可以认为是定义了但未使用的类。
不过__DATA.__objc_classrefs段和__DATA.__objc_classlist段中都只提供了类在二进制文件中的位置地址,而没有提供类名等可读信息。所以在获取到差集后,还需要结合

otool -o BinaryName

命令的输出,将地址转换成可读的类名。

使用脚本筛选出差集对应的类后,还需要进行一遍人工选择。因为动态使用的类、从nib或storyboard初始化的类以及在同一个文件中定义的多个类会被误判为未使用的类。这需要结合业务进行一次梳理。

排查无用方法

所有已经被实现的方法可以通过linkmap来获取,对linkmap做grep操作即可获得结果:

grep '[+|-]\[.*\s.*\]'

而所有已经被使用的方法可以通过对二进制文件逆向获得。使用otool工具逆向二进制文件的__DATA.__objc_selrefs 段,提取可执行文件里引用到的方法名:

otool -v -s __DATA __objc_selrefs

使用这种方法取到的差集,还需要排除掉系统API中的protocol,accessor方法等。

使用这个方法,头条排查出了无用方法2000余个,总共累积约2MB,其中最长的方法约7KB。考虑到删除方法的工作量和风险都相对较大,目前我们仅对其中很小一部分进行了删除。

HTTP 代理存在两种形式:
第一种是 RFC 7230 - HTTP/1.1: Message Syntax and Routing(即修订后的 RFC 2616,HTTP/1.1 协议的第一部分)描述的普通代理。这种代理扮演的是「中间人」角色,对于连接到它的客户端来说,它是服务端;对于要连接的服务端来说,它是客户端。它就负责在两端之间来回传送 HTTP 报文。

第二种是 Tunneling TCP based protocols through Web proxy servers(通过 Web 代理服务器用隧道方式传输基于 TCP 的协议)描述的隧道代理。它通过 HTTP 协议正文部分(Body)完成通讯,以 HTTP 的方式实现任意基于 TCP 的应用层协议代理。这种代理使用 HTTP 的 CONNECT 方法建立连接,但 CONNECT 最开始并不是 RFC 2616 - HTTP/1.1 的一部分,直到 2014 年发布的 HTTP/1.1 修订版中,才增加了对 CONNECT 及隧道代理的描述,详见 RFC 7231 - HTTP/1.1: Semantics and Content。实际上这种代理早就被广泛实现。

本文描述的第一种代理,对应《HTTP 权威指南》一书中第六章「代理」;第二种代理,对应第八章「集成点:网关、隧道及中继」中的 8.5 小节「隧道」。

第一种 Web 代理原理特别简单:

HTTP 客户端向代理发送请求报文,代理服务器需要正确地处理请求和连接(例如正确处理 Connection: keep-alive),同时向服务器发送请求,并将收到的响应转发给客户端。

假如我通过代理访问 A 网站,对于 A 来说,它会把代理当做客户端,完全察觉不到真正客户端的存在,这实现了隐藏客户端 IP 的目的。当然代理也可以修改 HTTP 请求头部,通过 X-Forwarded-IP 这样的自定义头部告诉服务端真正的客户端 IP。但服务器无法验证这个自定义头部真的是由代理添加,还是客户端修改了请求头,所以从 HTTP 头部字段获取 IP 时,需要格外小心。

给浏览器显式的指定代理,需要手动修改浏览器或操作系统相关设置,或者指定 PAC(Proxy Auto-Configuration,自动配置代理)文件自动设置,还有些浏览器支持 WPAD(Web Proxy Autodiscovery Protocol,Web 代理自动发现协议)。显式指定浏览器代理这种方式一般称之为正向代理,浏览器启用正向代理后,会对 HTTP 请求报文做一些修改,来规避老旧代理服务器的一些问题.

还有一种情况是访问 A 网站时,实际上访问的是代理,代理收到请求报文后,再向真正提供服务的服务器发起请求,并将响应转发给浏览器。这种情况一般被称之为反向代理,它可以用来隐藏服务器 IP 及端口。一般使用反向代理后,需要通过修改 DNS 让域名解析到代理服务器 IP,这时浏览器无法察觉到真正服务器的存在,当然也就不需要修改配置了。反向代理是 Web 系统最为常见的一种部署方式.

但是,使用我们这个代理服务后,HTTPS 网站完全无法访问,这是为什么呢?答案很简单,这个代理提供的是 HTTP 服务,根本没办法承载 HTTPS 服务。那么是否把这个代理改为 HTTPS 就可以了呢?显然也不可以,因为这种代理的本质是中间人,而 HTTPS 网站的证书认证机制是中间人劫持的克星。普通的 HTTPS 服务中,服务端不验证客户端的证书,中间人可以作为客户端与服务端成功完成 TLS 握手;但是中间人没有证书私钥,无论如何也无法伪造成服务端跟客户端建立 TLS 连接。当然如果你拥有证书私钥,代理证书对应的 HTTPS 网站当然就没问题了。

隧道代理

第二种 Web 代理的原理也很简单:

HTTP 客户端通过 CONNECT 方法请求隧道代理创建一条到达任意目的服务器和端口的 TCP 连接,并对客户端和服务器之间的后继数据进行盲转发。

假如我通过代理访问 A 网站,浏览器首先通过 CONNECT 请求,让代理创建一条到 A 网站的 TCP 连接;一旦 TCP 连接建好,代理无脑转发后续流量即可。所以这种代理,理论上适用于任意基于 TCP 的应用层协议,HTTPS 网站使用的 TLS 协议当然也可以。这也是这种代理为什么被称为隧道的原因。对于 HTTPS 来说,客户端透过代理直接跟服务端进行 TLS 握手协商密钥,所以依然是安全的

浏览器与代理进行 TCP 握手之后,发起了 CONNECT 请求,报文起始行如下:

CONNECT wangkejie.com:443 HTTP/1.1

对于 CONNECT 请求来说,只是用来让代理创建 TCP 连接,所以只需要提供服务器域名及端口即可,并不需要具体的资源路径。代理收到这样的请求后,需要与服务端建立 TCP 连接,并响应给浏览器这样一个 HTTP 报文:

HTTP/1.1 200 Connection Established

浏览器收到了这个响应报文,就可以认为到服务端的 TCP 连接已经打通,后续直接往这个 TCP 连接写协议数据即可。通过 Wireshark 的 Follow TCP Steam 功能.

而 CONNECT 暴露的域名和端口,对于普通的 HTTPS 请求来说,中间人一样可以拿到(IP 和端口很容易拿到,请求的域名可以通过 DNS Query 或者 TLS Client Hello 中的 Server Name Indication 拿到),所以这种方式并没有增加不安全性。

当时,项目中有个需求让画柱状图折线图表格,无意间看到了一个非常好的 Swift 库,是安卓的 iOS 是实现版,思路清晰,建议学习。Charts

为什么我要先说这个库呢,因为我觉得它用到了面相协议的思想,比如绘制,数据, 表格等 Provider。

  • 说的好听点,就是组件化了,每个组件负责自己的事情,通过 Protocol 向外传输职责的能力.
  • 还可以这么说,就是抽象类,类似于接口, 也类似于多继承。
  • 面向协议其实是建立在面向对象的基础上,这些对象具有共同的行为,称之为遵守某种协议,也就是抽象出来的面相协议。
  • 我不关心内部实现,我只关心行为。你可以吃饭,他也可以吃饭,但我只关心吃饭这件事儿,并不关心你们怎么吃的,吃的啥。。。 也就是组件化,你吃你的,他吃他的,你们自己实现怎么吃,我只下命令该吃饭了!

引用网上说的:
简单来说,面向协议编程是在面向对象编程基础上演变而来,将程序设计过程中遇到的数据类型的抽取(抽象)由使用基类进行抽取改为使用协议(Java语言中的接口)进行抽取。更简单点举个栗子来说,一个猫类、一个狗类,我们很容易想到抽取一个描述动物的基类,也会有人想到抽取一个动物通用的协议,那后者就可以被叫做面向协议编程了。什么?就是这样而已?苹果官方那么正式的称Swift是一门支持面向协议编程的语言,难道就是这么简单的内容?当然不会,有过面向对象编程经验的人都会清楚,协议的使用限制很多,并不能适用于大多数情况下数据类型的抽象。而在Swift语言中,协议被赋予了更多的功能和更广阔的使用空间,在Swift 2.0中,更为协议增加了扩展功能,使其能够胜任绝大多数情况下数据类型的抽象,所以苹果开始声称Swift是一门支持面向协议编程的语言。

RunLoop

惯例先上文档和源码:
CFRunloopRef 源码
RunLoop文档

Swift-Corelibs-foundation

官方文档翻译

参看我之前Runloop章节,RunLoop线程是一一对应的;以及若干个Mode、若干个commonModeItem,还有一个当前运行的CurrentMode。如果在RunLoop中需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。

CFRunLoopSource

对应CFRunLoopModeRef,其结构如下

1
2
3
4
5
6
7
8
9
10
11
struct __CFRunLoopSource {
CFRuntimeBase _base;
uint32_t _bits; //用于标记Signaled状态,source0只有在被标记为Signaled状态,才会被处理
pthread_mutex_t _lock;
CFIndex _order; /* immutable */
CFMutableBagRef _runLoops;
union {
CFRunLoopSourceContext version0; /* immutable, except invalidation */
CFRunLoopSourceContext1 version1; /* immutable, except invalidation */
} _context;
};

Source分为Source、Observer、Timer三种,他们统称为modeItem。

__CFRunLoopSource是事件产生的地方。Source有两个版本:Source0 和 Source1。

  • source0 只包含了一个回调(函数指针),source0是需要手动触发的Source,它并不能主动触发事件,必须要先把它标记为signal状态。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,也就是通过uint32_t _bits来实现的,只有_bits标记Signaled状态才会被处理。然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。
  • source1 包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。这种 Source 能主动唤醒 RunLoop 的线程。简单来说就是更加偏向于底层。

ource1除了多个了getPort。其余的字段含义和source0相同。作用就是当source被添加到mode中的时候,从这个函数中获得具体mach_port_t。

CFRunLoopTimer

对应RunLoopTimerRef,结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct __CFRunLoopTimer {
CFRuntimeBase _base;
uint16_t _bits; //标记fire状态
pthread_mutex_t _lock;
CFRunLoopRef _runLoop; //添加该timer的runloop
CFMutableSetRef _rlModes; //存放所有包含该timer的 mode的 modeName,意味着一个timer可能会在多个mode中存在
CFAbsoluteTime _nextFireDate;
CFTimeInterval _interval; //理想时间间隔 /* immutable */
CFTimeInterval _tolerance; //时间偏差 /* mutable */
uint64_t _fireTSR; /* TSR units */
CFIndex _order; /* immutable */
CFRunLoopTimerCallBack _callout; /* immutable */
CFRunLoopTimerContext _context; /* immutable, except invalidation */
};

它和 NSTimer 是toll-free bridged 的 , 根据上面的分析t一个timer可能会在多个mode中存在。

CFRunLoopObserver

1
2
3
4
5
6
7
8
9
10
struct __CFRunLoopObserver {
CFRuntimeBase _base;
pthread_mutex_t _lock;
CFRunLoopRef _runLoop;
CFIndex _rlCount;
CFOptionFlags _activities; /* immutable */
CFIndex _order; /* immutable */
CFRunLoopObserverCallBack _callout; /* immutable 设置回调函数*/
CFRunLoopObserverContext _context; /* immutable, except invalidation */
};

CFRunLoopObserver是观察者,可以观察RunLoop的各种状态,每个 Observer 都包含了一个回调(也就是上面的CFRunLoopObserverCallBack函数指针),当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化。状态定义在_CF_OPTIONS:

1
2
3
4
5
6
7
8
9
10
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), //即将进入run loop
kCFRunLoopBeforeTimers = (1UL << 1), //即将处理timer
kCFRunLoopBeforeSources = (1UL << 2),//即将处理source
kCFRunLoopBeforeWaiting = (1UL << 5),//即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6),//被唤醒但是还没开始处理事件
kCFRunLoopExit = (1UL << 7),//run loop已经退出
kCFRunLoopAllActivities = 0x0FFFFFFFU
};

下面是回调函数的原型:

typedef void (*CFRunLoopObserverCallBack)(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info);

小结

根据上面的数据结构,总结出如下内容。

一个model中有多个item,这些item由source、observe、timer组成。对于我们来讲用的最多的应该是observe和timer,常常通过回调来得知当前runloop的状态,进行来优化应用程序(比如监控在waiting状态下,这个时候做一些优化的事情)。其次设置定时器执行定时任务也是很常见的。

__CFRunloopRun(核心!!)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
/**
* 运行run loop
*
* @param rl 运行的RunLoop对象
* @param rlm 运行的mode
* @param seconds run loop超时时间
* @param stopAfterHandle true:run loop处理完事件就退出 false:一直运行直到超时或者被手动终止
* @param previousMode 上一次运行的mode
*
* @return 返回4种状态
*/
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
//获取系统启动后的CPU运行时间,用于控制超时时间
uint64_t startTSR = mach_absolute_time();

//如果RunLoop或者mode是stop状态,则直接return,不进入循环
if (__CFRunLoopIsStopped(rl)) {
__CFRunLoopUnsetStopped(rl);
return kCFRunLoopRunStopped;
} else if (rlm->_stopped) {
rlm->_stopped = false;
return kCFRunLoopRunStopped;
}

//mach端口,在内核中,消息在端口之间传递。 初始为0
mach_port_name_t dispatchPort = MACH_PORT_NULL;
//判断是否为主线程
Boolean libdispatchQSafe = pthread_main_np() && ((HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && NULL == previousMode) || (!HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && 0 == _CFGetTSD(__CFTSDKeyIsInGCDMainQ)));
//如果在主线程 && runloop是主线程的runloop && 该mode是commonMode,则给mach端口赋值为主线程收发消息的端口
if (libdispatchQSafe && (CFRunLoopGetMain() == rl) && CFSetContainsValue(rl->_commonModes, rlm->_name)) dispatchPort = _dispatch_get_main_queue_port_4CF();

#if USE_DISPATCH_SOURCE_FOR_TIMERS
mach_port_name_t modeQueuePort = MACH_PORT_NULL;
if (rlm->_queue) {
//mode赋值为dispatch端口_dispatch_runloop_root_queue_perform_4CF
modeQueuePort = _dispatch_runloop_root_queue_get_port_4CF(rlm->_queue);
if (!modeQueuePort) {
CRASH("Unable to get port for run loop mode queue (%d)", -1);
}
}
#endif

//GCD管理的定时器,用于实现runloop超时机制
dispatch_source_t timeout_timer = NULL;
struct __timeout_context *timeout_context = (struct __timeout_context *)malloc(sizeof(*timeout_context));

//立即超时
if (seconds <= 0.0) { // instant timeout
seconds = 0.0;
timeout_context->termTSR = 0ULL;
}
//seconds为超时时间,超时时执行__CFRunLoopTimeout函数
else if (seconds <= TIMER_INTERVAL_LIMIT) {
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, DISPATCH_QUEUE_OVERCOMMIT);
timeout_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
dispatch_retain(timeout_timer);
timeout_context->ds = timeout_timer;
timeout_context->rl = (CFRunLoopRef)CFRetain(rl);
timeout_context->termTSR = startTSR + __CFTimeIntervalToTSR(seconds);
dispatch_set_context(timeout_timer, timeout_context); // source gets ownership of context
dispatch_source_set_event_handler_f(timeout_timer, __CFRunLoopTimeout);
dispatch_source_set_cancel_handler_f(timeout_timer, __CFRunLoopTimeoutCancel);
uint64_t ns_at = (uint64_t)((__CFTSRToTimeInterval(startTSR) + seconds) * 1000000000ULL);
dispatch_source_set_timer(timeout_timer, dispatch_time(1, ns_at), DISPATCH_TIME_FOREVER, 1000ULL);
dispatch_resume(timeout_timer);
}
//永不超时
else { // infinite timeout
seconds = 9999999999.0;
timeout_context->termTSR = UINT64_MAX;
}

//标志位默认为true
Boolean didDispatchPortLastTime = true;
//记录最后runloop状态,用于return
int32_t retVal = 0;
do {
//初始化一个存放内核消息的缓冲池
uint8_t msg_buffer[3 * 1024];
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
mach_msg_header_t *msg = NULL;
mach_port_t livePort = MACH_PORT_NULL;
#elif DEPLOYMENT_TARGET_WINDOWS
HANDLE livePort = NULL;
Boolean windowsMessageReceived = false;
#endif
//取所有需要监听的port
__CFPortSet waitSet = rlm->_portSet;

//设置RunLoop为可以被唤醒状态
__CFRunLoopUnsetIgnoreWakeUps(rl);

//2.通知observer,即将触发timer回调,处理timer事件
if (rlm->_observerMask & kCFRunLoopBeforeTimers) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
//3.通知observer,即将触发Source0回调
if (rlm->_observerMask & kCFRunLoopBeforeSources) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);

//执行加入当前runloop的block
__CFRunLoopDoBlocks(rl, rlm);

//4.处理source0事件
//有事件处理返回true,没有事件返回false
Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
if (sourceHandledThisLoop) {
//执行加入当前runloop的block
__CFRunLoopDoBlocks(rl, rlm);
}

//如果没有Sources0事件处理 并且 没有超时,poll为false
//如果有Sources0事件处理 或者 超时,poll都为true
Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);

//第一次do..whil循环不会走该分支,因为didDispatchPortLastTime初始化是true
if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) {
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
//从缓冲区读取消息
msg = (mach_msg_header_t *)msg_buffer;
//5.接收dispatchPort端口的消息,(接收source1事件)
if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0)) {
//如果接收到了消息的话,前往第9步开始处理msg
goto handle_msg;
}
#elif DEPLOYMENT_TARGET_WINDOWS
if (__CFRunLoopWaitForMultipleObjects(NULL, &dispatchPort, 0, 0, &livePort, NULL)) {
goto handle_msg;
}
#endif
}

didDispatchPortLastTime = false;

//6.通知观察者RunLoop即将进入休眠
if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
//设置RunLoop为休眠状态
__CFRunLoopSetSleeping(rl);
// do not do any user callouts after this point (after notifying of sleeping)

// Must push the local-to-this-activation ports in on every loop
// iteration, as this mode could be run re-entrantly and we don't
// want these ports to get serviced.

__CFPortSetInsert(dispatchPort, waitSet);

__CFRunLoopModeUnlock(rlm);
__CFRunLoopUnlock(rl);

#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
#if USE_DISPATCH_SOURCE_FOR_TIMERS
//这里有个内循环,用于接收等待端口的消息
//进入此循环后,线程进入休眠,直到收到新消息才跳出该循环,继续执行run loop
do {
if (kCFUseCollectableAllocator) {
objc_clear_stack(0);
memset(msg_buffer, 0, sizeof(msg_buffer));
}
msg = (mach_msg_header_t *)msg_buffer;
//7.接收waitSet端口的消息
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY);
//收到消息之后,livePort的值为msg->msgh_local_port,
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);
#else
if (kCFUseCollectableAllocator) {
objc_clear_stack(0);
memset(msg_buffer, 0, sizeof(msg_buffer));
}
msg = (mach_msg_header_t *)msg_buffer;
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY);
#endif


#elif DEPLOYMENT_TARGET_WINDOWS
// Here, use the app-supplied message queue mask. They will set this if they are interested in having this run loop receive windows messages.
__CFRunLoopWaitForMultipleObjects(waitSet, NULL, poll ? 0 : TIMEOUT_INFINITY, rlm->_msgQMask, &livePort, &windowsMessageReceived);
#endif

__CFRunLoopLock(rl);
__CFRunLoopModeLock(rlm);

// Must remove the local-to-this-activation ports in on every loop
// iteration, as this mode could be run re-entrantly and we don't
// want these ports to get serviced. Also, we don't want them left
// in there if this function returns.

__CFPortSetRemove(dispatchPort, waitSet);


__CFRunLoopSetIgnoreWakeUps(rl);

// user callouts now OK again
//取消runloop的休眠状态
__CFRunLoopUnsetSleeping(rl);
//8.通知观察者runloop被唤醒
if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);

//9.处理收到的消息
handle_msg:;
__CFRunLoopSetIgnoreWakeUps(rl);

#if DEPLOYMENT_TARGET_WINDOWS
if (windowsMessageReceived) {
// These Win32 APIs cause a callout, so make sure we're unlocked first and relocked after
__CFRunLoopModeUnlock(rlm);
__CFRunLoopUnlock(rl);

if (rlm->_msgPump) {
rlm->_msgPump();
} else {
MSG msg;
if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE | PM_NOYIELD)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}

__CFRunLoopLock(rl);
__CFRunLoopModeLock(rlm);
sourceHandledThisLoop = true;

// To prevent starvation of sources other than the message queue, we check again to see if any other sources need to be serviced
// Use 0 for the mask so windows messages are ignored this time. Also use 0 for the timeout, because we're just checking to see if the things are signalled right now -- we will wait on them again later.
// NOTE: Ignore the dispatch source (it's not in the wait set anymore) and also don't run the observers here since we are polling.
__CFRunLoopSetSleeping(rl);
__CFRunLoopModeUnlock(rlm);
__CFRunLoopUnlock(rl);

__CFRunLoopWaitForMultipleObjects(waitSet, NULL, 0, 0, &livePort, NULL);

__CFRunLoopLock(rl);
__CFRunLoopModeLock(rlm);
__CFRunLoopUnsetSleeping(rl);
// If we have a new live port then it will be handled below as normal
}


#endif
if (MACH_PORT_NULL == livePort) {
CFRUNLOOP_WAKEUP_FOR_NOTHING();
// handle nothing
//通过CFRunloopWake唤醒
} else if (livePort == rl->_wakeUpPort) {
CFRUNLOOP_WAKEUP_FOR_WAKEUP();
//什么都不干,跳回2重新循环
// do nothing on Mac OS
#if DEPLOYMENT_TARGET_WINDOWS
// Always reset the wake up port, or risk spinning forever
ResetEvent(rl->_wakeUpPort);
#endif
}
#if USE_DISPATCH_SOURCE_FOR_TIMERS
//如果是定时器事件
else if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {
CFRUNLOOP_WAKEUP_FOR_TIMER();
//9.1 处理timer事件
if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
// Re-arm the next timer, because we apparently fired early
__CFArmNextTimerInMode(rlm, rl);
}
}
#endif
#if USE_MK_TIMER_TOO
//如果是定时器事件
else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort) {
CFRUNLOOP_WAKEUP_FOR_TIMER();
// On Windows, we have observed an issue where the timer port is set before the time which we requested it to be set. For example, we set the fire time to be TSR 167646765860, but it is actually observed firing at TSR 167646764145, which is 1715 ticks early. The result is that, when __CFRunLoopDoTimers checks to see if any of the run loop timers should be firing, it appears to be 'too early' for the next timer, and no timers are handled.
// In this case, the timer port has been automatically reset (since it was returned from MsgWaitForMultipleObjectsEx), and if we do not re-arm it, then no timers will ever be serviced again unless something adjusts the timer list (e.g. adding or removing timers). The fix for the issue is to reset the timer here if CFRunLoopDoTimers did not handle a timer itself. 9308754
//9.1处理timer事件
if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
// Re-arm the next timer
__CFArmNextTimerInMode(rlm, rl);
}
}
#endif
//如果是dispatch到main queue的block
else if (livePort == dispatchPort) {
CFRUNLOOP_WAKEUP_FOR_DISPATCH();
__CFRunLoopModeUnlock(rlm);
__CFRunLoopUnlock(rl);
_CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)6, NULL);
#if DEPLOYMENT_TARGET_WINDOWS
void *msg = 0;
#endif
//9.2执行block
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
_CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)0, NULL);
__CFRunLoopLock(rl);
__CFRunLoopModeLock(rlm);
sourceHandledThisLoop = true;
didDispatchPortLastTime = true;
} else {
CFRUNLOOP_WAKEUP_FOR_SOURCE();
// Despite the name, this works for windows handles as well
CFRunLoopSourceRef rls = __CFRunLoopModeFindSourceForMachPort(rl, rlm, livePort);
// 有source1事件待处理
if (rls) {
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
mach_msg_header_t *reply = NULL;
//9.2 处理source1事件
sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;
if (NULL != reply) {
(void)mach_msg(reply, MACH_SEND_MSG, reply->msgh_size, 0, MACH_PORT_NULL, 0, MACH_PORT_NULL);
CFAllocatorDeallocate(kCFAllocatorSystemDefault, reply);
}
#elif DEPLOYMENT_TARGET_WINDOWS
sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls) || sourceHandledThisLoop;
#endif
}
}
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
if (msg && msg != (mach_msg_header_t *)msg_buffer) free(msg);
#endif

__CFRunLoopDoBlocks(rl, rlm);

if (sourceHandledThisLoop && stopAfterHandle) {
//进入run loop时传入的参数,处理完事件就返回
retVal = kCFRunLoopRunHandledSource;
}else if (timeout_context->termTSR < mach_absolute_time()) {
//run loop超时
retVal = kCFRunLoopRunTimedOut;
}else if (__CFRunLoopIsStopped(rl)) {
//run loop被手动终止
__CFRunLoopUnsetStopped(rl);
retVal = kCFRunLoopRunStopped;
}else if (rlm->_stopped) {
//mode被终止
rlm->_stopped = false;
retVal = kCFRunLoopRunStopped;
}else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
//mode中没有要处理的事件
retVal = kCFRunLoopRunFinished;
}
//除了上面这几种情况,都继续循环
} while (0 == retVal);

if (timeout_timer) {
dispatch_source_cancel(timeout_timer);
dispatch_release(timeout_timer);
} else {
free(timeout_context);
}

return retVal;
}

RunLoop操作

  • 线程和 RunLoop 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里。
  • 线程刚创建时并没有 RunLoop(没有加到对应的runloop字典中),如果你不主动获取,那它一直都不会有。
  • RunLoop 的创建是发生在第一次获取时。一般是获取主线程的时候。
  • RunLoop 的销毁是发生在线程结束时。
  • 只能在一个线程的内部获取其 RunLoop(主线程除外),否则就这个Runloop就没有注册销毁回调。这一点是根据pthread_equal(t, pthread_self())后面的代码,如果是当前线程后面才会注册销毁回调。因为上面讲过Runlopp暴露给外部的创建方式只有CFRunLoopGetMain() 和 CFRunLoopGetCurrent()两种,所以这种情况不用考虑。下面是CFRunloop.h的头文件暴露接口,可以看到获取方式只有两种。

应用场景

还是YY大神的博客

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
{
/// 1. 通知Observers,即将进入RunLoop
/// 此处有Observer会创建AutoreleasePool: _objc_autoreleasePoolPush();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry);
do {

/// 2. 通知 Observers: 即将触发 Timer 回调。
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers);
/// 3. 通知 Observers: 即将触发 Source (非基于port的,Source0) 回调。
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources);
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);

/// 4. 触发 Source0 (非基于port的) 回调。
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0);
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);

/// 6. 通知Observers,即将进入休眠
/// 此处有Observer释放并新建AutoreleasePool: _objc_autoreleasePoolPop(); _objc_autoreleasePoolPush();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeWaiting);

/// 7. sleep to wait msg.
mach_msg() -> mach_msg_trap();


/// 8. 通知Observers,线程被唤醒
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopAfterWaiting);

/// 9. 如果是被Timer唤醒的,回调Timer
__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(timer);

/// 9. 如果是被dispatch唤醒的,执行所有调用 dispatch_async 等方法放入main queue 的 block
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block);

/// 9. 如果如果Runloop是被 Source1 (基于port的) 的事件唤醒了,处理这个事件
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1);


} while (...);

/// 10. 通知Observers,即将退出RunLoop
/// 此处有Observer释放AutoreleasePool: _objc_autoreleasePoolPop();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopExit);
}

Block

在最开始介绍CFRunloop的时候就简单提了一下其中关于block的两个字段blocks_head,blocks_tail。并且也提到在runloop周期中会对此调用__CFRunLoopDoBlocks来执行加入到这个runloop的block。下面从源码来说明一下block如何与runloop结合的。

先来看看最基本的block_item 数据结构,特别注意这里保存了runloop的model,决定了block是否应该执行。

1
2
3
4
5
struct _block_item {
struct _block_item *_next;
CFTypeRef _mode; // CFString or CFSet
void (^_block)(void);
};

在执行block的时候会传入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
/**
执行block
@param rl runloop
@param rlm 当前的model
@return 是否执行
*/
static Boolean __CFRunLoopDoBlocks(CFRunLoopRef rl, CFRunLoopModeRef rlm) { // Call with rl and rlm locked
//如果头结点没有、或者model不存在则强制返回,什么也不做
if (!rl->_blocks_head) return false;
if (!rlm || !rlm->_name) return false;
Boolean did = false;//记录其中一个block结点是否被执行过
//取出头尾结点,并且将当前runloop保存的头尾节点置位NULL
struct _block_item *head = rl->_blocks_head;
struct _block_item *tail = rl->_blocks_tail;
rl->_blocks_head = NULL;
rl->_blocks_tail = NULL;
//取出被标记为common的所有mode、及当前modelname
CFSetRef commonModes = rl->_commonModes;
CFStringRef curMode = rlm->_name;
__CFRunLoopModeUnlock(rlm);
__CFRunLoopUnlock(rl);

//定义两个临时变量,用于对保存block链表的遍历
struct _block_item *prev = NULL;
struct _block_item *item = head;//记录头指针,从头部开始遍历
//开始遍历block链表
while (item) {
struct _block_item *curr = item;
item = item->_next;
Boolean doit = false;//表示是否应该执行这个block,注意和前面的did区分开

//从blockitem结构体就知道,其中的_mode只能是CFString 或者CFSet
//如果block结点保存的modelCFString类型
if (CFStringGetTypeID() == CFGetTypeID(curr->_mode)) {
//是否执行block只需要满足下面三个条件中的一个
//1. blockitem 中保存的model是当前的model
//2. blockitem 中保存的model是标记为kCFRunLoopCommonModes的model
//3. 当前model保存在commonModes数组
doit = CFEqual(curr->_mode, curMode) || (CFEqual(curr->_mode, kCFRunLoopCommonModes) && CFSetContainsValue(commonModes, curMode));
} else {
//如果block结点保存的model是CFSet类型,步骤和上面一样,等于换成了包含。
doit = CFSetContainsValue((CFSetRef)curr->_mode, curMode) || (CFSetContainsValue((CFSetRef)curr->_mode, kCFRunLoopCommonModes) && CFSetContainsValue(commonModes, curMode));
}

//如果不执行block,则直接移动当前结点,进行下一个blockitem的判断
if (!doit) prev = curr;
if (doit) {
//如果执行block,则先移动结点。
if (prev) prev->_next = item;
if (curr == head) head = item;
if (curr == tail) tail = prev;

void (^block)(void) = curr->_block;
CFRelease(curr->_mode);
free(curr);
if (doit) {
//最终在这里执行block,__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__的函数原型就是调用block
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
did = true;
}
Block_release(block); // do this before relocking to prevent deadlocks where some yahoo wants to run the run loop reentrantly from their dealloc
}
}
__CFRunLoopLock(rl);
__CFRunLoopModeLock(rlm);
//重建循环链表
if (head) {
tail->_next = rl->_blocks_head;
rl->_blocks_head = head;
if (!rl->_blocks_tail) rl->_blocks_tail = tail;
}
return did;
}

通过上面分析可以知道:

  • block其实在runloop中通过循环链表保存的
  • 如果block可以加入到多个model下面,但是执行block只有在加入的那个model下才能之后,或者加入modle用common标记
  • 每次调用__CFRunLoopDoBlocks,会把加入的block遍历执行,然后重置循环链表。

AutoreleasePool

App启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()。

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

第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。

在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,开发者也不必显示创建 Pool 了。

手势识别

上面可以看到第二个observe就是_UIGestureRecognizerUpdateObserver,关于手势识别的。

当上面的 _UIApplicationHandleEventQueue() 识别了一个手势时,其首先会调用 Cancel 将当前的 touchesBegin/Move/End 系列回调打断。随后系统将对应的 UIGestureRecognizer 标记为待处理。

苹果注册了一个 Observer 监测 BeforeWaiting (Loop即将进入休眠) 事件,这个Observer的回调函数是 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行GestureRecognizer的回调。

当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。

界面更新

上面可以看到第三和四个observe分别是_beforeCACommitHandler与_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv,是关于动画及界面更新的。

当在操作 UI 时,比如改变了 Frame、更新了 UIView/CALayer 的层次时,或者手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去。

苹果注册了一个 Observer 监听 BeforeWaiting(即将进入休眠) 和 Exit (即将退出Loop) 事件,回调去执行一个很长的函数:
_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。这个函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。

定时器

上面截图中还有个timer

NSTimer 其实就是 CFRunLoopTimerRef,他们之间是 toll-free bridged 的。一个 NSTimer 注册到 RunLoop 后,RunLoop 会为其重复的时间点注册好事件。例如 10:00, 10:10, 10:20 这几个时间点。RunLoop为了节省资源,并不会在非常准确的时间点回调这个Timer。Timer 有个属性叫做 Tolerance (宽容度),标示了当时间点到后,容许有多少最大误差。

如果某个时间点被错过了,例如执行了一个很长的任务,则那个时间点的回调也会跳过去,不会延后执行。就比如等公交,如果 10:10 时我忙着玩手机错过了那个点的公交,那我只能等 10:20 这一趟了。

PerformSelecter

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

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

GCD

回到主线程
dispatch_async(dispatch_get_main_queue(), ^{
<##>
});

网络请求

常使用 NSURLConnection 时,你会传入一个 Delegate,当调用了 [connection start] 后,这个 Delegate 就会不停收到事件回调。实际上,start 这个函数的内部会会获取 CurrentRunLoop,然后在其中的 DefaultMode 添加了4个 Source0 (即需要手动触发的Source)。CFMultiplexerSource 是负责各种 Delegate 回调的,CFHTTPCookieStorage 是处理各种 Cookie 的。

Runloop在平时开发中的应用

  • AFN线程保活

AF希望能在后台线程接收 Delegate 回调。为此 AFNetworking 单独创建了一个线程,并在这个线程中启动了一个 RunLoop。过程在networkRequestThreadEntryPoint中,因为RunLoop 启动前内部必须要有至少一个 Timer/Observer/Source,所以 AFNetworking 在 [runLoop run] 之前先创建了一个新的 NSMachPort 添加进去了。通常情况下,调用者需要持有这个 NSMachPort (mach_port) 并在外部线程通过这个 port 发送消息到 loop 内;但此处添加 port 只是为了让 RunLoop 不至于退出,并没有用于实际的发送消息。

当需要这个后台线程执行任务时,AFNetworking 通过调用 [NSObject performSelector:onThread:..] 将这个任务扔到了后台线程的 RunLoop 中,具体内容对应start方法。

  • AsyncDisplayKit

ASDK 仿照 QuartzCore/UIKit 框架的模式,实现了一套类似的界面更新的机制:即在主线程的 RunLoop 中添加一个 Observer,监听了 kCFRunLoopBeforeWaiting 和 kCFRunLoopExit 事件,在收到回调时,遍历所有之前放入队列的待处理的任务,然后一一执行。

可以直接看源码进行分析AsyncDisplayKit,但是现在更名为Texture

RunLoop的启动和退出

  • 启动RunLoop的三种方式
    通过[NSRunLoop currentRunLoop]或者CFRunLoopGetCurrent()可以获取当前线程的runloop。
    1
    2
    3
    - (void)run;  
    - (void)runUntilDate:(NSDate *)limitDate;
    - (void)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;
    这三种方式无论通过哪一种方式启动runloop,如果没有一个输入源或者timer附加于runloop上,runloop就会立刻退出。
    (1) 第一种方式,runloop会一直运行下去(线程常驻),在此期间会处理来自输入源的数据,并且会在NSDefaultRunLoopMode模式下重复调用runMode:beforeDate:方法。
    (2) 第二种方式,可以设置超时时间,在超时时间到达之前,runloop会一直运行,在此期间runloop会处理来自输入源的数据,并且也会在NSDefaultRunLoopMode模式下重复调用runMode:beforeDate:方法。
    (3) 第三种方式,runloop会运行一次,超时时间到达或者第一个input source被处理,则runloop就会退出。
    前两种启动方式会重复调用runMode:beforeDate:方法。
  • 退出RunLoop的方式

(1) 第一种启动方式的退出方法
文档说,如果想退出runloop,不应该使用第一种启动方式来启动runloop。
如果runloop没有input sources或者附加的timer,runloop就会退出。
虽然这样可以将runloop退出,但是苹果并不建议我们这么做,因为系统内部有可能会在当前线程的runloop中添加一些输入源,所以通过手动移除input source或者timer这种方式,并不能保证runloop一定会退出。
(2)第二种启动方式的退出方法 runUntilDate:
可以通过 设置超时时间 来退出 runloop。
(3)第三种启动方式的退出方法 runMode:beforeDate:
通过这种方式启动,runloop只会运行一次,当超时时间到达或者第一个输入源被处理,runloop就会退出。

如果我们想控制 runloop 的退出时机,而不是在处理完一个输入源事件之后就退出,那么就要重复调用runMode:beforeDate:
具体可以参考苹果文档给出的方案,如下:

1
2
3
4
5
NSRunLoop *myLoop  = [NSRunLoop currentRunLoop];
myPort = (NSMachPort *)[NSMachPort port];
[myLoop addPort:_port forMode:NSDefaultRunLoopMode];
BOOL isLoopRunning = YES; // global
while (isLoopRunning && [myLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]);
1
2
3
4
5
6
//在关闭runloop的地方
- (void)quitLoop
{
isLoopRunning = NO;
CFRunLoopStop(CFRunLoopGetCurrent());
}

总之
如果不想退出runloop可以使用第一种方式启动runloop;
使用第二种方式启动runloop,可以通过设置超时时间来退出;
使用第三种方式启动runloop,可以通过设置超时时间或者使用CFRunLoopStop方法来退出。

AFNetWorking

由于AFNetWorking在 NSURLSession初始化的时候,用到了一个代理方法。Session在ARC下不会及时的释放

在使用instruments做内存泄漏分析时,发现所有使用如下语句的地方都有内存泄漏,:

1
2
[AFHTTPSessionManager manager];
@property (nullable, readonly, retain) id <NSURLSessionDelegate> delegate;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/*
* Customization of NSURLSession occurs during creation of a new session.
* If you only need to use the convenience routines with custom
* configuration options it is not necessary to specify a delegate.
* If you do specify a delegate, the delegate will be retained until after
* the delegate has been sent the URLSession:didBecomeInvalidWithError: message.
*/
//+ (NSURLSession *)sessionWithConfiguration:(NSURLSessionConfiguration *)configuration;

/*翻译:
NSURLSession的自定义发生在创建新会话期间。如果只需要使用带有自定义配置选项的便利例程,则不需要指定委托。
如果您指定了一个委托,那么该委托将被保留(retain),直到URLSession:didBecomeInvalidWithError: message发送给委托之后。
*/

+ (NSURLSession *)sessionWithConfiguration:(NSURLSessionConfiguration *)configuration delegate:(nullable id <NSURLSessionDelegate>)delegate delegateQueue:(nullable NSOperationQueue *)queue;

解决方法:
一般我们通过写单例的方式

1
2
3
4
5
6
7
8
9
static AFHTTPSessionManager *manager ;`
-(AFHTTPSessionManager *)sharedHTTPSession{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
manager = [AFHTTPSessionManager manager];
manager.requestSerializer.timeoutInterval = 10;
});
return manager;
}

这样的话,就不会创造出更多的session,避免了泄露

暂停