美文网首页iOS
Runloop学习总结

Runloop学习总结

作者: 806349745123 | 来源:发表于2016-11-23 09:25 被阅读45次

什么是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

相关文章

  • NSRunLoop

    前言 RunLoop的初期学习总结,后续会持续研究更新。 一、Runloop定义及作用 1. 什么是Runloop...

  • RunLoop学习总结

    RunLoop的定义 当有持续的异步任务需求时,我们会创建一个独立的生命周期可控的线程。RunLoop就是控制线程...

  • RunLoop学习总结

    简介 Runloop可以保证程序会一直运行并且时时刻刻在等待用户的输入操作。RunLoop可以在需要的时候运行,在...

  • Runloop学习总结

    什么是Runloop · 一般来讲,一个线程一次只能执行一个任务,执行完成后线程就会退出。如果我们需要一个机制,让...

  • RunLoop学习总结

    什么是RunLoop 从字面上看,就是运行循环,跑圈 其实它内部就是do-while循环,在这个循环内部不断地处理...

  • RunLoop学习总结

    通过以下文章学习记录 关于Runloop的原理探究及基本使用 深入理解RunLoop RunLoop完全指南 Ru...

  • GeekBand - iOS 多线程和RunLoop 总结

    iOS 开发高级进阶 第三周 多线程 Runloop iOS 多线程以及 RunLoop 学习总结 基础知识 什么...

  • RunLoop的一些学习与总结

    最近在学习一些OC底层的东西, 下面是学习了RunLoop的一些总结和感受^^ 首先,RunLoop的作用 从字面...

  • RunLoop学习与总结

    RunLoop一个运行循环保持程序的持续运行监听处理 APP 各种事件(触摸,定时器,selector)节省 CP...

  • RunLoop

    RunLoop 文章已经很多了,结合各大文章做个总结 什么是 RunLoop RunLoop 人如其名,run 跑...

网友评论

    本文标题:Runloop学习总结

    本文链接:https://www.haomeiwen.com/subject/wmcmpttx.html