概要
在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要用同步方法,保证在主线程里进行