NSRunLoop

①为什么总是要把RunLoop和线程放在一起来讲?

总的来讲就是:RunLoop是保证线程不会退出,并且能在不处理消息的时候让线程休眠,节约资源,在接收到消息的时候唤醒线程做出对应处理的消息循环机制。它是寄生于线程的,所以提到RunLoop必然会涉及到线程。

②如何创建RunLoop?

苹果不允许直接创建 RunLoop,它只提供了四个自动获取的函数

1

2

3

4

[NSRunLoop currentRunLoop];//获取当前线程的RunLoop

[NSRunLoop mainRunLoop];

CFRunLoopGetMain();

CFRunLoopGetCurrent();

线程和 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

- (void)viewDidLoad {

    [super viewDidLoad];

     

    NSLog(@"%@----开辟子线程",[NSThread currentThread]);

    

    NSThread *subThread = [[MyThread alloc] initWithTarget:self selector:@selector(subThreadTodo) object:nil];

    subThread.name = @"subThread";

    [subThread start];

  

}

 

- (void)subThreadTodo

{

    NSLog(@"%@----开始执行子线程任务",[NSThread currentThread]);

    //获取当前子线程的RunLoop

    NSRunLoop *runLoop = [NSRunLoop currentRunLoop];

    //注释掉下面这行和不注释掉下面这行分别运行一次

    [runLoop addPort:[NSMachPort port] forMode:NSRunLoopCommonModes];

    NSLog(@"RunLoop:%@",runLoop);

    //让RunLoop跑起来

    [runLoop run];

    NSLog(@"%@----执行子线程任务结束",[NSThread currentThread]);

}

注释掉得到的结果

不注释得到的结果

注释掉以后我们看似run了RunLoop但是最后线程还是结束了任务,然后销毁了。与没注释得到的结果比较,造成这一切的原因就在上面两张图片中标注部分的区别上。要解释这一部分就又要开始讲到让我们抓耳挠腮的概念部分,我们先来看一张眼熟到不行的RunLoop结构图。

一开始接触RunLoop我看到这张图的时候也是懵逼的,现在我们结合刚才的打印结果来理解。

  1. 图中RunLoop蓝色部分就对应我们打印结果中,整个RunLoop部分的打印结果

  2. 多个绿色部分共同被包含在RunLoop内就对应,打印结果中modes中同时包含多个Mode(这里可是看打印结果中标注出来的第一行往上再数两行。modes = ... count = 1。一个RunLoop可以包含多个Mode,每个Mode的Name不一样,只是在这个打印结果当中目前刚好Mode个数为1)

  3. 每一个绿色部分Mode整体就对应,打印结果中被标注出来的整体。

  4. 黄色部分Source对应标注部分source0+source1

  5. 黄色部分Observer对应标注部分observer部分

  6. 黄色部分Timer对应标注部分timers部分

一般我们常用的Mode有三种

1

2

3

4

5

6

7

8

9

10

11

12

1.kCFRunLoopDefaultMode(CFRunLoop)/NSDefaultRunLoopMode(NSRunLoop)

默认模式,在RunLoop没有指定Mode的时候,默认就跑在DefaultMode下。一般情况下App都是运行在这个mode下的

 

2.(CFStringRef)UITrackingRunLoopMode(CFRunLoop)/UITrackingRunLoopMode(NSRunLoop)

一般作用于ScrollView滚动的时候的模式,保证滑动的时候不受其他事件影响。

 

3.kCFRunLoopCommonModes(CFRunLoop)/NSRunLoopCommonModes(NSRunLoop)

这个并不是某种具体的Mode,而是一种模式组合,在主线程中默认包含了NSDefaultRunLoopMode和 UITrackingRunLoopMode。子线程中只包含NSDefaultRunLoopMode。

注意:

①在选择RunLoop的runMode时不可以填这种模式否则会导致RunLoop运行不成功。

②在添加事件源的时候填写这个模式就相当于向组合中所有包含的Mode中注册了这个事件源。

