Objective-C之我所理解的Runloop

前言

RunLoop是iOS和OSX开发中非常基础的一个概念,学习Runloop能够帮助我们更清楚的了解APP为何能够持续运行。虽然在平时的工作场景中使用Runloop的机会很少,但是理解RunLoop可以帮助开发者更好的利用多线程编程。网上关于Runloop的文章千篇一律,但"一千个读者,就有一千个哈姆雷特",每个人都有自己不同的理解。


Runloop

通俗概念

  • 可以从字面上理解成“运行循环”、“跑圈”,通俗一点,它就是一个死循环,相当于一个do..while;
  • 正是因为这个死循环的存在,才能保持程序的持续运行,不会像一块代码,执行完了就完了;
  • 有事情的时候做事情,没事情的时候就休息,充分节省了CPU的性能;
  • 所谓的“事情”,实际上就是App中的各种事件(比如触摸事件、定时器事件、Selector事件);

官方解释

  • 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的目的就是当有任务到来的时候保持当前线程处于繁忙状态, 当没有任务需要处理的时候让当前线程处于休眠状态。

如果没有Runloop

image
  • 代码从上到下执行,到第三行就结束了

有了Runloop以后

image
  • 由于main函数里面启动了RunLoop,所以程序并不会马上退出,保持持续运行状态
  • 在代码main.m里都默认在主线程中启动了runloop
  • 所以UIApplicationMain函数一直没有返回,保持了程序的持续运行
#import <UIKit/UIKit.h>
#import "AppDelegate.h"

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

RunLoop对象

  • iOS中为我们提供了两套API访问和操作runloop,一套是面向OC的,一套是基于C语言的
    • Foundation下的NSRunLoop
    • Core Foundation下的CFRunLoopRef
  • NSRunLoop是基于CFRunLoopRef的一层OC包装,所以要了解RunLoop内部结构,需要多研究CFRunLoopRef层面的API(Core Foundation层面)
  • 更多细节我们可以查阅苹果官方文档深入学习
  • 苹果官方文档
  • RunLoop 官方编程手册翻译

Runloop与线程的关系

  • 每条线程都有唯一的一个与之对应的RunLoop对象
  • 主线程的RunLoop已经自动创建好了,子线程的RunLoop需要主动创建(子线程的RunLoop默认是关闭的,因为有时候开了个线程但却没有必要开一个RunLoop,不然反而浪费了资源)。
  • RunLoop在第一次获取时创建,在线程结束时销毁(内部实际上是一个懒加载)

获得RunLoop对象

  • Foundation下的获取方法
    • 获得当前线程的RunLoop对象,在子线程调用该方法相当于新创建一个runloop
      • [NSRunLoop currentRunLoop];
    • 获得主线程的RunLoop对象
      • [NSRunLoop mainRunLoop];
  • Core Foundation下的获取方法
    • 获得当前线程的RunLoop对象,在子线程调用该方法相当于新创建一个runloop
      • CFRunLoopGetCurrent();
    • 获得主线程的RunLoop对象
      • CFRunLoopGetMain();

Runloop的相关类

  • 着重了解Core Foundation下的类
    • CFRunLoopRef
    • CFRunLoopModeRef
    • CFRunLoopSourceRef
    • CFRunLoopTimerRef
    • CFRunLoopObserverRef
  • 它们的关系如图所示
  • 一个runloop内部包含若干个Mode,而每个Mode下又包含了若干个timer,observer,source.
  • 调用 RunLoop 的主函数时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode
  • 如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。
image

CFRunLoopRef

  • 即代表runloop本身

CFRunLoopModeRef

  • 代表RunLoop的运行模式
  • 系统默认注册了5个mode,前两个常用,后三个基本用不到
    • kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个Mode下运行
    • UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
    • UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用
    • GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到
    • kCFRunLoopCommonModes: 这是一个占位用的Mode,不是一种真正的Mode

CFRunLoopSourceRef

  • 代表事件源(输入源)
  • 以port进行区分,而port可以为系统
  • 分为系统方法和自定义方法
    • sourse0:非基于port的,自定义方法,响应
    • sourse1:基于port的,系统提供的方法

CFRunLoopTimerRef

  • 基于时间的触发器
  • 基本上就等效于NSTimer

CFRunLoopObserverRef

  • 观察者,用于监听RunLoop的状态改变
  • 监听以下几个状态
/* Run Loop Observer Activities */
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
};

图解

image

image
  • 从图上看就是一直在循环,然后响应对应的事件,有就处理,没有就休息。
  • 在即将进入loop的时候会有一个判空操作,如果内部没有任何的source、timer、observer等待着处理,那么runloop会直接退出,所以当我们在子线程开启runloop的时候需要注意两点:
// 1.要给runloop添加一个事件,让它先跑起来再说
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
 // 2.需要手动开启它
[[NSRunLoop currentRunLoop] run];

Runloop的应用

