iOS知识点iOS DeveloperiOS

RunLoop 学习及常见问题

2017-05-03  本文已影响361人  花与少年_

什么是 RunLoop

通常在终端中输入命令,执行任务的线程执行完就退出了,等我们再次输入命令,终端再开始执行任务。但在我们的 app 中,要保持一直运行(除非app被挂起),不断接受用户的输入,循环的接受、处理事件,类似于这样:

while(AppIsRunning){  //只要 app 处于运行状态,就要不断等待着处理事件
    id whoWakesMe = SleepForWakingUp();
    id event = GetEvent(whoWakesMe);
    HandleEvent(event);
}

RunLoop 来帮助线程管理一个或多个事件或消息,接受用户输入等事件源,在事件到达时,RunLoop 立刻唤醒线程来处理事件;没有事件需要处理时,RunLoop 帮助线程休眠,避免其占用资源,这里是帮助其休眠,而不是直接退出。
RunLoop 还决定了程序在何时应该处理那些事件,并且为被调用的对象维护一个消息队列,被调用方从这个消息队列中取出需要他处理的事件。

主线程的 RunLoop 默认开启,而子线程需要调用[NSRunLoop currentRunLoop]创建和获取 RunLoop,RunLoop 的销毁发生在线程结束时。

RunLoop 与线程的关系
每个线程创建的时候,都有一个 RunLoop 循环,与线程一一对应。

RunLoop 构成

RunLoop构成

如图可以看到 RunLoop 的大致构成,它与线程一一对应,而拥有多个CFRunLoopMode,mode 是一系列输入事件源、计时器、runLoop 观察者的集合。

RunLoop Mode

RunLoop 只能选择一个 Mode 启动,同时在“跑”的时候,总是在特定的唯一的 mode 下,每次运行 RunLoop 都要显式或隐式的指定运行 mode。这个 mode 包含了当前需要处理的 Source/Timer/Observer,所以 RunLoop 在时刻内,仅能处理与当前 mode 相关联的事件,只有和模式相关的源才会被监视,并允许他们传递事件消息。

为了保证其中的 Source/Timer/Observer 与其他 mode 的相隔离,切换 mode 时,只能先退出当前RunLoop,再以要切换的 mode 重新进入RunLoop。

开发中,通常会遇到这几种Mode:

RunLoop Source

线程的异步事件源,数据源。有两种Source,可以用是否基于Mach Port(进程间通讯接口)区分:

RunLoop Timer

线程的同步事件源,在预设的时间点到了之后同步的发给线程处理此事件。

RunLoop Observer

Observer 可对 RunLoop 的状态变化进行观察,可观察的变化:

因为Observer可对这些事件进行观察追踪,所以也可被看作是一种事件源。

RunLoop处理的流程

RunLoop_1.png

第7步中,当线程进入休眠,发生下列事件,线程将被唤醒:

第9步中,处理唤醒时收到的消息,并且:

RunLoop应用举例

在漫长长长长的理论说明后,让我们看看实际开发中,有哪些地方会用到 RunLoop 呢?

解决 NSTimer "不准"的问题

我们有时候会发现 NSTimer "不太准",明明时间已经到了,该执行的回调却未发生,这是因为我们常常将 NSTimer 默认设置为default mode,如果这时屏幕滚动,mode切换为TrackingMode,时间到了,但是 TrackingMode 无法处理 defaultMode下的回调,造成"不准"。
在 SVProgressHUD 中,我们可以设置转圈的提示框自动消失,可开启一个定时器,在到了设定的时间点后消失,如下

strongSelf.fadeOutTimer = [NSTimer timerWithTimeInterval:duration target:strongSelf selector:@selector(dismiss) userInfo:nil repeats:NO];
[[NSRunLoop mainRunLoop] addTimer:strongSelf.fadeOutTimer forMode:NSRunLoopCommonModes];

strongSelf 即为提示框,将它消失的定时器添加在 RunLoop 的common 模式下,不管时间点到了的那一时刻 RunLoop 运行在哪个mode下,都会处理消失的回调,"准点消失"。

用 dispatch_after 定时,就准了吗
我发现有很多博客写,NSTimer 造成定时不准的问题可以通过 GCD 中的 dispatch_after 来解决,但是 dispatch_after 并不是说在指定时间后执行处理,而只是在指定时间将操作追加到 Dispatch Queue 中。如果指定时间到了,需要加入的队列正在进行耗时操作,定时操作并不能立即执行,也会造成不准。
验证如下:

    //获取主队列
    dispatch_queue_t mainQueue = dispatch_get_main_queue();
    //定时时间
    int64_t delay = 5 * NSEC_PER_SEC;
    //定时时间,即从现在到定时的时间
    dispatch_time_t delayTime = dispatch_time(DISPATCH_TIME_NOW, delay);

    NSLog(@"开始计时: %@", [NSDate date]);

    dispatch_after(delayTime, mainQueue, ^{
        NSLog(@"时间到: %@", [NSDate date]);
    });

    //在这里设置一些复杂操作,比方来10000次网络请求


