iOS之runloop篇(面试可用)

最近听说好多面试的小伙伴被问到runloo,runtime底层的问题,哎。。。看来现在就业形势很严峻啊,所以转载了一个很不错runloop文章,给正在找工作小伙伴一个参考,也给我自己留个参考,毕竟公司项目基本没用到过,万一以后能用到呢,笑Cry。

本文章转载自http://www.jianshu.com/p/3676065e5d5d 东方_未明(简书作者)

0. RunLoop资料
1. RunLoop对象

iOS中有两套API来访问和使用RunLoop

  • Foundation框架(OC) --> NSRunLoop
  • Core Foundation框架(C) -->CFRunLoopRef

NSRunLoop和CFRunLoopRef都代表着RunLoop对象
NSRunLoop是基于CFRunLoopRef的一层OC包装, 所以要了解RunLoop内部结构, 需要多研究CFRunLoopRef层面的API(Core Foundation层面)

1. main函数中的RunLoop

main函数中的RunLoop.png
  • main函数中的RunLoop: UIApplicationMain函数内部就启动了一个RunLoop, 所以UIApplicationMain函数一直没有返回,保持了程序的持续运行, 这个默认启动的RunLoop是跟主线程相关联的
  • 由于main函数里面启动了个RunLoop,所以程序并不会马上退出,保持持续运行状态
2. RunLoop与线程
  • 每条线程都有唯一的一个与之对应的RunLoop对象(如果我也想开一个子线成,并且让线程不死,则子线程开一个RunLoop)
  • 主线程的RunLoop已经自动创建好了,子线程的RunLoop需要主动创建
  • RunLoop在第一次获取时创建,在线程结束时销毁
3. 获得RunLoop对象
  • Foundation
    [NSRunLoop currentRunLoop]; // 获得当前线程的RunLoop对象
    [NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象
  • Core Foundation
    CFRunLoopGetCurrent(); // 获得当前线程的RunLoop对象
    CFRunLoopGetMain(); // 获得主线程的RunLoop对象
    如果在主线程中: 当前线程的RunLoop对象和主线程的RunLoop对象取得的是相同的, 代码演示:

- (void)viewDidLoad {
    [super viewDidLoad];

    // 主线程中打印的mainRunLoop, currentRunLoop内存地址是相同的
    NSLog(@"%p---%p", [NSRunLoop mainRunLoop], [NSRunLoop currentRunLoop]);

    // 创建一个子线程
    NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
    [thread start];
}

- (void)run
{
    // 开启的子线程默认是没有runloop的

    // 手动创建runloop对象, [NSRunLoop currentRunLoop]创建, 懒加载的创建方法,第一次访问创建,以后就不会创建了
    NSRunLoop *currentLoop = [NSRunLoop currentRunLoop];
    NSLog(@"thred--%p", currentLoop); //打印的内存地址和主线程的不同
}

我们打开开源的CFRunLoop.c代码, 里面的有部分核心代码为下面:
(下面的函数传一个线程进来,返回一个CFRunLoopRef对象, 说明一个线程对应一个runloop)

// should only be called by Foundation
// t==0 is a synonym for "main thread" that always works
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
    if (pthread_equal(t, kNilPthreadT)) {
    t = pthread_main_thread_np();
    }
    __CFLock(&loopsLock);
    if (!__CFRunLoops) {
        __CFUnlock(&loopsLock);
    CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
    CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
    CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
    if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
        CFRelease(dict);
    }
    CFRelease(mainLoop);
        __CFLock(&loopsLock);
    }
    CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    __CFUnlock(&loopsLock);
    if (!loop) {
    CFRunLoopRef newLoop = __CFRunLoopCreate(t);
        __CFLock(&loopsLock);
    loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    if (!loop) {
        CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
        loop = newLoop;
    }
        // don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
        __CFUnlock(&loopsLock);
    CFRelease(newLoop);
    }
    if (pthread_equal(t, pthread_self())) {
        _CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
        if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
            _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
        }
    }
    return loop;
}

上面的代码表示: 当调用[NSRunLoop currentRunLoop]/CFRunLoopGetCurrent()时:

  1. 它会先创建一个可变字典CFMutableDictionaryRef, 然后创建一个主线程的CFRunLoopRef, 并将主线程pthread_main_thread_np()作为key, 主线程的CFRunLoopRef对象作为value, 放入字典中(也就意味着,在访问其他线程的时候, 系统会先把主线程的runloop创建好, 所以主线程不用我们创建runloop)
  2. 然后将当前开辟的线程传进入, 看看当前线程的线程的runloop在不在字典中, 如果不存在, 则创建当前开辟线程的CFRunLoopRef, 然后用新线程作为key, 用新线程的CFRunLoopRef对象作为value, 放入字典中

  3. 一个key对应一个value, 所以每条线程都有唯一的一个与之对应的RunLoop对象

