美文网首页
iOS底层探索 --- RunLoop(实战)

iOS底层探索 --- RunLoop(实战)

作者: Jax_YD | 来源:发表于2021-04-25 17:17 被阅读0次

    日常开发中我们常用的RunLoop场景有:

    • 线程保活
    • Timer相关
    • APP卡顿检测

    • 线程保活
      首先我们应该达成的共识就是,在日常的开发过程中,线程是我们避不开的话题。比如为了不影响主线程UI的刷新,对数据的加载和解析往往会放在子线程里面去做。
      有时候我们总会有一些特殊的需求,比如我们需要做一个心跳包,来保持与服务器的通讯。当然,这种数据的传输一定是放在子线程中的,那么问题就来了,子线程的任务在执行完毕之后,子线程就会被销毁。我们怎么来保证子线程不被销毁呢?
      • 第一点,我们先来验证,子线程的销毁。
        首先我们创建一个类继承自NSThread:
    #import "J_Thread.h"
    
    @implementation J_Thread
    
    - (void)dealloc
    {
        NSLog(@"线程被销毁:%s", __func__);
    }
    
    @end
    

    然后我们创建子线程并添加延时任务:

    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view.
        
        J_Thread *jThread = [[J_Thread alloc] initWithTarget:self selector:@selector(threadFunc) object:nil];
        [jThread start];
    }
    
    
    - (void)threadFunc {
        @autoreleasepool {
            NSLog(@"子线程任务开始 ---- %@", [NSThread currentThread]);
            [NSThread sleepForTimeInterval:3.0];
            NSLog(@"子线程任务结束 --- %@", [NSThread currentThread]);
        }
    }
    

    直接结果如下:

    子线程任务开始 ---- <J_Thread: 0x600003784840>{number = 6, name = (null)}
    子线程任务结束 --- <J_Thread: 0x600003784840>{number = 6, name = (null)}
    线程被销毁:-[J_Thread dealloc]
    

    可以看到,在子线程任务结束的时候,子线程立马就被销毁了。

    • 第二点、常驻子线程
      这个时候,可能有人会想,既然要持续的在子线程中执行任务,那么直接定义一个计时器,不断的去开辟子线程不就可以了吗?
      首先这种想法肯定是错误的,因为频繁的开启和销毁线程,会造成资源浪费,也会增加CUP负担。这个时候,我们就可以利用RunLoop来让线程常驻,不被销毁。
      代码如下:
    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view.
        
        _jThread = [[J_Thread alloc] initWithTarget:self selector:@selector(addRunLoop) object:nil];
        [_jThread start];
    }
    
    /// 向子线程中添加RunLoop
    - (void)addRunLoop {
        @autoreleasepool {
            NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
            [runLoop addPort:[NSMachPort port] forMode:NSRunLoopCommonModes];
            [runLoop run];
        }
    }
    
    /// 子线程任务
    - (void)threadFunc {
        NSLog(@"子线程任务开始 ---- %@", [NSThread currentThread]);
        [NSThread sleepForTimeInterval:3.0];
        NSLog(@"子线程任务结束 --- %@", [NSThread currentThread]);
    }
    
    ///模拟心跳包,隔一段时间点击一次,查看子线程是否销毁
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        [self performSelector:@selector(threadFunc) onThread:_jThread withObject:nil waitUntilDone:NO];
    }
    

    输出结果如下:

    点击屏幕 -------
    子线程任务开始 ---- <J_Thread: 0x600000295100>{number = 6, name = (null)}
    子线程任务结束 --- <J_Thread: 0x600000295100>{number = 6, name = (null)}
    点击屏幕 -------
    子线程任务开始 ---- <J_Thread: 0x600000295100>{number = 6, name = (null)}
    子线程任务结束 --- <J_Thread: 0x600000295100>{number = 6, name = (null)}
    点击屏幕 -------
    子线程任务开始 ---- <J_Thread: 0x600000295100>{number = 6, name = (null)}
    子线程任务结束 --- <J_Thread: 0x600000295100>{number = 6, name = (null)}
    

    可以看到,我们再没有从新开启子线程的情况先,就做到了线程常驻。其实这样的需要还有很多应用场景,比如音乐播放,后台下载等等。

    • 在使用的过程中有几点需要注意:
      1、获取RunLoop只能使用[NSRunLoop CurrentRunLoop]/[NSRunLoop mainRunLoop]
      2、即使RunLoop开始运行,如果RunLoop中的modes为空,或者要执行的mode里面没有item,那么RunLoop会直接在当前Loop中返回,并进入睡眠状态。(可以参看iOS底层探索 --- RunLoop(概念)
      3、自己创建的Thread的任务是在KCFRunLoopDefualtMode这个mode中执行的。
      4、在子线程创建好之后,最好所有的任务都放在AutoreleasePool中执行。(这一点我们之后单独对AutoreleasePool进行探讨)。

    • Timer相关
      我们常见的情况就是TableViewNSTimer的关系了。比如我们在当前线程中添加了一个计时器,定时打印一些信息(当然也可以是其他事情)。我们会发现,但我们去滑动TableView的时候,计时器的任务不会执行。
      我们在日常开发中常用的NSTimer使用方法如下:
    [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {}];
    
    [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {}];
    
    NSTimer *timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(timerFunc) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    

    上面这三种方法创建的timer都是在NSDefaultRunLoopMode模式下面运行的,因为前两种没有指定mode,默认是NSDefaultRunLoopMode
    这就要引出另外一个问题,既然我们正常创建的timer默认是在NSDefaultRunLoopMode下的,那么我们上面讲到的问题,当tableView被滑动的时候,timer不再执行,那是不是说tableView的滑动,改变了当前线程的RunLoopmode?
    下面我们在tableView滑动的时候,打印一下当前的mode

    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view.
        
        NSLog(@"开始的mode--%@",[NSRunLoop currentRunLoop].currentMode);
        UITableView *tableView = [[UITableView alloc] initWithFrame:self.view.bounds style:UITableViewStylePlain];
        tableView.delegate = self;
        tableView.dataSource = self;
        [self.view addSubview:tableView];
        
    }
    
    - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
        return 5;
    }
    
    
    - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
        return 50.0;
    }
    
    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
        
        NSLog(@"mode--%@",[NSRunLoop currentRunLoop].currentMode);
        return [UITableViewCell new];
    }
    

    打印结果如下:


    通过打印结果可以看到,当刚开始创建的时候,modekCFRunLoopDefaultMode,但是当我们滑动tableView的时候,mode就变成了UITrackingRunLoopMode
    注意,我们上面创建的timer都是添加在kCFRunLoopDefaultMode里面。
    既然找到了原因,那么修改起来也是很方便的。这里可能就有人要问,两个不同的mode,而且,要保证timer在两种mode下都能正常运行,要怎么去设置。其实我们只需要设置NSRunLoopCommonModes就可以了。(有疑问的可以阅读iOS底层探索 --- RunLoop(概念))。
    • 其实要解决这个问题,还有另一种办法,那就会是在子线程中添加timer。因为RunLoop和线程是一一对应的。所以,当其他线程的RunLoop发成改变的时候,并不会影响timer所在线程的RunLoop

    • APP卡顿检测
      我们可以通过获取kCFRunLoopBeforeSourceskCFRunLoopBeforeWaiting再到kCFRunLoopAfterWaiting的状态,来判断是否有卡顿。
      这是因为,我们运行中的主要业务处理,就是在第4步到第9步之间运行的:

      这里我们设置一个监听的单例J_MonitorObject
    @interface J_MonitorObject : NSObject
    
    + (instancetype)sharedInstance;
    /// 开始监控
    /// @param interval 定义器间隔时间
    /// @param fault 卡顿的阀值
    - (void)startWithInterval:(NSTimeInterval)interval WithFault:(NSTimeInterval)fault;
    
    /// 开始监控
    - (void)start;
    /// 停止监控
    - (void)end;
    
    @end
    
    #import "J_MonitorObject.h"
    
    
    @interface J_MonitorObject ()
    
    @property (nonatomic, strong) NSThread *monitorThread; ///监控线程
    @property (nonatomic, assign) CFRunLoopObserverRef observer; ///观察者
    @property (nonatomic, assign) CFRunLoopTimerRef timer; ///定时器
    @property (nonatomic, strong) NSDate *startDate; ///开始执行的时间
    @property (nonatomic, assign) BOOL excuting; ///执行中
    @property (nonatomic, assign) NSTimeInterval interval; ///定时器间隔时间
    @property (nonatomic, assign) NSTimeInterval fault; ///卡顿的阀值
    
    @end
    
    @implementation J_MonitorObject
    
    static J_MonitorObject *instance = nil;
    
    + (instancetype)sharedInstance {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            instance = [[J_MonitorObject alloc] init];
            instance.monitorThread = [[NSThread alloc] initWithTarget:self selector:@selector(monitorThreadEntryPoint) object:nil];
            [instance.monitorThread start];
        });
        return instance;
    }
    
    - (void)startWithInterval:(NSTimeInterval)interval WithFault:(NSTimeInterval)fault {
        _interval = interval;
        _fault = fault;
        
        ///如果已经再监听,就不再创建监听器
        if (_observer) {
            return;
        }
        
        //1、创建Observer
        
        ///设置RunLoopObserver的运行环境
        CFRunLoopObserverContext context = {0,(__bridge void*)self, NULL, NULL, NULL};
        
        ///第一个参数:用于分配Observer对象的内存
        ///第二个参数:用于设置Observer所要关注的事件,也就是runLoopObserverCallBack中的那些事件
        ///第三个参数:用于标识该Observer是在第一次进入RunLoop时执行,还是每次进入进入RunLoop处理时都执行。如果为NO,则在调用一次之后该Observer就无效了。如果为YES,则可以多次重复调用。
        ///第四个参数:用于设置该Observer的优先级
        ///第五个参数:用于设置该Observer的回调函数
        ///第六个参数:用于设置该Observer的运行环境
        _observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, &runLoopObserverCallBack, &context);
        
        //2、将obse添加到主线程的RunLoop中
        CFRunLoopAddObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);
        
        //3、创建一个timer,并添加到子线程的RunLoop中
        [self performSelector:@selector(addTimerToMonitorThread) onThread:_monitorThread withObject:nil waitUntilDone:NO modes:@[NSRunLoopCommonModes]];
    }
    
    - (void)start {
        ///默认定时器间隔时间是1秒,阀值是2秒
        [self startWithInterval:1.0 WithFault:2.0];
    }
    
    - (void)end {
        if (_observer) {
            CFRunLoopRemoveObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);
            CFRelease(_observer);
            _observer = NULL;
        }
        
        [self performSelector:@selector(removeMonitorTimer) onThread:_monitorThread withObject:nil waitUntilDone:NO modes:@[NSRunLoopCommonModes]];
    }
    
    
    #pragma mark - 监控
    ///TODO: - 开启RunLoop
    + (void)monitorThreadEntryPoint {
        @autoreleasepool {
            [[NSThread currentThread] setName:@"J_Monitor"];
            NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
            [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
            [runLoop run];
        }
    }
    
    ///TODO: - observer回调函数
    static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
        J_MonitorObject *monitor = (__bridge  J_MonitorObject*)info;
        switch (activity) {
            case kCFRunLoopEntry:
                NSLog(@"kCFRunLoopEntry");
                break;
            case kCFRunLoopBeforeTimers:
                NSLog(@"kCFRunLoopBeforeTimers");
                break;
            case kCFRunLoopBeforeSources:
                NSLog(@"kCFRunLoopBeforeSources");
                monitor.startDate = [NSDate date];
                monitor.excuting = YES;
                break;
            case kCFRunLoopBeforeWaiting:
                NSLog(@"kCFRunLoopBeforeWaiting");
                monitor.excuting = NO;
                break;
            case kCFRunLoopAfterWaiting:
                NSLog(@"kCFRunLoopAfterWaiting");
                break;
            case kCFRunLoopExit:
                NSLog(@"kCFRunLoopExit");
                break;
            default:
                break;
        }
    }
    
    ///TODO: - 在监控线程里面添加计时器
    - (void)addTimerToMonitorThread {
        if (_timer) {
            return;
        }
        
        //1、创建timer
        CFRunLoopRef currentRunLoop = CFRunLoopGetCurrent();
        CFRunLoopTimerContext context = {0, (__bridge void*)self, NULL, NULL, NULL};
        _timer = CFRunLoopTimerCreate(kCFAllocatorDefault, 0.1, _interval, 0, 0, &runLoopTimerCallBack, &context);
        
        //2、添加子线程到RunLoop中
        CFRunLoopAddTimer(currentRunLoop, _timer, kCFRunLoopCommonModes);
    }
    
    ///TODO: - 计时器回到函数
    static void runLoopTimerCallBack(CFRunLoopTimerRef timer, void *info) {
        J_MonitorObject *monitor = (__bridge J_MonitorObject*)info;
        
        //如果为NO,说明进入休眠状态,此时不会有卡顿
        if (!monitor.excuting) {
            return;
        }
        
        //每个一段时间,检查一下是RunLoop是否在执行,然后检测一下当前循环执行的时长
        //如果主线程正在执行任务,并且这一次 loop 执行到现在还没有执行完,那就需要计算时间差
        NSTimeInterval excuteTime = [[NSDate date] timeIntervalSinceDate:monitor.startDate];
        NSLog(@"定时器 --- %@", [NSThread currentThread]);
        NSLog(@"主线程执行了 --- %f秒", excuteTime);
        
        //跟阀值做比较,看一下是否卡顿
        if (excuteTime >= monitor.fault) {
            NSLog(@"线程卡顿了 --- %f秒 ---", excuteTime);
            [monitor handleStackInfo];
        }
    }
    
    
    ///TODO: - 将卡顿信息上传给服务器
    - (void)handleStackInfo {
        NSLog(@"将卡顿信息上传给服务器");
    }
    
    ///TODO: - 移除计时器
    - (void)removeMonitorTimer {
        if (_timer) {
            CFRunLoopRef currentRunLoop = CFRunLoopGetCurrent();
            CFRunLoopRemoveTimer(currentRunLoop, _timer, kCFRunLoopCommonModes);
            CFRelease(_timer);
            _timer = NULL;
        }
    }
    

    监听卡顿的一个主要流程是:
    i:创建一个子线程,并在当前线程中创建RunLoop
    ii:在子线程的RunLoop中添加计时器(时间间隔自定)
    iii:创建Observer,监听主线程RunLoop的状态
    iiii:根据上面的分析,我们只需要在计算kCFRunLoopBeforeSourceskCFRunLoopBeforeWaiting的耗时,然后再跟设定好的阀值进行比较,就可以判定主线程是否卡顿。


    参考文档:
    https://juejin.cn/user/3157453124927672
    https://www.jianshu.com/p/71cfbcb15842

    相关文章

      网友评论

          本文标题:iOS底层探索 --- RunLoop(实战)

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