iOS NSRunloop详解

什么是Runloop

Runloop即运行循环。为什么你的APP放在那里不去动它,在某个时间点去操作它,它还会给你反馈。就是因为Runloop的存在。
总结一下,因为Runloop的存在,保证你的程序不会死。

主要负责什么?
  1. 使程序一直运行并接受用户输入
  2. 决定程序在何时处理一些Event
  3. 调用解耦(Message Queue)
  4. 节省CPU时间(没事的时候闲着,有事的时候处理)
谁依赖NSRunloop
  1. NSTimer
  2. UIEvent
  3. autorelease
  4. NSObject(NSDelaydPerforming)
  5. NSObject(NSThreadPerformAddtion)
  6. CADisplayLink
  7. CATransition
  8. CAAnimation
  9. dispatch_get_main_queue()
  10. AFNetworking(NSURLConnection)
  11. ...

主线程几乎所有的函数都从以下的6个之1的调起

__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__
__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__

构成元素

Snip20160907_437.png

因为NSRunloop是对CFRunloop的封装,所以这里只看CFRunLoop就可以了。

CFRunLoopTimer的封装

系统提供的NSTimer、CADisplayLink、performSelector等都是对CFRunLoopTimer的封装。

CFRunLoopSource

Source是RunLoop的数据源抽象类(用OC的话来讲就是protocol)。
RunLoop定义了两个版本的Source,分别是Source0和Source1。

  1. Source0:处理APP内部事件、APP自己负责管理(触发),如UIEvent、CFSocket
  2. Source1:由RunLoop和内核管理,Mach Port驱动,如CFMachPort、CFMessagePort
CFRunLoopObserver

观察者,向外部报告RunLoop当前状态的更改

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry         = (1UL << 0), // 即将进入Loop
    kCFRunLoopBeforeTimers  = (1UL << 1), // 即将处理 Timer
    kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
    kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
    kCFRunLoopAfterWaiting  = (1UL << 6), // 刚从休眠中唤醒
    kCFRunLoopExit          = (1UL << 7), // 即将退出Loop
};

框架中很多机制都由CFRunLoopObserver触发,比如CAAnimation
举例:

self.navigationController pushViewController:<#(nonnull UIViewController *)#> animated:<#(BOOL)#>

当程序执行完这行代码时,我们可以看到经历push动画之后,到达了一个新的界面。
但其实并不是执行完这行代码就出现了Push的动画。
其实,执行这段代码时不会立刻就掉push动画,而是要RunLoop循环一圈收集所有的Animation操作,汇集起来一起去调。

CFRunLoopObserver与AutoreleasePool

对象的释放并不是在{}括号结束。而是稍微延迟了一点。
堆栈如下:

_wrapRunLoopAutoreleasePoolHandler
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__

UIKit通过RunLoopOberser在RunLoop两次Sleep间对AutoreleasePool进行Pop和Push,将这次Loop产生的Autorelease对象释放。
也就是RunLoop跑一圈没事了就睡,被唤醒了再跑下一圈,在两次sleep之间对自动释放池进行释放。

CFRunLoopMode

注意

RunLoop在同一段时间只能且必须在一种特定Mode下Run。
更换Mode时,需要停止当前Loop,然后重启新Mode。
Mode是iOS滑动顺畅的关键。

类型
  1. NSDefaultRunLoopMode
    默认状态(空闲状态),比如点击按钮都是这个状态
  2. UITrackingRunLoopMode
    滑动时的Mode。比如滑动UIScrollView时。
  3. UIInitializationRunLoopMode
    私有的,APP启动时。就是从iphone桌面点击APP的图标进入APP到第一个界面展示之前,在第一个界面显示出来后,UIInitializationRunLoopMode就被切换成了NSDefaultRunLoopMode。
  4. NSRunLoopCommonModes
    它是NSDefaultRunLoopMode和UITrackingRunLoopMode的集合。结构类似于一个数组。在这个mode下执行其实就是两个mode都能执行而已。
    典型的应用场景这样:当前界面有开启一个NSTimer,并且滑动UIScrollView。正常开启NSTimer后,滑动UIScrollView时它是不滑动的。解决办法就是把这个timer加入到当前的RunLoop,并把RunLoop的mode设置为NSRunLoopCommonModes。这样就可以保证不管你是NSDefaultRunLoopMode里跑,还是UITrackingRunLoopMode里跑,这个timer都可以执行。
self.timer = [NSTimer scheduledTimerWithTimeInterval:0.0625
                                                  target:self
                                                selector:@selector(progressChange)
                                                userInfo:nil
                                                 repeats:YES];
    
    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];

当你开始滑动UIScrollView时,RunLoop的mode状态变化如下:

NSDefaultRunLoopMode -> UITrackingRunLoopMode -> NSDefaultRunLoopMode

开始滑动时,第一次mode的切换会把NSDefaultRunLoopMode停掉。然后开启新的UITrackingRunLoopMode。当滑动停止时,由UITrackingRunLoopMode切换回NSDefaultRunLoopMode,这时UITrackingRunLoopMode被停止,又切换回了老的NSDefaultRunLoopMode(这个老的NSDefaultRunLoopMode应该是重新开始的)。

RunLoop和GCD的关系

RunLoop和GCD的关系,准确来说是只要使用了dispatch_get_main_queue(),就与RunLoop有了关系。