4. RunLoop相关的类

Core Foundation中关于RunLoop的5个类:

  • CFRunLoopRef
  • CFRunLoopModeRef
  • CFRunLoopSourceRef
  • CFRunLoopTimerRef
  • CFRunLoopObserverRef

这5个类的关系图.png

由上面的5个类的关系图, 可以看出: runloop要想跑起来, 里面必须有Mode, Mode中必须有SourceTimer (RunLoop包含Mode,Mode包含其他的三个类,其他的这三个类分别用Set,Arrya,Arrya装着)
这里注意: 根据源码,runloop要跑起来先判断mode是否为空,如果为空退出,
然后判断source0是否为空,如果为空退出,然后判断source1是否为空,如果为空退出,然后判断是否有timer,如果没有就退出,并没有判断是否有observer,所以runloop如果要跑起来,必须有source或者timer的其中一个

4.1.1 CFRunLoopModeRef
  • CFRunLoopModeRef代表RunLoop的运行模式
  • 一个RunLoop包含若干个Mode,每个Mode又包含若干个Source/Timer/Observer
  • 每次RunLoop启动时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode(可以获取到[NSRunLoop currentRunLoop].currentMode)
  • 如果需要切换Mode,只能退出Loop再重新指定一个Mode进入(因为RunLoop是一个运行循环, 一直在跑圈, 换另一个模式,必须先退出, 然后按照另一个模式跑圈)
  • 这样做主要是为了分隔开不同组的Source/Timer/Observer,让其互不影响(切换模式是为了,让它按照另一个模式的Source,Timer,Observer来跑圈, 互不影响)

RunLoop 启动必须要传入一个模式,RunLoop有多个模式, 但是每次只能运行一种模式

//创建runloop对象
NSRunLoop *currentLoop = [NSRunLoop currentRunLoop];
//启动(必须添加启动模式)
[currentLoop runMode:<#(nonnull NSString *)#> beforeDate:<#(nonnull NSDate *)#>];
4.1.2 CFRunLoopModeRef的5个模式:

系统默认注册了5个Mode:

  • kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个Mode下运行
  • UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响 (当我们滑动ScrollView,TableView等继承于ScrollView的控件是, 系统会切换模式为: UITrackingRunLoopMode, 跟踪你的触摸事件, 当停止滚动的时候, 系统会切换模式为: kCFRunLoopDefaultMode)
    这个有什么用呢? 答案是这样的:

    当ScrollView滚动时候, runloop对象只会处理UITrackingRunLoopMode这个模式下的定时器,Source, 以前添加的事件是不会处理的(以前添加的按钮等的事件是在kCFRunLoopDefaultMode模式下的),所以这个模式专门用来处理滚动的, 使滚动更加流畅,提高性能...举个栗子: 如果有TableView上有轮播图(NSTimer), 则在滚动TableView的时候,定时器是不好使的, 因为添加定时器默认是在kCFRunLoopDefaultMode下的

  • UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用(这个是苹果系统在使用,我们一般用不到)

    那么可以得出: 打开APP的时候, RunLoop先进入UIInitializationRunLoopMode模式,然后切换到kCFRunLoopDefaultMode, 如果有滚动,则切换到UITrackingRunLoopMode模式)

  • GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到 (GS 绘图渲染等)

  • kCFRunLoopCommonModes: 这是一个占位用的Mode,不是一种真正的Mode (RunLoop要启动需要传递一个模式才可以启动, 这个模式不能用来启动)

其实RunLoop真正的模式为四个, 最后一个不算真正的模式, 而我们可以使用的基本上就是上面的两个模式, 最后一个kCFRunLoopCommonModes算是一个标签, 打上这个模式的标签就是通用模式, 都可以跑, 下面讲解定时器案例时候说明, 默认的定时器添加到kCFRunLoopDefaultMode模式下,然后我们打上kCFRunLoopCommonModes通用标签, 就解决定时器在TableView滚动时停止的问题了

4.2 CFRunLoopTimerRef
  • CFRunLoopTimerRef是基于时间的触发器(基本上说的就是NSTimer), 一个模式下可以有多个Timer(Arrar中存放)
4.2.1 CFRunLoopTimerRef -->NSTimer

