插播另外一条知识点, 我们在使用timer的时候, 除了NSTimer, 还有其他的选择吗? 答案是有, GCD定时器就是一个很好的选择, xcode甚至为我们提供了默认的定时器代码块, 方便你的使用:

- (void)gcdTimer {    /* 
     参数1 : 需要创建的源的种类, timer 也是一种数据源
     参数2,参数3: 在你创建timer的时候直接传0就可以了
     参数4: timer触发的代码运行的队列
     */
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0));
    /* 
     参数1 : timer定时器
     参数2 : 从什么时间开始触发定时器, DISPATCH_TIME_NOW代表现在
     参数3 : 时间间隔
     参数4 : 表示精度, 传0表示绝对精准
     */
    dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 2.0 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
    /* 
     封装timer需要触发的操作
     */
    dispatch_source_set_event_handler(timer, ^{
        NSLog(@"GCDTimer-----%@",[NSThread currentThread]);
        NSLog(@"%@",[NSRunLoop currentRunLoop].currentMode);
    });
    dispatch_resume(timer);
    /* 
     用强指针引用, 防止timer释放
     */
    self.timer = timer;}

在这里我还是要推荐下我自己建的iOS开发学习群:680565220,群里都是学ios开发的,如果你正在学习ios ,小编欢迎你加入,今天分享的这个案例已经上传到群文件,大家都是软件开发党,不定期分享干货(只有iOS软件开发相关的),包括我自己整理的一份2018最新的iOS进阶资料和高级开发教程

RunLoop输入源

CFRunLoopSourceRef是输入源, 输入源的种类有两种分类方法:

  • 以前的分法:
    1.Port-Based Sources
    2.Custom Input Sources
    3.Cocoa Perform Selector Sources

  • 现在的分法:
    1.Source0:非基于端口的
    2.Source1:基于端口的

那么根据现在的分法, 什么是基于端口的呢? 就是系统默认的, 非基于端口的就是用户主动触发的事件, 比如用户点击了一个按钮等. 假设现在用户点击了一个按钮, 我们来观察函数调用栈的情况:

iOS--RunLoop知识点总结(2)

点击按钮时的函数调用栈

我们可以看出, 点击按钮时, 运行循环RunLoop处理的输入源是source0

RunLoop Observer

CFRunLoopObserverRef是观察者, 是用来监听RunLoop状态改变的

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), 即将进入运行循环
kCFRunLoopBeforeTimers = (1UL << 1), 即将处理定时器事件
kCFRunLoopBeforeSources = (1UL << 2), 即将处理输入源事件
kCFRunLoopBeforeWaiting = (1UL << 5), 即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), 刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7), 退出运行循环
kCFRunLoopAllActivities = 0x0FFFFFFFU 运行循环所有活动
};

添加观察者到运行循环的代码:

- (void)observer {    /* 
     参数1 :分配内存空间的方式, 传默认
     参数2 :RunLoop的运行状态
     参数3 :是否持续观察
     参数4 :优先级, 传0
     参数5 :观察者观测到状态改变时触发的方法
     */
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {        /* 
         typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
         kCFRunLoopEntry = (1UL << 0),
         kCFRunLoopBeforeTimers = (1UL << 1),
         kCFRunLoopBeforeSources = (1UL << 2),
         kCFRunLoopBeforeWaiting = (1UL << 5),
         kCFRunLoopAfterWaiting = (1UL << 6),
         kCFRunLoopExit = (1UL << 7),
         kCFRunLoopAllActivities = 0x0FFFFFFFU
         };
         */
        switch (activity) {            case kCFRunLoopEntry:                NSLog(@"即将被唤醒");                break;            case kCFRunLoopBeforeTimers:                NSLog(@"即将处理定时器事件");                break;            case kCFRunLoopBeforeSources:                NSLog(@"即将处理输入源事件");                break;            case kCFRunLoopBeforeWaiting:                NSLog(@"即将进入休眠");                break;            case kCFRunLoopAfterWaiting:                NSLog(@"休眠结束");                break;            case kCFRunLoopExit:                NSLog(@"运行循环退出");                break;            default:                break;
        }
    });    /* 
     参数1 :运行循环, 传入当前的运行循环
     参数2 :观察者, 观察运行循环的各种状态
     参数3 :运行循环的模式
     */
    CFRunLoopAddObserver(CFRunLoopGetCurrent(),observer, kCFRunLoopDefaultMode);
}

重要的函数就两个

  • CFRunLoopObserverCreateWithHandler(_,_,_,_,_)创建观察者(包括block回调)

  • CFRunLoopAddObserver(_,_,_)添加观察者到运行循环

RunLoop运行流程

我们先看一下苹果官方文档的示意图:

iOS--RunLoop知识点总结(2)

RunLoop运行流程

