日常开发中我们常用的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相关
我们常见的情况就是TableView
与NSTimer
的关系了。比如我们在当前线程中添加了一个计时器,定时打印一些信息(当然也可以是其他事情)。我们会发现,但我们去滑动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
的滑动,改变了当前线程的RunLoop
的mode
?
下面我们在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];
}
打印结果如下:
通过打印结果可以看到,当刚开始创建的时候,
mode
为kCFRunLoopDefaultMode
,但是当我们滑动tableView
的时候,mode
就变成了UITrackingRunLoopMode
。
注意,我们上面创建的
timer
都是添加在kCFRunLoopDefaultMode
里面。
既然找到了原因,那么修改起来也是很方便的。这里可能就有人要问,两个不同的
mode
,而且,要保证timer
在两种mode
下都能正常运行,要怎么去设置。其实我们只需要设置NSRunLoopCommonModes
就可以了。(有疑问的可以阅读iOS底层探索 --- RunLoop(概念))。
- 其实要解决这个问题,还有另一种办法,那就会是在子线程中添加
timer
。因为RunLoop
和线程是一一对应的。所以,当其他线程的RunLoop
发成改变的时候,并不会影响timer
所在线程的RunLoop
。
- APP卡顿检测
我们可以通过获取kCFRunLoopBeforeSources
到kCFRunLoopBeforeWaiting
再到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
:根据上面的分析,我们只需要在计算kCFRunLoopBeforeSources
到 kCFRunLoopBeforeWaiting
的耗时,然后再跟设定好的阀值
进行比较,就可以判定主线程是否卡顿。
参考文档:
https://juejin.cn/user/3157453124927672
https://www.jianshu.com/p/71cfbcb15842