代码演练:

    /**
     *  这个方法内部实现是: 创建timer,添加到RunLoop中的默认的Mode中,RunLoop启动这个mode,取出这个mode中timer来用
     */
    [NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(run) userInfo:nil repeats:YES];

    /**
     *  上面的代码等同于下面的
     */
    // 创建Timer
    NSTimer *timer = [NSTimer timerWithTimeInterval:0.5 target:self selector:@selector(run) userInfo:nil repeats:YES];

    // 定时器只运行在 NSDefaultRunLoopMode 模式下, 一旦RunLoop进入其他模式,这个定时器就不会工作
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];



    // 如果拖动时, 我们将定时器添打上这个NSRunLoopCommonModes的标记
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

     NSLog(@"-----------%@", [NSRunLoop currentRunLoop]);
   /**
     *  定时器会跑在标记为common modes的模式下(这个模式只是个标记)
     *  RunLoop会寻找带有common标签的模式,有这个标签的,都可以跑
     *  打印当前的RunLoop信息输出为:(有common modes标签的有两个,UITrackingRunLoopMode和kCFRunLoopDefaultMode),所以定时器可以在这两个模式下跑, RunLoop只会运行一种模式

     common modes = <CFBasicHash 0x7fb8b2700490 [0x10ec6ba40]>{type = mutable set, count = 2,
     entries =>
     0 : <CFString 0x10fba2210 [0x10ec6ba40]>{contents = "UITrackingRunLoopMode"}
     2 : <CFString 0x10ec8c5e0 [0x10ec6ba40]>{contents = "kCFRunLoopDefaultMode"}
     }
     */

我们可以用这个来做什么呢:
比如: 有些事情,我拖拽的时候,你不能做, 我手松开才可以做, 那么我们可以除了监听ScrollView的滚动来判断外, 我们也可以将想做的事情放到默认的模式kCFRunLoopDefaultMode中 ,滚动时,事情不做,停止滚动,才做事情

如果我们将定时器放到UITrackingRunLoopMode模式下, 则只有在拖动的时候,定时器才可以工作, 代码如下:

 [[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
// 调用了scheduledTimer返回的NSTimer的定时器对象,已经被自动添加到当前的runLoop中(一个线程对应一个runloop,如果在子线程中添加定时器..添加到子线程的runloop中),默认为NSDefaultRunLoopMode模式
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(run) userInfo:nil repeats:YES];

// 如果需要更改模式, 直接这样就可以
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
4.2.2 CFRunLoopTimerRef -->CADisplayLink

CADisplayLink如果NSTimer一样, 也是添加到模式中
CADisplayLink的方法:

+ (CADisplayLink *)displayLinkWithTarget:(id)target selector:(SEL)sel;
- (void)addToRunLoop:(NSRunLoop *)runloop forMode:(NSString *)mode;
- (void)removeFromRunLoop:(NSRunLoop *)runloop forMode:(NSString *)mode;
- (void)invalidate;
5 CFRunLoopSourceRef

CFRunLoopSourceRef是事件源(输入源): 一些触摸事件,点击事件等都是在source中, 由source来触发的, 所以有一些事件过来,就是有一些source过来了, 处理source就是处理事件

  • 以前的分法:(官方文档,source的分类)(理论)
    • Port-Based Sources: 基于端口的,和其他线程进行交互的或者说是一些内核过来的消息
    • Custom Input Sources:自定义输入源
    • Cocoa Perform Selector Sources: [self performSelector:@selector(xxx)];
  • 现在的分法:(按照源码,函数的调用栈来看,source的分类)(实践)
    • Source0:非基于Port的 处理事件
    • Source1:基于Port的, 通过内核和其他线程通信,接收,分发系统事件

我们添加一个按钮,看一下函数的调用栈:


调用栈.png

source1是用来接收事件的, 虽然调用栈里面没有source1, 我们触摸屏幕,先摸到硬件(屏幕),屏幕表面的事件会先包装成Event, Event先告诉source1, 然后source1将事件Event分发给source0,然后由source0来处理
所以点击屏幕,触摸到硬件也会唤醒runloop..

performSelector和按钮的点击事件等等事件都是由source来处理的,这些事件一旦发生了,这个事件就会来到runloop,runloop里面就会把source0,source1处理一下,如果没有事件,也没有timer,则runloop就会睡眠, 如果有,则runloop就会被唤醒,然后跑一圈

6 CFRunLoopObserverRef

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



说明:

1UL <<0 , 20次方 1
2UL <<5,  25次方 32

代码演练

    // 这个方法是KVO,并不是观察runloop的状态,下面的方法才是监听状态
//    [NSRunLoop currentRunLoop] addObserver:<#(nonnull NSObject *)#> forKeyPath:<#(nonnull NSString *)#> options:<#(NSKeyValueObservingOptions)#> context:<#(nullable void *)#>


    /**
     *  创建runloop观察着
     *
     *  @param allocator#>  默认的allocator 点进去选择默认的即可
     *  @param activities#> 监听的状态(即将进入runloop,即将处理timer,即将处理source,即将进入休眠,刚从休眠中唤醒,即将退出runloop,监听所有状态)
     *  @param repeats#>    是否重复监听 YES 重复监听 description#>
     *  @param order#>      传0即可, description#>
     *  @param observer     runloop观察着
     *  @param activity     监听到的状态(枚举值)
     *
     *  @return runloop观察着
     */
    CFRunLoopObserverRef observe = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {

        NSLog(@"监听到runloop状态发生改变---%zd", activity);
        // 这个方法也挺有用的,比如我们想在处理timer之前做一些事情,处理事件之前做一些事情
    });


    /**
     *  添加观察着,监听runloop的状态
     *
     *  @param rl#>       runloop对象 description#>
     *  @param observer#> runloop观察着 description#>
     *  @param mode#>     runloop的模式 description#>
     */
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observe, kCFRunLoopDefaultMode);

    /**
     *  CF CoreFounction框架的东西不受ARC控制,需要自己手动释放
     *  释放observe
     */
    CFRelease(observe);

