iOS-RunLoop浅析

RunLoop是iOS事件响应与任务处理最核心的机制,它贯穿iOS整个系统,自动释放池,延迟处理,触摸事件,屏幕刷新都是通过RunLoop实现的.Foundation中的NSRunLoop和Core Foundation中CFRunLoop 是RunLoop的主要实现.

基础实现

RunLoop通过do-while循环保持整个App的持续运行,同时能在运行和睡眠状态之间切换,节省CPU资源.Android中的Looper跟iOS中的RunLoop类似,接收异步消息,控制应用程序的生命周期.一般情况一个线程只能执行一个任务,执行完成后就会退出.我们可以通过Runloop保证线程能随时处理事件,并不退出.

Apple不允许直接创建 RunLoop,提供了两个自动获取的函数:CFRunLoopGetMain() 和 CFRunLoopGetCurrent().

CFRunLoopRef CFRunLoopGetMain(void) {
    CHECK_FOR_FORK();
    static CFRunLoopRef __main = NULL; // no retain needed
    if (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np()); // no CAS needed
    return __main;
}

CFRunLoopRef CFRunLoopGetCurrent(void) {
    CHECK_FOR_FORK();
    CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
    if (rl) return rl;
    return _CFRunLoopGet0(pthread_self());
}

pthread_self获取当前线程,pthread_main_thread_np获取主线程,通过线程获取当前的Runloop.线程和Runloop被保存在全局字典__CFRunLoops中,如果字典中存在则会取出,如果不存在则会创建.

// 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;
}

Runloop运行:

void CFRunLoopRun(void) {   /* DOES CALLOUT */
    int32_t result;
    do {
        result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
        CHECK_FOR_FORK();
    } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}

运行机制

RunLoop是线程中的一个循环,在循环中不断检测通过Input sources(输入源)和Timer sources(定时源)两种来源等待接受事件,然后对接受到的事件通知线程进行处理,并在没有事件的时候进行睡眠.

RunLoop与线程之间的关系密不可分:
1.线程与RunLoop是一一对应的,一个线程对应一个RunLoop对象,根RunLoop可以嵌套子RunLoop.
2.主线程的RunLoop在应用启动的时候会自动创建,非主线程的RunLoop需要在该线程自己启动.
3.RunLoop对象在第一次获取RunLoop时创建,销毁则是在线程结束的时候.
4.不能自己创建RunLoop.
5.RunLoop并不是线程安全的,只能在当前线程中操作当前线程的RunLoop,而不能去操作其他线程的RunLoop,同时也需要避免在其他线程上调用当前线程的RunLoop.

一个 RunLoop 对象包含若干个 Mode 对象,每个 Mode 又包含若干个 Source/Timer/Observer,RunLoop 一次运行只能在一个 Mode 之下,如果需要切换 Mode,需要退出 Loop 才能重新指定一个 Mode.这样做主要是为了分隔开不同组Source/Timer/Observer,让其互不影响.

一个 Source 对象是一个事件,Source 有两个版本:Source0 和 Source1,Source0 只包含一个函数指针,并不能主动触发,需要将 Source0 标记为待处理,在 RunLoop 运转的时候,才会处理这个事件(如果 RunLoop 处于休眠状态,则不会被唤醒去处理),而 Source1 包含了一个 mach_port 和一个函数指针,mach_port 是 iOS 系统提供的基于端口的输入源,可用于线程或进程间通讯。而 RunLoop 支持的输入源类型中就包括基于端口的输入源,可以做到对 mach_port 端口源事件的监听。所以监听到 source1 端口的消息时,RunLoop 就会自己醒来去执行 Source1 事件(也能称为被消息唤醒)。也就是 Source0 是直接添加给 RunLoop 处理的事件,而 Source1 是基于端口的,进程或线程之间传递消息触发的事件.