③你也可以通过调用CFRunLoopAddCommonMode()方法将自定义Mode放到 kCFRunLoopCommonModes组合。

Source是什么?

source就是输入源事件,分为source0和source1这两种。

1

2

3

4

1.source0:诸如UIEvent(触摸,滑动等),performSelector这种需要手动触发的操作。

2.source1:处理系统内核的mach_msg事件(系统内部的端口事件)。诸如唤醒RunLoop或者让RunLoop进入休眠节省资源等。

一般来说日常开发中我们需要关注的是source0,source1只需要了解。

之所以说source0更重要是因为日常开发中,我们需要对常驻线程进行操作的事件大多都是source0,稍后的实验会讲到。

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

    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {

 

        NSLog(@"----监听到RunLoop状态发生改变---%zd", activity);

 

    });

 

    // 添加观察者:监听RunLoop的状态

    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);

由于它与这一问的关系并不大所以暂时不做过多阐述,希望进一步了解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

- (void)viewDidLoad {

    [super viewDidLoad];

    

    NSLog(@"%@----开辟子线程",[NSThread currentThread]);

    

    NSThread *subThread = [[MyThread alloc] initWithTarget:self selector:@selector(subThreadTodo) object:nil];

    subThread.name = @"subThread";

    [subThread start];

    

}

 

- (void)subThreadTodo

{

    NSLog(@"%@----开始执行子线程任务",[NSThread currentThread]);

    //获取当前子线程的RunLoop

    NSRunLoop *runLoop = [NSRunLoop currentRunLoop];

    //给RunLoop添加一个事件源,注意添加的Mode

    //关于这里的[NSMachPort port]我的理解是,给RunLoop添加了一个占位事件源,告诉RunLoop有事可做,让RunLoop运行起来。

    //但是暂时这个事件源不会有具体的动作,而是要等RunLoop跑起来过后等有消息传递了才会有具体动作。

    [runLoop addPort:[NSMachPort port] forMode:UITrackingRunLoopMode];

 

    [runLoop run];

    NSLog(@"%@----执行子线程任务结束",[NSThread currentThread]);

}

又到了愉快的猜谜时间,是否像我们猜测的那样,只要有Mode,并且Mode包含事件源RunLoop就可以正常运行了呢?

1

2

3

4

{number = 1, name = main}----开辟子线程

{number = 3, name = subThread}----开始执行子线程任务

{number = 3, name = subThread}----执行子线程任务结束

subThread线程被释放了

最后跟我们想的不一样,线程释放了,RunLoop没有成功启用。原因就出在[runLoop run];上面。

这句的意思是,在NSDefaultRunLoopMode下运行RunLoop。而我们添加的事件源是在另外一个Mode下,NSDefaultRunLoopMode仍然空空如也,所以RunLoop也就直接退出了。所以我们还要加一个条件,RunLoop正常运行的条件是:1.有Mode。2.Mode有事件源。3.运行在有事件源的Mode下。

⑤除了[runLoop run]还有那些方法启动RunLoop?

NSRunLoop中总共包装了3个方法供我们使用

1

1.- (void)run;

除非希望子线程永远存在,否则不建议使用,因为这个接口会导致Run Loop永久性的运行NSDefaultRunLoopMode模式,即使使用 CFRunLoopStop(runloopRef);也无法停止RunLoop的运行,那么这个子线程也就无法停止,只能永久运行下去。

1

2.- (void)runUntilDate:(NSDate *)limitDate;

比上面的接口好点,有个超时时间,可以控制每次RunLoop的运行时间,也是运行在NSDefaultRunLoopMode模式。这个方法运行RunLoop一段时间会退出给你检查运行条件的机会,如果需要可以再次运行RunLoop。注意CFRunLoopStop(runloopRef);仍然无法停止RunLoop的运行,因此最好自己设置一个合理的RunLoop运行时间。比如

1

2

3

while (!Stop){

    [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:10]];

}

1

3.- (BOOL)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;

有一个超时时间限制,而且可以设置运行模式

这个接口在非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

performSelectorOnMainThread:withObject:waitUntilDone:

