iOS底层-Runloop(二)

RunLoop的应用

1. 定时器的影响

  1. 用NSTimer时,一旦页面滚动定时器就会停止(前面已举例)
    1. 原因:默认定时器在kCFRunLoopDefaultMode模式下,一点滚动就会切换到UITrackingRunLoopMode模式下,所以定时器滚动时无效
    2. 解决办法:使用NSRunLoopCommonModes模式
  2. 除了上面的解决办法,还可以使用GCD
    1. 上面说过,一般情况下GCD不依赖于Runloop,它有自己的逻辑处理,跟Runloop无关
    2. 代码举例:

       #import "ViewController.h"
       @interface ViewController ()
       /** 定时器(这里不用带*,因为dispatch_source_t就是个类,内部已经包含了*) */
       @property (nonatomic, strong) dispatch_source_t timer;
       @end
       @implementation ViewController
       int count = 0;
       - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
       {
           // 获得队列
           dispatch_queue_t queue = dispatch_get_main_queue();
           // 创建一个定时器(dispatch_source_t本质还是个OC对象)
           self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
           // 设置定时器的各种属性(几时开始任务,每隔多长时间执行一次)
           // GCD的时间参数,一般是纳秒(1秒 == 10的9次方纳秒)NSEC_PER_SEC = 1s
           // dispatch_time(DISPATCH_TIME_NOW, 3.0 * NSEC_PER_SEC) 比当前时间晚3秒
          //当前时间直接写:DISPATCH_TIME_NOW
           dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC));
           uint64_t interval = (uint64_t)(1.0 * NSEC_PER_SEC);
           dispatch_source_set_timer(self.timer, start, interval, 0);
           // 设置回调
           dispatch_source_set_event_handler(self.timer, ^{
               NSLog(@"------------%@", [NSThread currentThread]);
               count++;
                       if (count == 4) {
                           // 取消定时器
                           dispatch_cancel(self.timer);
                           self.timer = nil;
                       }
           });
           // 启动定时器
           dispatch_resume(self.timer);
       }
       @end
      

2. 常驻线程

  1. 常驻线程:让一个线程永远不死
  2. 示例代码

     #import "ViewController.h"
     #import "ZHThread.h"
     @interface ViewController ()
     @property (nonatomic,strong) ZHThread *thread;
     @end
     /*
      通过继承NSThread并且从写delloc监听线程,可以看到,开启线程后执行完任务立马死掉
      注意:不能简单搞个属性强引用,因为线程一旦执行完毕,即使不释放掉,当前的线程也处于消亡状态,从新开启([self.thread start])会崩溃
      当然不通过start调用,通过performselector调用虽然不会崩溃,但是不会调用run2,因为线程处于消亡状态
      //解决办法:线程中添加runloop
      好处:可以随时让拿到这个线程让他做一些事情.没有做事情之前,这个runloop处于休眠状态,一旦调用该线程处理事件,就回唤醒runloop去执行该线程的事件
      使用:搞一个常驻线程监控联网状态
      原理:
      1.创建一个子线程并开启线程
      2.在开启方法(run)中添加一个runloop,并开启runloop
      3.runloop会一直跑圈不会执行完毕,阻止了开启方法的执行完毕即卡住子线程不让他执行完毕
      4.一旦子线程中有事件,runloop会被唤醒,让改事件在该线程中执行,一旦执行完毕,继续跑圈卡住该线程
      如果用一个死循环代替runloop是不可以的,因为死循环一直在处理这个事件,不会停下来先去执行触发事件
      */
     @implementation ViewController
      - (void)viewDidLoad {
         [super viewDidLoad];
         self.thread = [[ZHThread alloc] initWithTarget:self selector:@selector(run) object:nil];
         //执行线程
         [self.thread start];
     }
     -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
         [super touchesBegan:touches withEvent:event];
         //随时拿到这个子线程执行事件
         [self performSelector:@selector(run2) onThread:self.thread withObject:nil waitUntilDone:nil];
     }
     //目的:让这个子线程保活
     -(void)run{
         NSLog(@"=====run====");
         //往里面添加source(mode中啥都没有,runloop没用)
         [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
        //启动runloop
         [[NSRunLoop currentRunLoop] run];
          NSLog(@"=====runloop退出======");
     }
     //子线程调用
     -(void)run2{
          NSLog(@"=====run2====");
     }
    
    1. 上面代码用图解如下:

    图4

  3. 应用(在子线程搞个定时器一直调用)

     //创建子线程初始化调用这个方法(将上面viewdidload方法中run改为runx即可)
     -(void)runx{
     @autoreleasepool {
         //创建定时器
         NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run2) userInfo:nil repeats:YES];
         //将定时器添加到runloop
         [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
         [[NSRunLoop currentRunLoop] run];
         //或者
     //    [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run2) userInfo:nil repeats:YES];
         //放在主线程中就不会用到这句,因为主线程一直在run,不用手动run
     //    [[NSRunLoop currentRunLoop] run];
     }
     }
    