1.处理NSTimer滑动暂停的问题。
// 通过timerWithTimeInterval创建出来的timer,默认不会被添加到runloop,需要手动添加指定mode
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];

// 通过timer的scheduledTimerWithTimeInterval创建出来的timer,默认被添加到runloop的NSDefaultRunLoopMode下
// 当滑动scrollView时,runloop会切换到UITrackingRunLoopMode
// 也就导致之前NSDefaultRunLoopMode下的timer暂停
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
// 解决方法,手动将timer添加到NSDefaultRunLoopMode下
// NSDefaultRunLoopMode表示timer既能响应UITrackingRunLoopMode,
// 也能响应NSDefaultRunLoopMode
// 相当于将timer拷贝了一份放在这两个mode下
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];

2.在某个mode下调用方法。
// 只在NSDefaultRunLoopMode模式下显示图片
[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"placeholder"] afterDelay:3.0 inModes:@[NSDefaultRunLoopMode]];

3.更好的理解自动释放池(@autoreleasepool)
  • @autoreleasepool会在runloop进入休眠前统一释放,在下一次即将进入runloop时重新创建
  • 具体验证可以通过创建observer来观察
// 创建observer
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
    NSLog(@"----监听到RunLoop状态发生改变---%zd", activity);
});

// 添加观察者:监听RunLoop的状态
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);

// 释放Observer
CFRelease(observer);

4. 创建一个常驻线程
  • 比如我们需要创建一个子线程,让这个线程不死,一直循环做一些事情,比如说后台不停的监控用户的网络状态,扫描文件等。
  • 这时就可以为子线程创建一个runloop,让它跑起来,有事情的时候做事情,没事情的时候休息
#import "ViewController.h"

@interface ViewController ()
/** 线程对象 */
@property (nonatomic, strong) NSThread *thread;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
    [self.thread start];
}

- (void)run
{
// 让线程不死的一种取巧做法,
// 不停的开启runloop,
// 每次runloop发现如果没有任何的port就直接退出了,
// 当我们调用touchesBegan为runloop添加了一个source时,runloop才正在跑起来了
    while (1) {
        [[NSRunLoop currentRunLoop] run];
    }
}

- (void)run1
{
    // 推荐开启常驻线程的办法
    // 手动添加一个port,让它跑起来
    [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop] run];
}

- (void)test
{
    NSLog(@"----------test----%@", [NSThread currentThread]);
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    [self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
}


关于runloop的面试题

1.什么是Runloop?

  • 从字面意思上理解为:运行循环、跑圈;
  • 其实它内部是do-while循环,在这个循环内部不断的处理各种任务(比如Timer、Sources、Observer)
  • 一个线程对应一个runloop,主线程的runloop默认已经启动,子线程的runloop需要手动开启(通过调用run方法)
  • runloop只能选择一个mode启动,如果当前mode中没有任何Timer、Sources、Observer。那么则直接退出runloop.

2.自动释放池什么时候释放?

  • 在runloop睡眠之前释放(kCFRunLoopBeforeWaiting),在下一次跑圈的时候重新创建.

3.在开发中如何使用runloop?

  • 开启一个常驻线程(即让一个子线程不进入消亡状态,等待其他线程发来消息,处理其他事件)
    • 在子线程开启定时器
    • 在子线程进行一些长期监控(比如用户的网络状态,扫描用户的文件等)
  • 可以控制定时器在特定mode下运行
  • 可以让某些事件(行为、任务)在特定mode下执行
  • 可以添加observe监听runloop的一些状态,比如监听点击事件的处理(在所有点击事件之前做一些事情)

4.ARC下是否还需要进行内存管理?

  • 需要。即便项目是ARC的情况下,针对Core Foundation下创建的对象也需要进行内存管理,因为ARC是针对OC而言的,而Core Foundation针对的是C语言.
    凡是带有Create、Copy、Retain等字眼的函数,最后都要记得调用CFRelease(对象)

5.NSTimer受runloop的影响,精度上存在误差,如何解决?

  • 使用GCD创建timer,创建出来的timer不受runloop的影响,不会被添加到任何mode下,精度更高.

写在最后

很久很久没有更新博客,一直忙于日常的工作,有时候学了新东西想写一写,可能发现网上早已经有了很多关于这方面的文章,于是便放弃了写下去的念头。其实,人都是有惰性的,总觉得看现成的比自己去写一写要来得快一些,但是整理知识点的过程,我们实际上也在加深一遍理解,而学习是一个不断重复的过程。对于已经掌握了相关知识的人,这种总结性文章可能毫无意义,但是对于想入门学习的人,文章能够做到浅显易懂,它就是有价值的。做了很久的开发,发现实际编码中我们真的很渺小,我们总是在搭建UI,创建model,网络请求,数据填充,搞点炫酷的动画,和产品经理撕逼。即便你会各种黑魔法,各种超能力,能够用到的机会其实并不多。所以越是基础的东西越需要打牢,有了基础才能举一反三,才能一步一步的去解决更多刁钻的需求。

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

推荐阅读更多精彩内容