performSelectorOnMainThread:withObject:waitUntilDone:modes:

在主线程中响应指定Selector。这两个方法给你提供了选项来阻断当前线程(不是执行Selector的线程而是调用上述方法的线程)直到selector被执行完毕。

 

performSelector:onThread:withObject:waitUntilDone:

performSelector:onThread:withObject:waitUntilDone:modes:

在某个子线程(NSThread对像)中响应指定Selector。这两个方法同样给你提供了选项来阻断当前线程直到Selector被执行完毕。

 

performSelector:withObject:afterDelay:

performSelector:withObject:afterDelay:inModes:

在当前线程中执行Selector,并附加了延迟选项。多个排队的Selector会按照顺序一个一个的执行。

其实,这几个方法都是向线程中的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

- (void)viewDidLoad {

    [super viewDidLoad];

   

    NSLog(@"%@----开辟子线程",[NSThread currentThread]);

    

    NSThread *tmpThread = [[MyThread alloc] initWithTarget:self selector:@selector(subThreadTodo) object:nil];

    //subThread用weak声明,用weak声明,用weak声明

    self.subThread = tmpThread;

    self.subThread.name = @"subThread";

    [self.subThread start];

   

}

//子线程执行的内容

- (void)subThreadTodo

{

   NSLog(@"%@----开始执行子线程任务",[NSThread currentThread]);

    

   NSRunLoop *runLoop = [NSRunLoop currentRunLoop];

   [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];

 

   [runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];

   NSLog(@"%@----执行子线程任务结束",[NSThread currentThread]);

}

//我们希望放在子线程中执行的任务

- (void)wantTodo{

    //断点2

    NSLog(@"当前线程:%@执行任务处理数据", [NSThread currentThread]);

   

}

//屏幕点击事件

- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent *)event{

    //断点1

    //在子线程中去响应wantTodo方法

    [self performSelector:@selector(wantTodo) onThread:self.subThread withObject:nil waitUntilDone:NO];

运行之前我们先把Xcode左边侧栏中选到显示CPU、Memory使用情况那一页,把最下面的按钮中第一个按钮选中状态取消掉。(否则看不见RunLoop的堆栈信息。)

然后我们运行程序,先暂时不做任何操作。

 

1

2

{number = 1, name = main}----开辟子线程

{number = 3, name = subThread}----开始执行子线程任务

子线程开启,RunLoop正常运行,似乎与刚才没有任何不同。然后我们点击屏幕任何一个地方。断点1触发,我们来查看左侧的堆栈。

前面提到过UIEvent事件属于source0,从这里的堆栈就可以得到印证。我们在主线程中触发了touchesBegan,然后主线程的RunLoop就开始响应source0事件源,然后去调用对应的方法。我们放过断点继续查看。

同样是前面提到的,performSelector也是source0依然可以从堆栈得到印证。放过断点1后调用了performSelector,然后subThread的RunLoop开始响应source0事件源,然后去调用对应的方法,所以来到了断点2。放过断点2查看结果,整个流程结束,打印日志如下。

 

1

2

3

{number = 1, name = main}----开辟子线程{number = 3, name = subThread}----开始执行子线程任务

当前线程:{number = 3, name = subThread}执行任务处理数据{number = 3, name = subThread}----执行子线程任务结束

subThread线程被释放了

最后子线程任务结束然后被释放是因为之前提到的,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

 static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__();

 static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__();

 static void __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__();

 static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__();

 static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__();

 static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__();

上面几个函数之所以那么长,我估计官方是想让我们观名知意,看名字就可以猜出作用。但是同样的,刚接触RunLoop的时候过多的接触这些深层次的东西反而会觉得找不到方向。我觉得入门还是先从表面一些的东西入手比较好。等有一些比较全面的了解以后想要深入理解了再来看这部分,现在有个印象知道这个概念就好。想要深入的话可以查看文末的文档或者RunLoop入门学习补充资料(4.RunLoop回调函数触发逻辑)。

3.让线程定时执行某任务(Timer)

说到timer估计大家都不陌生,日常开发中我们经常都会用到。可能很多人听说RunLoop还是在使用NSTimer的时候。NSTimer有如下几个创建方式:

1

2

3

4

5

6

 + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;

 + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo

 + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block ;//iOS10以后新加的方法

 + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;

 + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo

 + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block ;//iOS10以后新加的方法

<1>初识NSTimer遇到的坑

刚接触Timer的时候,很多人(包括我)肯定都踩过一个坑,那就是创建了Timer却没有启动,百思不得其解,然后才知道还要把Timer加到一个叫RunLoop的东西里面才能正常运行。就像下面一样:

1

2

3

4

5

6

7

- (void)viewDidLoad {

    [super viewDidLoad];

 

    NSTimer *timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(wantTodo) userInfo:nil repeats:YES];

    //timerWith开头的方法创建的Timer如果不加下面一句无法运行。

    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];

}