Timer 是基于时间的触发器,CFRunLoopTimerRef 和 NSTimer 可以通过 Toll-free bridging 技术混用,Toll-free bridging 是一种允许某些 ObjC 类与其对应的 CoreFoundation 类之间可以互换使用的机制,当将 Timer 加入到 RunLoop 时,RunLoop 会注册对应的时间点,当时间点到时,RunLoop 会被唤醒以执行 Timer 回调.

__CFRunLoopMode定义如下:

struct __CFRunLoopMode {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;  /* must have the run loop locked before locking this */
    CFStringRef _name;
    Boolean _stopped;
    char _padding[3];
    CFMutableSetRef _sources0;
    CFMutableSetRef _sources1;
    CFMutableArrayRef _observers;
    CFMutableArrayRef _timers;
    CFMutableDictionaryRef _portToV1SourceMap;
    __CFPortSet _portSet;
    CFIndex _observerMask;
#if USE_DISPATCH_SOURCE_FOR_TIMERS
    dispatch_source_t _timerSource;
    dispatch_queue_t _queue;
    Boolean _timerFired; // set to true by the source when a timer has fired
    Boolean _dispatchTimerArmed;
#endif
#if USE_MK_TIMER_TOO
    mach_port_t _timerPort;
    Boolean _mkTimerArmed;
#endif
#if DEPLOYMENT_TARGET_WINDOWS
    DWORD _msgQMask;
    void (*_msgPump)(void);
#endif
    uint64_t _timerSoftDeadline; /* TSR */
    uint64_t _timerHardDeadline; /* TSR */
};
RunLoop.jpg

基础知识

RunLoop运行状态通过CFRunLoopActivity可以查看:

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),
    kCFRunLoopBeforeTimers = (1UL << 1),
    kCFRunLoopBeforeSources = (1UL << 2),
    kCFRunLoopBeforeWaiting = (1UL << 5),
    kCFRunLoopAfterWaiting = (1UL << 6),
    kCFRunLoopExit = (1UL << 7),
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};
运行时.png
  1. kCFRunLoopEntry -- 进入runloop循环
  2. kCFRunLoopBeforeTimers -- 处理定时调用前回调
  3. kCFRunLoopBeforeSources -- 处理input sources的事件
  4. kCFRunLoopBeforeWaiting -- runloop睡眠前调用
  5. kCFRunLoopAfterWaiting -- runloop唤醒后调用
  6. kCFRunLoopExit -- 退出runloop

CoreFoundation中关于RunLoop的5个重要的类:

  1. CFRunLoopRef:运行循环对象,也就是它自身.
  2. CFRunLoopModeRef:指定runloop的运行模式.给事件源分组,避免互相影响.一个runLoop可以有很多个Mode,1个Mode可以有很多个Source,Observer,Timer,但是在同一时刻只能同时执行一种Mode.
  3. CFRunLoopSourceRef:输入源.
  4. CFRunLoopTimerRef:定时源,定时器,必须加入到runloop.
  5. CFRunLoopObserverRef(观察者,观察是否有事件).

关于五个主要的类可以描述为一个RunLoop对象中包含若干个运行模式(CFRunLoopModeRef),而每一个运行模式下又包含若干个输入源(CFRunLoopSourceRef)、定时源(CFRunLoopTimerRef)、观察者(CFRunLoopObserverRef).

单次Runloop可以处理Source1(触摸/锁屏/摇晃),Source0事件(需要手动触发),Timer事件和观察者事件.

ibireme.png

Runloop 模式

系统默认定义了多种运行模式(CFRunLoopModeRef),常见的有五种:

  1. kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个 Mode 下运行的.

  2. UITrackingRunLoopMode:界面跟踪 Mode,用于ScrollView 追踪触摸滑动,保证界面滑动时不受其他Mode 影响.

  3. UIInitializationRunLoopMode:在刚启动 App 时第进入的第一个Mode,启动完成之后就不再使用.

  4. GSEventReceiveRunLoopMode:接收系统事件的内部 Mode,通常用不到.

  5. kCFRunLoopCommonModes:占位用的 Mode,不是一种真正的 Mode.

