1、NSTimer失效

1.1、失效原因

添加一个Timer,如果在TableView或其他可滚动的View滚动时,Timer就会暂停执行:

  1. [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
  2. NSLog(@"timer working on %@",[NSRunLoop currentRunLoop].currentMode);
  3. }];

打印结果:

  1. ~: timer working on kCFRunLoopDefaultMode

原因是RunLoop同一时间只能运行在一种模式下,RunLoop默认是在DefaultMode模式下工作,当拖拽UI时,RunLoop会切换到UITrackingModel模式下工作。
而Timer通过scheduled方式创建,默认会被添加到RunLoop中的Default模式下工作,所以当RunLoop切换到UITracking模式后,Timer会暂停工作。

1.2、解决办法

如果要解决这个问题,需要让Timer在Default和UITracking两种模式下都能工作:

  1. NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
  2. NSLog(@"timer working on %@",[NSRunLoop currentRunLoop].currentMode);
  3. }];
  4. [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

打印结果:

  1. ~: timer working on kCFRunLoopDefaultMode
  2. ~: timer working on UITrackingRunLoopMode

默认Timer会在Default模式下工作,当拖拽ScrollView时,Timer会切换到UITracking模式下工作。

1.3、CommonModes

CommonModes并不是一个真正的模式,它只是一个标记,timer在设置了Common标记后,它就能在RunLoop中的_commonModes数组中的模式下工作。_commonModes包括NSDefaultRunLoopMode和UITrackingRunLoopMode,所以Timer可以在两种模式下运行。
被标记为CommonModes的timer会被保存在RunLoop的_commonModeItems中。
(_commonModes和_commonModeItems参考 RunLoop对象

2、线程保活

2.1、保活原因

创建的子线程,默认执行完任务后就会自动销毁,比如:

  1. - (void)viewDidLoad {
  2. [super viewDidLoad];
  3. XLThread *thread = [[XLThread alloc] initWithTarget:self selector:@selector(run) object:nil];
  4. [thread start];
  5. }
  6. - (void)run {
  7. NSLog(@"%s - %@",__func__, [NSThread currentThread]);
  8. }

打印结果:

  1. ~: -[ViewController run] - <XLThread: 0x600000434d80>{number = 7, name = (null)}
  2. ~: -[XLThread dealloc]

如果想要自线程能够长时间的存在,就需要想办法保住线程的命(让线程保持激活状态)。
即使使用强指针指向线程对象,能够使线程对象不会被销毁,但是它在执行完任务后就会失效。

2.2、线程保活

子线程的RunLoop默认没有开启,需要手动获取并开启,并在RunLoop中添加Source/Timer/Observer保证RunLoop不退出,这样就可以保持线程不在执行完任务后失效(保持激活状态)。
添加线程,开启RunLoop并添加NSPort对象:

  1. - (void)viewDidLoad {
  2. [super viewDidLoad];
  3. self.thread = [[XLThread alloc] initWithTarget:self selector:@selector(keepThreadAlive) object:nil];
  4. [self.thread start];
  5. }
  6. // 线程保活逻辑
  7. - (void)keepThreadAlive {
  8. NSLog(@"%s - %@",__func__, [NSThread currentThread]);
  9. // 添加source/timner/observer
  10. [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
  11. [[NSRunLoop currentRunLoop] run];
  12. NSLog(@"%s --end--", __func__);
  13. }

子线程处理任务:

  1. - (void)run {
  2. NSLog(@"%s - %@",__func__, [NSThread currentThread]);
  3. }
  4. - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
  5. [self performSelector:@selector(run) onThread:self.thread withObject:nil waitUntilDone:YES];
  6. }

2.3、控制线程生命周期

开启线程中的RunLoop后,会保住线程的命,但是RunLoop的run方法是无法停止的,它专门用于开启一个永不销毁的线程。所以需要自己去仿照run方法,添加一个while循环,和一个结束条件self.stopRunLoop:

  1. - (void)keepThreadAlive {
  2. NSLog(@"%@ start %s", [NSThread currentThread], __func__);
  3. // 添加source/timner/observer
  4. [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
  5. // 添加结束循环条件
  6. while (!self.stopRunLoop) {
  7. [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
  8. }
  9. NSLog(@"%@ end %s", [NSThread currentThread], __func__);
  10. }

停止线程需要调用killThread方法,结束子线程中的runLoop:

  1. // 杀死线程
  2. - (void)killThread {
  3. [self performSelector:@selector(stopCurrentRunLoop) onThread:self.thread withObject:nil waitUntilDone:YES];
  4. }
  5. // 停止线程中的RunLoop
  6. - (void)stopCurrentRunLoop {
  7. self.stopRunLoop = YES;
  8. CFRunLoopStop(CFRunLoopGetCurrent());
  9. }

*waitUntilDone:代表是否要等待子线程处理完毕,如果killThread方法是在VC的dealloc方法中调用,这里需要设置成YES,避免VC被提前释放产生坏内存访问。

2.4、线程保活封装

如果想在多个地方使用线程保活可以封装一个自定义的线程,控制线程生命周期的逻辑都封装到内部,外部只调用就可以,具体可参考:XLThread