iOS后台运行计时器
最近做的一个项目,主要关于效率类的,涉及到得主要功能也就是计时器。如果只是一个简单的计时器,应该是不费事的,但是这次项目中的计时器有几个必须实现的部分:一个是长时间后台运行,另一个是有周期,到时提醒。这些刚开始做的时候感觉很简单,计时器直接套用一个MZTimerLabel这个类,实现正反计时很方便,代码如下:
//计时器
timer1= [[MZTimerLabelalloc]initWithFrame:CGRectMake(0,kScreenHeight*.24,self.view.frame.size.width,40)];
[selfsetTime:timer1];
[self.viewaddSubview:timer1];
- (void)setTime:(MZTimerLabel*)time{
time.timerType=MZTimerLabelTypeTimer;
time.timeLabel.backgroundColor= [UIColorclearColor];
time.timeLabel.textColor= [UIColorwhiteColor];
time.timeLabel.font= [UIFontfontWithName:Gongtisize:28];
time.timeFormat=@"mm : ss";
time.timeLabel.textAlignment=NSTextAlignmentCenter;
time.resetTimerAfterFinish=YES;
}
接下来就要实现周期循环,计时提醒的功能,因为之前对计时器了解不多,用起来不是很得心应手。最开始涉及循环这块,我直接采用了
dispatch_async(dispatch_get_global_queue(0,0), ^{
dispatch_apply([self.count integerValue], dispatch_get_main_queue(), myBlock);
[[NSRunLoop currentRunLoop]run];
});
但是这个方法无法实现计时器的重复运行,这一点让我很废神,但因为赶的紧,所以直接用了NSTimer的一个方法:
[NSTimer scheduledTimerWithTimeInterval:time target:self selector:@selector(runTime) userInfo:nil repeats:YES];
这个方法的缺陷在于周期必须固定,如果一旦某一段任务时间执行超出,那么就会产生代码执行混乱。这里产生的问题,让我一直再想有没有更好的方法,后来就采用了更简单粗暴的方式,方法里套用方法,每执行一次,计数加一,直到指定的周期停止。到这里,循环的问题解决了,后来我就加了声音提醒,手机震动之类的,这里插播一下系统手机声音,震动的方法:先导入AudioToolBox.framework;然后在需要调用的文件里#import <AudioToolbox/AudioToolbox.h>,再添加以下两句代码就可以实现提示音播放和震动了
AudioServicesPlaySystemSound(1005);
AudioServicesPlaySystemSound(kSystemSoundID_Vibrate);
如果想自己添加音乐,可以看下如下链接http://www.blogjava.net/writegull/archive/2012/04/16/374729.html
本来以为到这应该就结束了,结果出现了如下问题:一是无法长时间运行,二是只要计时器置于后台,声音,震动都无法执行。这些问题本来就是iOS自身的问题,因为应用置于后台后,不久就会挂起,大概时间是10分钟。刚开始没想到什么比较好的方法,就在网上查了一些后台长时间运行的方法,尝试了添加后台音频模式,位置更新,但这两种一是没有成功,再个是应用审查可能会被挂掉。一筹莫展,等到第二天起床的时候,闹铃响起,我看了看手机,才想到可以用本地通知的方式啊,怎么之前没想到,应该是从来没用过的原因。本地通知用起来很方便,到时会有提醒之类的,简单写下相关代码
//设置本地通知
+ (void)registerLocalNotification:(NSInteger)alertTime words:(NSString *)words{
UILocalNotification * notification = [[UILocalNotification alloc]init];
//设置触发通知的时间
NSDate * fireDate = [NSDate dateWithTimeIntervalSinceNow:alertTime];
notification.fireDate = fireDate;
//时区
notification.timeZone = [NSTimeZone defaultTimeZone];
//设置重复的间隔
// notification.repeatInterval = kCFCalendarUnitSecond;
//通知内容
notification.alertBody = words;
notification.applicationIconBadgeNumber = 1;
//通知触发播放的声音
notification.soundName = UILocalNotificationDefaultSoundName;
//通知参数
NSDictionary * userDict= [NSDictionary dictionaryWithObject:words forKey:@"key"];
notification.userInfo = userDict;
// ios8后,需要添加这个注册,才能得到授权
if ([[UIApplication sharedApplication] respondsToSelector:@selector(registerUserNotificationSettings:)]) {
UIUserNotificationType type = UIUserNotificationTypeAlert | UIUserNotificationTypeBadge | UIUserNotificationTypeSound;
UIUserNotificationSettings *settings = [UIUserNotificationSettings settingsForTypes:type
categories:nil];
[[UIApplication sharedApplication] registerUserNotificationSettings:settings];
// 通知重复提示的单位,可以是天、周、月
notification.repeatInterval = NSCalendarUnitDay;
} else {
// 通知重复提示的单位,可以是天、周、月
notification.repeatInterval = NSCalendarUnitDay;
}
// 执行通知注册
[[UIApplication sharedApplication] scheduleLocalNotification:notification];
}
// 取消某个本地推送通知
+ (void)cancelLocalNotificationWithKey:(NSString *)key {
// 获取所有本地通知数组
NSArray *localNotifications = [UIApplication sharedApplication].scheduledLocalNotifications;
for (UILocalNotification *notification in localNotifications) {
NSDictionary *userInfo = notification.userInfo;
if (userInfo) {
// 根据设置通知参数时指定的key来获取通知参数
NSString *info = userInfo[key];
// 如果找到需要取消的通知,则取消
if (info != nil) {
[[UIApplication sharedApplication] cancelLocalNotification:notification];
break;
}
}
}
}
//取消特定通知
[jishiVC cancelLocalNotificationWithKey:@"key"];
//取消全部
[[UIApplication sharedApplication]cancelAllLocalNotifications];
// 本地通知回调函数,当应用程序在前台时调用
- (void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification {
// 获取通知所带的数据
NSString *notMess = [notification.userInfo objectForKey:@"key"];
UIAlertController * alert=[UIAlertController alertControllerWithTitle:nil message:notMess preferredStyle:UIAlertControllerStyleAlert];
[self.window.rootViewController presentViewController:alert animated:YES completion:^{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self.window.rootViewController dismissViewControllerAnimated:YES completion:nil];
});
}];
// 更新显示的徽章个数
NSInteger badge = [UIApplication sharedApplication].applicationIconBadgeNumber;
badge--;
badge = badge >= 0 ? badge : 0;
[UIApplication sharedApplication].applicationIconBadgeNumber = badge;
// 在不需要再推送时,可以取消推送
[jishiVC cancelLocalNotificationWithKey:@"key"];
}
但是时间长了以后,即使提醒,计时器还是挂掉了,页面也不在了...
唯有重写计时器页面,每次点进去都要刷新一遍计时,但是这样也会导致一些问题,因为有两个计时页面,后台挂起后,应用重新进入要判断哪个计时页面,计时部分也要做大量的判断,因为会被分成不同阶段,然后有不同的提示。刚开始这么写的时候,很乱。后来就想试试看苹果的计时器是怎么做的,然后用手机自带的计时器设置了半小时的时长,半小时后再打开计时器,页面还在,无论中途打开也依旧如此,这个里边一定有鬼,于是在网上查了相关的资料,发现了如下两段代码,加进去之后,有如神来之笔,计时器可以一段时间内运行,页面不会挂掉:
[[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(appGoToBackground:) name:UIApplicationDidEnterBackgroundNotification object:nil];
[[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(appHasGoneInForeground:) name:UIApplicationWillEnterForegroundNotification object:nil];
查了一下原因:
UIApplicationDidEnterBackgroundNotification(通知名称)----->应用程序在此方法中释放所有可在以后重新创建的资源,保存所有用户数据,关闭网络连接等。如果需要,也可以在这里请求在后台运行更长时间。如果在这里花费了太长时间(超过5秒),系统将断定应用程序的行为异常并终止他。
UIApplicationWillEnterForegroundNotification(通知名称) ---->当应用程序在applicationDidEnterBackground:花费了太长时间,终止后,应该实现此方法来重新创建在applicationDidEnterBackground中销毁的内容,比如重新加载用户数据、重新建立网络连接等。
这是项目中仅仅计时器这块出现的问题,虽然某些问题解决了,但是深层次的原因还是不清楚,希望有了解此类问题的童鞋能多多指教。。。