手记

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下的获取方法

    • [NSRunLoop mainRunLoop];

    • [NSRunLoop currentRunLoop];

    • 获得当前线程的RunLoop对象,在子线程调用该方法相当于新创建一个runloop

    • 获得主线程的RunLoop对象

  • Core Foundation下的获取方法

    • CFRunLoopGetMain();

    • CFRunLoopGetCurrent();

    • 获得当前线程的RunLoop对象,在子线程调用该方法相当于新创建一个runloop

    • 获得主线程的RunLoop对象


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,需要手动添加指定modeNSTimer *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来观察

// 创建observerCFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {    NSLog(@"----监听到RunLoop状态发生改变---%zd", activity);
});// 添加观察者:监听RunLoop的状态CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);// 释放ObserverCFRelease(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,网络请求,数据填充,搞点炫酷的动画,和产品经理撕逼。即便你会各种黑魔法,各种超能力,能够用到的机会其实并不多。所以越是基础的东西越需要打牢,有了基础才能举一反三,才能一步一步的去解决更多刁钻的需求。



作者:RocKwok
链接:https://www.jianshu.com/p/f930c8bf75ec


0人推荐
随时随地看视频
慕课网APP