RunLoop在实际开中的应用
- 解决NSTimer在滑动时停止工作的问题
- 控制线程生命周期(线程保活)
- 监控应用卡顿
- 性能优化
一、解决NSTimer在滑动时停止工作的问题
- 在拖拽时定时器不工作
__block int count = 0;
// 默认添加到default模式
[NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"%d", ++count);
}];
打印结果
- 在拖拽时人仍然正常工作
static int count = 0;
// 2.添加到指定模式下
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"%d", ++count);
}];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
// NSDefaultRunLoopMode、UITrackingRunLoopMode才是真正存在的模式
// NSRunLoopCommonModes并不是一个真的模式,它只是一个标记
// timer能在_commonModes数组中存放的模式下工作
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
二 线程保活
我们先封装一个长久活命的线程
PermanentThread.h
// 声明一个block - 用于执行任务
typedef void(^PermanentThreadTask)(void);
/** 线程保活 */
@interface PermanentThread : NSObject
// 在当前线程执行一个任务
- (void)executeTask:(PermanentThreadTask)task;
// 结束线程
- (void)stop;
@end
PermanentThread.m
/** CSThread **/
@interface CSThread : NSThread
@end
@implementation CSThread
- (void)dealloc {
NSLog(@"%s", __func__);
}
@end
@interface PermanentThread()
/** 线程*/
@property(nonatomic,strong)CSThread *thread;
/** 是否停止*/
@property(nonatomic,assign, getter=isStopped)BOOL stopped;
@end
@implementation PermanentThread
// 初始化方法
- (instancetype)init {
self = [super init];
if (self) {
self.stopped = NO;
// 初始化线程
__weak typeof(self) weakSelf = self;
self.thread = [[CSThread alloc] initWithBlock:^{
// runloop只有添加事件才会执行
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc]init] forMode:NSDefaultRunLoopMode];
// 当当前对象存在并且变量为false的时候,才一直执行
while (weakSelf && !weakSelf.isStopped) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
}];
// 开启线程
[self.thread start];
}
return self;
}
- (void)dealloc {
NSLog(@"%s", __func__);
[self stop];
}
#pragma mark - public method
// 执行任务
- (void)executeTask:(PermanentThreadTask)task {
// 如果线程释放或者无任务,则退出
if (!self.thread || !task) {
return;
}
// 开始执行任务
[self performSelector:@selector(innerExecuteTask:) onThread:self.thread withObject:task waitUntilDone:NO];
}
// 停止
- (void)stop {
if (!self.thread) {
return;
}
[self performSelector:@selector(innerStop) onThread:self.thread withObject:nil waitUntilDone:YES];
}
#pragma mark - private method
// 执行任务
- (void)innerExecuteTask:(PermanentThreadTask)task {
task();
}
// 停止线程 runloop
- (void)innerStop {
self.stopped = YES;
CFRunLoopStop(CFRunLoopGetCurrent());
self.thread = nil;
}
@end
外部调用
- (void)viewDidLoad {
[super viewDidLoad];
// 2.线程保活
self.thread = [[PermanentThread alloc] init];
}
- (void)dealloc {
NSLog(@"%s", __func__);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self.thread executeTask:^{
NSLog(@"执行任务 - %@", [NSThread currentThread]);
}];
}
- (void)stopBtnClick {
[self.thread stop];
}
运行结果
三、让UITableView、UICollectionView延迟加载图片
首先创建一个单例,单例中定义了几个数组,用来存要在runloop循环中执行的任务,然后为主线程的runloop添加一个CFRunLoopObserver,当主线程在NSDefaultRunLoopMode中执行完任务,即将睡眠前,执行一个单例中保存的一次图片渲染任务。关键代码看 RunLoopWorkDistribution即可。
四、监测主线程的卡顿,并将卡顿时的线程堆栈信息保存下来,选择合适时机上传到服务器
第一步,创建一个子线程,在线程启动时,启动其RunLoop。
+ (instancetype)shareMonitor
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[[self class] alloc] init];
instance.monitorThread = [[NSThread alloc] initWithTarget:self selector:@selector(monitorThreadEntryPoint) object:nil];
[instance.monitorThread start];
});
return instance;
}
+ (void)monitorThreadEntryPoint
{
@autoreleasepool {
[[NSThread currentThread] setName:@"FluencyMonitor"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
}
第二步,在开始监测时,往主线程的RunLoop中添加一个observer,并往子线程中添加一个定时器,每0.5秒检测一次耗时的时长。
- (void)start
{
if (_observer) {
return;
}
// 1.创建observer CFRunLoopObserverContext context = {0,(__bridge void*)self, NULL, NULL, NULL};
_observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES,
0,
&runLoopObserverCallBack,
&context);
// 2.将observer添加到主线程的RunLoop中
CFRunLoopAddObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);
// 3.创建一个timer,并添加到子线程的RunLoop中
[self performSelector:@selector(addTimerToMonitorThread) onThread:self.monitorThread withObject:nil waitUntilDone:NO modes:@[NSRunLoopCommonModes]];
}
- (void)addTimerToMonitorThread
{
if (_timer) {
return;
}
// 创建一个timer
CFRunLoopRef currentRunLoop = CFRunLoopGetCurrent();
CFRunLoopTimerContext context = {0, (__bridge void*)self, NULL, NULL, NULL};
_timer = CFRunLoopTimerCreate(kCFAllocatorDefault, 0.1, 0.01, 0, 0, &runLoopTimerCallBack, &context);
// 添加到子线程的RunLoop中
CFRunLoopAddTimer(currentRunLoop, _timer, kCFRunLoopCommonModes);
}
第三步,补充观察者回调处理
static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){
FluencyMonitor *monitor = (__bridge FluencyMonitor*)info;
NSLog(@"MainRunLoop---%@",[NSThread currentThread]);
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;
}
}
RunLoop进入睡眠状态的时间可能会非常短,有时候只有1毫秒,有时候甚至1毫秒都不到,静止不动时,则会长时间进入睡觉状态。
因为主线程中的block、交互事件、以及其他任务都是在kCFRunLoopBeforeSources 到 kCFRunLoopBeforeWaiting 之前执行,所以我在即将开始执行Sources 时,记录一下时间,并把正在执行任务的标记置为YES,将要进入睡眠状态时,将正在执行任务的标记置为NO。
第四步,补充timer 的回调处理
static void runLoopTimerCallBack(CFRunLoopTimerRef timer, void *info)
{
FluencyMonitor *monitor = (__bridge FluencyMonitor*)info;
if (!monitor.excuting) {
return;
}
// 如果主线程正在执行任务,并且这一次loop 执行到 现在还没执行完,那就需要计算时间差
NSTimeInterval excuteTime = [[NSDate date] timeIntervalSinceDate:monitor.startDate];
NSLog(@"定时器---%@",[NSThread currentThread]);
NSLog(@"主线程执行了---%f秒",excuteTime);
if (excuteTime >= 0.01) {
NSLog(@"线程卡顿了%f秒",excuteTime);
[monitor handleStackInfo];
}
}
timer 每 0.01秒执行一次,如果当前正在执行任务的状态为YES,并且从开始执行到现在的时间大于阙值,则把堆栈信息保存下来,便于后面处理。
为了能够捕获到堆栈信息,我把timer的间隔调的很小(0.01),而评定为卡顿的阙值也调的很小(0.01)。 实际使用时这两个值应该是比较大,timer间隔为1s,卡顿阙值为2s即可。