http://blog.cnbluebox.com/blog/2014/07/01/cocoashen-ru-xue-xi-nsoperationqueuehe-nsoperationyuan-li-he-shi-yong/html
目前在 iOS 和 OS X 中有两套先进的同步 API 可供咱们使用:NSOperation 和 GCD 。其中 GCD 是基于 C 的底层的 API ,而 NSOperation 则是 GCD 实现的 Objective-C API。 虽然 NSOperation 是基于 GCD 实现的, 可是并不意味着它是一个 GCD 的 “dumbed-down” 版本, 相反,咱们能够用NSOperation 轻易的实现一些 GCD 要写大量代码的事情。 所以, NSOperationQueue 是被推荐使用的, 除非你遇到了 NSOperationQueue 不能实现的问题。ios
1. 为何优先使用NSOperationQueue而不是GCD
曾经我有一段时间我很是喜欢使用GCD来进行并发编程,由于虽然它是C的api,可是使用起来却很是简单和方便, 不过这样也就容易使开发者忘记并发编程中的许多注意事项和陷阱。c++
好比你可能写过相似这样的代码(这样来请求网络数据):程序员
1
2 3 4 5 6 7 8 9 10 |
dispatch_async(_Queue, ^{ //请求数据 NSData *data = [NSData dataWithContentURL:[NSURL URLWithString:@"http://domain.com/a.png"]]; dispatch_async(dispatch_get_main_queue(), ^{ [self refreshViews:data]; }); }); |
没错,它是能够正常的工做,可是有个致命的问题:这个任务是没法取消的 dataWithContentURL:
是同步的拉取数据,它会一直阻塞线程直到完成请求,若是是遇到了超时的状况,它在这个时间内会一直占有这个线程;在这个期间并发队列就须要为其余任务新建线程,这样可能致使性能降低等问题。web
所以咱们不推荐这种写法来从网络拉取数据。编程
操做队列(operation queue)是由 GCD 提供的一个队列模型的 Cocoa 抽象。GCD 提供了更加底层的控制,而操做队列则在 GCD 之上实现了一些方便的功能,这些功能对于 app 的开发者来讲一般是最好最安全的选择。NSOperationQueue相对于GCD来讲有如下优势:windows
- 提供了在 GCD 中不那么容易复制的有用特性。
- 能够很方便的取消一个NSOperation的执行
- 能够更容易的添加任务的依赖关系
- 提供了任务的状态:isExecuteing, isFinished.
名词: 本文中提到的 “任务”, “操做” 即表明要再NSOperation中执行的事情。 api
2. Operation Queues的使用
2.1 NSOperationQueue
NSOperationQueue
有两种不一样类型的队列:主队列和自定义队列。主队列运行在主线程之上,而自定义队列在后台执行。在两种类型中,这些队列所处理的任务都使用 NSOperation
的子类来表述。安全
1
2 3 4 5 6 |
NSOperationQueue *mainQueue = [NSOperationQueue mainQueue]; //主队列 NSOperationQueue *queue = [[NSOperationQueue alloc] init]; //自定义队列 NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{ //任务执行 }]; [queue addOperation:operation]; |
咱们能够经过设置 maxConcurrentOperationCount
属性来控制并发任务的数量,当设置为 1
时, 那么它就是一个串行队列。主对列默认是串行队列,这一点和 dispatch_queue_t
是类似的。网络
2.2 NSOperation
你可使用系统提供的一些现成的 NSOperation
的子类, 如 NSBlockOperation
、 NSInvocationOperation
等(如上例子)。你也能够实现本身的子类, 经过重写 main
或者 start
方法 来定义本身的 operations 。
使用 main
方法很是简单,开发者不须要管理一些状态属性(例如 isExecuting 和 isFinished),当 main 方法返回的时候,这个 operation 就结束了。这种方式使用起来很是简单,可是灵活性相对重写 start 来讲要少一些, 由于main方法执行完就认为operation结束了,因此通常能够用来执行同步任务。
1
2 3 4 5 6 |
@implementation YourOperation - (void)main { // 任务代码 ... } @end |
若是你但愿拥有更多的控制权,或者想在一个操做中能够执行异步任务,那么就重写 start
方法, 可是注意:这种状况下,你必须手动管理操做的状态, 只有当发送 isFinished
的 KVO 消息时,才认为是 operation 结束
1
2 3 4 5 6 7 8 9 10 11 12 |
@implementation YourOperation - (void)start { self.isExecuting = YES; // 任务代码 ... } - (void)finish //异步回调 { self.isExecuting = NO; self.isFinished = YES; } @end |
当实现了start方法时,默认会执行start方法,而不执行main方法
为了让操做队列可以捕获到操做的改变,须要将状态的属性以配合 KVO
的方式进行实现。若是你不使用它们默认的 setter 来进行设置的话,你就须要在合适的时候发送合适的 KVO
消息。
须要手动管理的状态有:
isExecuting
表明任务正在执行中isFinished
表明任务已经执行完成isCancelled
表明任务已经取消执行
手动的发送 KVO
消息, 通知状态更改以下 :
1
2 3 |
[self willChangeValueForKey:@"isCancelled"]; _isCancelled = YES; [self didChangeValueForKey:@"isCancelled"]; |
为了能使用操做队列所提供的取消功能,你须要在长时间操做中时不时地检查 isCancelled
属性, 好比在一个长的循环中:
1
2 3 4 5 6 7 8 9 |
@implementation MyOperation - (void)main { while (notDone && !self.isCancelled) { // 任务处理 } } @end |
3. RunLoop
在cocoa中讲到多线程,那么就不得不讲到RunLoop。 在ios/mac的编码中,咱们彷佛不须要过多关心代码是如何执行的,一切仿佛那么天然。好比咱们知道当滑动手势时,tableView就会滚动,启动一 个NSTimer以后,timer的方法就会定时执行, 可是为何呢,实际上是RunLoop在帮咱们作这些事情:分发消息。
3.1 什么是RunLoop
你应该看过这样的伪代码解释ios的app中main函数作的事情:
1
2 3 4 5 6 |
int main(int argc, char * argv[]) { while (true) { [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; } } |
也应该看过这样的代码用来阻塞一个线程:
1
2 3 |
while (!complete) { [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; } |
或许你感受到他们有些神奇,但愿个人解释能让你明白一些.
咱们先思考一个问题: 当咱们打开一个IOS应用以后,什么也不作,这时候看起来是没有代码在执行的,为何应用没有退出呢?
咱们在写c的简单的只有一个main函数的程序时就知道,当main的代码执行完,没有事情可作的时候,程序就执行完毕退出了。而咱们IOS的应用是如何作到在没有事情作的时候维持应用的运行的呢? 那就是RunLoop。
RunLoop的字面意思就是“运行回路”,听起来像是一个循环。实际它就是一个循环,它在循环监听着事件源,把消息分发给线程来执行。RunLoop并非线程,也不是并发机制,可是它在线程中的做用相当重要,它提供了一种异步执行代码的机制。
3.2 事件源
由图中能够看出NSRunLoop只处理两种源:输入源、时间源。而输入源又能够分为:NSPort
、自定义源、performSelector:OnThread:delay:
, 下面简单介绍下这几种源:
3.2.1 NSPort 基于端口的源
Cocoa和 Core Foundation 为使用端口相关的对象和函数建立的基于端口的源提供了内在支持。Cocoa中你从不须要直接建立输入源。你只须要简单的建立端口对象,并使用NSPort 的方法将端口对象加入到run loop。端口对象会处理建立以及配置输入源。
NSPort通常分三种: NSMessagePort
(基本废弃)、NSMachPort
、 NSSocketPort
。 系统中的NSURLConnection
就是基于NSSocketPort
进行通讯的,因此当在后台线程中使用NSURLConnection
时,须要手动启动RunLoop, 由于后台线程中的RunLoop默认是没有启动的,后面会讲到。
3.2.2 自定义输入源
在Core Foundation程序中,必须使用CFRunLoopSourceRef类型相关的函数来建立自定义输入源,接着使用回调函数来配置输入源。Core Fundation会在恰当的时候调用回调函数,处理输入事件以及清理源。常见的触摸、滚动事件等就是该类源,由系统内部实现。
通常咱们不会使用该种源,第三种状况已经知足咱们的需求
3.2.3 performSelector:OnThread
Cocoa提供了能够在任一线程执行函数(perform selector)的输入源。和基于端口的源同样,perform selector请求会在目标线程上序列化,减缓许多在单个线程上容易引发的同步问题。而和基于端口的源不一样的是,perform selector执行完后会自动清除出run loop。
此方法简单实用,使用也更普遍。
3.2.4 定时源
定时源就是NSTimer了,定时源在预设的时间点同步地传递消息。由于Timer是基于RunLoop的,也就决定了它不是实时的。
3.3 RunLoop观察者
咱们能够经过建立CFRunLoopObserverRef
对象来检测RunLoop的工做状态,它能够检测RunLoop的如下几种事件:
- Run loop入口
- Run loop将要开始定时
- Run loop将要处理输入源
- Run loop将要休眠
- Run loop被唤醒但又在执行唤醒事件前
- Run loop终止
3.4 Run Loop Modes
RunLoop对于上述四种事件源的监视,能够经过设置模式来决定监视哪些源。 RunLoop只会处理与当前模式相关联的源,未与当前模式关联的源则处于暂停状态。
cocoa和Core Foundation预先定义了一些模式(Apple文档翻译):
Mode | Name | Description |
---|---|---|
Default | NSDefaultRunLoopMode (Cocoa) kCFRunLoopDefaultMode (Core Foundation) | 缺省状况下,将包含全部操做,而且大多数状况下都会使用此模式 |
Connection | NSConnectionReplyMode (Cocoa) | 此模式用于处理NSConnection的回调事件 |
Modal | NSModalPanelRunLoopMode (Cocoa) | 模态模式,此模式下,RunLoop只对处理模态相关事件 |
Event Tracking | NSEventTrackingRunLoopMode (Cocoa) | 此模式下用于处理窗口事件,鼠标事件等 |
Common Modes | NSRunLoopCommonModes (Cocoa) kCFRunLoopCommonModes (Core Foundation) | 此模式用于配置”组模式”,一个输入源与此模式关联,则输入源与组中的全部模式相关联。 |
咱们也能够自定义模式,能够参考ASIHttpRequest
在同步执行时,自定义了 runLoop 的模式叫 ASIHTTPRequestRunLoopMode
。ASI的Timer源就关联了此模式。
3.5 常见问题一:为何TableView滑动时,Timer暂停了?
咱们作个测试: 在一个 viewController 的 scrollViewWillBeginDecelerating:
方法里面打个断点, 而后滑动 tableView
。 待断点处, 使用 lldb
打印一下 [NSRunLoop currentRunLoop]
。 在描述中能够看到当前的RunLoop的运行模式:
current mode = UITrackingRunLoopMode common modes = <CFBasicHash 0x14656e60 [0x3944dae0]>{type = mutable set, count = 2, entries => 0 : <CFString 0x398d54c0 [0x3944dae0]>{contents = "UITrackingRunLoopMode"} 1 : <CFString 0x39449d10 [0x3944dae0]>{contents = "kCFRunLoopDefaultMode"} }
也就是说,当前主线程的 RunLoop 正在以 UITrackingRunLoopMode
的模式运行。 这个时候 RunLoop 只会处理与 UITrackingRunLoopMode
“绑定”的源, 好比触摸、滚动等事件;而 NSTimer
是默认“绑定”到 NSRunLoopDefaultMode
上的, 因此 Timer
是事情是不会被 RunLoop 处理的,咱们的看到的时定时器被暂停了!
常见的解决方案是把Timer“绑定”到 NSRunLoopCommonModes
模式上, 那么Timer就能够与:
1
|
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; |
这样这个Timer就能够和当前组中的两种模式 UITrackingRunLoopMode
和 kCFRunLoopDefaultMode
相关联了。 RunLoop在这两种模式下,Timer均可以正常运行了。
注意: 由上面能够发现 NSTimer
是不许确的。 由于RunLoop只负责分发源的消息。若是线程当前正在处理繁重的任务,好比循环,就有可能致使Timer本次延时,或者少执行一次。网上有人作过实验:
上面的Log是一个间隔为 1 s
的计时器,咱们能够发如今 12.836s ~ 15.835s
之间的时间段内, 明显的 13s
的方法没有执行。 14s
的方法有所延迟。
所以当咱们用NSTimer来完成一些计时任务时,若是须要比较精确的话,最好仍是要比较“时间戳”。
3.6 常见问题二:后台的NSURLConnection不回调,Timer不运行
咱们知道每一个线程都有它的RunLoop, 咱们能够经过 [NSRunLoop currentRunLoop]
或 CFRunLoopGetCurrent()
来获取。 可是主线程和后台线程是不同的。主线程的RunLoop是一直在启动的。然后台线程的RunLoop是默认没有启动的。
后台线程的RunLoop没有启动的状况下的现象就是:“代码执行完,线程就结束被回收了”。就像咱们简单的程序执行完就退出了。 因此若是咱们但愿在代码执行完成后还要保留线程等待一些异步的事件时,好比NSURLConnection和NSTimer, 就须要手动启动后台线程的RunLoop。
启动RunLoop,咱们须要设定RunLoop的模式,咱们能够设置 NSDefaultRunLoopMode
。 那默认就是监听全部时间源:
1
2 3 4 5 |
//Cocoa [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; //Core Foundation CFRunLoopRun(); |
咱们也能够设置其余模式运行,可是咱们就须要把“事件源” “绑定”到该模式上:
1
2 3 4 5 6 7 8 |
//NSURLConnection [_connection scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; [[NSRunLoop currentRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate distantFuture]]; //Timer [[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes]; [[NSRunLoop currentRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate distantFuture]]; |
3.7 问题三:本节开头的例子为什么能够阻塞线程
1
2 3 |
while (!complete) { [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; } |
你应该知道这样一段代码能够阻塞当前线程,你可能会奇怪:RunLoop就是不停循环来检测源的事件,为何还要加个 while
呢?
这是由于RunLoop的特性,RunLoop会在没有“事件源”可监听时休眠。也就是说若是当前没有合适的“源”被RunLoop监听,那么这步就跳过了,不能起到阻塞线程的做用,因此仍是要加个while循环来维持。
同时注意:由于这段代码能够阻塞线程,因此请不要在主线程写下这段代码,由于它极可能会致使界面卡住。
4. 线程安全
讲了这么多,你是否已经对并发编程已经跃跃欲试了呢? 可是并发编程一直都不是一个轻松的事情,使用并发编程会带来许多陷阱。哪怕你是一个很成熟的程序员和架构师,也很难避免线程安全的问题;使用的越多,出错的可能就越大,所以能够不用多线程就不要使用。
关于并发编程的不可预见性有一个很是有名的例子:在1995年, NASA (美国宇航局)发送了开拓者号火星探测器,可是当探测器成功着陆在咱们红色的邻居星球后不久,任务嘎然而止,火星探测器莫名其妙的不停重启,在计算机领域 内,遇到的这种现象被定为为优先级反转,也就是说低优先级的线程一直阻塞着高优先级的线程。在这里咱们想说明的是,即便拥有丰富的资源和大量优秀工程师的 智慧,并发也仍是会在很多状况下反咬你你一口。
4.1 资源共享和资源饥饿
并发编程中许多问题的根源就是在多线程中访问共享资源。资源能够是一个属性、一个对象,通用的内存、网络设备或者一个文件等等。在多线程中任何一个共享的资源均可能是一个潜在的冲突点,你必须精心设计以防止这种冲突的发生。
通常咱们经过锁来解决资源共享的问题,也就是能够经过对资源加锁保证同时只有一个线程访问资源
4.1.1 互斥锁
互斥访问的意思就是同一时刻,只容许一个线程访问某个特定资源。为了保证这一点,每一个但愿访问共享资源的线程,首先须要得到一个共享资源的互斥锁。 对资源加锁会引起必定的性能代价。
4.1.2 原子性
从语言层面来讲,在 Objective-C 中将属性以 atomic 的形式来声明,就能支持互斥锁了。事实上在默认状况下,属性就是 atomic 的。将一个属性声明为 atomic 表示每次访问该属性都会进行隐式的加锁和解锁操做。虽然最把稳的作法就是将全部的属性都声明为 atomic,可是加解锁这也会付出必定的代价。
4.1.3 死锁
互斥锁解决了竞态条件的问题,但很不幸同时这也引入了一些其余问题,其中一个就是死锁。当多个线程在相互等待着对方的结束时,就会发生死锁,这时程序可能会被卡住。
好比下面的代码:
1
2 3 4 5 |
dispatch_sync(_queue, ^{ dispatch_sync(_queue, ^{ //do something }); }) |
再好比:
1
2 3 4 5 |
main() { dispatch_sync(dispatch_get_main_queue(), ^{ //do something }); } |
上面两个例子也能够说明 dispatch_sync
这个API是危险的,因此尽可能不要用。
当你的代码有死锁的可能时,它就会发生
4.1.4 资源饥饿
当你认为已经足够了解并发编程面临的问题时,又出现了一个新的问题。锁定的共享资源会引发读写问题。大多数状况下,限制资源一次只能有一个线程进行 读取访问实际上是很是浪费的。所以,在资源上没有写入锁的时候,持有一个读取锁是被容许的。这种状况下,若是一个持有读取锁的线程在等待获取写入锁的时候, 其余但愿读取资源的线程则由于没法得到这个读取锁而致使资源饥饿的发生。
4.2 优先级反转
优先级反转是指程序在运行时低优先级的任务阻塞了高优先级的任务,有效的反转了任务的优先级。GCD提供了3种级别的优先级队列,分别是 Default, High, Low。 高优先级和低优先级的任务之间共享资源时,就可能发生优先级反转。当低优先级的任务得到了共享资源的锁时,该任务应该迅速完成,并释放掉锁,这样高优先级 的任务就能够在没有明显延时的状况下继续执行。然而高优先级任务会在低优先级的任务持有锁的期间被阻塞。若是这时候有一个中优先级的任务(该任务不须要那 个共享资源),那么它就有可能会抢占低优先级任务而被执行,由于此时高优先级任务是被阻塞的,因此中优先级任务是目前全部可运行任务中优先级最高的。此 时,中优先级任务就会阻塞着低优先级任务,致使低优先级任务不能释放掉锁,这也就会引发高优先级任务一直在等待锁的释放。以下图:
使用不一样优先级的多个队列听起来虽然不错,但毕竟是纸上谈兵。它将让原本就复杂的并行编程变得更加复杂和不可预见。所以咱们写代码的时候最好只用Default优先级的队列,不要使用其余队列来让问题复杂化。
关于dispatch_queue的底层线程安全设计可参考:底层并发 API
5. 总结
本文主要讲了 NSOperationQueue、 NSRunLoop、 和线程安全等三大块内容。 但愿能够帮助你理解 NSOperation的使用, NSRunLoop的做用, 还有并发编程带来的复杂性和相关问题。
并发其实是一个很是棒的工具。它充分利用了现代多核 CPU 的强大计算能力。可是由于它的复杂性,因此咱们尽可能使用高级的API,尽可能写简单的代码,让并发模型保持简单; 这样能够写出高效、结构清晰、且安全的代码。
NSRunLoop的进一步理解
http://www.devdiv.com/nsrunloop_-article-2360-1.html
iPhone应用开发中关于NSRunLoop的概述是本文要介绍的内容,NSRunLoop是一种更加高明的消息处理模式,他就高明在对消息处理过程进 行了更好的抽象和封装,这样才能是的你不用处理一些很琐碎很低层次的具体消息的处理,在NSRunLoop中每个消息就被打包在input source或者是timer source中了,来看详细内容。
1.什么是NSRunLoop
咱们会常常看到这样的代码:
- - (IBAction)start:(id)sender
- {
- pageStillLoading = YES;
- [NSThread detachNewThreadSelector:@selector(loadPageInBackground:)toTarget:self withObject:nil];
- [progress setHidden:NO];
- while (pageStillLoading) {
- [NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
- }
- [progress setHidden:YES];
- }
这 段代码很神奇的,由于他会“暂停”代码运行,并且程序运行不会由于这里有一个while循环而受到影响。在[progress setHidden:NO]执行以后,整个函数想暂停了同样停在循环里面,等loadPageInBackground里面的操做都完成了之后才让 [progress setHidden:YES]运行。这样作就显得简介,并且逻辑很清晰。若是你不这样作,你就须要在loadPageInBackground里面表示 load完成的地方调用[progress setHidden:YES],显得代码不紧凑并且容易出错。
[iGoogle有话说:应用程序框架主线程已经封装了对NSRunLoop runMode:beforeDate:的调用;它和while循环构成了一个消息泵,不断获取和处理消息;可能你们会比较奇怪,既然主线程中已经封装好 了对NSRunLoop的调用,为何这里还能够再次调用,这个就是它与Windows消息循环的区别,它能够嵌套调用.当再次调用 while+NSRunLoop时候程序并无中止执行,它还在不停提取消息/处理消息.这一点与Symbian中Active Scheduler的嵌套调用达到同步做用原理是同样的.]
那么具体什么是NSRunLoop呢?其实NSRunLoop的本质是一个消息机制的处理模式。若是你对vc++编程有必定了解,在windows中,有 一系列很重要的函数SendMessage,PostMessage,GetMessage,这些都是有关消息传递处理的API。
可是在你进入到Cocoa的编程世界里面,我不知道你是否是走的太快太匆忙而忽视了这个很重要的问题,Cocoa里面就没有说起到任何关于消息处理的 API,开发者历来也没有本身去关心过消息的传递过程,好像一切都是那么天然,像大天然同样天然?在Cocoa里面你不再用去本身定义 WM_COMMAD_XXX这样的宏来标识某个消息,也不用在switch-case里面去对特定的消息作特别的处理。难道是Cocoa里面就没有了消息 机制?答案是否认的,只是Apple在设计消息处理的时候采用了一个更加高明的模式,那就是RunLoop。
2. NSRunLoop工做原理
接下来看一下NSRunLoop具体的工做原理,首先是官方文档提供的说法,看图:
经过全部的“消息”都被添加到了NSRunLoop中去,而在这里这些消息并分为“input source”和“Timer source” 并在循环中检查是否是有事件须要发生,若是须要那么就调用相应的函数处理。为了更清晰的解释,咱们来对比VC++和iOS消息处理过程。
VC++中在一切初始化都完成以后程序就开始这样一个循环了(代码是从户sir mfc程序设计课程的slides中截取):
- int APIENTRY WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,LPSTR lpCmdLine,int nCmdShow){
- ...
- while (GetMessage(&msg, NULL, 0, 0)){
- if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg)){
- TranslateMessage(&msg);
- DispatchMessage(&msg);
- }
- }
- }
能够看到在GetMessage以后就去分发处理消息了,而iOS中main函数中只是调用了UIApplicationMain,那么咱们能够介意猜出UIApplicationMain在初始化完成以后就会进入这样一个情形:
- int UIApplicationMain(...){
- ...
- while(running){
- [NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
- }
- ...
- }
所 以在UIApplicationMain中也是一样在不断处理runloop才是的程序没有退出。刚才的我说了NSRunLoop是一种更加高明的消息处 理模式,他就高明在对消息处理过程进行了更好的抽象和封装,这样才能是的你不用处理一些很琐碎很低层次的具体消息的处理,在NSRunLoop中每个消 息就被打包在input source或者是timer source中了,当须要处理的时候就直接调用其中包含的相应对象的处理函数了。
因此对外部的开发人员来说,你感觉到的就是,把source/timer加入到runloop中,而后在适当的时候相似于[receiver action]这样的事情发生了。甚至不少时候,你都没有感觉到整个过程前半部分,你只是感受到了你的某个对象的某个函数调用了。
好比在UIView被触摸时会用touchesBegan/touchesMoved等等函数被调用,也许你会想,“该死的,我都不知道在那里被告知有触 摸消息,这些处理函数就被调用了!?”因此,消息是有的,只是runloop已经帮你作了!为了证实个人观点,我截取了一张debug touchesBegan的call stack,有图有真相,如图:
如今会过头来看看刚才的那个会“暂停”代码的例子,有没有更加深刻的认识了呢?