RunLoop 相关

RunLoop 相关_第1张图片
随便的配图

谨以此篇总结归纳记录iOS中一大知识点 RunLoop 的相关知识,以作备忘。

iOS中,提供了两种runloop:NSRunLoop 和 CFRunLoopRef

  • CFRunLoopRef : 是CoreFoundation框架内的纯C函数的API,所有API都是线程安全的;
  • NSRunLoop : 是基于CFRunLoopRef的封装,提供了面向对象的API,但这些API不是线程安全的;

RunLoop与线程的关系

  • runloop寄生于线程。RunLoop是保证线程不会退出,并且能在不处理消息的时候让线程休眠,节约资源,在接收到消息的时候唤醒线程做出对应处理的消息循环机制。

runloop的创建

苹果不允许直接创建 RunLoop,它只提供了四个自动获取的函数
[NSRunLoop currentRunLoop];//获取当前线程的RunLoop
[NSRunLoop mainRunLoop];
CFRunLoopGetMain();
CFRunLoopGetCurrent();

这些函数内部的大概逻辑(注:这并不是源码,而是大神为了方便我们理解,对源码进行了一些可读性优化后的结果):

/// 全局的Dictionary,key 是 线程, value 是 CFRunLoopRef
static CFMutableDictionaryRef loopsDic;
/// 访问 loopsDic 时的锁
static CFSpinLock_t loopsLock;
 
/// 获取一个 pthread 对应的 RunLoop。
CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
    OSSpinLockLock(&loopsLock);
    
    if (!loopsDic) {
        // 第一次进入时,初始化全局Dic,并先为主线程创建一个 RunLoop。
        loopsDic = CFDictionaryCreateMutable();
        CFRunLoopRef mainLoop = _CFRunLoopCreate();
        CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
    }
    
    /// 直接从 Dictionary 里获取。
    CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));
    
    if (!loop) {
        /// 取不到时,创建一个
        loop = _CFRunLoopCreate();
        CFDictionarySetValue(loopsDic, thread, loop);
        /// 注册一个回调,当线程销毁时,顺便也销毁其对应的 RunLoop。
        _CFSetTSD(..., thread, loop, __CFFinalizeRunLoop);
    }
    
    OSSpinLockUnLock(&loopsLock);
    return loop;
}
 
CFRunLoopRef CFRunLoopGetMain() {
    return _CFRunLoopGet(pthread_main_thread_np());
}
 
CFRunLoopRef CFRunLoopGetCurrent() {
    return _CFRunLoopGet(pthread_self());
}

通过看这部分代码我们可以产生一个概念,那就是,线程和 RunLoop 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里。线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。你只能在一个线程的内部获取其 RunLoop(主线程除外)。

RunLoop的结构

RunLoop 相关_第2张图片
runloop结构图.png
  1. 图中蓝色部分表示RunLoop整体;
  2. 图中绿色部分表示RunLoop中的mode,一个RunLoop中可以包含多个mode;
  3. 图中黄色部分表示mode下对应的各类任务或者说动作;
    RunLoop就像一个工厂的车间,mode是车间里的不同生产线,而该车间同时只能开启一条生产线;mode下的动作就像这条生产线上的各个工序,工序必须存在,否则开启生产线就没有意义:所以如果runloop中没有加入任务,RunLoop就会退出,从而对应的线程就会销毁;而mode的存在是为了让RunLoop在不同的模式下执行不同的任务时互不影响:因为RunLoop同时只能在一种mode下运行。
  • 常用的mode:
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组合。
  • mode的item:源事件
  1. source输入源事件:
1.source0:诸如UIEvent(触摸,滑动等),performSelector这种需要手动触发的操作。
2.source1:处理系统内核的mach_msg事件(系统内部的端口事件)。诸如唤醒RunLoop或者让RunLoop进入休眠节省资源等。
一般来说日常开发中我们需要关注的是source0,source1只需要了解。
之所以说source0更重要是因为日常开发中,我们需要对常驻线程进行操作的事件大多都是source0,稍后的实验会讲到。
  1. Timer定时源事件:
Timer即为定时源事件。通俗来讲就是我们很熟悉的NSTimer,其实NSTimer定时器的触发正是基于RunLoop运行的,所以使用NSTimer之前必须注册到RunLoo
p,但是RunLoop为了节省资源并不会在非常准确的时间点调用定时器,如果一个任务执行时间较长,那么当错过一个时间点后只能等到下一个时间点执行,并不会
延后执行(NSTimer提供了一个tolerance属性用于设置宽容度,如果确实想要使用NSTimer并且希望尽可能的准确,则可以设置此属性)。
  1. observer:
    消息循环中的监听器,随时通知外部当前RunLoop的运行状态。NSRunLoop没有相关方法,只能通过CFRunLoop相关方法创建。
    注:它不能作为让RunLoop正常运行的条件,只有Observer的RunLoop也是无法正常运行的。
// 创建observer
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {

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

    });

    // 添加观察者:监听RunLoop的状态
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);

当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化。可以观测的时间点有以下几个:

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry         = (1UL << 0), // 即将进入Loop
    kCFRunLoopBeforeTimers  = (1UL << 1), // 即将处理 Timer
    kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
    kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
    kCFRunLoopAfterWaiting  = (1UL << 6), // 刚从休眠中唤醒
    kCFRunLoopExit          = (1UL << 7), // 即将退出Loop
};
上面的 Source/Timer/Observer 被统称为 mode item,一个item可以被同时加入多个mode。但一个item被重复加入同一个mode时是不会有效果的。如果一个mode中一个item 都没有(只有Observer也不行),则 RunLoop 会直接退出,不进入循环。
  • runLoop的内部逻辑
RunLoop 相关_第3张图片
RunLoop内部逻辑

总结:runLoop正常运行的条件:1.有mode;2.mode有事件源(非observer);3.运行在有事件源的mode下.


RunLoop应用

一.线程保活

在平时开发中,遇到一些耗时操作往往是要开辟子线程去处理,当处理结束,子线程自动退出:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSLog(@"%@---开辟子线程",[NSThread currentThread]);
    
    MyThread *thread1 = [[MyThread alloc]initWithTarget:self selector:@selector(thread1Method) object:nil];
    thread1.name = @"thread-1";
    [thread1 start];
}

- (void)thread1Method {
    NSLog(@"%@---执行子线程任务",[NSThread currentThread]);
}

执行结果:

2018-09-06 16:53:26.868150+0800 RunLoop[19234:963726] {number = 1, name = main}---开辟子线程
2018-09-06 16:53:26.868683+0800 RunLoop[19234:963785] {number = 3, name = thread-1}---执行子线程任务
2018-09-06 16:53:26.869586+0800 RunLoop[19234:963785] thread-1---线程被释放了

如果我们像其他变量一样使用强引用将该线程保持起来,试试是否可以重复使用该线程:

@interface ViewController ()
@property (nonatomic, strong) MyThread *myThread;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSLog(@"%@---开辟子线程",[NSThread currentThread]);
    
    MyThread *thread1 = [[MyThread alloc]initWithTarget:self selector:@selector(thread1Method) object:nil];
    thread1.name = @"thread-1";
    self.myThread = thread1;
    
    [self.myThread start];
    //重复使用该线程
    [self.myThread start];
}

- (void)thread1Method {
    NSLog(@"%@---执行子线程任务",[NSThread currentThread]);
}

执行结果:

2018-09-06 17:24:28.797712+0800 RunLoop[19532:983497] {number = 1, name = main}---开辟子线程
2018-09-06 17:24:28.798080+0800 RunLoop[19532:983542] {number = 3, name = thread-1}---执行子线程任务
2018-09-06 17:24:28.815303+0800 RunLoop[19532:983497] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[MyThread start]: attempt to start the thread again'