RunLoop与自动释放池:
自动释放池寄生于Runloop,程序启动后,主线程注册了两个Observer监听runloop的进出和睡眠.一个最高优先级OB监测Entry状态,一个最低优先级OB监听BeforeWaiting状态和Exit状态.

RunLoop 实战

1.维护线程的生命周期,让线程不自动退出,isFinished为Yes时退出.

NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
while (!self.isCancelled && !self.isFinished) {
    @autoreleasepool {
            [runLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:3]];
    }
}

2.创建常驻线程,执行一些会一直存在的任务,该线程的生命周期跟App相同:

@autoreleasepool {
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
}

创建常驻线程最经典的例子是AFNetWorking 2.x版本中代码:

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

        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
    }
}

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

    return _networkRequestThread;
}

3.在一定时间内监听某种事件,或执行某种任务的线程
如下代码,在30分钟内,每隔30s执行onTimerFired.这种场景一般会出现在,如我需要在应用启动之后,在一定时间内持续更新某项数据,如果用来监控屏幕的卡顿也可以.

@autoreleasepool {
    NSRunLoop * runLoop = [NSRunLoop currentRunLoop];
    NSTimer * udpateTimer = [NSTimer timerWithTimeInterval:30
                                                    target:self
                                                  selector:@selector(onTimerFired:)
                                                  userInfo:nil
                                                   repeats:YES];
    [runLoop addTimer:udpateTimer forMode:NSRunLoopCommonModes];
    [runLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:60*30]];
}

4.UITableView滚动加载图片
当tableView的cell上有需要从网络获取的图片的时候,滚动tableView,异步线程回去加载图片,加载完成后主线程会设置cell的图片,但是会造成卡顿。可以设置图片的任务在CFRunloopDefaultMode下进行,当滚动tableView的时候,Runloop切换到UITrackingRunLoopMode,不去设置图片,而是而是当停止的时候,再去设置图片.

[self performSelector:@selector(download:) withObject:url afterDelay:0 inModes:NSDefaultRunLoopMode];

5.NSTimer失效
如果页面有计时器同时有滑动视图的时候,需要注意NSTimer的模式,视图滑动的过程会切换至UITrackingMode模式下,造成Timer短暂失效,将Timer的模式设置为CommonMode即可.

self.upTimer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(upTimeUpdate) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:self.upTimer forMode:NSRunLoopCommonModes];
    [self.upTimer fire];
    
    self.bottomTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(bottomTimeUpdate) userInfo:nil repeats:YES];

参考资料:
CF框架源码
RunLoop 原理和核心机制
CoreFoundation
深入理解RunLoop
滑动卡顿优化
官方文档
CF源码
http://blog.raozhizhen.com/post/2016-08-18

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

推荐阅读更多精彩内容

  • 前言 最近离职了,可以尽情熬夜写点总结,不用担心第二天上班爽并蛋疼着,这篇的主角 RunLoop 一座大山,涵盖的...
    zerocc2014阅读 12,365评论 13 67
  • 一、什么是runloop 字面意思是“消息循环、运行循环”。它不是线程,但它和线程息息相关。一般来讲,一个线程一次...
    WeiHing阅读 8,107评论 11 111
  • 注:本篇博客只在 ibireme 的 深入理解RunLoop 基础上做了点方便自己复习该知识点的修改,能力有限,如...
    AidenRao阅读 2,825评论 6 26
  • RunLoop的定义与概念RunLoop的主要作用main函数中的RunLoopRunLoop与线程的关系RunL...
    __silhouette阅读 1,004评论 0 6
  • 任浊世繁华 风景如画 在夕阳西下 你们就是我最深的牵挂 两周的别离 虽说不上三秋 也是漫长如戈壁黄沙 此刻再美的风...
    刘寒霜阅读 177评论 8 9