iOS RunLoop在实际开发中的应用

WSBackgroundTaskCategory
本来是想Runtime和RunLoop一起写,所以写了个Runtime的Demo

是一个AppDelegate的分类,使用时需要引入文件后再

- (void)applicationDidEnterBackground:(UIApplication *)application {
    
    // beginBackgroundTask
    [self beginBackgroundTaskWithBlock:^{
        NSLog(@"搞一些事情");
    }];
}

- (void)applicationDidBecomeActive:(UIApplication *)application {
    
    // beginBackgroundTask
    [self endBackgroundTask];
}

概要

在iOS开发中,Runtime与RunLoop应该是iOS Developer技术进阶时需要掌握的两方面知识,相对来说,它俩也比较接近底层,就现在环境来看,面试时也比较容易问到。

关于这两项,网上的文章大多是讲了很多知识点,然而实际开发中用不到,那就找一个很简单的问题来把这两项知识实战一下

锁屏或切换至后台时计时器停止

相信大家都遇到过的问题:在注册页面有一个NSTimer实现的验证码倒计时的按钮,在手机切出app,把app在后台挂起时,倒计时是停止的,如你切出时时间剩余50秒,当你从后台返回时,倒计时依然是50秒。

什么原因?

如果你有关于RunLoop的知识,你应该知道

  • 每一个线程都有一个自己的RunLoop,他们的关系是一一对应的
  • NSTimer 其实就是 CFRunLoopTimerRef

上面两点不懂可以来这里 -- 深入理解RunLoop

CFRunLoopTimerRef 是基于时间的触发器,它和 NSTimer 是toll-free bridged 的,可以混用。其包含一个时间长度和一个回调(函数指针)。当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调

 简单来说就是NSTimer在初始化以后,必须被加入到RunLoop中才会生效
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)target selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)repeats

使用此方法初始化时,会自动将我们创建的NSTimer加入到RunLoop中

造成NSTimer停止的原因,是当app在后台挂起时,线程同时被挂起,RunLoop也就被挂起,而NSTimer是运行在RunLoop中的,所以在app挂起时,NSTimer就同时停止了工作。

关于NSTimer想更深入的可以参考iOS开发之 不要告诉我你会用NSTimer!

怎么解决?

知道了NSTimer停止的原因是因为线程不活跃

那解决NSTimer停止的方法就是app在挂起时,让其所在RunLoop的线程处于活跃状态

我们需要的是UIApplication的:

- (UIBackgroundTaskIdentifier)beginBackgroundTaskWithExpirationHandler:(void(^ __nullable)(void))handler

这个方法,这个方法就是开启一个后台任务,使线程处于活跃状态便于执行此后台任务,线程活跃了,NSTimer也就可以继续跑下去不会停止,当然这个方法只能让主线程活跃180秒

- (UIBackgroundTaskIdentifier)beginBackgroundTaskWithExpirationHandler:(void(^ __nullable)(void))handler

- (void)endBackgroundTask:(UIBackgroundTaskIdentifier)identifier

是一对,你调用了前者开启了一个后台任务,就要调用后者来结束这个任务

// AppDelegate 中声明一个标识
 @property (nonatomic, unsafe_unretained) UIBackgroundTaskIdentifier backgroundTaskIdentifier;

然后在app进入后台时

- (void)applicationDidEnterBackground:(UIApplication *)application{
    // 返回一个任务标识
    self.backgroundTaskIdentifier = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^(void){
        //  申请的时间到期后进入这里,即马上将被挂起,不再活跃
         [[UIApplication sharedApplication] endBackgroundTask:self.backgroundTaskIdentifier];
        //  将标识符标记为 UIBackgroundTasksInvalid,任务结束
        self.backgroundTaskIdentifier = UIBackgroundTaskInvalid; 
    }];
}

当然如果app切回来的话也要把任务结束

- (void)applicationDidBecomeActive:(UIApplication *)application {
    
     [[UIApplication sharedApplication] endBackgroundTask:self.backgroundTaskIdentifier];
     self.backgroundTaskIdentifier = UIBackgroundTaskInvalid;
}

到此我们解决了一个关于定时器停止的问题,看似解决问题的是开启/结束 后台任务的两个方法,但实际上是我们运用了RunLoop的知识来解决的,这是RunLoop知识运用在实际开发中的一个案例

对了上述我们用到的方法也可以用于解决程序挂起时的复杂操作

比如需要在程序挂起时向服务器post一些数据,以前做的一款产品就是要收集各种操作信息,收集用户的操作路径啊云云,包括用户切换至后台这样的操作都要收集。
用这个方法同样可以解决,但是要注意:

post要用同步方法,保证在主线程里进行

推荐阅读更多精彩内容