①为什么总是要把RunLoop和线程放在一起来讲?
总的来讲就是:RunLoop是保证线程不会退出,并且能在不处理消息的时候让线程休眠,节约资源,在接收到消息的时候唤醒线程做出对应处理的消息循环机制。它是寄生于线程的,所以提到RunLoop必然会涉及到线程。
②如何创建RunLoop?
苹果不允许直接创建 RunLoop,它只提供了四个自动获取的函数
1 2 3 4 |
|
线程和 RunLoop 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里。线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。你只能在一个线程的内部获取其 RunLoop(主线程除外)。
③线程默认不开启RunLoop,为什么我们的App或者说主线程却可以一直运行而不会结束?
主线程是唯一一个例外,当App启动以后主线程会自动开启一个RunLoop来保证主线程的存活并处理各种事件。而且从上面的源代码来看,任意一个子线程的RunLoop都会保证主线程的RunLoop的存在。
④RunLoop能正常运行的条件是什么?
看到刚才代码中注释说暂时不管的代码,第一次接触肯定会想[runLoop addPort:[NSMachPort port] forMode:NSRunLoopCommonModes];这一句是什么意思?为什么必须加这一句RunLoop才能正常运行?
我们仍然通过实验看现象来理解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
注释掉得到的结果
不注释得到的结果
注释掉以后我们看似run了RunLoop但是最后线程还是结束了任务,然后销毁了。与没注释得到的结果比较,造成这一切的原因就在上面两张图片中标注部分的区别上。要解释这一部分就又要开始讲到让我们抓耳挠腮的概念部分,我们先来看一张眼熟到不行的RunLoop结构图。
一开始接触RunLoop我看到这张图的时候也是懵逼的,现在我们结合刚才的打印结果来理解。
图中RunLoop蓝色部分就对应我们打印结果中,整个RunLoop部分的打印结果
多个绿色部分共同被包含在RunLoop内就对应,打印结果中modes中同时包含多个Mode(这里可是看打印结果中标注出来的第一行往上再数两行。modes = ... count = 1。一个RunLoop可以包含多个Mode,每个Mode的Name不一样,只是在这个打印结果当中目前刚好Mode个数为1)
每一个绿色部分Mode整体就对应,打印结果中被标注出来的整体。
黄色部分Source对应标注部分source0+source1
黄色部分Observer对应标注部分observer部分
黄色部分Timer对应标注部分timers部分
一般我们常用的Mode有三种
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Source是什么?
source就是输入源事件,分为source0和source1这两种。
1 2 3 4 |
|
Timer是什么?
Timer即为定时源事件。通俗来讲就是我们很熟悉的NSTimer,其实NSTimer定时器的触发正是基于RunLoop运行的,所以使用NSTimer之前必须注册到RunLoop,但是RunLoop为了节省资源并不会在非常准确的时间点调用定时器,如果一个任务执行时间较长,那么当错过一个时间点后只能等到下一个时间点执行,并不会延后执行(NSTimer提供了一个tolerance属性用于设置宽容度,如果确实想要使用NSTimer并且希望尽可能的准确,则可以设置此属性)。
Observer是什么?
它相当于消息循环中的一个监听器,随时通知外部当前RunLoop的运行状态。NSRunLoop没有相关方法,只能通过CFRunLoop相关方法创建
1 2 3 4 5 6 7 8 9 |
|
由于它与这一问的关系并不大所以暂时不做过多阐述,希望进一步了解Observer可以查看文末的文档或者RunLoop入门学习补充资料(3.Observer)。
重点:它不能作为让RunLoop正常运行的条件,只有Observer的RunLoop也是无法正常运行的。
上面的 Source/Timer/Observer 被统称为 mode item,一个item可以被同时加入多个mode。但一个item被重复加入同一个mode时是不会有效果的。如果一个mode中一个item 都没有(只有Observer也不行),则 RunLoop 会直接退出,不进入循环。
对比刚才的打印日志,再结合刚才讲到的RunLoop结构内容,我们不妨做个猜测。RunLoop能正常运行的条件就是,至少要包含一个Mode(RunLoop默认就包含DefaultMode),并且该Mode下需要有至少一个的事件源(Timer/Source)。事实上经过NSRunLoop封装后,只可以往mode中添加两类事件源:NSPort(对应的是source1)和NSTimer(Timer源放在后面讲)。接下来我们还是用实验来加强理解。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
又到了愉快的猜谜时间,是否像我们猜测的那样,只要有Mode,并且Mode包含事件源RunLoop就可以正常运行了呢?
1 2 3 4 |
|
最后跟我们想的不一样,线程释放了,RunLoop没有成功启用。原因就出在[runLoop run];上面。
这句的意思是,在NSDefaultRunLoopMode下运行RunLoop。而我们添加的事件源是在另外一个Mode下,NSDefaultRunLoopMode仍然空空如也,所以RunLoop也就直接退出了。所以我们还要加一个条件,RunLoop正常运行的条件是:1.有Mode。2.Mode有事件源。3.运行在有事件源的Mode下。
⑤除了[runLoop run]还有那些方法启动RunLoop?
NSRunLoop中总共包装了3个方法供我们使用
1 |
|
除非希望子线程永远存在,否则不建议使用,因为这个接口会导致Run Loop永久性的运行NSDefaultRunLoopMode模式,即使使用 CFRunLoopStop(runloopRef);也无法停止RunLoop的运行,那么这个子线程也就无法停止,只能永久运行下去。
1 |
|
比上面的接口好点,有个超时时间,可以控制每次RunLoop的运行时间,也是运行在NSDefaultRunLoopMode模式。这个方法运行RunLoop一段时间会退出给你检查运行条件的机会,如果需要可以再次运行RunLoop。注意CFRunLoopStop(runloopRef);仍然无法停止RunLoop的运行,因此最好自己设置一个合理的RunLoop运行时间。比如
1 2 3 |
|
1 |
|
有一个超时时间限制,而且可以设置运行模式
这个接口在非Timer事件触发、显式的用CFRunLoopStop停止RunLoop或者到达limitDate后会退出返回。如果仅是Timer事件触发并不会让RunLoop退出返回,但是如果是PerfromSelector事件或者其他Input Source事件触发处理后,RunLoop会退出返回YES。同样可以像上面那样用while包起来使用。
(6)初体验结论
①.RunLoop是寄生于线程的消息循环机制,它能保证线程存活,而不是线性执行完任务就消亡。
②.RunLoop与线程是一一对应的,每个线程只有唯一与之对应的一个RunLoop。我们不能创建RunLoop,只能在当前线程当中获取线程对应的RunLoop(主线程RunLoop除外)。
③.子线程默认没有RunLoop,需要我们去主动开启,但是主线程是自动开启了RunLoop的。
④.RunLoop想要正常启用需要运行在添加了事件源的Mode下。
⑤.RunLoop有三种启动方式run、runUntilDate:(NSDate *)limitDate、runMode:(NSString *)mode beforeDate:(NSDate *)limitDate。第一种无条件永远运行RunLoop并且无法停止,线程永远存在。第二种会在时间到后退出RunLoop,同样无法主动停止RunLoop。前两种都是在NSDefaultRunLoopMode模式下运行。第三种可以选定运行模式,并且在时间到后或者触发了非Timer的事件后退出。
2.保持线程的存活后,让线程在我们需要的时候响应消息。
前面讲到了几个在某个线程内响应某方法的方法,现在我们就来讲讲这几个方法的具体含义
1 2 3 4 5 6 7 8 9 10 11 |
|
其实,这几个方法都是向线程中的RunLoop发送了消息,然后RunLoop接收到了消息就唤醒线程,去做对应的事情。所以想要正常使用这几个方法,响应selector的线程必须开启了RunLoop。惯例用例子来感受。
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 |
|
运行之前我们先把Xcode左边侧栏中选到显示CPU、Memory使用情况那一页,把最下面的按钮中第一个按钮选中状态取消掉。(否则看不见RunLoop的堆栈信息。)
然后我们运行程序,先暂时不做任何操作。
1 2 |
|
子线程开启,RunLoop正常运行,似乎与刚才没有任何不同。然后我们点击屏幕任何一个地方。断点1触发,我们来查看左侧的堆栈。
前面提到过UIEvent事件属于source0,从这里的堆栈就可以得到印证。我们在主线程中触发了touchesBegan,然后主线程的RunLoop就开始响应source0事件源,然后去调用对应的方法。我们放过断点继续查看。
同样是前面提到的,performSelector也是source0依然可以从堆栈得到印证。放过断点1后调用了performSelector,然后subThread的RunLoop开始响应source0事件源,然后去调用对应的方法,所以来到了断点2。放过断点2查看结果,整个流程结束,打印日志如下。
1 2 3 |
|
最后子线程任务结束然后被释放是因为之前提到的,runMode:(NSString *)mode beforeDate:(NSDate *)limitDate这种启动RunLoop的方式有一个特性,那就是这个接口在非Timer事件触发(此处是达成了这个条件)、显式的用CFRunLoopStop停止RunLoop或者到达limitDate后会退出。而例子当中也没有用while把RunLoop包围起来,所以RunLoop退出后子线程完成了任务最后退出了。
前面两种方法的使用大概就如同这个例子,大同小异。而第三种afterDelay的与前两种不同,并不是属于source0的,而是属于Timer源放在后面来讲。
看了刚才的堆栈信息可能会有疑问,标注出来的部分中,最长的那一串是什么,是干嘛的?为啥在执行发送给RunLoop的消息对应的事件之前,总要调用这么一长串?
其实RunLoop进行回调时,一般都是通过一个很长的函数(call out)调用出去(无论是Observer的状态通知还是Timer、Source的处理),而系统在回调时通常使用如下几个函数进行回调(换句话说你的代码其实最终都是通过下面几个函数来负责调用的)
1 2 3 4 5 6 |
|
上面几个函数之所以那么长,我估计官方是想让我们观名知意,看名字就可以猜出作用。但是同样的,刚接触RunLoop的时候过多的接触这些深层次的东西反而会觉得找不到方向。我觉得入门还是先从表面一些的东西入手比较好。等有一些比较全面的了解以后想要深入理解了再来看这部分,现在有个印象知道这个概念就好。想要深入的话可以查看文末的文档或者RunLoop入门学习补充资料(4.RunLoop回调函数触发逻辑)。
3.让线程定时执行某任务(Timer)
说到timer估计大家都不陌生,日常开发中我们经常都会用到。可能很多人听说RunLoop还是在使用NSTimer的时候。NSTimer有如下几个创建方式:
1 2 3 4 5 6 |
|
<1>初识NSTimer遇到的坑
刚接触Timer的时候,很多人(包括我)肯定都踩过一个坑,那就是创建了Timer却没有启动,百思不得其解,然后才知道还要把Timer加到一个叫RunLoop的东西里面才能正常运行。就像下面一样:
1 2 3 4 5 6 7 |
|
当时我们可能不理解为什么,只知道必须要这么做才能正常启动Timer。但是现在我们可以知道原因了。其实NSTimer定时器的触发正是基于RunLoop运行的,所以使用NSTimer之前必须注册到RunLoop。同时我们也应该知道Timer并不是严格的按照设定的时间点来触发的,RunLoop为了节省资源并不会在非常准确的时间点调用定时器,如果一个任务执行时间较长,那么当错过一个时间点后只能等到下一个时间点执行,并不会延后执行。(NSTimer提供了一个tolerance属性用于设置宽容度,如果确实想要使用NSTimer并且希望尽可能的准确,则可以设置此属性)
注意:GCD的timer与NStimer不是一个东西。他俩中只有NSTimer是与RunLoop相关的。关于GCDTimer与NStimer对比放在了RunLoop入门学习补充资料(5.GCDTimer与NStimer对比)
但是凡事都有例外,似乎scheduedTimerWith开头的方法创建的NSTimer就不需要添加到RunLoop中就可以运行。事实上,这一系列方法的真实逻辑是,创建一个定时器并自动添加到当前线程RunLoop的NSDefaultRunLoopMode中。在声明一次,不添加到RunLoop中的NSTimer是无法正常工作的
<2>使用NSTimer遇到的坑
不管是跟着网上说的[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];后发现Timer能正常使用,还是自己摸索发现直接用scheduedTimerWith创建的Timer直接就生效。用Timer的时间长了总有一天突然发现,为啥我的Timer运行的好好的突然就时好时坏了。于是找半天原因,发现是在进行Scrollview的滚动操作时Timer不进行响应,滑动结束后timer又恢复正常了。发现现象了但是,为啥啊?抓半天头发然后网上搜资料,然后我们就发现又回到了RunLoop的Mode这个点上。以前我们不懂为什么,现在对RunLoop有一定了解了,我们不妨来分析一下以便加深理解。
在之前讲Mode的时候提到过,RunLoop每次只能运行在一个Mode下,其意义是让不同Mode中的item互不影响。
NSTimer是一个Timer源(item),在上面哪个例子中不管是`[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];`还是`scheduedTimerWith`我们都是把Timer加到了主线程RunLoop的NSDefaultRunLoopMode中。一般情况下主线程RunLoop就运行在NSDefaultRunLoopMode下,所以定时器正常运行。
当Scrollview开始滑动时,主线程RunLoop自动切换了当前运行的Mode(currentMode),变成了UITrackingRunLoopMode。所以现在RunLoop要处理的就是UITrackingRunLoopMode中item。
我们的timer是添加在NSDefaultRunLoopMode中的,并没有添加到UITrackingRunLoopMode中。即我们的timer不是UITrackingRunLoopMode中的item。
本着不同Mode中的item互不影响的原则,RunLoop也就不会处理非当前Mode的item,所以定时器就不会响应。
当Scrollview滑动结束,主线程RunLoop自动切换了当前运行的Mode(currentMode),变成了NSDefaultRunLoopMode。我们的Timer是NSDefaultRunLoopMode的item,所以RunLoop会处理它,所以又正常响应了。
如果想Timer在两种Mode中都得到响应怎么办?前面提到过,一个item可以被同时加入多个mode。让Timer同时成为两种Mode的item就可以了(分别添加或者直接加到commonMode中),这样不管RunLoop处于什么Mode,timer都是当前Mode的item,都会得到处理。
<3>NSTimer导致的ViewController无法释放问题
https://www.cnblogs.com/kenshincui/p/6823841.html 在这篇文档的NSTimer部分提到,用iOS10之前的(非Block的方法)方法创建NSTimer会因为设置target为self导致Timer对ViewController有一个强引用,最后结果就是ViewController无法释放。这一部分因为篇幅太长而且与RunLoop本身关系不是那么紧密所以不在这部分展开,感兴趣的可以看下。总的来说timer更推荐使用GCDTimer。(对比参考RunLoop入门学习补充资料5.GCDTimer与NStimer对比)
<4>performSelecter:afterDelay:
前面在讲performSelecter方法时提到过,这个方法与其他两种方法不同,不同在哪,我们来验证下。由于这个方法是作用于当前线程的,所以为了在RunLoop比较干净纯粹的子线程中响应这个方法会比较绕,不过一开始与之前的例子都没什么太大区别,区别在于wantTodo函数部分。
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 |
|
跟之前perform的例子中一样的操作,到断点1的时候结果与之前也没什么不同,我们放过断点1,来到断点2.
可以看到,跟之前调用其他performSelecter时的DoSource0等等一系列堆栈不一样了,全部变成了Timer相关。实际上,当调用 performSelecter:afterDelay: 后,其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中,所以这个方法是属于Timer源的。关于这个方法有个比较经典的用法,很多文档都说过。
当tableview的cell上有需要从网络获取的图片的时候,异步线程会去加载图片,加载完成后主线程就会设置cell的图片,有可能会造成卡顿。可以让设置图片的任务在CFRunLoopDefaultMode下进行,当滚动tableView的时候,RunLoop是在 UITrackingRunLoopMode 下进行,不去设置图片,而是当停止的时候,再去设置图片。(这个场景的核心还是利用不同Mode的切换的思想,可以拓展其他地方)
1 2 3 4 |
|
当然不是说这种方法就一定好,毕竟他在滑动的时候不会显示图片,万一你的需求跟这刚好相反呢,而且现在SDWebImage处理的已经很好了,已经很少有人用这种方法了。但是这个利用Mode切换的思想可以借鉴,万一其他地方用上就很合适呢。
4.监听Observer达到一些目的
这个就比现在我们讲的更进一阶,因为它涉及到的包括RunLoop的运行逻辑还有一些其他你想实现的功能的拓展。暂时我们只知道有这个用法就行,如果现在来钻研可能就迷失了。
目前知道的比较有名的有:
1.sunnyxx的UITableView+FDTemplateLayoutCell利用Observer在界面空闲状态下计算出UITableViewCell的高度并进行缓存。
2.FaceBook的AsyncDisplayKit
共同之处在于,通过合理利用RunLoop机制,将很多不是必须在主线程中执行的操作放在子线程中实现,然后在合适的时机同步到主线程中,这样可以节省在主线程执行操作的时间,避免卡顿。
关于RunLoop的释放问题(RunLoop带autoreleasepool的正确写法)
<1>分析
Timer和Source以及一些回调block等等,都需要占用一部分存储空间,所以要释放掉,如果不释放掉,就会一直积累,占用的内存也就越来越大。
在主线程中
当RunLoop开启时,会自动创建一个自动释放池。
当RunLoop在休息之前会释放掉自动释放池的东西。
然后重新创建一个新的空的自动释放池。
当RunLoop被唤醒重新开始跑圈时,Timer,Source等新的事件就会放到新的自动释放池中。
重复2-4。
所以主线程中,有关RunLoop的释放问题不需要我们关心。
注意:这里说的是主线程(关于子线程的autoreleasepool是否需要手动创建还有个研究过程,因为网上众说纷纭,有的说不需要创建有的说需要。)这部分的资料也比较少,总结了有限的资料加上自己的一些理解我认为RunLoop正确的写法应该是下面这样的,如果不对希望指正。
1.Sunnyxx 孙源大神在《黑幕背后的Autorelease》中提到"在没有手加Autorelease Pool的情况下,Autorelease对象是在当前的runloop迭代结束时释放的,而它能够释放的原因是系统在每个runloop迭代中都加入了自动释放池Push和Pop(这里说的是主线程)"加上之前提到的"1.当RunLoop开启时,会自动创建一个自动释放池。"我们可以得到一个结论:系统自动管理的autoreleasepool,或者说系统自己管理的Autorelease对象的自动释放的实现依赖于RunLoop
2.只有主线程的RunLoop是自动开启了的,子线程中,RunLoop是需要我们手动获取(或者说手动激活)的,就更不可能自动创建了autoreleasepool(自动管理对象的释放)。所以子线程的autoreleasepool需要我们手动创建。(这一点可以参考《iOS中autoreleasepool的理解和使用》中对苹果文档的翻译:"你生成了一个辅助线程。
一旦线程开始执行你必须自己创建自动释放池。否则,应用将泄漏对象。")
3.结论:NSThread和NSOperationQueue开辟子线程需要手动创建autoreleasepool。GCD开辟子线程不需要手动创建autoreleasepool,因为GCD的每个队列都会自行创建autoreleasepool。(参考自:《关于iOS子线程上的autorelease对象释放问题?》)
所以得出以下两个写法
<2>需要用while循环控制的RunLoop
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
<3>不需要用while循环控制的RunLoop
1 2 3 4 5 6 7 8 |
|
<4>关于主线程中autoreleasepool的题外话
主线程中一般不需要我们手动添加autoreleasepool,但是,如果你希望某个对象或者变量尽快释放的时候我们也可以手动添加。比如下面这种情况:
1 2 3 4 5 6 7 8 |
|
"在没有手加Autorelease Pool的情况下,Autorelease对象是在当前的runloop迭代结束时释放的,而它能够释放的原因是系统在每个runloop迭代中都加入了自动释放池Push和Pop。按照我们以前的想法,每次循环的大括号结束后,当前的变量就会释放。可是现在我们了解的自动释放的相关知识后就知道其实要等到一次主线程的RunLoop迭代以后才会一起释放这些变量。而如果还来不及等到RunLoop迭代结束去释放变量,这期间就积累了足够多的变量,就会导致内存警告或者崩溃。所以我们可以手动添加autoreleasepool,让对象尽快释放。
1 2 3 4 5 6 7 8 9 10 |
|
关于子线程RunLoop切换Mode的思考
学习RunLoop查看文档的过程中,很多地方都会讲到RunLoop的Mode切换,就像之前提到的主线程RunLoop中defaultMode和UITrackingRunLoopMode的切换。同时关于这一部分说的最多的也是"同一时间只能运行一个Mode,如果想切换Mode需要退出当前Mode重新设定运行Mode"。
但是,关于如何切换,我没有找到文档有提及(也许是我看的文档还是太少,如果知道有相关文档的大大麻烦给个链接),就像RunLoop的Mode切换是自动进行的一样。事实上主线程的RunLoop的Mode切换确实是自动的,不需要我们来管理的。那么子线程中呢?或许对大神们来说这个问题不是问题,但是作为一个小白我还是想搞清楚这个问题。虽然暂时不清楚是不是子线程中根本不存在Mode切换的问题还是不存在这种用法,但是研究一下总是好的,万一哪天用上了呢。或者这是不可行的,以后别人提起你可以明确的说不行。
本文最大干货,子线程中模拟主线程DefaultMode与TrackingMode的切换,长代码预警
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 |
|
好了代码看完了,激动人心的时刻到了,我们来看效果。
1 |
|
程序启动以后,打印如上,没什么好说的,一切正常,接下来才是重点。来让我们拖拽textView。
1 2 3 |
|
成功了!!!依照我们所想的Mode切换成了UITrackingRunLoopMode。好就差最后一步了,我们松手看看是什么情况。
1 2 3 4 5 6 |
|
可以看到即使我们松手,Timer依旧在执行,说明Mode切换失败了。可是我们的逻辑是对的啊,然后我多种查找原因。最后发现,切换成UITrackingRunLoopMode后我们的执行逻辑是没错的,都是可以进断点1和断点2的地方。但是,虽然执行到了断点1和断点2,却不会走到断点3的位置去。说明performSelector的方法根本没被执行,所以切换Mode失败了。
折腾半天,我曾一度以为子线程中切换Mode是不现实的,因为刚好在这个时候看到一个文档说UITrackingRunLoopMode在子线程RunLoop中无效。我以为UITrackingRunLoopMode下不会响应performSelector方法。但是转念一想,主线程RunLoop又是怎么样的呢?于是我做了一个小测试。
对上面的代码稍作改动,看看主线程在UITrackingRunLoopMode下会不会响应performSelector方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
打印结果很长我只截图了比较关键的部分并标注。
从日志搜索结果和与响应perform方法之前的日志对比(对比请自行进行,太长了)综合来看,图中标识的perform事件源就是我们在UITrackingRunLoopMode下调用的那个perform方法。事实证明UITrackingRunLoopMode下也是可以响应perform方法,我们之前的实验失败并不是UITrackingRunLoopMode的锅。
那么是怎么回事呢?原因就出在第一张 截图的第二块标识部分。主线程的NSRunLoopCommonModes默认是包含UITrackingRunLoopMode和NSDefaultRunLoopMode。而子线程的NSRunLoopCommonModes默认是只包含,只包含,只包含,重要的事情说三次,只包含NSDefaultRunLoopMode的。而从后面截图标识部分来看,performSelector:onThread:withObject:waitUntilDone:,其实就是向RunLoop中所有标识为CommonMode的Mode添加一个source0。
这也就是为什么主线程在UITrackingRunLoopMode下可以响应perform方法而子线程却不行。因为这个source0在子线程中根本没有被添加到UITrackingRunLoopMode下,也就不会做出对应的响应。
知道为什么就简单了,我们只需在子线程中把UITrackingRunLoopMode标识为CommonMode就可以了。前面提到过只有CFRunLoop有相关方法可以添加CommonMode,那么我们只需要添加下面这一行代码就行了(也就是把刚才例子中注释的部分取消注释)。
1 |
|
接下来我们再看看效果
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
最后就实现了类似主线程的mode切换效果。拖拽滑动期间是UITrackingRunLoopMode,会触发timer的内容打印日志。
滑动停止以后就切换成了kCFRunLoopDefaultMode,不在打印timer的日志。
到这一步mode切换的实验就结束了,虽然我仍然没想到啥时候会用上,但至少我们知道了子线程中的Mode切换是可行的,并且搞清楚了与RunLoop相关的那几个perform方法其实是向所有注册了CommonMode的Mode添加source0而不是向某一个特定Mode添加。如果我这个实验有错误请大大们指出。
更正:
1 2 |
|
这两个方法,是向所有注册了CommonMode的Mode添加source0
1 2 3 |
|
这三个方法是给特定modes(参数是包含至少一个mode的数组)添加source0(前两个)或者timer(afterDelay)。
1 |
|
这个方法是向NSDefaultRunLoopMode添加timer源
更正过后,我们就有两种方式实现这个子线程切换mode的实验。
1.跟之前写的实验逻辑一样不需要改变。
2.不使用CFRunLoopAddCommonMode添加UITrackingRunLoopMode,而是做出如下改动。
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 |
|