Runloop学习总结

什么是Runloop

· 一般来讲,一个线程一次只能执行一个任务,执行完成后线程就会退出。如果我们需要一个机制,让线程能随时处理事件但并不退出,通常的代码逻辑是这样的:

function loop() {
    initialize();
    do {
        var message = get_next_message();
        process_message(message);
    } while (message != quit);
}

· Runloop类似于一个while循环,循环执行代码,保持程序的持续运行。
· RunLoop 实际上就是一个对象,这个对象管理了其需要处理的事件和消息,并提供了一个入口函数来执行上面 Event Loop 的逻辑。线程执行了这个函数后,就会一直处于这个函数内部 "接受消息->等待->处理" 的循环中,直到这个循环结束(比如传入 quit 的消息),函数返回。

· 在iOS的工程的main.m文件中我们可以看到这样的代码:

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

UIApplicationMain函数内部就启动了一个Runloop,使App一直运行,这个默认开启的Runloop默认和主线程关联起来。

· 新建一个工程,在storyboard上加上按钮,运行


Paste_Image.png

结果如下:

Paste_Image.png

从Xcode左上角看的出来程序一直在运行


Paste_Image.png

当把代码改为:

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

main函数直接返回0,AppDelegate里面的方法没有执行,然后程序就就退出了。

Paste_Image.png

再把代码修改如下:

int main(int argc, char * argv[]) {
    @autoreleasepool {
        NSLog(@"%@", @"这里会打印");
        int result = UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
        NSLog(@"%@", @"这里不会打印");
        return result;
    }
}

运行结果如下:

Paste_Image.png

程序执行了UIApplicationMain后开启了默认的Runloop,一直循环15行,所以16行代码永远没有执行。
Runloop可以看作下面的伪代码:

int main(int argc, char * argv[]) {
   BOOL AppIsRunning = YES;
   while (AppIsRunning) {
        id whoWakesMe = SleepForWakingUp();
        id event = GetEvent(whoWakesMe);
        HandleEvent(event);
    }
    return 0;
}

Runloop有什么用处

1、使程序一直运行接受用户输入
2、决定程序在何时应该处理哪些Event
3、调用解耦(对于编程经验为0的完全没搞懂这个意思,解释为Message Queue)
4、节省CPU时间


<br />

Runloop的机制

(套用sunnnyxx 在视频中提供的资料)

Paste_Image.png
Paste_Image.png
Paste_Image.png

Runloop事件队列

Paste_Image.png
Paste_Image.png
Paste_Image.png

RunLoop的挂起与唤醒
从伪代码可以看出

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

<br />
Runloop对象
1.iOS中有2tAPI来访问和使用RunLoop
-Foundation 框架
NSRunLoop
-Core Foundation
CFRunLoopRef
2.NSRunLoop和CFRunLoopRef都代表着RunLoop对象
3.NSRunLoop是基于CFRunLoopRef的一层OC包装, 所以要了解RunLoop内部结构,需要多研究CFRunLoopRef层面的API (Core Foundation 层面)

a 主线程的runloop自动创建,子线程的runloop默认不创建(在子线程中调用NSRunLoop *runloop = [NSRunLoop currentRunLoop];获取RunLoop对象的时候,就会创建RunLoop);
b runloop退出的条件:app退出;线程关闭;设置最大时间到期;modeItem为空;
c 同一时间一个runloop只能在一个mode,切换mode只能退出runloop,再重进指定mode(隔离modeItems使之互不干扰);
d 一个item可以加到不同mode;一个mode被标记到commonModes里(这样runloop不用切换mode)。

<br />Source是RunLoop的数据源抽象类(protocol)

RunLoop定义了两个Version的Source:
1、Source0:处理App内部事件、App自己负责管理(触发),如UIEvent、CFSocket
2、Source1:由RunLoop和内核管理,Mach port驱动,如CFMachPort、CFMessagePort
如有需要,可从中选择一种来实现自己的Source
上一条基本不会发生

<br />RunLoopTimer的封装

// 创建但是不会加入当前 Runloop
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;

// 创建但是加入当前 Runloop 的 NSDefaultRunLoopMode 并执行
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;

- (void)performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay inModes:(NSArray *)modes;

+ (CADisplayLink *)displayLinkWithTarget:(id)target selector:(SEL)sel;
- (void)addToRunLoop:(NSRunLoop *)runloop forMode:(NSString *)mode;

<br />CFRunLoopObserver
向外部报告RunLoop当前状态的更改,框架中很多机制都由RunLoopObserver触发,如CAAnimation

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