从图中可以看出的是: 当线程中的运行循环开启之后, 将循环处理两大source事件:

  • Input Sources

  1. 端口事件
    2.用户输入源事件
    3.performSelector事件

Timer Sources
定时器事件

然后运行循环就循环处理这些事件, 如果没有, 就休眠并等待被唤醒

详细的官方文档步骤见下图(感谢网友提供):

iOS--RunLoop知识点总结(2)

RunLoop运行流程

RunLoop的应用:常驻线程

开启常驻线程的作用: 让一个子线程不被销毁, 等待其他线程发来消息, 处理事件

#import "ViewController.h"@interface ViewController ()/**
 thread
 */@property (nonatomic,strong)NSThread *thread;@[email protected] ViewController- (IBAction)creatNewThread:(UIButton *)sender {    self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(task1) object:nil];
    [self.thread start];
}
- (IBAction)stableThread:(UIButton *)sender {     /* 在子线程中实现方法 */
    [self performSelector:@selector(task2) onThread:self.thread withObject:nil waitUntilDone:YES];
}
- (void)task1 {    NSLog(@"创建了一个新的线程----%@",[NSThread currentThread]);    /* 获取当前线程 */
    NSRunLoop *runLoop = [NSRunLoop currentRunLoop];     /* 添加端口, 保证运行循环不退出 */
    [runLoop addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
    [runLoop run];
}
- (void)task2 {    NSLog(@"%s",__func__);
}@end

最重要的就是添加端口到当前runLoop中,因为当前的runLoop就是子线程的runLoop, 添加端口的目的就是保证运行循环不退出, 当然你也可以添加timer到运行循环中, 但timer本身起不到任何用, 反而浪费资源

如上图所示, 我先创建了一个子线程, 在子线程里我创建了子线程的runLoop,然后在子线程中添加端口保证其不退出, 然后调用run方法让runLoop跑起来, 这样, 我不管在程序运行的任何阶段在这个子线程中调用方法, 该子线程都可以响应, 这就是常驻线程.

以上就是RunLoop的相关知识点, 再补充一个小的知识点, 即自动释放池的创建和销毁

  • 在启动RunLoop的时候, 会第一次创建自动释放池

  • RunLoop退出的时候, 会销毁自动释放池

  • 其他创建和销毁自动释放池的时机:

    • 当RunLoop即将进入睡眠的时候, 说明之前的事件都已经全部处理完毕, 那么之前的变量就没有必要再留着, 这时候会销毁自动释放池

    • 同时, 再创建一个新的自动释放池

NSURLConnection与RunLoop

  • 当我们创建一个网络连接, 并且设置代理的时候, 我们通常是这样做的:

[NSURLConnection connectionWithRequest:request delegate:self];

此时,代理回调会在子线程执行
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response

比如上面这个代理方法, 是会在主线程执行的, 如果我们需要设置在子线程执行, 我们还需要写这么一句代码:

[connection setDelegateQueue:[[NSOperationQueue alloc] init]];
需要注意的是设置代理回调线程, 必须是子线程,不能设置[NSOperationQueue mainQueue], 因为默认就是在主线程调用的, 如此设置了之后, 代理方法反而不走

  • 但如果我们用另外一种方式创建网络连接, 情况又会如何呢?

 NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO];

最后startImmediately里面的这个参数如果填YES的话, 跟前面那种[NSURLConnection connectionWithRequest:request delegate:self];创建没有任何区别

如果填写NO的话, 则网络连接不会被加入到运行循环RunLoop中,不被加到RunLoop中, connection立马就被释放掉了, 代理当然不会调用, 此时, 我们使用

[connection start];
这个方法, start内部会帮我们开启当前线程的runLoop, 并且帮我们把网络连接加入到这个runLoop中去.

  • 如果我们在子线程中用第一种方式创建网络连接又如何呢?

- (void)newThreadDelegate1 {    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{        NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"网页链接]];        NSURLConnection *connection = [NSURLConnection connectionWithRequest:request delegate:self];
        [[NSRunLoop currentRunLoop] run];        NSLog(@"这里的线程是:%@",[NSThread currentThread]);
    });
}

我们知道, 主线程的runLoop默认是开启的, 子线程的runLoop需要手动创建, 创建了之后还要runLoop跑起来. 这样网络连接才能被加入到运行循环中, 代理回调才可以执行, 此时代理回调的执行线程默认和网络连接的线程是同一个线程, 如果, 网络请求重新重新分配代理回调执行的线程,[connection setDelegateQueue:[[NSOperationQueue alloc] init]]; 那么执行线程就不同了.

  • 如果我们在子线程中用第二种方式去创建网络连接的话

    • 也分两种情况, 当startImmediately的参数是YES, 那么跟上面那种情况没什么区别

    • 如果参数为NO, 则connection需要调用start方法, 这个方法会自动创建当前线程的运行循环runLoop, 并将网络连接添加到该运行循环runLoop中去.

相关文章: