美文网首页iOS 知识大全IT技术篇iOS
RunLoop的进阶之相关的类

RunLoop的进阶之相关的类

作者: 31313_iOS | 来源:发表于2019-05-03 15:32 被阅读49次
    本篇主要是介绍 RunLoop在Core Foundation中相关的5个类。

    在此之前,先来回顾一下上一篇中介绍到的一些重要的内容:

    1. 线程与RunLoop之间的关系
    • 每条线程都有唯一的一个与之对应的RunLoop对象
    • 主线程的RunLoop已经自动创建好了,子线程的RunLoop需要手动创建>>
    • RunLoop在第一次获取是创建,在线程结束是销毁,该做事的时候做事,该休息就休息
    1. RunLoop底层的创建和保存方式

    RunLoop在C层面上其实是以一个线程为参数来的创建,pthread_t

    • 使用 CFMutableDictionaryRef来创建一个可变字典
    • 创建 CFRunLoopCreate(pthread_t)来创建一个RunLoop
    • 使用字典以线程为key,runloop为value进行保存
    1. 获取RunLoop对象和转换
    //获取主线程对应的RunLoop
      NSRunLoop * mainRunLoop = [NSRunLoop mainRunLoop];
     //获取当前对应的RunLoop
       NSRunLoop * currentRunLoop = [NSRunLoop currentRunLoop];
     
      NSLog(@"%p --- %p", mainRunLoop, currentRunLoop);
    
     //获取主线程对应的RunLoop
      CFRunLoopRef mainRunLoopf =  CFRunLoopGetMain();
     //获取当前对应的RunLoop
     CFRunLoopRef currentRunLoopf = CFRunLoopGetCurrent();
      NSLog(@"%p --- %p", mainRunLoopf, currentRunLoopf);
    
     //转C的RunLoop对象  使用  mainRunLoop.getCFRunLoop
     NSLog(@"%p --- %p",  mainRunLoop.getCFRunLoop, mainRunLoopf);
    
    • 注意:
      1. currentRunLoop在没有创建子线程的时候他获取到的就是和>>mainRunLoop是一致的。
      2. 获得子线程的RunLoop,currentRunLoop 该方法本身是一个懒加载,>>如果是第一次调用,则会创建当前线程对应的RunLoop并保存,以后调用>>则直接获取

    进入本文的主要内容,先把之前关于的RunLoop的官方图拿进来

    979F7961-3C84-498B-A119-7146D0440B6B.png

    一、 Core Foundation中关于RunLoop的5个类

    • CFRunLoopRef --- RunLoop本身类
    • CFRunLoopModeRef ------- RunLoop的运行模式类
    • CFRunLoopSourceRef ------- RunLoop的事件的类,对应的是事件源中的InPut Sources
    • CFRunLoopTimerRef ------- RunLoop的定时器事件的类,对应的是事件源中的Timer Sources
    • CFRunLoopObserverRef ------- RunLoop的监听状态的类

    二、CFRunLoopModeRef (RunLoop的运行模式)

    C37C90EF-0EC1-4F10-907D-7427347888DE.png

    如上图所示,RunLoop有三个甚至更多的运行模式,那么塔启动的时候需要选择一种运行模式,然后判断这种运行模式是否为空。
    判断的依据是
    1.source中是否有事件、Timer中是否有事件
    2. 如果source和Timer中一个事件都没有则为空,那么RunLoop将退出
    3. 如果source和Timer中有一个或者多个事件,则启动运行循环
    值得注意的是:Observer观察者并不会参与 判断运行模式是否为空,也就是说运行模式判读是否为空跟Observer没什么关系

    CFRunLoopModeRef的说明:

    一个RunLoop包含若干个Model,每个Mode有包含若干个Source/Timer/Observer
    每次RunLoop启动的时候,只能选定其中的一个Mode,这个Mode称之为CurrentMode
    如果需要切换Mode,只能退出Loop,再重新指定一个Mode进入,这样的目的是为了隔开不同组的Source/Timer/Observer ,让他们互不影响

    系统默认注册了5个Mode:

    • kCFRunLoopDefaultMode (App的默认Mode,通常主线程是在这个Mode下运行的)
    • UITrackingRunLoopMode(界面跟踪Mode,用于ScrollView追踪触摸滑动,保证界面滑动的时候不受其他Mode的影响)
    • UIInitalizationRunLoopMode (在刚启动App时进入的第一个Mode,启动完成以后就不再使用)
    • GSEventReceiveRunLoopMode (接受系统事件的内部Mode,通常用不到)
    • kCFRunLoopCommonModes (这是一个占位用的Mode,不是一种真正的Mode)

    三、CFRunLoopTimerRef 、CFRunLoopTimerRef和 CFRunLoopModeRef的混合使用

    1. timerWithTimeInterval方式创建的定时器
    - (void)timer1 {
        //1. 创建定时器 timerWithTimeInterval这种方式创建的定时器需要手动添加到RunLoop中
        NSTimer * timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
        //2.添加到RunLoop中
        //Mode: RunLoop的5种运行模式(默认| 界面跟踪、 占位)
        //把定时器添加到runloop中,并指定为默认模式,并且当运行模式为NSDefaultRunLoopMode的时候,定时器才能工作
        [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    }
    
    - (void)run {
        NSLog(@"run --- %@", [NSRunLoop currentRunLoop].currentMode);
    }
    

    注意点: 如果在此种的定时器下,页面添加一个滚动视图并且在滚动的时候,定时器将停止工作。
    原因是:主运行模式会(kCFRunLoopDefaultMode) 切换成 界面追踪运行模式(UITrackingRunLoopMode)

    如果想要实现在拖动情况下也能正常工作,则可以这样写

    1.比较2B的写法

        [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
        [[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
    
    1. 推荐以下的方式
    //CommonMode这中模式
        [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
    

    在主运行模式下,被指定的CommonModes 包含了 kCFRunLoopDefaultMode和 UITrackingRunLoopMode

     NSLog(@"%@", [NSRunLoop currentRunLoop]);
    //打印结果
      RunLoopMode[17128:1191536] <CFRunLoop 0x600001dcc400 [0x10495fae8]>{wakeup port = 0x1e07, stopped = false, ignoreWakeUps = false,
            current mode = kCFRunLoopDefaultMode,
            common modes = <CFBasicHash 0x600002fe9c50 [0x10495fae8]>{type = mutable set, count = 2,
                entries =>
                0 : <CFString 0x107d2f070 [0x10495fae8]>{contents = "UITrackingRunLoopMode"}
                2 : <CFString 0x104971ed8 [0x10495fae8]>{contents = "kCFRunLoopDefaultMode"}
            }
    
    1. scheduledTimerWithTimeInterval方式创建的定时器
    - (void)timer2 {
        //创建定时器 这种方式创建默认添加到了CurrenRunLoop中,并指定运行模式为NSDefaultRunLoopMode
        //这种模式下 页面滚动的时候也会影响定时器的工作
        NSTimer * timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil  repeats:YES];
        
        ///想要实现页面滚动的时候定时器也能正常工作,需要加入
        [[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
    
    }
    

    3.在子线程里面创建Timer

    创建一个子线程

     //开启一个子线程
        [self performSelectorInBackground:@selector(timer2) withObject:nil];
    

    先 子线程runloop再添加定时器是无法正常工作的,因为RunLoop的mode在检查的时候,是为空的所以他会退出.
    所以 先添加定时器,再手动创建RunLoop

    - (void)timer2 {
        NSLog(@"timer ----- %@", [NSThread currentThread]);
        
        //需要手动创建子线程runloop
      //  [[NSRunLoop currentRunLoop] run]; //放在这里的时候,runloop会立即退出,即定时器无法正常使用,因为RunLoop的mode在检查的时候,是为空的所以他会退出
        
        //创建定时器 这种方式创建默认添加到了CurrenRunLoop中,并指定运行模式为NSDefaultRunLoopMode
        //这种模式下 页面滚动的时候也会影响定时器的工作
        NSTimer * timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil  repeats:YES];
        
            //需要手动创建子线程runloop
        [[NSRunLoop currentRunLoop] run];
    
    }
    

    #######补充一个精准的定时器
    GCD-定时器,不受runloop的影响

    @interface ViewController ()
    {
        dispatch_source_t _timer;
    }
    @end
    
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        
        ///dispatchQueue 队列(GCD -4),dispatch_get_main_queue() 主队列就在主线程,非主队列在子线程
        _timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
        dispatch_source_set_timer(_timer, DISPATCH_TIME_NOW, 2.0 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
        dispatch_source_set_event_handler(_timer, ^{
            //执行发的任务
            NSLog(@"555");
        });
        //开启定时器
        dispatch_resume(_timer);
    }
    

    四、 CFRunLoopSourceRef 事件输入源

    以前对于事件输入源的分法,即按照官方文档

    • Port-Based Sources
    • Custom Input Sources
    • Cocoa Perform Selector Sources

    现在对于事件输入源的分法,即按照函数调用栈

    • Source0: 非基于Port的, 用于用户主动触发事件
    • Source1:基于Port的 , 通过内核和其他线程相互发送消息

    五、 CFRunLoopObserverRef

    CFRunLoopObserverRef 是观察者,能够监听RunLoop的状态改变,可以监听的时间点有以下几个

    /* Run Loop Observer Activities */
    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  全部状态
    };
    

    实现RunLoop的监听只能使用C语言的实现,只需要实现以下的代码:

     /**
         创建观察者
         allocator 分配存储空间 默认   activities 要监听的状态 repeats  是否持续监听  order 优先级相关,暂不用考虑
         runloop状态改变回调 observer  监听对象  activity 回调时候的状态
         */
        
        CFRunLoopObserverRef observerRef = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), 0, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
            switch (activity) {
                case kCFRunLoopEntry:
                    NSLog(@"runloop启动");
                    break;
                case kCFRunLoopBeforeTimers:
                    NSLog(@"runloop即将处理 Timer事件");
                    break;
                case kCFRunLoopBeforeSources:
                    NSLog(@"runloop即将处理 Source事件");
                    break;
                case kCFRunLoopBeforeWaiting:
                    NSLog(@"runloop即将进入休眠");
                    break;
                case kCFRunLoopAfterWaiting:
                    NSLog(@"runloop即将被唤醒");
                    break;
                case kCFRunLoopExit:
                    NSLog(@"runloop退出");
                    break;
                default:
                    break;
            }
        });
        
        /**
         2.监听runloop的状态
         CFRunLoopRef rl runloop对象   CFRunLoopObserverRef observer监听者  CFRunLoopMode mode runloop模式
         NSDefaultRunLoopMode == kCFRunLoopDefaultMode
         NSRunLoopCommonModes == kCFRunLoopCommonModes
         **/
        CFRunLoopAddObserver(CFRunLoopGetCurrent(), observerRef, kCFRunLoopDefaultMode);
    

    RunLoop处理逻辑具体请查看下图(改图来源于网络):

    90a43b793bbcc763.png

    理解上图的逻辑图,基本就了解RunLoop的具体工作流程。

    六、 RunLoop的应用

    • NSTimer 上面已经有例子提到
    • ImageView显示
    • PerformSelector
      //两秒钟以后给uiimageView添加图片
        [self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@""] afterDelay:2.0];
    

    注: 如果此时,页面有个滚动视图,你不断滑动,那么这个imageView一直都不会展示图片
    原因是: performSelector该方法会自动把事件添加到runloop中,指定运行模式为默认

    此时就需要我们设置该runloop的运行模式为

    1. NSDefaultRunLoopMode和 UITrackingRunLoopMode
      2.或者设置 NSRunLoopCommonModes
    [self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@""] afterDelay:2.0 inModes:@[NSDefaultRunLoopMode, UITrackingRunLoopMode]];
    
    • 常驻线程

    下面是一个小小的案例,我们创建两个按钮,一个按钮创建一条线程,另一个按钮点击让创建的哪条线程继续执行任务。

    //点击按钮创建一个线程
    - (IBAction)createThread:(id)sender {
            //1.创建一个线程
        NSThread * thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadRun) object:nil];
            //2.启动线程
        [thread start];
        self.thread = thread;
    }
    
    - (void)threadRun {
        NSLog(@"threadRun --- %@", [NSThread currentThread]);
    }
    
    ///点击按钮让上个线程继续执行任务
    - (IBAction)goOnThread:(id)sender {
        //这个是线程之间的通信 让之前创建的线程继续干活 这是从 主线程 -> 子线程
        [self performSelector:@selector(goOnTask) onThread:self.thread withObject:nil waitUntilDone:YES];
        
    }
    - (void)goOnTask {
        NSLog(@"threadRun ==== %@", [NSThread currentThread]);
    }
    
    

    这个时候,点击第二个按钮让刚刚创建的线程继续执行任务,那么程序就会崩溃。原因是threadRun 执行完毕,线程对象会进入死亡状态。想要这个线程执行goOnTask 里面的任务,那么就要保证线程不死,也就是要让threadRun 方法里面的任务执行不完,即进入运行循环。那么这个时候RunLoop就可以发挥作用啦!

    那么修改一下 threadRun方法

    - (void)threadRun {
        NSLog(@"threadRun --- %@", [NSThread currentThread]);
        
        //1.创建一个runLoop, 子线程需要手动创建 + 启动
        //运行模式(默认模式), 判断运行模式是否为空
        NSRunLoop * runloop = [NSRunLoop currentRunLoop];
        
        //2.运行模式添加一个事件source事件或者Timer事件
        [NSTimer scheduledTimerWithTimeInterval:4.0 target:self selector:@selector(threadTimerRun) userInfo:nil repeats:YES];
        
        //3.启动runloop
        [runloop run];
    }
    
    - (void)threadTimerRun {
        NSLog(@"threadTimerRun ====");
    
    }
    
    

    这时候是可以达到了效果了,但是一直会有一个定时器在跑,我们这里是不要的,所以这种Timer事件在这里不合适,所以我们在这里应该使用Source事件。
    如下所示,完美处理:

    - (void)threadRun {
        NSLog(@"threadRun --- %@", [NSThread currentThread]);
        
        //1.创建一个runLoop, 子线程需要手动创建 + 启动
        //运行模式(默认模式), 判断运行模式是否为空
        NSRunLoop * runloop = [NSRunLoop currentRunLoop];    
        //2.运行模式添加一个source事件 port、custom、 selector
        [runloop addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
        
        //3.启动runloop
        [runloop run];
    }
    
    • 自动释放池

    自动释放池第一次创建: 当RunLoop启动的时候
    自动释放池最后一次销毁: 当RunLoop退出的时候
    自动释放池其他时间的创建和销毁: 当RunLoop即将进入到休眠的时候,会把之前的自动释放池释放,重新创建一个新的自动释放池

    相关文章

      网友评论

        本文标题:RunLoop的进阶之相关的类

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