可以看到,程序崩溃了,虽然持有了线程以后,该对象没有被释放,但是已经不能被重复使用,因为这个线程执行完任务以后已经死亡,不允许重新开启。

由上可知,想要让线程不死亡,就必须让他有任务一直在运行,而想要重复利用该线程,就要让他在有任务是进行处理,而没任务是进入类似休眠等待状态,而要达到这种目的,就要开启线程的runloop。

  • 以下方法用于在某个线程内相应某个方法:
performSelectorOnMainThread:withObject:waitUntilDone:
performSelectorOnMainThread:withObject:waitUntilDone:modes:
在主线程中响应指定Selector。这两个方法给你提供了选项来阻断当前线程(不是执行Selector的线程而是调用上述方法的线程)直到selector被执行完毕。
属于source0事件

performSelector:onThread:withObject:waitUntilDone:
performSelector:onThread:withObject:waitUntilDone:modes:
在某个子线程(NSThread对像)中响应指定Selector。这两个方法同样给你提供了选项来阻断当前线程直到Selector被执行完毕。
属于source0事件

performSelector:withObject:afterDelay:
performSelector:withObject:afterDelay:inModes:
在当前线程中执行Selector,并附加了延迟选项。多个排队的Selector会按照顺序一个一个的执行
属于Timer事件

其实,这几个方法都是向线程中的RunLoop发送了消息,然后RunLoop接收到了消息就唤醒线程,去做对应的事情。所以想要正常使用这几个方法,响应selector的线程必须开启了RunLoop。

  • 接下来尝试在子线程中开启runloop:
- (void)viewDidLoad {
    [super viewDidLoad];

    NSLog(@"%@---开辟子线程",[NSThread currentThread]);
    
    MyThread *thread1 = [[MyThread alloc]initWithTarget:self selector:@selector(thread1Method) object:nil];
    thread1.name = @"thread-1";
//    self.myThread = thread1;
    
    [thread1 start];
    
//    [self.myThread start];
    //重复使用该线程
//    [self.myThread start];
}

- (void)thread1Method {
    
    NSLog(@"%@---执行子线程任务",[NSThread currentThread]);
    
    //获取当前线程的runloop
    NSRunLoop *runloop = [NSRunLoop currentRunLoop];
    //向runloop中添加任务,如果不添加,runloop会直接退出循环
    [runloop addPort:[NSMachPort port] forMode:NSRunLoopCommonModes];
    //启动runloop,runloop默认运行在NSDefaultRunLoopMode模式下,所以上边的任务要添加到default模式下或者common模式下,否则RunLoop仍然会退出
    [runloop run];
    
    NSLog(@"%@---执行子线程任务结束",[NSThread currentThread]);
}

运行结果:

2018-09-08 16:17:57.952431+0800 RunLoop[43303:1820366] {number = 1, name = main}---开辟子线程
2018-09-08 16:17:57.953460+0800 RunLoop[43303:1820728] {number = 3, name = thread-1}---执行子线程任务

RunLoop本质就是个Event Loop的do while循环,所以运行到 [runloop run]这一行以后子线程就一直在进行接受消息->等待->处理的循环。所以不会运行[runLoop run]之后的代码,也就不会因为任务结束导致线程死亡进而销毁。

  • runLoop启动方法:
1.- (void)run;
这个接口会导致Run Loop永久性的运行NSDefaultRunLoopMode模式,即使使用 CFRunLoopStop(runloopRef);也无法停止RunLoop的运行,那么这个子
线程也就无法停止,只能永久运行下去。
2.- (void)runUntilDate:(NSDate *)limitDate;
可以控制每次RunLoop的运行时间,也是运行在NSDefaultRunLoopMode模式。注意CFRunLoopStop(runloopRef);仍然无法停止RunLoop的运行,因此最
好自己设置一个合理的RunLoop运行时间。
3.- (BOOL)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;
有一个超时时间限制,而且可以设置运行模式这个接口在非Timer事件触发、显式的用CFRunLoopStop停止RunLoop或者到达limitDate后会退出返回。如果仅
是Timer事件触发并不会让RunLoop退出返回,但是如果是PerfromSelector事件或者其他Input Source事件触发处理后,RunLoop会退出返回YES。
  • 让线程响应消息
