美文网首页iOS知识点iOS DeveloperiOS
RunLoop 学习及常见问题

RunLoop 学习及常见问题

作者: 花与少年_ | 来源:发表于2017-05-03 16:48 被阅读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:

    • kCFRunLoopDefaultMode:app的默认 Mode,通常主线程在这个 Mode 下运行。
    • UITrackingRunLoopMode:界面跟踪 Mode,ScrollView 的触摸滑动 mode (在iOS中,触摸滑动很流畅的原因是在滑动时,只处理此 mode 下的事件且不受其他mode影响)。
    • UIInitializationRunLoopMode:刚启动 app 进入的第一个 mode,起到过渡的作用,启动完成后不再使用。
    • GSEventReceiveRunLoopMode: Graphic 相关事件的 mode,通常用不到。
    • kCFRunLoopCommonModes:将 mode 标记为"common"属性,当 RunLoop 运行在标记为"common"属性的任一 mode 下,发生事件时,里面的 mode 都会被触发。
    RunLoop Source

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

    • source0:不基于Mach Port,处理app内部事件,用户自定义的thread发出。当我们使用 NSObject 中的 performSelector 系列方法时,都是source0 事件源。
    • source1:基于Mach Port,是由RunLoop和内核管理的。
    RunLoop Timer

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

    RunLoop Observer

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

    • 刚进入此 RunLoop 中
    • RunLoop 准备处理一个 Timer
    • RunLoop 准备处理一个 Input Source
    • RunLoop 准备进入睡眠
    • RunLoop 将被唤醒处理事件之前
    • RunLoop 准备退出

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

    RunLoop处理的流程

    RunLoop_1.png

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

    • 基于 Port 的事件发生
    • 计时器到时
    • 被代码显式唤醒

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

    • 如果是用户定义的计时器到时,处理事件并重启 RunLoop
    • 如果有input 事件源,传递这个消息
    • 如果runloop显式被唤醒,且没有超时,重启RunLoop
      之后,跳回第2步

    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个人小结

    相关文章

      网友评论

        本文标题:RunLoop 学习及常见问题

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