iOS RunLoop漫谈

在iOS开发或者面试中,我们多多少少会听到或者用到RunLoop的相关知识,彻底吃透RunLoop这一块内容,对我们技术能力的提升大有裨益。最近看了一些RunLoop的文章和代码,作为总结写了这篇文章。本篇文章将会从RunLoop的原理和应用两个方面剖析RunLoop,帮助大家更好地理解、应用RunLoop。本篇文章主要内容:

  • RunLoop的概念
  • RunLoop存在的意义
  • RunLoop的构成
  • RunLoop的应用

RunLoop的概念

通常情况下,线程执行完成某个任务后,线程就会退出;但某些情况下,我们需要线程常驻并且随时被唤醒以执行其他任务。线程执行一次就退出和循环多次执行可以用图形表示为:

runloop0.png

线程循环多次执行用伪代码可以简单表示为:

do {
   //获取消息
   //处理消息
} while (消息 != 退出)

上面的这种循环模型被称作 Event Loop,事件循环模型在众多系统里都有实现,具体到iOS和OSX中就是我们要介绍的RunLoop。当然RunLoop的实现绝不会是一个简单的while循环所能搞定的,其内部实现会复杂很多,在后面的内容中会详细介绍。

RunLoop存在的意义

RunLoop的官方定义为:

Run loops are part of the fundamental infrastructure associated with threads. A run loop is an event processing loop that you use to schedule work and coordinate the receipt of incoming events. The purpose of a run loop is to keep your thread busy when there is work to do and put your thread to sleep when there is none.

从中我们可以得知RunLoop的主要目的是:当有任务到来时,可以立马唤醒线程让其处理任务,没有任务时保持线程休眠。RunLoop的机制既能有效减少系统资源的浪费,又能使线程随时待命,不至于执行完成后就退出。

RunLoop的构成

在iOS和OSX中,与RunLoop对应的两种对象是NSRunLoopCFRunLoopRef。两者的关系如下图所示:

runloop1.png

CFRunLoopRef是开源的,所以我们研究其源码是最合适不过的。CFRunloopRef整体结构组成如下:

runloop2.png

先通过源码看下RunLoop和线程的关系:

CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
    if (pthread_equal(t, kNilPthreadT)) {
    t = pthread_main_thread_np();//如果线程t为空,将其赋值为主线程
    }
    __CFSpinLock(&loopsLock);//加锁,保证多线程访问的安全性
    if (!__CFRunLoops) {
        __CFSpinUnlock(&loopsLock);
    CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);//如果维护线程和runloop关系的字典不存在,就新建一个
    CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());//主线程的runloop总会被默认创建,且创建一次
    CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);//将主线程和主runloop存放到dict中维护
    if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {//比较、交换dict和__CFRunLoops,也就是说dict是临时字典,实际上是由__CFRunLoops全局字典维护线程和runloop的关系
        CFRelease(dict);
    }
    CFRelease(mainLoop);
        __CFSpinLock(&loopsLock);
    }
    CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));//在全局字典__CFRunLoops查找线程t对应的runloop
    __CFSpinUnlock(&loopsLock);
    if (!loop) {
    CFRunLoopRef newLoop = __CFRunLoopCreate(t);//如果没找到就新建一个
        __CFSpinLock(&loopsLock);
    loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    if (!loop) {
        CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);//把新建的runloop和线程t存放到__CFRunLoops字典中
        loop = newLoop;
    }
        // don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
        __CFSpinUnlock(&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;//返回线程t对应的runloop
}

从代码可以看出:线程和RunLoop是一一对应的关系,这种对应关系维护在一个全局的字典中,主线程的RunLoop会被系统自动创建,其他线程的RunLoop在第一次获取的时候才会创建,不获取的话其RunLoop是不存在的。

一个CFRunLoop包含多个CFRunLoopMode,一个CFRunLoopMode包含多个CFRunLoopSourceCFRunLoopTimerCFRunLoopObserver。RunLoop在任何时刻只能以一种Mode运行(叫做currentMode),切换Mode时要先退出当前RunLoop。下面我们从源码具体解析:

CFRunLoop结构体:

struct __CFRunLoop {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;          /* locked for accessing mode list */
    __CFPort _wakeUpPort;           // used for CFRunLoopWakeUp 
    Boolean _unused;
    volatile _per_run_data *_perRunData;              // reset for runs of the run loop
    pthread_t _pthread;
    uint32_t _winthread;
    CFMutableSetRef _commonModes;
    CFMutableSetRef _commonModeItems;
    CFRunLoopModeRef _currentMode; //RunLoop当前运行的Mode
    CFMutableSetRef _modes;        //RunLoop包含的所有Mode
    struct _block_item *_blocks_head;
    struct _block_item *_blocks_tail;
    CFTypeRef _counterpart;
};RunLoopModeRef _currentMode;
    CFMutableSetRef _modes;
    struct _block_item *_blocks_head;
    struct _block_item *_blocks_tail;
    CFTypeRef _counterpart;
};

代码中可以看出:

(1)所有的Mode是被RunLoop的一个集合维护的

(2)当前正在运行的Mode就是currentMode

(3)一个普通的Mode可以被标记为commonMode,所有的commonMode也是被RunLoop的一个集合维护的

(4)commonMode里面的Source、Timer、Observer都叫做commonModeItem,所有的commonModeItem也是被RunLoop的一个集合维护的,不同的commonMode之间是可以同步、共享commonModeItem信息的。

(5)苹果为我们提供了两种Mode:kCFRunLoopDefaultMode (NSDefaultRunLoopMode) 和 UITrackingRunLoopMode

CFRunLoopMode结构体:

struct __CFRunLoopMode {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;  /* must have the run loop locked before locking this */
    CFStringRef _name; //Mode名称
    Boolean _stopped;
    char _padding[3];
    CFMutableSetRef _sources0;//事件源source0的集合
    CFMutableSetRef _sources1;//事件源source1的集合
    CFMutableArrayRef _observers;//观察者observer的数组
    CFMutableArrayRef _timers;//计时器timer的数组
    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 */
};

代码中可以看出:
(1)CFRunLoopMode中维护一个管理多个事件源的集合,事件源又可以分为Soure0Source1
Source0: 基于非mach_port的,例如Custom Input Sources 、CFSocket等。
Source1: 基于mach_port的,能够主动触发RunLoop的线程处理消息,并且执行相应的回调。

(2)CFRunLoopMode中维护一个管理多个CFRunLoopTimerRef的数组
CFRunLoopTimerRef:包含一个时间长度和一个回调,在Timer被注册到RunLoop的时刻,再经过这个时间长度后,就会执行这个回调,相当于定时器的概念。

(3)CFRunLoopMode中维护一个管理多个CFRunLoopObserverRef的数组。
CFRunLoopObserverRef:可以理解为实时观察RunLoop的执行状态,在重要的时间点向外报告也就是执行相应的回调,这些重要的时间点包括:

    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的具体执行流程,可以参照ibireme博客上的流程图帮助理解:

runloop3.png

RunLoop的应用

AutoreleasePool的实现

runloop4.png

主线程上执行的代码被AutoreleasePool包围着,不会出现内存泄漏的情况,所以也不用我们手动创建释放池了。

NSTimer

NSTimer可以看做是CFRunLoopTimerRef的封装,当一个NSTimer被添加到RunLoop后,经过一段时间后会执行相应的回调,具体执行一次还是重复多次执行,由repeats参数决定,具体使用方法简单,这里就不贴代码了

UI刷新

当界面元素的位置、大小发生变化,或者做连续几个动画时,这些变化不会立马体现在界面上,而是暂时保存这些变化,等到RunLoop休眠或者即将退出时,再去执行相应的回调,回调中的代码用于更新界面。

GCD用到RunLoop的情况

GCD中,子线程通过dispatch_async提交任务到主线程时,主线程的RunLoop会收到消息,激活后通过回调处理任务,但这仅限于提交到主线程的任务,如果提交任务到子线程,则还是由GCD本身处理的,就与RunLoop无关了。

参考链接

深入理解RunLoop
Run Loops

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

推荐阅读更多精彩内容