当时我们可能不理解为什么,只知道必须要这么做才能正常启动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有一定了解了,我们不妨来分析一下以便加深理解。

  1. 在之前讲Mode的时候提到过,RunLoop每次只能运行在一个Mode下,其意义是让不同Mode中的item互不影响。

  2. NSTimer是一个Timer源(item),在上面哪个例子中不管是`[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];`还是`scheduedTimerWith`我们都是把Timer加到了主线程RunLoop的NSDefaultRunLoopMode中。一般情况下主线程RunLoop就运行在NSDefaultRunLoopMode下,所以定时器正常运行。

  3. 当Scrollview开始滑动时,主线程RunLoop自动切换了当前运行的Mode(currentMode),变成了UITrackingRunLoopMode。所以现在RunLoop要处理的就是UITrackingRunLoopMode中item。

  4. 我们的timer是添加在NSDefaultRunLoopMode中的,并没有添加到UITrackingRunLoopMode中。即我们的timer不是UITrackingRunLoopMode中的item。

  5. 本着不同Mode中的item互不影响的原则,RunLoop也就不会处理非当前Mode的item,所以定时器就不会响应。

  6. 当Scrollview滑动结束,主线程RunLoop自动切换了当前运行的Mode(currentMode),变成了NSDefaultRunLoopMode。我们的Timer是NSDefaultRunLoopMode的item,所以RunLoop会处理它,所以又正常响应了。

  7. 如果想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

- (void)viewDidLoad {

    [super viewDidLoad];

  

    NSLog(@"%@----开辟子线程",[NSThread currentThread]);

   

    NSThread *tmpThread = [[MyThread alloc] initWithTarget:self selector:@selector(subThreadTodo) object:nil];

   

    self.subThread = tmpThread;

    self.subThread.name = @"subThread";

    [self.subThread start];

 

}

    

- (void)subThreadTodo

{

   NSLog(@"%@----开始执行子线程任务",[NSThread currentThread]);

 

   NSRunLoop *runLoop = [NSRunLoop currentRunLoop];

   [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];

 

   [runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];

   NSLog(@"%@----执行子线程任务结束",[NSThread currentThread]);

}

 

- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent *)event{

    

    [self performSelector:@selector(wantTodo) onThread:self.subThread withObject:nil waitUntilDone:NO];

    

}

 

- (void)wantTodo{

    //断点1

    //1.这个方法是作用于当前线程,现在在子线程中调用这个函数,所以会作用于子线程的RunLoop

    //self这个位置只要是继承自NSObject的对象都能填,但是他要有后面SEL的方法,否则会崩溃。(直白的说这个方法就是在当前线程中延迟调用某个对象的某个方法。)

    [self performSelector:@selector(afterDelayTodo) withObject:nil afterDelay:0];

    

}

 

- (void)afterDelayTodo{

    //断点2

    NSLog(@"当前线程:%@执行任务处理数据", [NSThread currentThread]);

}

跟之前perform的例子中一样的操作,到断点1的时候结果与之前也没什么不同,我们放过断点1,来到断点2.

