前言
最近上班空闲时间相对来说宽裕一些,加上本身也在复习功课,在学习过程中对于iOS开发中经常使用到的 performSelector 执行方法的底层做一次详细的学习,并且对一些资料进行整理放到博客中来,文中很多内容都是摘录至别人的博客。
简介
performSelector 系列的函数我们都不陌生,但是对于它不同的变种以及底层原理在很多时候还是容易分不清楚,所以笔者希望通过 runtime 源码以及 GUNStep 源码来一个个抽丝剥茧,把不同变种的 performSelector 理顺,并搞清楚每个方法的底层实现,如有错误,欢迎指正。
NSObject 下的 performSelector
1.1 探索
performSelector:(SEL)aSelector
performSelector 方法是最简单的一个 API,使用方法如下
1 | - (void)lz_performSelector |
performSelector: 方法只需要传入一个 SEL,在 runtime 底层实现为:
1 | - (id)performSelector:(SEL)sel { |
performSelector:(SEL)aSelector withObject:(id)object
1 | - (void)lz_performSelectorWithObj |
performSelector:withObject: 方法底层实现如下:
1 | - (id)performSelector:(SEL)sel withObject:(id)obj { |
performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2
这个方法相比上一个方法又多了一个参数:
1 | - (void)jh_performSelectorWithObj1AndObj2 |
performSelector:withObject:withObject: 方法底层实现如下:
1 | - (id)performSelector:(SEL)sel withObject:(id)obj1 withObject:(id)obj2 { |
1.2 小结
| 方法 | 底层实现 |
|---|---|
| performSelector: | ((id(*)(id, SEL))objc_msgSend)(self, sel) |
| performSelector:withObject: | ((id(*)(id, SEL, id))objc_msgSend)(self, sel, obj) |
| performSelector:withObject:withObject: | ((id(*)(id, SEL, id, id))objc_msgSend)(self, sel, obj1, obj2) |
这三个方法应该是使用频率很高的 performSelector 系列方法了,我们只需要记住这三个方法在底层都是执行的消息发送即可。
RunLoop 相关的 PerformSelector

如上图所示,在NSRunLoop 头文件中,定义了的两个分类,分别是
NSDelayedPerforming对应于NSObjectNSOrderedPerform对应于NSRunLoop
2.1 NSObject 分类 NSDelayedPerforming
2.1.1 探索
performSelector:WithObject:afterDelay:
1 | - (void)lz_performSelectorwithObjectafterDelay |
苹果官方注释文档如下:
This method sets up a timer to perform the aSelector message on the current thread’s run loop. The timer is configured to run in the default mode (NSDefaultRunLoopMode). When the timer fires, the thread attempts to dequeue the message from the run loop and perform the selector. It succeeds if the run loop is running and in the default mode; otherwise, the timer waits until the run loop is in the default mode.
这个方法会在当前线程所对应的 runloop 中设置一个定时器来执行传入的 SEL。定时器需要在 NSDefaultRunLoopMode 模式下才会被触发。当定时器启动后,线程会尝试从 runloop 中取出 SEL 然后执行。
如果 runloop 已经启动并且处于 NSDefaultRunLoopMode 的话,SEL 执行成功。否则,直到 runloop 处于 NSDefaultRunLoopMode 前,timer 都会一直等待
通过断点调试如下图所示,runloop 底层最终通过 __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__ () 来触发任务执行的。

因为NSRunLoop 并没有开源,所以我们只能通过 GNUStep 来窥探底层实现细节,如下所示:
1 | - (void) performSelector: (SEL)aSelector |
我们可以看到,在 performSelector:WithObject:afterDelay: 底层
- 获取当前线程的
NSRunLoop对象。 - 通过传入的
SEL、argument和delay初始化一个GSTimedPerformer实例对象,GSTimedPerformer类型里面封装了NSTimer对象。 - 然后把
GSTimedPerformer实例加入到RunLoop对象的_timedPerformers成员变量中 - 释放掉
GSTimedPerformer对象 - 以
default mode将timer对象加入到runloop中
performSelector:WithObject:afterDelay:inModes
performSelector:WithObject:afterDelay:inModes 方法相比上个方法多了一个 modes 参数,根据官方文档的定义,根据官方文档的定义,只有当 runloop 处于 modes 中的任意一个 mode 时,才会执行任务,如果 modes 为空,那么将不会执行任务。
1 | - (void)jh_performSelectorwithObjectafterDelayInModes |
注意:
这里我们如果把 modes 参数改为 UITrackingRunLoopMode,那么就只有在 scrollView 发生滚动的时候才会触发 timer
我们再看一下 GNUStep 对应的实现:
1 | - (void) performSelector: (SEL)aSelector |
可以看到,相比于上一个方法的底层实现不同的是,这里会往循环添加不同 mode 的 timer 对象到 runloop 中。
cancelPreviousPerformRequestsWithTarget:cancelPreviousPerformRequestsWithTarget:selector:object:
cancelPreviousPerformRequestsWithTarget:方法和 cancelPreviousPerformRequestsWithTarget:selector:object:方法是两个类方法,它们的作用是取消执行之前通过 performSelector:WithObject:afterDelay:方法注册的任务。使用起来如下所示:
1 | - (void)lz_performSelectorwithObjectafterDelayInModes |
这里有一个区别,就是 cancelPreviousPerformRequestsWithTarget: 类方法会取消掉 target 上所有的通过 performSelector:WithObject:afterDelay:实例方法注册的定时任务,而 cancelPreviousPerformRequestsWithTarget:selector:object: 只会通过传入的 SEL 取消匹配到的定时任务
在 GNUStep 中 cancelPreviousPerformRequestsWithTarget:方法底层实现如下:
1 | /* |
这里的逻辑其实很清晰:
- 取出当前
runloop对象的成员变量_timedPerformers - 判断定时任务数组是否为空,不为空才会继续往下走
- 初始化一个局部的空的任务数组,然后通过 getObjects 从成员量中取出任务
- 通过
while循环遍历所有的任务,如果匹配到了对应的target,则调用任务的invalidate方法,在这个方法内部会把定时器停掉然后销毁。接着还需要把成员变量_timedPerformers中对应的任务移除掉
另一个取消任务的方法底层实现如下:
1 | /* |
这里的实现不一样的地方就是除了判断 target 是否匹配外,还会判断 SEL 是否匹配,以及参数是否匹配。
2.1.2 小结
performSelector:WithObject:afterDelay:- 在该方法所在线程的
runloop处于default mode时,根据给定的时间触发给定的任务。底层原理是把一个timer对象以default mode加入到runloop对象中,等待唤醒。
- 在该方法所在线程的
performSelector:WithObject:afterDelay:inModes:- 在该方法所在线程的
runloop处于给定的任一mode时,根据给定的时间触发给定的任务。底层原理是循环把一个timer对象以给定的mode加入到runloop对象中,等待唤醒。
- 在该方法所在线程的
cancelPreviousPerformRequestsWithTarget:- 取消
target对象通过performSelector:WithObject:afterDelay:方法或performSelector:WithObject:afterDelay:inModes:方法注册的所有定时任务
- 取消
cancelPreviousPerformRequestsWithTarget:selector:object:- 取消
target对象通过performSelector:WithObject:afterDelay:方法或performSelector:WithObject:afterDelay:inModes:方法注册的指定的定时任务
- 取消
这四个方法是作为 NSObject 的 NSDelayedPerforming 分类存在于 NSRunLoop 源代码中,所以我们在使用的时候要注意一个细节,那就是执行这些方法的线程是否是主线程,如果是主线程,那么执行起来是没有问题的,但是,如果是在子线程中执行这些方法,则需要开启子线程对应的 runloop 才能保证执行成功。
1 | - (void)lz_performSelectorwithObjectafterDelay |
如上所示的代码,通过 GCD 的异步执行函数在全局并发队列上执行任务,并没有任何打印输出,我们加入 runloop 的启动代码后结果将完全不一样:

对于 performSelector:WithObject:afterDelay:inModes 方法,如果遇到这样的情况,也是一样的解决方案。
2.2 NSRunLoop 的分类 NSOrderedPerform
2.2.1 探索
performSelector:target:argument:order:modes:
performSelector:target:argument:order:modes: 方法的调用者是 NSRunLoop实例,然后需要传入要执行的 SEL,以及 SEL 对应的 target,和 SEL 要接收的参数 argument,最后是此次任务的优先级 order,以及一个 运行模式集合 modes,目的是当 runloop 的 currentMode 处于这个运行模式集合中的其中任意一个mode 时,就会按照优先级 order 来触发SEL的执行。具体使用如下:
1 | (void)lz_performSelectorTargetArgumentOrderModes |
可以看到输出结果就是按照我们传入的 order 参数作为任务执行的顺序。
GUNStep 中这个底层的底层实现如下:
1 | - (void) performSelector: (SEL)aSelector |
我们已经知道了 performSelector:WithObject:afterDelay:方法底层实现使用一个包裹 timer 对象的数据结构的方式,而这里是使用了一个包裹了 selector、target、argument 以及优先级 order的数据结构的方式来实现。同时在 context 上下文的成员变量 performers 中存储了要执行的任务队列,所以这里实际上就是一个简单的插入排序的过程。
cancelPerformSelector:target:argument:cancelPerformSelectorsWithTarget:
cancelPerformSelector:target:argument:和 cancelPerformSelectorsWithTarget:使用起来比较简单,一个需要传入 selector、target 和 argument,另一个只需要传入 target。它们的作用分别是根据给定的三个参数或 target 去 runloop 底层的 performers 任务队列中查找任务,找到了就从队列中移除掉。
而底层具体实现具体如下:
1 | /** |
2.2.2 小结
- performSelector:target:argument:order:modes:
- 在该方法所在线程的 runloop 处于给定的任一 mode 时,且处于下一次 runloop 消息循环的开头的时候触发给定的任务。底层原理是循环把一个类似于 timer 的对象加入到 runloop 的上下文的任务队列中,等待唤醒
- cancelPerformSelector:target:argument:
- 取消 target 对象通过 performSelector:target:argument:order:modes: 方法方法注册的指定的任务
- cancelPerformSelectorsWithTarget:
- 取消 target 对象通过 performSelector:target:argument:order:modes: 方法方法注册的所有任务
这里同样的也需要注意,如果是在子线程中执行这些方法,则需要开启子线程对应的 runloop 才能保证执行成功。
Thread 相关的 performSelector

如上图所示,在 NSThread 中定义了 NSObject 的分类 NSThreadPerformAdditions,其中定义了 5 个 performSelector 的方法。
3.1 探索
performSelector:onThread:withObject:waitUntilDone:performSelector:onThread:withObject:waitUntilDone:modes:
根据官方文档的解释,第一个方法相当于调用了第二个方法,然后 mode 传入的是 kCFRunLoopCommonModes。我们这里只研究第一个方法。
这个方法需要相比于 performSeletor:withObject:多了两个参数,分别是要哪个线程执行任务以及是否阻塞当前线程。但是使用这个方法一定要小心,如下图所示是一个常见的错误用法:

这里报的错是 target thread exited while waiting for the perform,就是说已经退出的线程无法执行定时任务。
熟悉 iOS 多线程的同学都知道 NSThread 实例化之后的线程对象在 start 之后就会被系统回收,而之后调用的 performSelector:onThread:withObject:waitUntilDone: 方法又在一个已经回收的线程上执行任务,显然就会崩溃。这里的解决方案就是给这个子线程对应的 runloop 启动起来,让线程具有 『有事来就干活,没事干就睡觉』 的功能,具体代码如下:

对于 waitUntilDone 参数,如果我们设置为 YES:

如果设置为 NO:

所以这里的 waitUntilDone 可以简单的理解为控制同步或异步执行。
在探索 GNUStep 对应实现之前,我们先熟悉一下 GSRunLoopThreadInfo
1 | /* Used to handle events performed in one thread from another. |
GSRunLoopThreadInfo 是每个线程特有的一个属性,存储了线程和 runloop 之间的一些信息,可以通过下面的方式获取:
1 | GSRunLoopThreadInfo * |
然后是另一个 GSPerformHolder:
1 | /** |
GSPerformHolder 封装了任务的细节(receiver, argument, selector)以及运行模式(mode)和一把条件锁( NSConditionLock )。
接着我们目光聚焦到源码 performSelector:onThread:withObject:waitUntilDone:modes:具体实现上:
1 | - (void) performSelector: (SEL)aSelector |
- 声明一个
GSRunLoopThreadInfo对象和一条NSThread线程 - 判断运行模式数组参数是否为空
- 获取当前线程,将结果赋值于第一步声明的局部线程变量
- 判断如果传入的要执行任务的线程
aThread如果为空,那么就把当前线程赋值于到aThread上 - 确保 aThread 不为空之后获取该线程对应的
GSRunLoopThreadInfo对象并赋值于第一步声明的局部info变量 - 确保
info有值后,判断是否是在当前线程上执行任务 - 如果是在当前线程上执行任务,接着判断是否要阻塞当前线程,或当前线程的
runloop为空。- 如果是的话,则直接调用
performSelector:withObject来执行任务 - 如果不是的话,则通过线程对应的
runloop对象调用performSelector:target:argument:order:modes:来执行任务
- 如果是的话,则直接调用
如果不是在当前线程上执行任务,声明一个
GSPerformHolder局部变量,声明一把空的条件锁NSConditionLock- 判断要执行任务的线程是否已经被回收,如果已被回收,则抛出异常
如果未被回收
- 判断是否要阻塞当前线程,如果传入的参数需要阻塞,则初始化条件锁
- 根据传入的参数及条件锁初始化 GSPerformHolder 实例
- 然后在 info 中加入 GSPerformHolder 实例
- 然后判断条件锁如果不为空,赋予条件锁何时加锁的条件,然后解锁条件锁,然后释放条件锁
- 判断 GSPerformHolder 局部变量是否已经被释放,如果没有被释放,抛出异常
performSelectorOnMainThread:withObject:waitUntilDone:performSelectorOnMainThread:withObject:waitUntilDone:modes:
顾名思义,这两个方法其实就是在主线程上执行任务,根据传入的参数决定是否阻塞主线程,以及在哪些运行模式下执行任务。使用方法如下:
1 | - (void)jh_performSelectorOnMainThreadwithObjectwaitUntilDone |
因为是在主线程上执行,所以并不需要手动开启 runloop。我们来看下这两个方法在 GNUStep 中底层实现:
1 | - (void) performSelectorOnMainThread: (SEL)aSelector |
不难看出,这里其实就是调用的 performSelector:onThread:withObject:waitUntilDone:modes 方法,但是有一个细节需要注意,就是有可能在 NSThread 类被初始化之前,就调用了 performSelectorOnMainThread方法,所以需要手动调用一下 [NSThread currentThread]。
performSelectorInBackground:withObject:
最后要探索的是 performSelectorInBackground:withObject:方法,这个方法用法如下:
1 | - (void)jh_performSelectorOnBackground |
根据输出我们可知,这里显然是开了一条子线程来执行任务,我们看一下 GNUStep 的底层实现:
1 | - (void) performSelectorInBackground: (SEL)aSelector |
可以看到在底层其实是调用的 NSThread 的类方法来执行传入的任务。关于 NSThread 细节我们后面会进行探索。
3.2 小结
performSelector:onThread:withObject:waitUntilDone:和performSelector:onThread:withObject:waitUntilDone:modes:- 在该方法所在线程的 runloop 处于给定的任一 mode 时,判断是否阻塞当前线程,并且处于下一次 runloop 消息循环的开头的时候触发给定的任务。
performSelectorOnMainThread:withObject:waitUntilDone:和performSelectorOnMainThread:withObject:waitUntilDone:modes:- 当主线程的 runloop 处于给定的任一 mode 时,判断是否阻塞主线程,并且处于下一次 runloop 消息循环的开头的时候触发给定的任务。
performSelectorInBackground:withObject:- 在子线程上执行给定的任务。底层是通过
NSThread的detachNewThread实现。
- 在子线程上执行给定的任务。底层是通过
总结