因为GCD中dispatch到main queue的block被分发到main RunLoop执行。

RunLoop的挂起和唤醒

我写了个demo,运行,然后点击debug栏的暂停,查看堆栈,如下:

Snip20160907_438.png
  1. 指定用于唤醒的mach_port接口
  2. 调用mach_msg监听唤醒端口,被唤醒前,系统内核将这个线程挂起,停留在mach_msg_trap状态
  3. 由另一个线程(或另一个进程中的某个线程)向内核发送这个端口的msg后,trap状态被唤醒,RunLoop继续开始干活。

RunLoop迭代执行顺序

伪代码:

SetupThisRunLoopRunTimeoutTimer();  //by GCD timer
do{
    __CFRunLoopDoObservers(kCFRunLoopBeforeTimers);
    __CFRunLoopDoObservers(kCFRunLoopBeforeSources);

    __CFRunLoopDoBlocks();
    __CFRunLoopDoSource0();

    CheckIfExistMessagesInMainDispatchQueue();    //GCD

    __CFRunLoopDoObservers(kCFRunLoopBeforeWaiting);
    var wakeUpPort = SleepAndWaitForWakingUpPorts();
    //mach_msg_trap
    //Zzz...
    //Received mach_msg , wake up
    __CFRunLoopDoObservers(kCFRunLoopAfterWaiting);
    //Handle msgs
    if(wakeUpPort == timerPort){
        __CFRunLoopDoTimers();
    }else if(wakeUpPort == mainDispatchQueuePort) {
        //GCD
        __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE_()
    }else {
          __CFRunLoopDoSource1();
    }
    __CFRunLoopDOBlocks();
}while(!stop && !timeout);

代码解读

//首先do..while循环不能是一个死循环,所以在这里设置一个过期时间
//这件事是GCD干的,用来检测do..while循环跑了多久
SetupThisRunLoopRunTimeoutTimer();

//开始跑循环
do{
    //告诉observer我要跑timer了
    __CFRunLoopDoObservers(kCFRunLoopBeforeTimers);
    //告诉observer我要跑source了
    __CFRunLoopDoObservers(kCFRunLoopBeforeSources);

    __CFRunLoopDoBlocks();
    //程序跑到这里会查询Source0有什么消息
    __CFRunLoopDoSource0();

    //询问GCD你有没有存在主线程的东西需要我帮你调
    CheckIfExistMessagesInMainDispatchQueue();  //GCD

    //告诉observer我要睡了,RunLoop进入到挂起状态
    __CFRunLoopDoObservers(kCFRunLoopBeforeWaiting);

    //进入trap状态,程序跑到这里就卡在这不动了,等待被某个Port唤醒
    var wakeUpPort = SleepAndWaitForWakingUpPorts();

    //被唤醒后,告诉observer我被唤醒了
    __CFRunLoopDoObservers(kCFRunLoopAfterWaiting);

    //假如是被timer唤醒的
    if(wakeUpPort == timerPort){
        //就去循环遍历和timer有关的回调
        __CFRunLoopDoTimers();
    }else if(wakeUpPort == mainDispatchQueuePort) {
        //如果是主线程的GCD把我唤醒的,那RunLoop就知道GCD要让它做事了,然后就取调GCD的这些事件
        __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE_()
    }else {
          //如果都不是,就是Source1,Source1是基于Port事件的,比如网络某个端口来数据了,就会把RunLoop唤醒,去对来的数据进行处理
          __CFRunLoopDoSource1();
    }
    __CFRunLoopDoBlocks();
}while(!stop && !timeout);
//判断条件:有没有被外部干掉 && 到了过期时间
//如果过期时间不手动进行设置的话,默认值是一个很大的值,可能是Int_Max

AFNetworking是如何玩转RunLoop的

+ (void)networkRequestThreadEntryPoint:(id)_unuserd object {
    @autoreleasepool {
        [[NSThread currentThread] setName:@"AFNetworking"];

        //为了不让runloop run起来没事干导致消失
        //所以给runloop加了一个NSMachPort,给它一个mode去监听
        //实际上port什么也没干,就是让runloop一直在等,目的就是让runloop一直活着
        //这是一个创建常驻服务线程的好方法
        NSRunloop *runloop = [NSRunLoop currentRunLoop];
        [runloop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runloop run];
    }
}

+ (NSThread *)networkRequestThread {
    static NSThread *_networkReuqestThread = nil;
    static dispatch_once_t oncePredicate;
    dispatch_once(&oncePredicate, ^{
        _networkRequestThread = 
        [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
        [_networkRequestThread start];
    });
    return _networkRequestThread;
}

一个TableView延迟加载图片的新思路

以前是怎么解决的?
通过UITableView的代理方法,判断如果处于滑动状态就不去设置cell上的图片,如果没有处于滑动状态就取设置cell上的图片。

而现在通过Runloop就有一个十分简便的方法。

//在cell里面把设置图片的事情在NSDefaultRunloopMode里面去做。
//当主线程的tableview不再滑动的时候就会去设置图片
UIImage *dowloadImage = ...;
[self.iconImageView performSelector:@selector(setImage:) withObject:dowloadImage afterDelay:0 inModes:@[NSDefaultRunloopMode]];

这样去设置图片就简便了很多,不用再去判断tableview的代理方法。代码也会很清爽。

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

推荐阅读更多精彩内容