可以看到,跟之前调用其他performSelecter时的DoSource0等等一系列堆栈不一样了,全部变成了Timer相关。实际上,当调用 performSelecter:afterDelay: 后,其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中,所以这个方法是属于Timer源的。关于这个方法有个比较经典的用法,很多文档都说过。

当tableview的cell上有需要从网络获取的图片的时候,异步线程会去加载图片,加载完成后主线程就会设置cell的图片,有可能会造成卡顿。可以让设置图片的任务在CFRunLoopDefaultMode下进行,当滚动tableView的时候,RunLoop是在 UITrackingRunLoopMode 下进行,不去设置图片,而是当停止的时候,再去设置图片。(这个场景的核心还是利用不同Mode的切换的思想,可以拓展其他地方)

1

2

3

4

[self.myImageView performSelector:@selector(setImage:)

                           withObject:[UIImage imageNamed:@""]

                           afterDelay:0

                              inModes:@[NSDefaultRunLoopMode]];

当然不是说这种方法就一定好,毕竟他在滑动的时候不会显示图片,万一你的需求跟这刚好相反呢,而且现在SDWebImage处理的已经很好了,已经很少有人用这种方法了。但是这个利用Mode切换的思想可以借鉴,万一其他地方用上就很合适呢。

4.监听Observer达到一些目的

这个就比现在我们讲的更进一阶,因为它涉及到的包括RunLoop的运行逻辑还有一些其他你想实现的功能的拓展。暂时我们只知道有这个用法就行,如果现在来钻研可能就迷失了。

目前知道的比较有名的有:

1.sunnyxx的UITableView+FDTemplateLayoutCell利用Observer在界面空闲状态下计算出UITableViewCell的高度并进行缓存。

2.FaceBook的AsyncDisplayKit

共同之处在于,通过合理利用RunLoop机制,将很多不是必须在主线程中执行的操作放在子线程中实现,然后在合适的时机同步到主线程中,这样可以节省在主线程执行操作的时间,避免卡顿。

关于RunLoop的释放问题(RunLoop带autoreleasepool的正确写法)

<1>分析

Timer和Source以及一些回调block等等,都需要占用一部分存储空间,所以要释放掉,如果不释放掉,就会一直积累,占用的内存也就越来越大。

在主线程中

  1. 当RunLoop开启时,会自动创建一个自动释放池。

  2. 当RunLoop在休息之前会释放掉自动释放池的东西。

  3. 然后重新创建一个新的空的自动释放池。

  4. 当RunLoop被唤醒重新开始跑圈时,Timer,Source等新的事件就会放到新的自动释放池中。

  5. 重复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

    @autoreleasepool {

        

        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];

        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];

        while (!self.isNeedStopRunLoop) {

           

            //这里RunLoop不需要添加autoreleasepool

            //每个RunLoop内部都会自动管理autoreleasepool

            //事件源等一些autorelease对象会在RunLoop的迭代中自动释放。

 

                [runLoop runMode:NSDefaultRunLoopMode

                  beforeDate:[NSDate distantFuture]];

            //[runLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:10]];

 

        }

       

    }

<3>不需要用while循环控制的RunLoop

1

2

3

4

5

6

7

8

    @autoreleasepool {

  

        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];

        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];

        [runLoop run];

        //[runLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:10]];

       

    }

<4>关于主线程中autoreleasepool的题外话

主线程中一般不需要我们手动添加autoreleasepool,但是,如果你希望某个对象或者变量尽快释放的时候我们也可以手动添加。比如下面这种情况:

1

2

3

4

5

6

7

8

1.很长的循环(while 同理)

    int lagerNum = 1024 1024 2 ;

    for(int i = 0 ; i < lagerNum; i++)

    {

        NSString *str = [NSString stringWithFormat:@"Hello"];

        str = [str uppercaseString];

        str = [NSString stringByAppendingFormat:@"-%@",@"World!"];

    }

