前言
最近上班空闲时间相对来说宽裕一些,加上本身也在复习功课,在学习过程中对于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
对应于NSObject
NSOrderedPerform
对应于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
实现。
- 在子线程上执行给定的任务。底层是通过