RunLoop的进阶之相关的类

本篇主要是介绍 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即将进入到休眠的时候,会把之前的自动释放池释放,重新创建一个新的自动释放池

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容

  • 如果没有RunLoop main函数中的RunLoop 第14行代码的UIApplicationMain函数内部就...
    JonesCxy阅读 529评论 0 4
  • ======================= 前言 RunLoop 是 iOS 和 OSX 开发中非常基础的一个...
    i憬铭阅读 873评论 0 4
  • RunLoop的基本了解 **1 . RunLoop字面的意思 : **运行循环 / 跑圈 **2 . 基本作用 ...
    Mario_ZJ阅读 509评论 1 3
  • 什么情况下使用runloop? runloop好比就是跑圈,就是一个线程一直在做某一件事情。 一般主线程会自动运行...
    进击的小杰阅读 4,409评论 4 7
  • Runloop 多线程编程指南 资料:1.开源网址中下载CF开头的包,CF是CoreFoundation的缩写,C...
    彼岸的黑色曼陀罗阅读 398评论 0 2