"在没有手加Autorelease Pool的情况下,Autorelease对象是在当前的runloop迭代结束时释放的,而它能够释放的原因是系统在每个runloop迭代中都加入了自动释放池Push和Pop。按照我们以前的想法,每次循环的大括号结束后,当前的变量就会释放。可是现在我们了解的自动释放的相关知识后就知道其实要等到一次主线程的RunLoop迭代以后才会一起释放这些变量。而如果还来不及等到RunLoop迭代结束去释放变量,这期间就积累了足够多的变量,就会导致内存警告或者崩溃。所以我们可以手动添加autoreleasepool,让对象尽快释放。

1

2

3

4

5

6

7

8

9

10

1.改

    int lagerNum = 1024 1024 2 ;

    for(int i = 0 ; i < lagerNum; i++)

    {

      @autoreleasepool{

           NSString *str = [NSString stringWithFormat:@"Hello"];

           str = [str uppercaseString];

           str = [NSString stringByAppendingFormat:@"-%@",@"World!"];

       }

    }

关于子线程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

@interface ViewController ()

 

@property (nonatomic, weak)NSThread *subThread;//子线程

 

@property (nonatomic, weak)NSRunLoopMode runLoopMode;//想设置的RunLoop的Mode

 

@property (nonatomic, assign)BOOL isNeedRunLoopStop;//控制是否需要停止RunLoop

 

@property (weak, nonatomic) IBOutlet UITextView *myTextView;//只要是Scrollview及其子类都行

 

@end

 

@implementation ViewController

 

- (void)viewDidLoad {

    [super viewDidLoad];

     

    self.myTextView.delegate = self;

     

    self.isNeedRunLoopStop = NO;

     

    NSLog(@"%@----开辟子线程",[NSThread currentThread]);

     

    NSThread *tmpThread = [[MyThread alloc] initWithTarget:self selector:@selector(subThreadTodo) object:nil];

   0

    self.subThread = tmpThread;

    self.subThread.name = @"subThread";

    [self.subThread start];   

}

   

- (void)subThreadTodo

{

   NSLog(@"%@----开始执行子线程任务",[NSThread currentThread]);

 

    @autoreleasepool{

        

        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];

        

        //NSDefaultRunLoopMode下暂时什么都不干,只是为了让RunLoop能在该模式下运行添加了一个source1

        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];

        

        NSTimer *timer = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(timerTodo) userInfo:nil repeats:YES];

         

        //给UITrackingRunLoopMode添加一个timer,为了等下切换到该模式的时候能看到效果。

        //提示:子线程RunLoop如果不给UITrackingRunLoopMode添加item就没有这个Mode,可以看前面初体验疑问的截图。

        //但是,NSDefaultRunLoopMode是无论如何都存在的,就算你不给他添加item,他也只是内容为空而已。

        [runLoop addTimer:timer forMode:UITrackingRunLoopMode];

        

        self.runLoopMode = NSDefaultRunLoopMode;

         

        //这一句先保持注释状态,之后再根据下面文章的提示取消注释看效果。

        //CFRunLoopAddCommonMode(CFRunLoopGetCurrent(), (CFStringRef)UITrackingRunLoopMode);

       

        while (!self.isNeedRunLoopStop) {//用while来控制RunLoop的运行与否

            //让RunLoop在我们希望的Mode下运行

            [runLoop runMode:self.runLoopMode beforeDate:[NSDate distantFuture]];          

        }        

    }        

}

 

- (void)changeSubThreadRunLoopMode:(NSRunLoopMode)mode{

 

    //改变我们希望RunLoop运行的Mode的方法

    //到时候用[performSelector:onThread:withObject:waitUntilDone:]来调用

    //结合[runMode:beforeDate:]触发非Timer的事件源会退出RunLoop的特性

    //再结合上面While的写法,就退出了之前的RunLoop并让RunLoop以我们希望的Mode重新Run。

 

    //断点3

    NSLog(@"当前线程:%@ RunLoop即将将Mode改变成:%@\n", [NSThread currentThread], mode);

    

    self.runLoopMode = mode;

}

 