可以看到虽然我们只设置延迟5秒进行,但事实上,在10秒才进行了延迟操作。但是日常的开发中,碰到这么这么复杂的情况应该是比较少的,所以 dispatch_after 也可以一用~~~
GCD 中除了主要的 Dispatch Queue 之外,还对 BSD 系内核惯有功能 kqueue 进行包装,可处理内核中发生的各种事件及方法。
其中的 DISPATCH_SOURCE_TYPE_TIMER 可作为定时器,帮助我们延迟调用:


    //获取主队列
    dispatch_queue_t mainQueue = dispatch_get_main_queue();
    //新生成一个定时器,且此定时器不能为局部变量,否则方法执行完就被销毁了,还怎么做定时后的回调呢?
    self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, mainQueue);
    //定时时间
    int64_t delay = 5 * NSEC_PER_SEC; 
    //一定容差范围时间
    int64_t leeway = 0.1 * NSEC_PER_SEC; 
    //定时时间,即从现在到定时的时间
    dispatch_time_t delayTime = dispatch_time(DISPATCH_TIME_NOW, delay);

    //设置定时器
    //下一次回调为DISPATCH_TIMER_FOREVER,表示不需要重复
    dispatch_source_set_timer(self.timer, delayTime,DISPATCH_TIMER_FOREVER, leeway);

    //设置时间到了后的回调
    __weak typeof(self) weakSelf = self;
    dispatch_source_set_event_handler(self.timer, ^{
        typeof(self) strongSelf = weakSelf;
        NSLog(@"计时结束: %@", [NSDate date]);
        dispatch_source_cancel(strongSelf.timer);
    });

    //启动定时器
    dispatch_resume(self.timer);

保证线程的持续运行

在 AFNetworking 2.3 中,需要一个自定义线程接受 connection 回调,一开始初始化线程时,没有需要执行的操作,线程会退出(RunLoop中没有source/timer/observer 会立即退出)。为其添加一个MachPort,为了保证线程的存活。

+ (NSThread *)networkRequestThread {
    static NSThread *_networkRequestThread = nil;
    static dispatch_once_t oncePredicate;
    dispatch_once(&oncePredicate, ^{
        //初始化线程时,调用networkRequestThreadEntryPoint方法
        _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
        [_networkRequestThread start];
    });
    return _networkRequestThread;
}

+ (void)networkRequestThreadEntryPoint:(id)__unused object {
    @autoreleasepool {
        [[NSThread currentThread] setName:@"AFNetworking"];
        //为线程创建RunLoop
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        //为RunLoop添加事件,保证其持续运行
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode]; 
        [runLoop run];
    }
}
解决TableView加载图片时,滑动很卡

TableView 需要加载大量图片时,滑动后,界面会卡,这是因为此时RunLoop 运行在 UITrackingRunLoopMode 下,图片加载在当前mode下,cpu 又要处理加载图片事件,又要处理滑动事件,造成卡顿。
可以显式地将图片的加载设置在 NSDefaultRunLoopMode 下,滑动时的 UITrackingRunLoopMode 并不会去加载图片,解决卡顿问题。

[self.imageView performSelector:@selector(setImage:) withObject:downloadImage afterDelay:0 inModes:@[NSDefaultRunLoopMode]];
自动释放池到底在何时释放?

我们知道,手动指定 autoreleasepool 中的对象,会在作用域结束时释放掉。而设置为 autorelease 的对象是在出了作用域之后,被自动添加到最近创建的自动释放池中。那么这个自动释放池迟早有被撑满需要释放的时刻,这个自动释放池具体是什么时候被释放呢?

在Cocoa框架中,相当于程序主循环的NSRunLoop或者在其他程序可运行的地方,对 NSAutoreleasePool 对象进行生成、持有和废弃处理。
---引自《Objective-C 高级编程》
而它能够释放的原因是系统在每个 runloop 迭代中都加入了自动释放池 Push 和 Pop

下面我们举例讨论下:

@property (nonatomic,weak)NSString * weakStr;

- (void)viewDidLoad {
        [super viewDidLoad];
        NSString *string = [NSString stringWithFormat:@"这个string要设置的很长长长长长长长长长长长长长长长长"];
        //因为苹果引用Tagged Pointer专门存储小的对象,直接存储其值,而不是存储地址
        //如果string很短,用Tagged Pointer存储,无法验证其自动释放,地址被收回的过程
        weakStr = string;

        NSLog(@"viewDidLoad:%@",weakStr);
        NSLog(@"currentRunLoop:%@",[[NSRunLoop currentRunLoop] currentMode]);
}

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
    NSLog(@"viewWillAppear:%@",weakStr);
    NSLog(@"currentRunLoop:%@",[[NSRunLoop currentRunLoop] currentMode]);
}
- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    NSLog(@"viewDidAppear:%@",weakStr);
    NSLog(@"currentRunLoop:%@",[[NSRunLoop currentRunLoop] currentMode]);
}

输出如图:


在mode改变,RunLoop一次循环结束后,autorelease对象被销毁 观察 weakStr 设置方法何时被调用 在viewWillAppear调用结束后,左边的堆栈中出现了一次AutoreleasePoolPage pop操作

我们在viewDidLoad方法中,用stringWithFormat类方法生成一个字符串,这种方法生成的字符串默认被添加进 autoreleasepool 中。
viewDidLoad 和 viewWillAppear 还在app初始化的 UIInitializationRunLoopMode 下,而 viewDidAppear 已经进入了默认mode下了。期间,autoreleasepool 出现了一次销毁,其中的对象也就被销毁了。
所以说,在没有手加Autorelease Pool的情况下,Autorelease对象是在当前的runloop迭代结束时释放的,而它能够释放的原因是系统在每个runloop迭代中都加入了自动释放池Push和Pop。

关于RunLoop的一道题
NSRunLoop 的描述正确的是( )
A. RunLoop 决定程序在何时应该处理哪些 Event
B. Cocoa 中的 NSRunLoop 类并不是线程安全的
C. RunLoop 可以使程序一直运行接受用户输入
D. RunLoop 起到了调用解耦的作用
我怎么觉得 ABCD 四个选项都对嘞……

参考文章:
RunLoops 官方文档
深入理解RunLoop
黑幕背后的Autorelease
Objective-C Autorelease Pool 的实现原理
RunLoop个人小结

上一篇下一篇

猜你喜欢

热点阅读