iOS底层探索 — RunLoop(实战)

时间:2021-7-3 作者:qvyue

日常开发中我们常用的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]);
    }
}

直接结果如下:

子线程任务开始 ---- {number = 6, name = (null)}
子线程任务结束 --- {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 *)touches withEvent:(UIEvent *)event {
    [self performSelector:@selector(threadFunc) onThread:_jThread withObject:nil waitUntilDone:NO];
}

输出结果如下:

点击屏幕 -------
子线程任务开始 ---- {number = 6, name = (null)}
子线程任务结束 --- {number = 6, name = (null)}
点击屏幕 -------
子线程任务开始 ---- {number = 6, name = (null)}
子线程任务结束 --- {number = 6, name = (null)}
点击屏幕 -------
子线程任务开始 ---- {number = 6, name = (null)}
子线程任务结束 --- {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];
}

打印结果如下:

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

通过打印结果可以看到,当刚开始创建的时候,modekCFRunLoopDefaultMode,但是当我们滑动tableView的时候,mode就变成了UITrackingRunLoopMode
注意,我们上面创建的timer都是添加在kCFRunLoopDefaultMode里面。
既然找到了原因,那么修改起来也是很方便的。这里可能就有人要问,两个不同的mode,而且,要保证timer在两种mode下都能正常运行,要怎么去设置。其实我们只需要设置NSRunLoopCommonModes就可以了。(有疑问的可以阅读iOS底层探索 — RunLoop(概念))。

  • 其实要解决这个问题,还有另一种办法,那就会是在子线程中添加timer。因为RunLoop和线程是一一对应的。所以,当其他线程的RunLoop发成改变的时候,并不会影响timer所在线程的RunLoop

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

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

    这里我们设置一个监听的单例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的耗时,然后再跟设定好的阀值进行比较,就可以判定主线程是否卡顿。

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

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

声明:本文内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:qvyue@qq.com 进行举报,并提供相关证据,工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。