浅显易懂的RunLoop总结

我的Github地址 : Jerry4me

最近在忙着找实习的事儿, 在忙着各种知识的巩固, 复习, 趁热打铁把一些知识点都总结起来, 也算是对自己的某种程度上的考验吧.


RunLoop是什么?

Run -> 跑, Loop -> 圈, RunLoop -> 跑圈. 对的, 简单地说, RunLoop就是一种比较高大上的do-while循环.

void runloop() {
  init(); 
  do {
      id msg = getMessage();
      handleMessage(msg);
  } while (msg != quit);
}

RunLoop也是对象, Foundation/Core Foundation中分别为NSRunLoop与CFRunLoopRef. 一般来说, 一个线程与一个RunLoop对象一一绑定, 主线程在程序的入口UIApplicationMain函数中就会自动创建一个Main RunLoop, 以便主线程就算处理完了所有事件, 任务也不会退出, 否则我们的程序一run, 就return没了, 就像一些命令行项目那样.

在我们手动开启一条子线程时, 系统并不会默认帮我们创建对应的RunLoop, 而是需要我们调用[NSRunLoop currentRunLoop]才会创建(如果已经存在RunLoop, 则直接返回; 否则, 创建RunLoop并返回).


RunLoop里有什么

每个RunLoop中有若干个mode, 每个mode可以有若干个source, timer, observer.

mode : CFRunLoopModeRef

RunLoop在同一时间只能且必须在一种mode下Run, 要更换mode时, 只能停止当前loop, 重启开启新的loop

  • NSDefaultRunLoopMode : 默认状态, 空闲状态
  • UITrackingRunLoopMode : 滑动scrollView时
  • UIInitializationRunLoopMode : 私有Mode, APP启动时用,之后无用
  • NSRunLoopCommonModes : 默认情况下包含Default和Tracking模式, 可自行添加mode, 相当于mode的集合

举个栗子 : NSTimer默认是在NSDefaultRunLoopMode中工作的, 当你啥也不干的时候, 它能正常运作, 但是当你开始拖拽UIScrollView的时候, 它就不干了. 原因是当你拖拽的时候, MainRunLoop切换到了UITrackingRunLoopMode, 所以timer便暂时失效了, 当你松手的时候, timer又继续工作了. 解决办法就是把这个timer加入到两个mode中, 也可以直接添加到NSRunLoopCommonModes中.

source : CFRunLoopSourceRef

其实是RunLoop的数据源抽象类, 相当于任何遵循这个protocol的对象都能当RunLoop的数据源

  • source0 : 处理App内部事件, App自己负责管理(触发), 如UIEvent, CFSocket
  • source1 : 由RunLoop和内核管理, Mach port驱动, 如CFMachPort, CFMessagePort

简单的说就是source0不基于mach port, source1基于mach port

timer : CFRunLoopTimerRef

NSTimer其实就是CFRunLoopTimerRef, 它们两个是toll-free bridged. 需要注意的是加入到timer上处理的事件并不是马上处理的, 如果你设置的delay属性是5秒, 事实上是经过5秒钟之后才把该事件送到RunLoop中, 等到下一次RunLoop的时候才处理.

observer : CFRunLoopObserverRef

根据RunLoop状态的改变通知observer, observer会向外部报告RunLoop的状态的更改, 框架中很多机制都由RunLoopObserver触发, 如CAAnimation

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

autoreleasepool

虽然RunLoop并不自带autoreleasepool, 但是如每个程序的main函数所示,

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

每个RunLoop都应该被至少一个autoreleasepool包围着, 这样才能保证在Loop中产生的autorelease对象能够正常释放而不至于造成内存泄漏. 所以RunLoop在处理任何任务以前必定是先push一个autoreleasepool, 在休眠之前要先pop再push一个autoreleasepool, 退出则是直接pop掉就可以了.

另外, 有一种情况是我们必须手动显式创建autoreleasepool的, 那就是当一个循环里, 例如for循环, 大量创建autorelease对象时, 我们应该在for循环中用一个autoreleasepool把代码都包住, 这样能防止内存达到峰值, 影响app的稳定运行. 苹果也在很多方法里也采用了这种方法, 例如字典的遍历方法, 内部就有一个autoreleasepool.


RunLoop是怎么工作的

上图的是苹果文档给的图, sources分为两种, 一种是input sources, 另一种为timer sources.

  • input sources : 传递异步事件, 通常消息来自于其他线程或程序.
  • timer sources : 传递同步事件, 发生在特定时间或者重复的时间间隔.

当sources到来的时候, 唤醒RunLoop, 该干嘛干嘛. 具体流程可如下所示 :

对应的伪代码如下 :

SetupThisRunLoopRunTimeoutTimer(); // by GCD timer, 此次Loop的过期时间
do {
    // 告诉observer我要跑timers和sources啦
    __CFRunLoopDoObserver(kCFRunLoopBeforeTimers);
    __CFRunLoopDoObserver(kCFRunLoopBeforeSources);

    __CFRunLoopDoBlocks(); // 处理timer事件
    __CFRunLoopDoSource0(); // 遍历source0, 去处理

    CheckIfExistMessagesInMainDispatchQueue(); // 问GCD是否有分到主线程的任务需要我帮你去调

    __CFRunLoopDoObservers(kCFRunLoopBeforeWaiting); // 告诉observer我要睡了(挂起)
    // **注意 : 这里会pop掉autoreleasepool, 并push一个新的autoreleasepool**

    var wakeUpPort = SleepAndWaitForWakingUpPorts(); // 进入trap状态, 代码卡在这里, 不会再往下执行, 直到被别人唤醒
    // mach_msg_trap
    // Zzz...

    // Received mach_msg, wake up
    __CFRunLoopDoObservers(kCFRunLoopAfterWaiting); // 告诉observer我被唤醒
    // Handle msgs
    if (wakeUpPort == timerPort) { // 假如是timer唤醒我的, 那就处理timer
    __CFRunLoopDoTimers();
    } else if (wakeUpPort == mainDispatchQueuePort) { // 假如是主线程的GCD把我唤醒的, 那就处理GCD的事件
    __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__();
    } else { // 基于port事件, 比如网络某端口数据到了, 就会去处理数据
    __CFRunLoopDoSource1(); 
    } // RunLoop一圈跑完
} while(!stop && !timeout);

RunLoop有什么用

1. 开启后台常驻线程处理用户事件, 如AFNetworking

+ (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 onceToken;
    dispatch_once(&onceToken, ^{
        _networkRequestThread =
        [[NSThread alloc] initWithTarget:self
                            selector:@selector(networkRequestThreadEntryPoint:)
                              object:nil];
        [_networkRequestThread start];
    });
    return _networkRequestThread;
}

AFN为了其在后台线程中能接受到delegate回调, 特意单独创建一个线程, 并在该线程启动一个RunLoop. 需要注意的是, RunLoop在开启之前必须要有至少一个source/observer/timer, 所以他添加了一个[NSMachPort port], 该port没有传递任何需要处理的事件, 只是为了保持RunLoop不至于退出, 仅此而已.

2. 决定程序何时处理Event(哪个模式下, 什么时候)

这里就应用到tableView上, 注册一个observer, 在Main RunLoop即将进入休眠, 没活干的时候, 杀出. 在这里做一些cell的高度计算及缓存, 数据处理操作. 还有在tableView滑动的时候在cell里设置当RunLoopMode为UITracking模式的时候不要设置图片, 为DefaultMode的时候才设置图片, 优化tableView的滑动卡顿问题.

AsyncDisplayKit

AsyncDisplayKit是 Facebook 推出的用于保持界面流畅性的框架.

UI线程(主线程)中一旦出现繁重的操作, 就会造成卡顿. 如排版, 绘制, UI对象操作.

  • 排版通常包括计算视图大小、计算文本高度、重新计算子式图的排版等操作。
  • 绘制一般有文本绘制 (例如 CoreText)、图片绘制 (例如预先解压)、元素绘制 (Quartz)等操作。
  • UI对象操作通常包括 UIView/CALayer 等 UI 对象的创建、设置属性和销毁。

ASDK做的就是把所有可以能放到后台线程操作的事情全部放到后台线程中操作, 不能的就尽可能的推迟其进行. ASDK是通过注册一个主线程的observer, 监听其即将进入休眠即将退出循环的两个状态, 在此把之前推迟的任务全部放到待处理队列中一一执行.


参考文章 :

YY大神的深入理解RunLoop

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

推荐阅读更多精彩内容