OC--RunLoop应用例子

作者: 啊哈呵 | 来源:发表于2017-09-05 23:22 被阅读38次

    知识点:
    1、RunLoop的基础知识
    2、RunLoop 与 NSTimer
    3、RunLoop 与 Perform Selector
    4、RunLoop、线程、AutoreleasePool三者联系
    5、RunLoop 与 线程通信
    6、RunLoop 的各种状态监听
    7、RunLoop 与 NSNotificationQueue

    什么是RunLoop?

    NSRunLoop苹果官方文档
    CoreFoundation源码

    RunLoop入门 看我就够了 - 简书
    RunLoop已入门?不来应用一下? - 简书

    深入理解RunLoop | Garan no dou

    基于runloop的线程保活、销毁与通信
    IOS---实例化讲解RunLoop

    RunLoop结构图 RunLoop跑圈图

    RunLoop 有五种运行模式,其中常见的有1.2两种

    1. kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个Mode下运行
    2. UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
    3. UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用
    4. GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到
    5. kCFRunLoopCommonModes: 这是一个占位用的Mode,作为标记kCFRunLoopDefaultMode和UITrackingRunLoopMode用,并不是一种真正的Mode
    

    RunLoop应用

    1、NSTimer

    主线程下:

    - (void)timer
    {
        NSTimer *aTimer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
    
        // 1、定时器只运行在NSDefaultRunLoopMode下,一旦RunLoop进入其他模式,这个定时器就不会工作
        [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
        
        // 2、定时器只运行在UITrackingRunLoopMode下(滑动UIScrollView),一旦RunLoop进入其他模式,这个定时器就不会工作
        [[NSRunLoop mainRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
        
        // 3、标记为NSRunLoopCommonModes的模式:UITrackingRunLoopMode和NSDefaultRunLoopMode兼容
        [[NSRunLoop mainRunLoop] addTimer:aTimer forMode:NSRunLoopCommonModes];
    }
    

    scheduledTimerWithTimeInterval

    - (void)viewDidLoad {
        [super viewDidLoad];
        
        //主线程
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
    
            // 这种方法是直接加入RunLoop:[[NSRunLoop currentRunLoop] addTimer: t forMode: NSDefaultRunLoopMode];
            [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(timerFired:)userInfo:nil repeats:YES];
            // 需要确保当前currentRunLoop run起来
            [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
        });
        
    }
    
    
    RunLoop的run与stop

    1、CFRunLoopStop能直接停止掉所有用CFRunloop运行起runloop

    - (void)testTimer
    {
        NSTimer *aTimer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(runrun) userInfo:nil repeats:YES];
        // 手动获取
        NSRunLoop *currentRunLoop = [NSRunLoop currentRunLoop];
        // 由于UI都是在主线程,子线程不会有UITrackingRunLoopMode干扰,所以使用NSDefaultRunLoopMode就可以了
        [currentRunLoop addTimer:aTimer forMode:NSDefaultRunLoopMode];
        // run起来的方式,有
        // 方式1
        // 方式2
        // 方式3
    }
    
    

    方式1:run

        /*方式1:
         永久性的运行在NSDefaultRunLoopMode模式
         停止runloop方式:
         1、使用CFRunLoopStop无效
         2、(1)停止移除timer或者移除port;(2)暴力强制退出线程(不是解决办法)
         */
        [currentRunLoop run];
    

    方式2:runUntilDate

        /*方式2:
         规定时间运行在NSDefaultRunLoopMode模式
         停止runloop方式:
         1、使用CFRunLoopStop无效
         2、(1)停止移除timer或者移除port;(2)到达指定时间
         */
        [currentRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:5]];//
    

    方式3:runMode: beforeDate: (其实调用CFRunLoopRunInMode)

        // returnAfterSourceHandled参数为YES,当触发一个非timer事件后,runloop就终止了
        CFRunLoopRunInMode (mode, seconds, returnAfterSourceHandled);returnAfterSourceHandled = YES的封装;
    
        /*方式3:
         规定runMode、规定时间,运行完返回运行状态
         停止runloop方式:
            (1)使用CFRunLoopStop;
            (2)停止移除timer或者移除port;
            (3)到达指定时间
         */
    
        BOOL result =  [currentRunLoop runMode:UITrackingRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:20]];
        if (result) {
            // 如果是PerfromSelector*事件或者其他Input Source事件触发处理后,Run Loop结束时候返回YES,其他返回NO。
        }else {
            // NO
        }
    
    }
    

    一定时间内监听某种事件,或执行某种任务的线程,如在30分钟内,每隔30s执行onTimerFired:

    @autoreleasepool {
        
        NSTimer * udpateTimer = [NSTimer timerWithTimeInterval:30
                                                        target:self
                                                      selector:@selector(onTimerFired:)
                                                      userInfo:nil
                                                       repeats:YES];
    
        NSRunLoop * runLoop = [NSRunLoop currentRunLoop];
        [runLoop addTimer:udpateTimer forMode:NSRunLoopCommonModes];
        [runLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:60*30]];
    }
    
    2、performSelector: withObject: afterDelay: inModes

    主线程

        // 延时performSelector在主线程会被RunLoopMode干扰
        // 延时performSelector其实里面就是一个NSTimer要实现afterDelay
        [self performSelector:@selector(performSelector) withObject:nil afterDelay:2   inModes:@[NSDefaultRunLoopMode]];
    

    子线程

        BBThread *thread = [[BBThread alloc] initWithBlock:^{
            
            [self performSelector:@selector(performSelector) withObject:nil afterDelay:2 inModes:@[NSDefaultRunLoopMode]];
            // 需要手动currentRunLoop跑起来
            NSRunLoop *currentRunLoop = [NSRunLoop currentRunLoop];
            [currentRunLoop run];
             
        }];
        [thread start];
    
    3、常驻线程

    AFNetworking 2.0中 创建了一条常驻线程专门处理所有请求的回调事件

    + (void)networkRequestThreadEntryPoint:(id)__unused object {
        @autoreleasepool {
            [[NSThread currentThread] setName:@"AFNetworking"];
    
            NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
             // 这里主要是监听某个 port,目的是让这个 Thread 不会回收
            [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode]; 
            [runLoop run];
        }
    }
    
    + (NSThread *)networkRequestThread {
        static NSThread *_networkRequestThread = nil;
        static dispatch_once_t oncePredicate;
        dispatch_once(&oncePredicate, ^{
            _networkRequestThread =
            [[NSThread alloc] initWithTarget:self
                                    selector:@selector(networkRequestThreadEntryPoint:)
                                      object:nil];
            [_networkRequestThread start];
        });
        return _networkRequestThread;
    }
    
    4、线程、RunLoop、AutoreleasePool三者关系
    线程与RunLoop

    CFRunLoopGetMain() 和 CFRunLoopGetCurrent()的源码

    /// 全局的Dictionary,key 是 pthread_t, 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(主线程除外)。

    AutoreleasePool与RunLoop

    1、一个线程对应一个RunLoop (主线程系统启动,其他线程需要使用来加载获取并启动)
    2、线程如果没有启动RunLoop,遇到autorelease对象会用下面情况:

    1)、线程里面如果有对象调用autorelease方法,系统就去找这个线程key对应的最栈顶poolpage;
    2)、如果找到page.add(obj)OK;
    3)、如果找不到就调用autoreleaseNoPage,新建这个线程的一个page;
    4)、线程释放,这个线程对应的page也pop。
    

    3、线程启动了RunLoop,RunLoop内部会管理AutoreleasePool

    1)、RunLoop开启时会objc_autoreleasePoolPush;
    2)、RunLoop休眠时会先objc_autoreleasePoolPop再objc_autoreleasePoolPush;
    3)、RunLoop退出时会objc_autoreleasePoolPop;
    

    4、另外
    在启动RunLoop之前建议用 @autoreleasepool {...}包裹
    意义:创建一个大释放池,释放{}期间创建的临时对象,一般好的框架的作者都会这么做 (上面的AFNetworking就是这么做)

    + (void)networkRequestThreadEntryPoint:(id)__unused object {
        @autoreleasepool {
            [[NSThread currentThread] setName:@"AFNetworking"];
    
            NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
             // 这里主要是监听某个 port,目的是让这个 Thread 不会回收
            [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode]; 
            [runLoop run];
        }
    }
    
    NSRunLoop与NSMachPort来线程通信的实例:
    - (void)testDemo3
    {
        //声明两个端口   随便怎么写创建方法,返回的总是一个NSMachPort实例
        NSMachPort *mainPort = [[NSMachPort alloc]init];
        NSPort *threadPort = [NSMachPort port];
        //设置线程的端口的代理回调为自己
        threadPort.delegate = self;
    
        //给主线程runloop加一个端口
        [[NSRunLoop currentRunLoop]addPort:mainPort forMode:NSDefaultRunLoopMode];
    
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
    
            //添加一个Port
            [[NSRunLoop currentRunLoop]addPort:threadPort forMode:NSDefaultRunLoopMode];
            [[NSRunLoop currentRunLoop]runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
    
        });
    
        NSString *s1 = @"hello";
    
        NSData *data = [s1 dataUsingEncoding:NSUTF8StringEncoding];
    
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSMutableArray *array = [NSMutableArray arrayWithArray:@[mainPort,data]];
            //过2秒向threadPort发送一条消息,第一个参数:发送时间。msgid 消息标识。
            //components,发送消息附带参数。reserved:为头部预留的字节数(从官方文档上看到的,猜测可能是类似请求头的东西...)
            [threadPort sendBeforeDate:[NSDate date] msgid:1000 components:array from:mainPort reserved:0];
    
        });
    
    }
    
    //这个NSMachPort收到消息的回调,注意这个参数,可以先给一个id。如果用文档里的NSPortMessage会发现无法取值
    - (void)handlePortMessage:(id)message
    {
    
        NSLog(@"收到消息了,线程为:%@",[NSThread currentThread]);
    
        //只能用KVC的方式取值
        NSArray *array = [message valueForKeyPath:@"components"];
    
        NSData *data =  array[1];
        NSString *s1 = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
        NSLog(@"%@",s1);
    
    //    NSMachPort *localPort = [message valueForKeyPath:@"localPort"];
    //    NSMachPort *remotePort = [message valueForKeyPath:@"remotePort"];
    
    }
    

    打印如下:

    2016-11-23 16:50:20.604 TestRunloop3[1322:120162] 收到消息了,线程为:<NSThread: 0x60800026d700>{number = 3, name = (null)}
    2016-11-23 16:50:26.551 TestRunloop3[1322:120162] hello
    
    
    自定义的输入源来实现线程通信
    - (void)testDemo4
    {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
    
            NSLog(@"starting thread.......");
    
            _runLoopRef = CFRunLoopGetCurrent();
            //初始化_source_context。
            bzero(&_source_context, sizeof(_source_context));
            //这里创建了一个基于事件的源,绑定了一个函数
            _source_context.perform = fire;
            //参数
            _source_context.info = "hello";
            //创建一个source
            _source = CFRunLoopSourceCreate(NULL, 0, &_source_context);
            //将source添加到当前RunLoop中去
            CFRunLoopAddSource(_runLoopRef, _source, kCFRunLoopDefaultMode);
    
            //开启runloop 第三个参数设置为YES,执行完一次事件后返回
            CFRunLoopRunInMode(kCFRunLoopDefaultMode, 9999999, YES);
    
            NSLog(@"end thread.......");
        });
    
    
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    
            if (CFRunLoopIsWaiting(_runLoopRef)) {
                NSLog(@"RunLoop 正在等待事件输入");
                //添加输入事件
                CFRunLoopSourceSignal(_source);
                //唤醒线程,线程唤醒后发现由事件需要处理,于是立即处理事件
                CFRunLoopWakeUp(_runLoopRef);
            }else {
                NSLog(@"RunLoop 正在处理事件");
                //添加输入事件,当前正在处理一个事件,当前事件处理完成后,立即处理当前新输入的事件
                CFRunLoopSourceSignal(_source);
            }
        });
    
    }
    
    //此输入源需要处理的后台事件
    static void fire(void* info){
    
        NSLog(@"我现在正在处理后台任务");
    
        printf("%s",info);
    }
    
    

    输出结果如下:

    2016-11-24 10:42:24.045 TestRunloop3[4683:238183] starting thread.......
    2016-11-24 10:42:26.045 TestRunloop3[4683:238082] RunLoop 正在等待事件输入 
    2016-11-24 10:42:31.663 TestRunloop3[4683:238183] 我现在正在处理后台任务
    hello
    2016-11-24 10:42:31.663 TestRunloop3[4683:238183] end thread.......
    
    
    CFRunLoopObserverRef是观察者,能够监听RunLoop的状态改变
    -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
    {
        //创建监听者
        /*
         第一个参数 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);
    }
    
    
    

    CFRunLoopObserverRef使用demo:
    利用主线程RunLoop空闲时候在处理,一些UI事情,减少卡顿(这就不需要多线程了)

     CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
            
            if (activity == kCFRunLoopBeforeWaiting) {
                // RunLoop要休息了
            }
        });
        CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopDefaultMode);
        CFRelease(observer);
    

    NSNotificationQueue也与runloop有关系OC--NSNotificationCenter重新认知

    相关文章

      网友评论

        本文标题:OC--RunLoop应用例子

        本文链接:https://www.haomeiwen.com/subject/vfipjxtx.html