@interface ViewController ()
@property (nonatomic, weak) MyThread *myThread;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    NSLog(@"%@---开辟子线程",[NSThread currentThread]);
    
    MyThread *thread1 = [[MyThread alloc]initWithTarget:self selector:@selector(thread1Method) object:nil];
    thread1.name = @"thread-1";
    self.myThread = thread1;
    
    [thread1 start];
    
//    [self.myThread start];
    //重复使用该线程
//    [self.myThread start];
}

- (void)thread1Method {
    
    NSLog(@"%@---执行子线程任务",[NSThread currentThread]);
    
    //获取当前线程的runloop
    NSRunLoop *runloop = [NSRunLoop currentRunLoop];
    //向runloop中添加任务,如果不添加,runloop会直接退出循环
    [runloop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
    //启动runloop
    [runloop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
    
    NSLog(@"%@---执行子线程任务结束",[NSThread currentThread]);
}

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
    
    //在子线程中执行方法
    [self performSelector:@selector(dealMessage) onThread:self.myThread withObject:nil waitUntilDone:NO];
    
}

//子线程响应的消息
- (void)dealMessage {
    
    NSLog(@"%@---响应消息",[NSThread currentThread]);
}
@end

运行结果:

2018-09-17 18:31:57.110042+0800 RunLoop[12719:4850481] {number = 1, name = main}---开辟子线程
2018-09-17 18:31:57.110862+0800 RunLoop[12719:4850754] {number = 4, name = thread-1}---执行子线程任务
2018-09-17 18:32:11.045773+0800 RunLoop[12719:4850754] {number = 4, name = thread-1}---响应消息
2018-09-17 18:32:11.046215+0800 RunLoop[12719:4850754] {number = 4, name = thread-1}---执行子线程任务结束
2018-09-17 18:32:11.047035+0800 RunLoop[12719:4850754] thread-1---线程被释放了

最后子线程任务结束然后被释放是因为之前提到的,runMode:(NSString *)mode beforeDate:(NSDate *)limitDate这种启动RunLoop的方式有一个特性,那就是这个接口在非Timer事件触发(此处是达成了这个条件)、显式的用CFRunLoopStop停止RunLoop或者到达limitDate后会退出。而例子当中也没有用while把RunLoop包围起来,所以RunLoop退出后子线程完成了任务最后退出了。

  • 主线程 (有 RunLoop 的线程) 几乎所有函数都从以下六个之一的函数调起:
CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION
CFRunloop is calling out to an abserver callback function
用于向外部报告 RunLoop 当前状态的更改,框架中很多机制都由 RunLoopObserver 触发,如 CAAnimation

CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK
CFRunloop is calling out to a block
消息通知、非延迟的perform、dispatch调用、block回调、KVO

CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE
CFRunloop is servicing the main desipatch queue

CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION
CFRunloop is calling out to a timer callback function
延迟的perform, 延迟dispatch调用

CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION
CFRunloop is calling out to a source 0 perform function
处理App内部事件、App自己负责管理(触发),如UIEvent、CFSocket。普通函数调用,系统调用

CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION
CFRunloop is calling out to a source 1 perform function
由RunLoop和内核管理,Mach port驱动,如CFMachPort、CFMessagePort
二.让线程执行定时任务:Timer
  • Timer创建方法
+ (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以后新加的方法

timer创建后要正常启动必须加入runloop

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

使用scheduledTimer...方法创建的Timer默认加入到了当前线程的runloop的NSDefaultRunLoopMode下,所以创建后可以直接运行。

  • 使用Timer和滑动列表注意事项:
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,都会得到处理。
三.监听RunLoop observer
//创建监听者
     /*
     第一个参数 CFAllocatorRef allocator:分配存储空间 CFAllocatorGetDefault()默认分配
     第二个参数 CFOptionFlags activities:要监听的状态 kCFRunLoopAllActivities 监听所有状态
     第三个参数 Boolean repeats:YES:持续监听 NO:不持续
     第四个参数 CFIndex order:优先级,一般填0即可
     第五个参数 :回调 两个参数observer:监听者 activity:监听的事件
     */
     /*
     所有事件
     typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
     kCFRunLoopEntry = (1UL << 0),   //   即将进入RunLoop
     kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理Timer
     kCFRunLoopBeforeSources = (1UL << 2), // 即将处理Source
     kCFRunLoopBeforeWaiting = (1UL << 5), //即将进入休眠
     kCFRunLoopAfterWaiting = (1UL << 6),// 刚从休眠中唤醒
     kCFRunLoopExit = (1UL << 7),// 即将退出RunLoop
     kCFRunLoopAllActivities = 0x0FFFFFFFU
     };
     */
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        switch (activity) {
            case kCFRunLoopEntry:
                NSLog(@"RunLoop进入");
                break;
            case kCFRunLoopBeforeTimers:
                NSLog(@"RunLoop要处理Timers了");
                break;
            case kCFRunLoopBeforeSources:
                NSLog(@"RunLoop要处理Sources了");
                break;
            case kCFRunLoopBeforeWaiting:
                NSLog(@"RunLoop要休息了");
                break;
            case kCFRunLoopAfterWaiting:
                NSLog(@"RunLoop醒来了");
                break;
            case kCFRunLoopExit:
                NSLog(@"RunLoop退出了");
                break;

            default:
                break;
        }
    });

    // 给RunLoop添加监听者
    /*
     第一个参数 CFRunLoopRef rl:要监听哪个RunLoop,这里监听的是主线程的RunLoop
     第二个参数 CFRunLoopObserverRef observer 监听者
     第三个参数 CFStringRef mode 要监听RunLoop在哪种运行模式下的状态
     */
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
     /*
     CF的内存管理(Core Foundation)
     凡是带有Create、Copy、Retain等字眼的函数,创建出来的对象,都需要在最后做一次release
     GCD本来在iOS6.0之前也是需要我们释放的,6.0之后GCD已经纳入到了ARC中,所以我们不需要管了
     */
    CFRelease(observer);

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

  • 分析 :Timer和Source以及一些回调block等等,都需要占用一部分存储空间,所以要释放掉,如果不释放掉,就会一直积累,占用的内存也就越来越大。
在主线程中:
  1. 当RunLoop开启时,会自动创建一个自动释放池;
  2. 当RunLoop在休息之前会释放掉自动释放池的东西,然后重新创建一个新的空的自动释放池;
  3. 当RunLoop被唤醒重新开始跑圈时,Timer,Source等新的事件就会放到新的自动释放池中;
  4. 重复2-3;
  5. 当 RunLoop 退出时,会释放低掉自动释放池的对象;
在子线程中:

RunLoop是需要我们手动获取的,就更不可能自动创建了autoreleasepool(自动管理对象的释放)。所以子线程的autoreleasepool需要我们手动创建。

  • 结论:NSThread和NSOperationQueue开辟子线程需要手动创建autoreleasepool。GCD开辟子线程不需要手动创建autoreleasepool,因为GCD的每个队列都会自行创建autoreleasepool。
写法:
  1. 需要用while循环控制的RunLoop
@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]];

        }
        
    }
  1. 不需要用while循环控制的RunLoop
@autoreleasepool {
    
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
        //[runLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:10]];
        
    }

子线程 RunLoop 切换 mode 实现

#import "ViewController.h"
#import "MyThread.h"

@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];
    
    self.subThread = tmpThread;
    self.subThread.name = @"subThread";
    [self.subThread start];
    
}

- (void)subThreadTodo
{
    NSLog(@"%@----开始执行子线程任务",[NSThread currentThread]);
    
    @autoreleasepool{
        
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        
        //监听 runloop
        CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
            switch (activity) {
                case kCFRunLoopEntry:
                    NSLog(@"RunLoop进入");
                    break;
                case kCFRunLoopBeforeTimers:
                    NSLog(@"RunLoop要处理Timers了");
                    break;
                case kCFRunLoopBeforeSources:
                    NSLog(@"RunLoop要处理Sources了");
                    break;
                case kCFRunLoopBeforeWaiting:
                    NSLog(@"RunLoop要休息了");
                    break;
                case kCFRunLoopAfterWaiting:
                    NSLog(@"RunLoop醒来了");
                    break;
                case kCFRunLoopExit:
                    NSLog(@"RunLoop退出了");
                    break;
                    
                default:
                    break;
            }
        });

        CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
        CFRelease(observer);
        
        //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;
        
        //子线程 runloop 中默认只有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。
    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) {
        [self performSelector:@selector(changeSubThreadRunLoopMode:) onThread:self.subThread withObject:NSDefaultRunLoopMode waitUntilDone:NO];
    }
}

@end
启动
2018-10-15 18:43:52.564292+0800 RunLoop[12154:3478050] {number = 1, name = main}----开辟子线程
2018-10-15 18:43:52.565429+0800 RunLoop[12154:3478096] {number = 3, name = subThread}----开始执行子线程任务
2018-10-15 18:43:52.566281+0800 RunLoop[12154:3478096] RunLoop进入
2018-10-15 18:43:52.566408+0800 RunLoop[12154:3478096] RunLoop要处理Timers了
2018-10-15 18:43:52.566530+0800 RunLoop[12154:3478096] RunLoop要处理Sources了
2018-10-15 18:43:52.567887+0800 RunLoop[12154:3478096] RunLoop要休息了
拖拽
2018-10-15 18:45:02.697843+0800 RunLoop[12154:3478096] RunLoop醒来了
2018-10-15 18:45:02.698191+0800 RunLoop[12154:3478096] RunLoop要处理Timers了
2018-10-15 18:45:02.698670+0800 RunLoop[12154:3478096] RunLoop要处理Sources了
2018-10-15 18:45:02.699404+0800 RunLoop[12154:3478096] 当前线程:
{number = 3, name = subThread} RunLoop即将将Mode改变成:UITrackingRunLoopMode
2018-10-15 18:45:02.700091+0800 RunLoop[12154:3478096] RunLoop退出了
2018-10-15 18:45:02.700535+0800 RunLoop[12154:3478096] Timer启动啦,当前RunLoopMode:UITrackingRunLoopMode
2018-10-15 18:45:04.566206+0800 RunLoop[12154:3478096] Timer启动啦,当前RunLoopMode:UITrackingRunLoopMode
放手
2018-10-15 18:45:08.891239+0800 RunLoop[12154:3478096] 当前线程:
{number = 3, name = subThread} RunLoop即将将Mode改变成:kCFRunLoopDefaultMode
2018-10-15 18:45:08.893010+0800 RunLoop[12154:3478096] RunLoop进入
2018-10-15 18:45:08.893403+0800 RunLoop[12154:3478096] RunLoop要处理Timers了
2018-10-15 18:45:08.893711+0800 RunLoop[12154:3478096] RunLoop要处理Sources了
2018-10-15 18:45:08.894009+0800 RunLoop[12154:3478096] RunLoop要休息了

参考文档

  1. iOS RunLoop入门小结
  2. runLoop

你可能感兴趣的:(RunLoop 相关)