您好,欢迎来电子发烧友网! ,新用户?[免费注册]

您的位置:电子发烧友网>源码下载>数值算法/人工智能>

再读苹果线程配置与Run Loop

大小:0.6 MB 人气: 2017-10-12 需要积分:1
 本文为再读苹果《Threading Programming Guide》笔记第二篇,作者付宇轩表示:如今关于iOS多线程的文章层出不穷,但我觉得若想更好的领会各个实践者的文章,应该先仔细读读官方的相关文档,打好基础,定会有更好的效果。
  文章中有对官方文档的翻译,也有自己的理解,官方文档中代码片段的示例在这篇文章中都进行了完整的重写,还有一些文档中没有的代码示例,并且都使用Swift完成,给大家一些Objective-C与Swift转换的参考。
  系列阅读
  初识线程线程配置与Run Loop
  线程属性配置
  线程也是具有若干属性的,自然一些属性也是可配置的,在启动线程之前我们可以对其进行配置,比如线程占用的内存空间大小、线程持久层中的数据、设置线程类型、优先级等。
  配置线程的栈空间大小
  在前文中提到过线程对内存空间的消耗,其中一部分就是线程栈,我们可以对线程栈的大小进行配置:
  Cocoa框架:在OS X v10.5之后的版本和iOS2.0之后的版本中,我们可以通过修改NSThread类的stackSize属性,改变二级线程的线程栈大小,不过这里要注意的是该属性的单位是字节,并且设置的大小必须得是4KB的倍数。POSIX API:通过pthread_attr_- setstacksize函数给线程属性pthread_attr_t结构体设置线程栈大小,然后在使用pthread_create函数创建线程时将线程属性传入即可。
  注意:在使用Cocoa框架的前提下修改线程栈时,不能使用NSThread的detachNewThreadSelector: toTarget:withObject:方法,因为上文中说过,该方法先创建线程,即刻便启动了线程,所以根本没有机会修改线程属性。
  配置线程存储字典 配置线程类型 设置线程优先级
  注意:设置线程的优先级时可以在线程运行时设置。
  线程执行的任务
  Autorelease Pool
  - (void)myThreadMainRoutine { NSAutoreleasePool*pool = [[NSAutoreleasePoolalloc] init]; // 顶层自动释放池// 线程执行任务的逻辑代码[pool release]; }
  - (void)myThreadMainRoutine { @autoreleasepool{ // 线程执行任务的逻辑代码} }
  intmain(intargc, char* argv[]) { @autoreleasepool{ returnUIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); } }
  func performInBackground(){ autoreleasepool({ // 线程执行任务的逻辑代码 print(“I am a event, perform in Background Thread.”)})}
  func performInBackground(){ autoreleasepool{ // 线程执行任务的逻辑代码print(“I am a event, perform in Background Thread.”)} }
  设置异常处理 创建Runloop
  简单的来说,RunLoop用于管理和监听异步添加到线程中的事件,当有事件输入时,系统唤醒线程并将事件分派给RunLoop,当没有需要处理的事件时,RunLoop会让线程进入休眠状态。这样就能让线程常驻在进程中,而不会过多的消耗系统资源,达到有事做事,没事睡觉的效果。
  终止线程
  Run Loop
  注:Core Foundation框架是一组C语言接口,它们为iOS应用程序提供基本数据管理和服务功能,比如线程和Run Loop、端口、Socket、时间日期等。
  注:在二级线程中获取Run Loop有两种方式,通过NSRunloop的类方法currentRunLoop获取Run Loop对象(NSRunLoop),或者通过Core Foundation框架中的CFRunLoopGetCurrent()函数获取当前线程的Run Loop对象(CFRunLoop)。NSRunLoop是CFRunLoop的上层封装。
  letnsrunloop = NSRunLoop.currentRunLoop() letcfrunloop = CFRunLoopGetCurrent()
  Run Loop的事件来源 Run Loop的观察者
  Run Loop准备开始运行时。当Run Loop准备要执行一个Timer Source事件时。当Run Loop准备要执行一个Input Source事件时。当Run Loop准备休眠时。当Run Loop被进入的事件消息唤醒并且还没有开始让处理器执行事件消息时。退出Run Loop时。
  Run Loop的观察者在NSRunloop中没有提供相关接口,所以我们需要通过Core Foundation框架使用它,可以通过CFRunLoopObserverCreate方法创建Run Loop的观察者,类型为CFRunLoopObserverRef,它其实是CFRunLoopObserver的重定义名称。上述的那些可以被监听的运行状态被封装在了CFRunLoopActivity结构体中,对应关系如下:
  CFRunLoopActivity.EntryCFRunLoopActivity.BeforeTimersCFRunLoopActivity.BeforeSourcesCFRunLoopActivity.BeforeWaitingCFRunLoopActivity.AfterWaitingCFRunLoopActivity.Exit
  Run Loop的观察者和Timer事件类似,可以只使用一次,也可以重复使用,在创建观察者时可以设置。如果只使用一次,那么当监听到对应的状态后会自行移除,如果是重复使用的,那么会留在Run Loop中多次监听Run Loop相同的运行状态。
  Run Loop Modes
  Run Loop Modes可以称之为Run Loop模式,这个模式可以理解为对Run Loop各种设置项的不同组合,举个例子,iPhone手机运行的iOS有很多系统设置项,假设白天我打开蜂窝数据,晚上我关闭蜂窝数据,而打开无线网络,到睡觉时我关闭蜂窝数据和无线网络,而打开飞行模式。假设在这三个时段中其他的所有设置项都相同,而只有这三个设置项不同,那么就可以说我的手机有三种不同的设置模式,对应着不同的时间段。那么Run Loop的设置项是什么呢?那自然就是前文中提到的不同的事件来源以及观察者了,比如说,Run Loop的模式A(Mode A),只包含接收Timer Source事件源的事件消息以及监听Run Loop运行时的观察者,而模式B(Mode B)只包含接收Input Source事件源的事件消息以及监听Run Loop准备休眠时和退出Run Loop时的观察者,如下图所示:
  再读苹果线程配置与Run Loop
  所以说,Run Loop的模式就是不同类型的数据源和不同观察者的集合,当Run Loop运行时要设置它的模式,也就是告知Run Loop只需要关心这个集合中的数据源类型和观察者,其他的一概不予理会。那么通过模式,就可以让Run Loop过滤掉它不关心的一些事件,以及避免被无关的观察者打扰。如果有不在当前模式中的数据源发来事件消息,那只能等Run Loop改为包含有该数据源类型的模式时,才能处理事件消息。
  在Cocoa框架和Core Foundation框架中,已经为我们预定义了一些Run Loop模式:
  默认模式:在NSRunloop中的定义为NSDefaultRunLoopMode,在CFRunloop中的定义为kCFRunLoopDefaultMode。该模式包含的事件源囊括了除网络链接操作的大多数操作以及时间事件,用于当前Run Loop处于空闲状态等待事件时,以及Run Loop开始运行时。NSConnectionReplyMode:该模式用于监听NSConnection相关对象的返回结果和状态,在系统内部使用,我们一般不会使用该模式。NSModalPanelRunLoopMode:该模式用于过滤在模态面板中处理的事件(Mac App)。NSEventTrackingRunLoopMode:该模式用于跟踪用户与界面交互的事件。模式集合:或者叫模式组,顾名思义就是将多个模式组成一个组,然后将模式组认为是一个模式设置给Run Loop,在NSRunloop中的定义为NSRunLoopCommonModes,在CFRunloop中的定义为kCFRunLoopCommonModes。系统提供的模式组名为Common Modes,它默认包含NSDefaultRunLoopMode、NSModalPanelRunLoopMode、NSEventTrackingRunLoopMode这三个模式。
  以上五种系统预定的模式中,前四种属于只读模式,也就是我们无法修改它们包含的事件源类型和观察者类型。而模式组我们可以通过Core Foundation框架提供的CFRunLoopAddCommonMode(_ rl: CFRunLoop!, _ mode: CFString!)方法添加新的模式,甚至是我们自定义的模式。这里需要注意的是,既然在使用时,模式组是被当作一个模式使用的,那么自然可以给它设置不同类型的事件源或观察者,当给模式组设置事件源或观察者时,实际是给该模式组包含的所有模式设置。比如说给模式组设置了一个监听Run Loop准备休眠时的观察者,那么该模式组里的所有模式都会被设置该观察者。
  Input Source
  前文中说过,Input Sources接收到各种操作输入事件消息,然后异步的分派给对应事件处理方法。在Input Sources中又分两大类的事件源,一类是基于端口事件源(Port-based source),在CFRunLoopSourceRef的结构中为source1,主要通过监听应用程序的Mach端口接收事件消息并分派,该类型的事件源可以主动唤醒Run Loop。另一类是自定义事件源(Custom source),在CFRunLoopSourceRef的结构中为source0,一般是接收其他线程的事件消息并分派给当前线程的Run Loop,比如performSwlwctor:onThread:。..系列方法,该类型的事件源无法自动唤醒Run Loop,而是需要手动将事件源设置为待执行的标记,然后再手动唤醒Run Loop。虽然这两种类型的事件源接收事件消息的方式不一样,但是当接收到消息后,对消息的分派机制是完全相同的。
  Port-Based Source
  Cocoa框架和Core Foundation框架都提供了相关的对象和函数用于创建基于端口的事件源。在Cocoa框架中,实现基于端口的事件源主要是通过NSPort类实现的,它代表了交流通道,也就是说在不同的线程的Run Loop中都存在NSPort,那么它们之间就可以通过发送与接收消息(NSPortMessage)互相通信。所以我们只需要通过NSPort类的类方法port创建对象实例,然后通过NSRunloop的方法将其添加到Run Loop中,或者在创建二级线程时将创建好的NSPort对象传入即可,无需我们再做消息、消息上下文、事件源等其他配置,都由Run Loop自行配置好了。而在Core Foundation框架中就比较麻烦一些,大多数配置都需要我们手动配置,在后面会详细举例说明。
  Custom Input Source
  Cocoa框架中没有提供创建自定义事件源的相关接口,我们只能通过Core Foundation框架中提供的对象和函数创建自定义事件源,手动配置事件源各个阶段要处理的逻辑,比如创建CFRunLoopSourceRef事件源对象,通过CFRunLoopScheduleCallBack回调函数配置事件源上下文并注册事件源,通过CFRunLoopPerformCallBack回调函数处理接收到事件消息后的逻辑,通过CFRunLoopCancelCallBack函数销毁事件源等等,在后文中会有详细举例说明。
  虽然Cocoa框架没有提供创建自定义事件源的相关对象和接口,但是它为我们预定义好了一些事件源,能让我们在当前线程、其他二级线程、主线程中执行我们希望被执行的方法,让我们看看NSObject中的这些方法:
  func performSelectorOnMainThread(_ aSelector: Selector, withObject arg: AnyObject?, waitUntilDone wait: Bool) func performSelectorOnMainThread(_ aSelector: Selector, withObject arg: AnyObject?, waitUntilDone wait: Bool, modes array: [String]?)
  这两个方法允许我们将当前线程中对象的方法让主线程去执行,可以选择是否阻塞当前线程,以及希望被执行的方法作为事件消息被何种Run Loop模式监听。
  注:如果在主线程中使用该方法,当选择阻塞当前线程,那么发送的方法会立即被主线程执行,若选择不阻塞当前线程,那么被发送的方法将被排进主线程Run Loop的事件队列中,并等待执行。
  func performSelector(_aSelector: Selector, withObjectanArgument: AnyObject?, afterDelaydelay: NSTimeInterval) func performSelector(_aSelector: Selector, withObjectanArgument: AnyObject?, afterDelaydelay: NSTimeInterval, inModesmodes: [String])
  这两个方法允许我们给当前线程发送事件消息,当前线程接收到消息后会依次加入Run Loop的事件消息队列中,等待Run Loop迭代执行。该方法还可以指定消息延迟发送时间及消息希望被何种Run Loop模式监听。
  注:该方法中的延迟时间并不是延迟Run Loop执行事件消息的事件,而是延迟向当前线程发送事件消息的时间。另外,即便不设置延迟时间,那么发送的事件消息也不一定立即被执行,因为在Run Loop的事件消息队列中可以已有若干等待执行的消息。
  func performSelector(_ aSelector: Selector, onThread thr: NSThread, withObject arg: AnyObject?, waitUntilDone wait: Bool) func performSelector(_ aSelector: Selector, onThread thr: NSThread, withObject arg: AnyObject?, waitUntilDone wait: Bool, modes array: [String]?)
  这两个方法允许我们给其他二级线程发送事件消息,前提是要取得目标二级线程的NSThread对象实例,该方法同样提供了是否阻塞当前线程的选项和设置Run Loop模式的选项。
  注:使用该方法给二级线程发送事件消息时要确保目标线程正在运行,换句话说就是目标线程要有启动着的Run Loop。并且保证目标线程执行的任务要在应用程序代理执行applicationDidFinishLaunching:方法前完成,否则主线程就结束了,目标线程自然也就结束了。
  funcperformSelectorInBackground(_ aSelector: Selector, withObject arg: AnyObject?)
  该方法允许我们在当前应用程序中创建一个二级线程,并将指定的事件消息发送给新创建的二级线程。
  classfunc cancelPreviousPerformRequestsWithTarget(_aTarget: AnyObject)classfunc cancelPreviousPerformRequestsWithTarget(_aTarget: AnyObject, selectoraSelector: Selector, objectanArgument: AnyObject?)
  这两个方法是NSObject的类方法,第一个方法作用是在当前线程中取消Run Lop中某对象通过performSelector:withObject:afterDelay:方法发送的所有事件消息执行请求。第二个方法多了两个过滤参数,那就是方法名称和参数,取消指定方法名和参数的事件消息执行请求。
  Timer Source
  Timer Source顾名思义就是向Run Loop发送在将来某一时间执行或周期性重复执行的同步事件消息。当某线程不需要其他线程通知而需要自己通知自己执行任务时就可以用这种事件源。举个应用场景,在iOS应用中,我们经常会用到搜索功能,而且一些搜索框具有自动搜索的能力,也就是说不用我们点击搜索按钮,只需要输入完我想要搜索的内容就会自动搜索,大家想一想如果每输入一个字就开始立即搜索,不但没有意义,性能开销也大,用户体验自然也很糟糕,我们希望当输入完这句话,或至少输入一部分之后再开始搜索,所以我们就可以在开始输入内容时向执行搜索功能的线程发送定时搜索的事件消息,让其在若干时间后再执行搜索任务,这样就有缓冲时间输入搜索内容了。
  这里需要注意的是Timer Source发送给Run Loop的周期性执行任务的重复时间是相对时间。比如说给Run Loop发送了一个每隔5秒执行一次的任务,每次执行任务的正常时间为2秒,执行5次后终止,假设该任务被立即执行,那么当该任务终止时应该历时30秒,但当第一次执行时出现了问题,导致任务执行了20秒,那么该任务只能再执行一次就终止了,执行的这一次其实就是第5次,也就是说不论任务的执行时间延迟与否,Run Loop都会按照初始的时间间隔执行任务,并非按Finish-To-Finish去算的,所以一旦中间任务有延时,那么就会丢失任务执行次数。关于Timer Source的使用,在后文中会有详细举例说明。
  Run Loop内部运行逻辑
  在Run Loop的运行生命周期中,无时无刻都伴随着执行等待执行的各种任务以及在不同的运行状态时通知不同的观察者,下面我们看看Run Loop中的运行逻辑到底是怎样的:
  通知对应观察者Run Loop准备开始运行。通知对应观察者准备执行定时任务。通知对应观察者准备执行自定义事件源的任务。开始执行自定义事件源任务。如果有基于端口事件源的任务准备待执行,那么立即执行该任务。然后跳到步骤9继续运转。通知对应观察者线程进入休眠。
  如果有下面的事件发生,则唤醒线程:
  * 接收到基于端口事件源的任务。
  定时任务到了该执行的时间点。Run Loop的超时时间到期。Run Loop被手动唤醒。
  通知对应观察者线程被唤醒。
  执行等待执行的任务。
  * 如果有定时任务已启动,执行定时任务并重启Run Loop。然后跳到步骤2继续运转。
  如果有非定时器事件源的任务待执行,那么分派执行该任务。如果Run Loop被手动唤醒,重启Run Loop。然后跳转到步骤2继续运转。
  通知对应观察者已退出Run Loop。
  以上这些Run Loop中的步骤也不是每一步都会触发,举一个例子:
  1.对应观察者接收到通知Run Loop准备开始运行 -》 3.对应观察者接收到通知Run Loop准备执行自定义事件源任务 -》 4.开始执行自定义事件源任务 -》 任务执行完毕且没有其他任务待执行 -》 6.线程进入休眠状态,并通知对应观察者 -》 7.接收到定时任务并唤醒线程 -》 8.通知对应观察者线程被唤醒 -》 9.执行定时任务并重启Run Loop -》 2.通知对应观察者准备执行定时任务 -》 Run Loop执行定时任务,并在等待下次执行任务的间隔中线程休眠 -》 6.线程进入休眠状态,并通知对应观察者…
  这里需要注意的一点是从上面的运行逻辑中可以看出,当观察者接收到执行任务的通知时,Run Loop并没有真正开始执行任务,所以观察者接收到通知的时间与Run Loop真正执行任务的时间有时间差,一般情况下这点时间差影响不大,但如果你需要通过观察者知道Run Loop执行任务的确切时间,并根据这个时间要进行后续操作的话,那么就需要通过结合多个观察者接收到的通知共同确定了。一般通过监听准备执行任务的观察者、监听线程进入休眠的观察者、监听线程被唤醒的观察者共同确定执行任务的确切时间。
 

非常好我支持^.^

(0) 0%

不好我反对

(0) 0%

再读苹果线程配置与Run Loop下载

相关电子资料下载

      发表评论

      用户评论
      评价:好评中评差评

      发表评论,获取积分! 请遵守相关规定!