- (void)timerTodo{

    //上面的Timer执行的函数,只是为了等下切换的mode后有打印好观察。

    NSLog(@"Timer启动啦,当前RunLoopMode:%@\n", [[NSRunLoop currentRunLoop] currentMode]);

}

 

- (void)scrollViewDidScroll:(UIScrollView *)scrollView{

   

    if (self.runLoopMode != UITrackingRunLoopMode) {

        //如果有滑动事件,并且RunLoop的Mode不为UITrackingRunLoopMode

        //就改变Mode并退出当前RunLoop然后让RunLoop以更改后的Mode重新Run

        //加if是为了避免重复操作,切换RunLoopMode只需要一次

        [self performSelector:@selector(changeSubThreadRunLoopMode:) onThread:self.subThread withObject:UITrackingRunLoopMode waitUntilDone:NO];

    }    

}

 

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{

    

    //拖拽结束会调用这个方法,如果还有拖拽后的滑动动画就不做操作

    if (!decelerate) {

        //如果没有后续动画了就切换Mode为NSDefaultRunLoopMode

        if (self.runLoopMode != NSDefaultRunLoopMode) {

            //断点1

            [self performSelector:@selector(changeSubThreadRunLoopMode:) onThread:self.subThread withObject:NSDefaultRunLoopMode waitUntilDone:NO];

        }

    }

}

 

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {

    

    //拖拽后的后续滑动动画结束(如果有才会到这,没有就不会到这个函数里面)

    //也就是说上面那个函数如果切换了Mode就不会走这里,否则说明需要在这里切换Mode

    //切换Mode为NSDefaultRunLoopMode

    if (self.runLoopMode != NSDefaultRunLoopMode) {

        //断点2

        [self performSelector:@selector(changeSubThreadRunLoopMode:) onThread:self.subThread withObject:NSDefaultRunLoopMode waitUntilDone:NO];

    }

}

 

@end

好了代码看完了,激动人心的时刻到了,我们来看效果。

1

{number = 1, name = main}----开辟子线程{number = 3, name = subThread}----开始执行子线程任务

程序启动以后,打印如上,没什么好说的,一切正常,接下来才是重点。来让我们拖拽textView。

 

1

2

3

{number = 1, name = main}----开辟子线程{number = 3, name = subThread}----开始执行子线程任务

当前线程:{number = 3, name = subThread} RunLoop即将将Mode改变成:UITrackingRunLoopMode

Timer启动啦,当前RunLoopMode:UITrackingRunLoopMode

成功了!!!依照我们所想的Mode切换成了UITrackingRunLoopMode。好就差最后一步了,我们松手看看是什么情况。

 

1

2

3

4

5

6

{number = 1, name = main}----开辟子线程{number = 3, name = subThread}----开始执行子线程任务

当前线程:{number = 3, name = subThread} RunLoop即将将Mode改变成:UITrackingRunLoopMode

Timer启动啦,当前RunLoopMode:UITrackingRunLoopMode

Timer启动啦,当前RunLoopMode:UITrackingRunLoopMode

Timer启动啦,当前RunLoopMode:UITrackingRunLoopMode

Timer启动啦,当前RunLoopMode:UITrackingRunLoopMode

可以看到即使我们松手,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

- (void)changeSubThreadRunLoopMode:(NSRunLoopMode)mode{

    NSLog(@"当前线程:%@ RunLoop即将将Mode改变成:%@\n", [NSThread currentThread], mode);

    NSLog(@"%@", [NSRunLoop currentRunLoop]);

     

}

 

- (void)scrollViewDidScroll:(UIScrollView *)scrollView{

     

    if (self.runLoopMode != UITrackingRunLoopMode) {

        //改变成UITrackingRunLoopMode后打印一次日志

        [self performSelectorOnMainThread:@selector(changeSubThreadRunLoopMode:) withObject:UITrackingRunLoopMode waitUntilDone:NO];

    }    

}

打印结果很长我只截图了比较关键的部分并标注。

从日志搜索结果和与响应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

CFRunLoopAddCommonMode(CFRunLoopGetCurrent(), (CFStringRef)UITrackingRunLoopMode);

