后台的 NSTimer 行为(addTimer:, beginBackgroundTaskWithExpirationHandler:)

NSTimer behavior in background (addTimer:, beginBackgroundTaskWithExpirationHandler:)

Xcode 6.3.1 ARC 启用,iOS 8.3

我需要帮助理解我在应用程序进入后台后尝试维护单例共享计时器时遇到的奇怪行为。

以前我不关心这个 NSTimer,因为它是使用后台位置服务在后台更新用户位置的。

但是,我想解决用户在后台或位置更新时拒绝位置更新的情况。位置和计时器对于我的应用来说是齐头并进的。

我在以下线程和网站 posts...

中浏览了相当多的信息

Apple's - Background Execution

http://www.devfright.com/ios-7-background-app-refresh-tutorial/

http://www.infragistics.com/community/blogs/stevez/archive/2013/01/24/ios-tips-and-tricks-working-in-the-background.aspx

How do I make my App run an NSTimer in the background?

How do I create a NSTimer on a background thread?

Scheduled NSTimer when app is in background?

http://www.raywenderlich.com/92428/background-modes-ios-swift-tutorial

iPhone - Backgrounding to poll for events

根据这些信息,我能够得出 (2) 种方法,通过这些方法可以使计时器 运行ning 保持约 8 小时,而不会在分配的 180 秒后 iOS 终止应用程序。 Apple 文档承诺的执行时间(3 分钟)。这是代码:

AppDelegate.h (方法#1 或#2 不变)

#import <UIKit/UIKit.h>

@interface MFAppDelegate : UIResponder <UIApplicationDelegate>
{
    UIBackgroundTaskIdentifier bgTask;
    NSInteger backgroundTimerCurrentValue;
}

@property (retain, nonatomic) NSTimer *someTimer;

@end

AppDelegate.m (方法#1)

<...

- (void)applicationDidEnterBackground:(UIApplication *)application
{
    // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 
    // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.

NSLog(@"applicationDidEnterBackground");

backgroundTimerCurrentValue = 0;

[[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:nil];

self.someTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerMethod) userInfo:nil repeats:true];

[[NSRunLoop currentRunLoop] addTimer:self.someTimer forMode:NSRunLoopCommonModes];

}

- (void)timerMethod
{
    NSTimeInterval backgroundTimeRemaining = [[UIApplication sharedApplication] backgroundTimeRemaining];

    if (backgroundTimeRemaining == DBL_MAX)
    {
        NSLog(@"Background Time Remaining = Undetermined");
    }
    else
    {
        NSLog(@"Background Time Remaining = %0.2f sec.", backgroundTimeRemaining);
    }

    if (!someBackgroundTaskStopCondition)
    {
        [self endBackgroundTask];
    }
    else
    {
        nil;
    }
}

...>

AppDelegate.m (方法#2)

<...

- (void)applicationDidEnterBackground:(UIApplication *)application
{
    // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 
    // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.

    NSLog(@"applicationDidEnterBackground");

    backgroundTimerCurrentValue = 0;

    bgTask = UIBackgroundTaskInvalid;

    self.backgroundTaskIdentifier = [application beginBackgroundTaskWithExpirationHandler:^{
        [application endBackgroundTask:bgTask];
    }];

    self.someTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerMethod) userInfo:nil repeats:true];

}

- (void)timerMethod
{
    NSTimeInterval backgroundTimeRemaining = [[UIApplication sharedApplication] backgroundTimeRemaining];

    if (backgroundTimeRemaining == DBL_MAX)
    {
        NSLog(@"Background Time Remaining = Undetermined");
    }
    else
    {
        NSLog(@"Background Time Remaining = %0.2f sec.", backgroundTimeRemaining);
    }

    if (backgroundTimerCurrentValue >= 500)
    {
        backgroundTimerCurrentValue = 0;
        [self.someTimer invalidate];
        [[UIApplication sharedApplication] endBackgroundTask:bgTask];
    }
    else
    {
        NSLog(@"backgroundTimerCurrentValue: %ld", backgroundTimerCurrentValue);
        backgroundTimerCurrentValue++;
    }
}

...>

我已经在一夜之间模拟了 (2) 种方法约 8 小时。 (28,800 秒)通过调整 (backgroundTimerCurrentValue >= 500) 的 if 语句。但是为了重新创建这个问题 post 的结果,我使用了 500 秒。这显然超过 180 秒。这些是两种方法的结果:

...在 self.someTimer 的 500 次迭代后,即 500 秒

现在,这很棒,但是在使用 iPhone 5s 与 Xcode 断开连接进行测试后,等等。奇怪的事情发生了... 我做了单个视图应用程序,用于检查标签中 self.someTimer 的值。

最多 180 秒。标签更新为正确的计时器值。 180 秒后。有时更快,应用程序似乎仍然可以从打开的应用程序幻灯片中选择,但是当我点击屏幕将应用程序置于前台时,应用程序会快速重新启动。

此行为是 Apple 承诺的,即 180 秒后。并且没有有效的 endBackgroundTask:method。方法部分是我认为无效的 UIBackgroundTaskIdentifier 类型,如方法 #2 和 nil 对于方法 #1。

所以我的问题如下:

1) 当 iPhone 插入我的 Mac 运行ning Xcode 时与断开时有什么不同,系统是否允许某些条件会发生这种看似无穷无尽的后台执行?

2) 我总是会对 start/stop 的位置服务感到棘手,这样计时器值就会从上次位置更新后过期的一些时间累积中继续更新,但是有没有人开发这个代码来实际 运行 断开连接 iPhone?

3) 如果在问题 2 中是这样,那么当我将我的应用程序提交到 App Store 时,这样的东西是否合法并且会被接受?

在此先感谢您的帮助。干杯!

是的,运行 应用程序通过调试器将保持活动状态,否则它会在 3 分钟后终止。因此,您显然不能在生产应用程序中依赖该功能。

不,尝试让应用程序在这段时间之后保持活动状态是不合法的(除非您已请求适当的后台操作,因为您的应用程序对后台操作有合法且迫切的需求,例如,它是音乐播放器、VOIP、导航应用程序等)。 Apple 可能会拒绝任何不符合 App Store Review Guidelines 第 2.16 节的此类应用程序。

请参阅iOS 应用程序编程指南的 Background Execution 章节,以更全面地讨论有效的后台操作。