UIKit通过RunLoopObserver在RunLoop两次Sleep间对AutoreleasePool进行Pop和Push,将这次Loop中产生的Autorelease对象释放
Runloop的寄生于线程:一个线程只能有唯一对应的runloop;但这个根runloop里可以嵌套子runloops;
自动释放池寄生于Runloop:程序启动后,主线程注册了两个Observer监听runloop的进出与睡觉。一个最高优先级OB监测Entry状态;一个最低优先级OB监听BeforeWaiting状态和Exit状态。
线程(创建)-->runloop将进入-->最高优先级OB创建释放池-->runloop将睡-->最低优先级OB销毁旧池创建新池-->runloop将退出-->最低优先级OB销毁新池-->线程(销毁)

Paste_Image.png

<br />CFRunLoopMode

  • RunLoop在同一段时间只能且必须在一种特定Mode下Run
  • 更换Mode时,需要停止当前Loop,然后重启新Loop
  • Mode是iOS App滑动顺畅的关键
  • 可以定制自己的Mode
// 默认状态、空闲状态
NSDefaultRunLoopMode
// 滑动ScrollView时
UITrackingRunLoopMode
// 私有,App启动时
UIInitializationRunLoopMode
// Mode集合,可以理解为 NSDefaultRunLoopMode 和 UITrackingRunLoopMode 的集合
NSRunLoopCommonModes
Runloop与GCD任务:

当调用 dispatch_async(dispatch_get_main_queue(), block) 时,libDispatch 会向主线程的 RunLoop 发送消息,RunLoop会被唤醒,并从消息中取得这个 block,并在回调里执行这个 block。Runloop只处理主线程的block,dispatch 到其他线程仍然是由 libDispatch 处理的。

关于网络请求

iOS 中,关于网络请求的接口自下至上有如下几层:

CFSocket
CFNetwork       ->ASIHttpRequest
NSURLConnection ->AFNetworking
NSURLSession    ->AFNetworking2, Alamofire

1.CFSocket 是最底层的接口,只负责 socket 通信。
2.CFNetwork 是基于 CFSocket 等接口的上层封装,ASIHttpRequest 工作于这一层。
3.NSURLConnection 是基于 CFNetwork 的更高层的封装,提供面向对象的接口,AFNetworking 工作于这一层。
4.NSURLSession 是 iOS7 中新增的接口,表面上是和 NSURLConnection 并列的,但底层仍然用到了 NSURLConnection 的部分功能 (比如 com.apple.NSURLConnectionLoader 线程),AFNetworking2 和 Alamofire 工作于这一层。

下面主要介绍下 NSURLConnection 的工作过程。

通常使用 NSURLConnection 时,你会传入一个 Delegate,当调用了 [connection start] 后,这个 Delegate 就会不停收到事件回调。实际上,start 这个函数的内部会会获取 CurrentRunLoop,然后在其中的 DefaultMode 添加了4个 Source0 (即需要手动触发的Source)。CFMultiplexerSource 是负责各种 Delegate 回调的,CFHTTPCookieStorage 是处理各种 Cookie 的。

当开始网络传输时,我们可以看到 NSURLConnection 创建了两个新线程:com.apple.NSURLConnectionLoader 和 com.apple.CFSocket.private。其中 CFSocket 线程是处理底层 socket 连接的。NSURLConnectionLoader 这个线程内部会使用 RunLoop 来接收底层 socket 的事件,并通过之前添加的 Source0 通知到上层的 Delegate。



NSURLConnectionLoader 中的 RunLoop 通过一些基于 mach port 的 Source 接收来自底层 CFSocket 的通知。当收到通知后,其会在合适的时机向 CFMultiplexerSource 等 Source0 发送通知,同时唤醒 Delegate 线程的 RunLoop 来让其处理这些通知。CFMultiplexerSource 会在 Delegate 线程的 RunLoop 对 Delegate 执行实际的回调。


Runloop实验

实验一
- (IBAction)buttonDidClick:(id)sender {
    [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];
}
     
- (void)timerTest {
    NSLog(@"%s", __func__);
}

输出结果

Paste_Image.png
实验二

把代码改成如下,输入结果一样

NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];
 [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];

如果把[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];屏蔽,会发现没有打印东西,因为timerWithTimeInterval这个方法只是创建了并没有加入Runloop

实验三 有scrollView的情况下使用Timer

在实验二的基础上,在vc中加一个textView,run起来,模拟器界面如下:

Paste_Image.png

点击按钮,然后滚动scrollView,在停止滚动,打印结果

Paste_Image.png

