iOS 知识大全IT技术篇iOS

RunLoop的进阶之相关的类

2019-05-03  本文已影响49人  31313_iOS
本篇主要是介绍 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个类

二、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:

三、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 事件输入源

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

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

五、 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的应用

  //两秒钟以后给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即将进入到休眠的时候,会把之前的自动释放池释放,重新创建一个新的自动释放池

上一篇 下一篇

猜你喜欢

热点阅读