接下来我们再看看效果

 

1

2

3

4

5

6

7

8

9

10

11

12

13

{number = 1, name = main}----开辟子线程

{number = 3, name = subThread}----开始执行子线程任务

当前线程:{number = 3, name = subThread} RunLoop即将将Mode改变成:UITrackingRunLoopMode

Timer启动啦,当前RunLoopMode:UITrackingRunLoopMode

Timer启动啦,当前RunLoopMode:UITrackingRunLoopMode

Timer启动啦,当前RunLoopMode:UITrackingRunLoopMode

Timer启动啦,当前RunLoopMode:UITrackingRunLoopMode

当前线程:{number = 3, name = subThread} RunLoop即将将Mode改变成:kCFRunLoopDefaultMode

当前线程:{number = 3, name = subThread} RunLoop即将将Mode改变成:UITrackingRunLoopMode

Timer启动啦,当前RunLoopMode:UITrackingRunLoopMode

Timer启动啦,当前RunLoopMode:UITrackingRunLoopMode

Timer启动啦,当前RunLoopMode:UITrackingRunLoopMode

当前线程:{number = 3, name = subThread} RunLoop即将将Mode改变成:kCFRunLoopDefaultMode

最后就实现了类似主线程的mode切换效果。拖拽滑动期间是UITrackingRunLoopMode,会触发timer的内容打印日志。

滑动停止以后就切换成了kCFRunLoopDefaultMode,不在打印timer的日志。

到这一步mode切换的实验就结束了,虽然我仍然没想到啥时候会用上,但至少我们知道了子线程中的Mode切换是可行的,并且搞清楚了与RunLoop相关的那几个perform方法其实是向所有注册了CommonMode的Mode添加source0而不是向某一个特定Mode添加。如果我这个实验有错误请大大们指出。

更正:

1

2

performSelectorOnMainThread:withObject:waitUntilDone:

performSelector:onThread:withObject:waitUntilDone:

这两个方法,是向所有注册了CommonMode的Mode添加source0

1

2

3

performSelectorOnMainThread:withObject:waitUntilDone:modes:

performSelector:onThread:withObject:waitUntilDone:modes:

performSelector:withObject:afterDelay:inModes:

这三个方法是给特定modes(参数是包含至少一个mode的数组)添加source0(前两个)或者timer(afterDelay)。

1

performSelector:withObject:afterDelay:

这个方法是向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

//因为在这个实验中触发下面两个代理的时候子线程RunLoop是处于UITrackingRunLoopMode

//而子线程CommonMode默认只有NSDefaultRunLoopMode

//所以调用performSelector:onThread:withObject:waitUntilDone:时,在UITrackingRunLoopMode下无法响应(理由可以看实验的分析过程)

//调用performSelector:onThread:withObject:waitUntilDone:modes:指定响应的mode为UITrackingRunLoopMode即可解决问题

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{

    

    //拖拽结束会调用这个方法,如果还有拖拽后的滑动动画就不做操作

    if (!decelerate) {

        //如果没有后续动画了就切换Mode为NSDefaultRunLoopMode

        if (self.runLoopMode != NSDefaultRunLoopMode) {

            //断点1

            [self performSelector:@selector(changeSubThreadRunLoopMode:) onThread:self.subThread withObject:NSDefaultRunLoopMode waitUntilDone:NO modes:@[UITrackingRunLoopMode]];

        }

    }

}

 

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {

    

    //拖拽后的后续滑动动画结束(如果有才会到这,没有就不会到这个函数里面)

    //也就是说上面那个函数如果切换了Mode就不会走这里,否则说明需要在这里切换Mode

    //切换Mode为NSDefaultRunLoopMode

    if (self.runLoopMode != NSDefaultRunLoopMode) {

        //断点2

        [self performSelector:@selector(changeSubThreadRunLoopMode:) onThread:self.subThread withObject:NSDefaultRunLoopMode waitUntilDone:NO modes:@[UITrackingRunLoopMode]];

    }

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值