可以看的出来滚动的时间段,timer并没有效果,那是因为滚动的时候主线程Runloop已经切换mode为UITrackingRunLoopMode,Runloop只能指定一个mode,而timer只是加在NSDefaultRunLoopMode,所以发生滚动的时候,Runloop并不会响应timer;当松开手的时候Runloop切换回NSDefaultRunLoopMode,timer就重新起作用。

当我们把timer的mode修改为NSRunLoopCommonModes,此时滚动scrollView的同时也能响应timer:

NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];
 [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

<br />

实验四 CFRunLoopSourseRef的实验

我们在button的响应注释,然后打个断点,run后点击button会发现如下类似这种UIEvent是属于Souce0

Paste_Image.png

<br />

实验五 CFRunLoopObserverRef的实验

- (void)createObserver {
    // 创建监听者对象
    // rl: RunLoop
    // observer: 监听者对象
    // mode: Runloop所在的mode
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        NSLog(@"observer--------%lu", activity);
    });
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
    CFRelease(observer);
}
Paste_Image.png

根据CFRunLoopActivity枚举,我们可以看出Runloop的状态变化
1:即将进入Runloop-> 2:即将处理NSTimer-> 4:即将处理Souce0 -> 32:即将进入休眠 -> 64:从休眠仲唤醒

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

实验更新

代码:

- (IBAction)buttonDidClick:(id)sender {
    NSLog(@"%s", __func__);
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        _myThread = [NSThread currentThread];
        [[NSRunLoop currentRunLoop] run];
        NSLog(@"%@",  @"+++++");
    });
}

- (void)timerTest {
    NSLog(@"%s", __func__);
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [self performSelector:@selector(myThreadTest) onThread:_myThread withObject:nil waitUntilDone:NO];
}

- (void)myThreadTest {
    NSLog(@"%s", __FUNCTION__);
}

点击按钮后打印出+++++,然后点击屏幕空白处- (void)myThreadTest并没有触发。

Paste_Image.png

这是因为_myThread中的Runloop只run了一次就退出了,从而子线程没有监听到屏幕的点击事件。只run一次的原因首先看这张图

Paste_Image.png

代码中只是让子线程的运行循环run了一次,并没有加入实质的source、port、Observer或者timer,Runloop直接跑一次直接退出了,导致点击时间没有Runloop来响应。

要响应- (void)myThreadTest必须要子线程的Runloop保持驻留状态,给Runloop添加一个port让其保持驻留,此时我们点击button之后再点击屏幕空白处可以看到打印出来的日志,可以看的出来点击事件已经起效了,并且+++++也没有打印出来,那是因为子线程的运行循环已经驻留,循环外面的代码就执行不到。

- (IBAction)buttonDidClick:(id)sender {
    NSLog(@"%s", __func__);
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        _myThread = [NSThread currentThread];
        [[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [[NSRunLoop currentRunLoop] run];
        NSLog(@"++++");
    });
}
Paste_Image.png

Runloop使用

AFNetworking中RunLoop的创建
+ (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;
}
利用Runloop有话UITableView

因为UITableView滚动的时候主线程Runloop的mode切换为UITrackingRunLoopMode,当停止滚动的时候会切回NSDefaultRunLoopMode,从而可以减轻UITableView的卡顿。

    UIImage *downloadedImage = ...;
    [self.avatarImageView performSelector:@selector(setImage:)
                               withObject:downloadedImage
                               afterDelay:0
                                  inModes:@[NSDefaultRunLoopMode]];

参考资料:
http://blog.ibireme.com/2015/05/18/runloop/
http://www.jianshu.com/p/37ab0397fec7
https://yun.baidu.com/share/link?shareid=2268593032&uk=2885973690

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

推荐阅读更多精彩内容

  • 通过阅读YY大神的博客深入理解RunLoop还有观看了孙源大大@sunnyxx录制的RunLoop视频,总算对Ru...
    巫师学徒阅读 305评论 0 0
  • 什么是RunLoop 从字面上看,就是运行循环,跑圈 其实它内部就是do-while循环,在这个循环内部不断地处理...
    zhazha阅读 1,409评论 1 7
  • Runloop是iOS和OSX开发中非常基础的一个概念,从概念开始学习。 RunLoop的概念 -般说,一个线程一...
    小猫仔阅读 976评论 0 1
  • 随着年龄的增长,在爱情中,最终和我们在一起的往往并不是当初我们爱的死去活来的人,取而代之的是那位愿意陪伴在我们身边...
    琛筱阅读 263评论 0 1
  • 步骤: 一、先打形,画出大概轮廓。(用笔要轻,尽量用直线画线,先不要纠结细节。) 二、根据大形画出花的具体轮廓,运...
    南方小花CX阅读 862评论 0 4