运行程序,打印结果如下: (对照上面的图查看runloop的状态)

2016-06-18 23:36:19.421 RunLoop[61612:760980] 监听到runloop状态发生改变---1
2016-06-18 23:36:19.422 RunLoop[61612:760980] 监听到runloop状态发生改变---2
2016-06-18 23:36:19.422 RunLoop[61612:760980] 监听到runloop状态发生改变---4
2016-06-18 23:36:19.423 RunLoop[61612:760980] 监听到runloop状态发生改变---2
2016-06-18 23:36:19.424 RunLoop[61612:760980] 监听到runloop状态发生改变---4
2016-06-18 23:36:19.424 RunLoop[61612:760980] 监听到runloop状态发生改变---2
2016-06-18 23:36:19.424 RunLoop[61612:760980] 监听到runloop状态发生改变---4
2016-06-18 23:36:19.424 RunLoop[61612:760980] 监听到runloop状态发生改变---32
2016-06-18 23:36:19.425 RunLoop[61612:760980] 监听到runloop状态发生改变---64
........

这时候那么问题来了, runloop的处理逻辑是什么呢?
看下图:


官方.png

整理.png

测试点击按钮事件:打印情况如下

2016-06-19 00:04:54.843 RunLoop[63122:782041] 监听到runloop状态发生改变---64  被唤醒
2016-06-19 00:04:54.843 RunLoop[63122:782041] 监听到runloop状态发生改变---2    即将处理timer
2016-06-19 00:04:54.844 RunLoop[63122:782041] 监听到runloop状态发生改变---4    即将处理source
2016-06-19 00:04:54.845 RunLoop[63122:782041] 监听到runloop状态发生改变---2
2016-06-19 00:04:54.846 RunLoop[63122:782041] 监听到runloop状态发生改变---4
2016-06-19 00:04:54.847 RunLoop[63122:782041] 监听到runloop状态发生改变---2
2016-06-19 00:04:54.847 RunLoop[63122:782041] 监听到runloop状态发生改变---4
2016-06-19 00:04:54.847 RunLoop[63122:782041] 监听到runloop状态发生改变---32
2016-06-19 00:04:54.921 RunLoop[63122:782041] 监听到runloop状态发生改变---64
2016-06-19 00:04:54.922 RunLoop[63122:782041] 监听到runloop状态发生改变---2 即将处理timer
2016-06-19 00:04:54.922 RunLoop[63122:782041] 监听到runloop状态发生改变---4 即将处理source
2016-06-19 00:04:54.923 RunLoop[63122:782041] ---ButtonClick---          按钮点击
2016-06-19 00:04:54.923 RunLoop[63122:782041] 监听到runloop状态发生改变---2
2016-06-19 00:04:54.924 RunLoop[63122:782041] 监听到runloop状态发生改变---4
2016-06-19 00:04:54.924 RunLoop[63122:782041] 监听到runloop状态发生改变---32
....后面还有, 最后变为即将进入休眠(32)

这里说明一点: runloop启动,先判断有没有模式,如果模式为空,则退出runloop,如果有模式,然后看看模式中有没有timer,source,observe,如果都没有,则也退出runloop..
这个可以根据runloop的源代码查看, 查看run的调用顺序得出...

7 runloop应用
  • NSTimer
  • ImageView显示
  • PerformSelector
  • 常驻线程
  • 自动释放池










每周一段:

记得初入公司时,写的代码乱七八糟,错误百出,bug连连,不仅项目经理骂我,其他同事也对我怨声载道。后来听朋友介绍,就报了一个培训班。经过1个月的刻苦学习,终于功夫不负有心人啊他们都骂不过我了。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值