手动控制一个线程的生命周期

  1. 上面的示例代码是线程在整个应用程序中都不死,那么,如果我们想手动控制一个线程的生命周期呢?
  2. 先使用上面代码,在控制器中也实现delloc,然后实现导航控制器的跳转,我们会发现,以下问题
    1. back到上一个控制器时,当前控制器不会销毁:说明上面代码有循环引用
    2. self.thread也不会销毁,那么是不是因为与控制器循环引用了呢?
  3. 代码分析结果:
    1. 经过分析发现 NSThread的initWithTarget:方法会捕获target,导致循环引用
    2. 那么改成这么创建

        self.thread = [[ZHThread alloc]initWithBlock:^{
           NSLog(@"=====run====");
           //往里面添加source(mode中啥都没有,runloop没用)
           [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
           //启动runloop
           [[NSRunLoop currentRunLoop] run];
            NSLog(@"===runloop退出======");
       }];
      
    3. 这么创建,会发现控制器delloc了,但是self.thread仍然没有销毁
      1. 因为self.thread内部有runloop,一直处于休眠状态,那么self.thread就必须一直处理事情,所以self.thread不死
      2. 那么接下来需要实现手动退出runloop
    4. 实现手动退出runloop
      1. 添加一个退出按钮事件

         //手动控制runloop退出
         - (IBAction)stop {
             [self performSelector:@selector(stop) onThread:self.thread withObject:self waitUntilDone:NO];
         }
         //必须是创建的那个线程的runloop,因此,这个方法必须在那个线程中(self.thread)调用
         //停止当前的Runloop,否则,线程永远不死
         -(void)stopThread{
             CFRunLoopStop(CFRunLoopGetCurrent());
             NSLog(@"=====%@", [NSThread currentThread]);
         }
        
      2. 点击手动退出后,然后在点击back到上一界面,发现self.thread仍然没有销毁,那是为什么呢?
        1. 经过排查发现,这个方法有问题:[[NSRunLoop currentRunLoop] run];
        2. 这个方法本质是一个死循环,会一直调用run,专门用来开启一个永不销毁的线程!!!即一旦开启runloop,就不可以控制runloop的退出
      3. 那么将这个方法替换成下面的方法

         [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
        
        1. 换成这个方法后,点击touch界面,处理一次子线程事件后发现,还没有点击退出按钮,runloop直接退出了,但是back上一个控制器,self.thread销毁了
        2. 这个方法,只会启动一次runloop,当前线程只要执行一次事件就会自动退出当前的runloop(即只会处理一次事件,就会退出runloop)
        3. 因此,下面我们只要实现使用这个方法一直开启runloop,而且能够随时控制它退出,就达到目的了
      4. 因此想要线程手动控制,而且不死那就改成这个

         - (void)viewDidLoad {
             [super viewDidLoad];
             __weak typeof(self) weakself  = self;
             self.thread = [[ZHThread alloc]initWithBlock:^{
                 NSLog(@"=====run====");
                 //往里面添加source(mode中啥都没有,runloop没用)
                 [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
                 //启动runloop
                 //搞一个循环,一直循环开启runloop,但是可以控制退出
                 while (!weakself.isstop) {
                     //这个方法,每次掉用就会启动一次runloop,当前线程只要执行一次事件就会自动退出当前的runloop
                     [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
                 }
                 NSLog(@"===runloop退出======");
             }];
             //执行线程
             [self.thread start];
         }
        
        1. 用一个属性来控制runloop是否开启
        2. stopThread方法设置isstop

           //停止当前的Runloop,否则,线程永远不死
           -(void)stopThread{
               self.isstop = YES;
               CFRunLoopStop(CFRunLoopGetCurrent());
               NSLog(@"=====%@", [NSThread currentThread]);
           }
          
  4. 最终代码如下:

     #import "ZHViewController.h"
     #import "ZHThread.h"
     @interface ZHViewController ()
     @property (nonatomic,strong) ZHThread *thread;
     @property (nonatomic, assign) BOOL isstop;
     @end
        
     @implementation ZHViewController
        
     - (void)viewDidLoad {
         [super viewDidLoad];
         self.view.backgroundColor = UIColor.whiteColor;
            
         __weak typeof(self) weakself  = self;
         //initWithTarget这个会捕获target导致循环引用
     //    self.thread = [[ZHThread alloc] initWithTarget:self selector:@selector(run) object:nil];
         self.thread = [[ZHThread alloc]initWithBlock:^{
             NSLog(@"=====run====");
             //run:默认为NSDefaultRunLoopMode模式,时间是永远,下面三行代码等价
             //往里面添加source(mode中啥都没有,runloop没用)
             [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
             //启动runloop
         //注意:!!!!这个方法本质是一个死循环,会一直调用run,专门用来开启一个永不销毁的线程!!!即一旦开启runloop,就不可以控制runloop的退出
             //[[NSRunLoop currentRunLoop] run];
                
             //搞一个循环,一直循环开启runloop,但是可以控制退出
             while (!weakself.isstop) {
                 //这个方法,每次掉用就会启动一次runloop,当前线程只要执行一次事件就会自动退出当前的runloop
                 [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
             }
             NSLog(@"===runloop退出======");
         }];
         //执行线程
         [self.thread start];
     }
        
     -(void)test{
         NSLog(@"==test===%@", [NSThread currentThread]);
     }
        
     //停止当前的Runloop,否则,线程永远不死
     -(void)stopThread{
         self.isstop = YES;
         CFRunLoopStop(CFRunLoopGetCurrent());
         NSLog(@"=====%@", [NSThread currentThread]);
         self.thread = nil;
     }
        
     //手动控制runloop退出
     - (IBAction)stop {
         if(!self.thread) return;
         [self performSelector:@selector(stopThread) onThread:self.thread withObject:self waitUntilDone:NO];
     }
        
     //随时使用子线程调用
     -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
         if(!self.thread) return;
         [self performSelector:@selector(test) onThread:self.thread withObject:self waitUntilDone:NO];
         NSLog(@"=====%s",__func__);
            
     }
        
     //一旦在这调用,程序就会崩溃????
     - (void)dealloc{
         NSLog(@"=====%s",__func__);
         [self stop];
     }
        
     @end
    
    1. 问题:
      1. 如果先点击stop退出runloop,在back到上一个控制器,则当前控制器、self.thread都销毁,没有问题
      2. 如果不点击stop退出runloop,直接back到上一个控制器,当前控制器指向delloc,但是程序会崩溃在 [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];这一句,为什么呢?
      3. 控制器的delloc里面已经执行stop了呀? 难道跟手动点击stop有什么区别呢?
    2. 经过代码分析,发现是这个方法的问题:performSelector:onThread:withObject:waitUntilDone:
      1. 最后一个参数:waitUntilDone的问题
      2. 如果传的是YES: 代表子线程的performSelector中方法执行完之后再往下执行主线程。
      3. 如果是NO:直接走主线程,不管子线程
        1. 由于上面代码中设置的是NO,因此,delloc会先走完,即先走完stop,然后在执行stopThread
        2. 由于已经delloc了,即当前控制器已经被释放了,然后再去执行停止runloop,因此会崩溃,报EXC_BAD_ACCESS 内存错误
        3. 只要把参数设置为YES即可
      4. 举例:

         //手动控制runloop退出
         - (IBAction)stop {
                        
             /**
              YES:
              //先执行stopThread
              =stoprun====<ZHThread: 0x600002208140>{number = 4, name = (null)}
              //再打印
               123456
                    
              NO:
              //先打印
              123456
              //再执行stopThread
              =stoprun====<ZHThread: 0x600002208140>{number = 4, name = (null)}
              */
             [self performSelector:@selector(stopThread) onThread:self.thread withObject:self waitUntilDone:YES];
             NSLog(@"123456");
         }
        
    3. 改为YES以后,点击back回到上一VC,会发现不崩溃了,控制器delloc了,但是ZHThread仍然没有delloc,为什么呢???
      1. 在while循环内部打断点,然后点击back,会发现仍然会走while循环内部
      2. 但是在stopThread方法中已经将self.isstop = YES;了啊?
      3. 打印weakself发现,此时为nil了,因此!weakself.isstop还是为YES,所以还会走while循环内部,那么久又开启了runloop,所以ZHThread仍然没有销毁。
      4. 解决办法,将while循环条件改为如下:

          //搞一个循环,一直循环开启runloop,但是可以控制退出
          //必须判断weakself是否为nil!!!!
         while ( weakself && !weakself.isstop) {
             //这个方法,每次掉用就会启动一次runloop,当前线程只要执行一次事件就会自动退出当前的runloop
             [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
         }
        
      5. 经过测试,完美解决手动控制子线程生命周期
  5. 封装一个手动控制子线程生命周期的API

     #import <Foundation/Foundation.h>
        
     @interface ZHPermenantThread : NSObject
        
     /**
      开启一个子线程
      */
     -(void)run;
        
     /**
      销毁一个子线程
      */
     -(void)stop;
        
     /**
      利用子线程执行一个任务
        
      @param task 任务内容
      */
     -(void)excuteTask:(void (^)(void))task;
     @end
        
     #import "ZHPermenantThread.h"
     /*********ZHThread*********/
     @interface ZHThread : NSThread
     @end
     @implementation ZHThread
     - (void)dealloc
     {
         NSLog(@"=====%s",__func__);
     }
     @end
        
     /*********ZHPermenantThread**********/
        
     @interface ZHPermenantThread ()
     @property (nonatomic,strong) ZHThread *innerThread;
     @property (nonatomic, assign) BOOL isstop;
     @end
        
     @implementation ZHPermenantThread
        
     - (instancetype)init
     {
         self = [super init];
         if (self) {
             self.isstop = NO;
             __weak typeof(self) weakself  = self;
             self.innerThread = [[ZHThread alloc] initWithBlock:^{
                  [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
             while ( weakself && !weakself.isstop) {
             //这个方法,每次掉用就会启动一次runloop,当前线程只要执行一次事件就会自动退出当前的runloop
                 [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
             }
             }];
         }
         return self;
     }
     -(void)run{
         if(!self.innerThread) return;
         [self.innerThread start];
     }
     -(void)stop{
         if(!self.innerThread) return;
         [self performSelector:@selector(__stop) onThread:self.innerThread withObject:self waitUntilDone:YES];
     }
     -(void)excuteTask:(void (^)(void))task{
         if(!self.innerThread || !task) return;
         //执行任务没有必要卡主主线程:NO
         [self performSelector:@selector(__excuteTask:) onThread:self.innerThread withObject:task waitUntilDone:NO];
     }
        
     #pragma mark - private methods
     //停止当前的Runloop,否则,线程永远不死
     -(void)__stop{
         self.isstop = YES;
         CFRunLoopStop(CFRunLoopGetCurrent());
         NSLog(@"=stoprun====%@", [NSThread currentThread]);
         self.innerThread = nil;
     }
     -(void)__excuteTask:(void (^)(void))task{
         task();
     }
        
        
     - (void)dealloc{
         //当前对象销毁,innerThread也要销毁
         [self stop];
         NSLog(@"=====%s",__func__);
     }
     @end
        
     //使用
     #import "ZHViewController.h"
     #import "ZHPermenantThread.h"
     @interface ZHViewController ()
     @property (nonatomic,strong) ZHPermenantThread *thread;
     @end
        
     @implementation ZHViewController
        
     - (void)viewDidLoad {
         [super viewDidLoad];
         self.view.backgroundColor = UIColor.whiteColor;
         self.thread = [[ZHPermenantThread  alloc] init];
         [self.thread run];
     }
        
        
     //手动控制runloop退出
     - (IBAction)stop {
         [self.thread stop];
     }
        
     //随时使用子线程调用
     -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
         [self.thread excuteTask:^{
              NSLog(@"==执行任务===%@", [NSThread currentThread]);
         }];
            
     }
     - (void)dealloc{
         NSLog(@"=====%s",__func__);
     }
     @end
    
    1. 也可以用C语言方式实现(更简单)

       - (instancetype)init
       {
           self = [super init];
           if (self) {
               self.isstop = NO;
               self.innerThread = [[ZHThread alloc] initWithBlock:^{
                          
                   NSLog(@"runloop--run");
                          
                   //创建上下文
                    CFRunLoopSourceContext text = {0}; //结构体初始化
                   //创建一个source
                   CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &text);
                   //添加source
                   CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
                          
                   //销毁sorce
                   CFRelease(source);
       //            while ( weakself && !weakself.isstop) {
       //                //启动runloop
       //                //第三个参数:true:代表执行完source后就会退出当前runloop,
       //                CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, true);
       //            }
                   //false:执行完任务不退出当前runloop,等价于上面的while循环
                   CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, false);
                   NSLog(@"runloop--stop");
               }];
           }
           return self;
       }
      

performSelector的使用

//延迟3秒设置图片并且值只在NSDefaultRunLoopMode模式下,也就是说,在滚动的时候,不显示图片,滚动停止,显示图片
[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"123"] afterDelay:3.0 inModes:@[NSDefaultRunLoopMode]

自动释放池

  1. 所有对象都放到这个池子中去,到池子释放的时候给池子中的每一个对象release
  2. 自动释放池什么时候释放?(看源码得到)
    1. runloop休眠之前会释放一次,因为休眠可能会很长,如果不释放,会堆积很多
    2. 释放后会同时创建一个新的释放池,处理新一轮事件
    3. 因此,在创建runloop的代码外层最好包装一层自动释放池
    4. 比如上面的全局定时器代码

RunLoop面试题

  1. 什么是RunLoop?
    • 从字面意思看:运行循环/跑圈
    • 其实它内部就是do-while循环,在这个循环内部不断地处理各种任务(比如:source/Timer/Observer)
    • 一个线程对应一个runloop,主线程的runloop默认已经启动,子线程的runloop需要自己手动启动(调用run方法)
    • RunLoop只能选择一个Mode启动,如果当Mode中没有任何source(source0/source1),Timer,那么就直接退出RunLoop
  2. 自动释放池什么时候释放?
    • 通过Observer监听RunLoop的状态,一旦监听到RunLoop即将进入休眠等待状态,就释放自动释放池(KCFRunLoopBeforeWaiting)
  3. 在开发中如何使用RunLoop?什么应用场景?
    • 开启一个常驻线程(让一个子线程不进入消亡状态,等待其他的线程发来消息,处理其他事件)
      在子线程中开启一个定时器 在子线程中进行一些长期监控
    • 可以控制定时器在特定模式下运行
    • 可以让某些事件(行为/任务)在特定模式下执行
    • 可以添加Observer监听runloop的状态,比如监听点击事件的处理(在所有点击事件之前做一些事情)
  4. 自动释放池与RunLoop
    1. kCFRunLoopEntry; // 创建一个自动释放池
    2. kCFRunLoopBeforeWaiting; // 销毁自动释放池,创建一个新的自动释放池
    3. kCFRunLoopExit; // 销毁自动释放池

IOS程序启动与运转

从Xcode的线程函数调用栈(注意看看)可以看到一些方法调用顺序。
(主线程开启)start->(加载framework,动态静态链接库,启动图片,Info.plist,pch等)->main函数->UIApplicationMain函数:{

- 初始化UIApplication单例对象
  - 初始化AppDelegate对象,并设为UIApplication对象的代理
  - 检查Info.plist设置的xib文件是否有效,如果有则解冻Nib文件并设置outlets,创建显示key window、rootViewController、与rootViewController关联的根view(没有关联则看rootViewController同名的xib),否则launch之后由程序员手动加载。
  - 建立一个主事件循环,其中包含UIApplication的Runloop来开始处理事件。
}

UIApplication:

  1. 通过window管理视图;
  2. 发送Runloop封装好的control消息给target;
  3. 处理URL,应用图标警告,联网状态,状态栏,远程事件等。

AppDelegate:

管理UIApplication生命周期和应用的五种状态(notRunning/inactive/active/background/suspend)。

Key Window:

  1. 显示view;
  2. 管理rootViewcontroller生命周期;
  3. 发送UIApplication传来的事件消息给view。

rootViewController:

  1. 管理view(view生命周期;view的数据源/代理;view与superView之间事件响应nextResponder的“备胎”);
  2. 界面跳转与传值;
  3. 状态栏,屏幕旋转。

view:

  1. 通过作为CALayer的代理,管理layer的渲染(顺序大概是先更新约束,再layout再display)和动画(默认layer的属性可动画,view默认禁止,在UIView的block分类方法里才打开动画)。layer是RGBA纹理,通过和mask位图(含alpha属性)关联将合成后的layer纹理填充在像素点内,GPU每1/60秒将计算出的纹理display在像素点中。
  2. 布局子控件(屏幕旋转或者子视图布局变动时,view会重新布局)。
  3. 事件响应:event和guesture。

runloop:

  1. (要让马儿跑)通过do-while死循环让程序持续运行:接收用户输入,调度处理事件时间。
  2. (要让马儿少吃草)通过mach_msg()让runloop没事时进入trap状态,节省CPU资源。
Table of Contents