iPhone - 后台轮询事件

68
有一段时间,我一直在寻找在我的iPhone应用程序中轮询每X分钟以检查数据计数器的方法。经过阅读后台执行文档和尝试了几个测试应用程序之后,我认为如果不滥用后台API,则不可能实现此目的。
上周我发现了这个应用程序,它完全做到了这一点。它在后台运行并跟踪您使用的移动数据/ WiFi 数据的数量。我怀疑开发人员正在将他的应用程序注册为跟踪位置更改,但是当应用程序运行时,位置服务图标不可见,这让我感到困惑。
有人知道如何实现这一点吗? http://itunes.apple.com/us/app/dataman-real-time-data-usage/id393282873?mt=8

另一个可以做到这个的应用程序:http://itunes.apple.com/gb/app/georing-tag-your-calls-music/id411898845?mt=8 我完全被难住了。它似乎没有使用位置服务,因为图标不可见,也不能将后台事件链接在一起,因为10分钟超时是绝对的,你无法启动另一个。从技术角度来看,我现在真的很烦恼,因为我不知道该怎么做。 - NeilInglis
你的问题解决了吗?我想知道Jack的答案是否解决了你的问题(没有VOIP功能)。 - Tuyen Nguyen
苹果公司的后台执行文档相关链接。 - sherb
7个回答

92

我也见过这种情况。尝试了很多后,我发现两件事可能会有所帮助。但我仍然不确定这会如何影响审核过程。

如果您使用其中一种后台功能,应用程序将在被系统退出后(通过系统)再次由iOS在后台启动。稍后我们将滥用它。

在我的情况下,我在我的plist中启用了VoIP后台。所有的代码都在你的AppDelegate中完成:

// if the iOS device allows background execution,
// this Handler will be called
- (void)backgroundHandler {

    NSLog(@"### -->VOIP backgrounding callback");
    // try to do sth. According to Apple we have ONLY 30 seconds to perform this Task!
    // Else the Application will be terminated!
    UIApplication* app = [UIApplication sharedApplication];
    NSArray*    oldNotifications = [app scheduledLocalNotifications];

     // Clear out the old notification before scheduling a new one.
    if ([oldNotifications count] > 0) [app cancelAllLocalNotifications];

    // Create a new notification
    UILocalNotification* alarm = [[[UILocalNotification alloc] init] autorelease];
    if (alarm)
    {
        alarm.fireDate = [NSDate date];
        alarm.timeZone = [NSTimeZone defaultTimeZone];
        alarm.repeatInterval = 0;
        alarm.soundName = @"alarmsound.caf";
        alarm.alertBody = @"Don't Panic! This is just a Push-Notification Test.";

        [app scheduleLocalNotification:alarm];
    }
}

注册完成后

- (void)applicationDidEnterBackground:(UIApplication *)application {

    // This is where you can do your X Minutes, if >= 10Minutes is okay.
    BOOL backgroundAccepted = [[UIApplication sharedApplication] setKeepAliveTimeout:600 handler:^{ [self backgroundHandler]; }];
    if (backgroundAccepted)
    {
        NSLog(@"VOIP backgrounding accepted");
    }
}

现在,神奇的事情发生了:我甚至不使用VoIP-Sockets。但是这10分钟的回调提供了一个不错的副作用:10分钟后(有时更早),我发现我的计时器和之前运行的线程会被短暂地执行一段时间。如果您在代码中放置一些NSLog(..),您可以看到这一点。这意味着这个短暂的“唤醒”会执行代码一段时间。根据苹果公司的说法,我们还剩下30秒的执行时间。我认为,像线程这样的后台代码将被执行近30秒。如果您必须“有时”检查某些内容,则此代码非常有用。
文档表示,所有后台任务(VoIP、音频、位置更新)将在应用程序终止后自动重新启动。VoIP 应用程序将在开机后自动启动!
通过滥用这种行为,您可以使您的应用程序看起来像“永远”运行。 注册一个后台进程(例如 VoIP)。这将导致您的应用程序在终止后重新启动。
现在编写一些“任务必须完成”的代码。根据苹果公司的说法,您还有一些时间(5秒?)来完成任务。我发现,这必须是CPU时间。所以这意味着:如果您什么也不做,您的应用程序仍然在执行!苹果建议在完成工作后调用一个expirationhandler。在下面的代码中,您可以看到我在expirationHandler处有一个注释。这将导致您的应用程序一直运行,直到系统允许您的应用程序终止。所有计时器和线程都会保持运行状态,直到iOS终止您的应用程序。
- (void)applicationDidEnterBackground:(UIApplication *)application {

    UIApplication*    app = [UIApplication sharedApplication];

    bgTask = [app beginBackgroundTaskWithExpirationHandler:^{
        [app endBackgroundTask:bgTask];
        bgTask = UIBackgroundTaskInvalid;
    }];


    // Start the long-running task and return immediately.
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

    // you can do sth. here, or simply do nothing!
    // All your background treads and timers are still being executed
    while (background) 
       [self doSomething];
       // This is where you can do your "X minutes" in seconds (here 10)
       sleep(10);
    }

    // And never call the expirationHandler, so your App runs
    // until the system terminates our process
    //[app endBackgroundTask:bgTask];
    //bgTask = UIBackgroundTaskInvalid;

    }); 
}

非常节约CPU时间,你的应用程序会运行更长时间!但有一件事是肯定的:你的应用程序将在一段时间后被终止。但是因为你将你的应用程序注册为VoIP或其他之一,系统会在后台重新启动应用程序,这将重新启动你的后台进程 ;-) 使用此PingPong,我可以做很多后台操作。但请记住非常节省CPU时间。并保存所有数据,以恢复你的视图 - 你的应用程序将在稍后的某个时候被终止。为使其看起来仍在运行,你必须在唤醒后跳回到你的最后一个“状态”。
我不知道这是否是你之前提到的应用程序的方法,但对我有效。
希望我能帮到你。
更新:
测量了BG任务的时间后,发现有一个惊喜。BG任务被限制在600秒内。这正好是VoIP minimumtime(setKeepAliveTimeout:600)的最小时间。
所以,以下代码会导致在后台无限执行:
头文件:
UIBackgroundTaskIdentifier bgTask; 

代码:

// if the iOS device allows background execution,
// this Handler will be called
- (void)backgroundHandler {

    NSLog(@"### -->VOIP backgrounding callback");

    UIApplication*    app = [UIApplication sharedApplication];

    bgTask = [app beginBackgroundTaskWithExpirationHandler:^{
        [app endBackgroundTask:bgTask];
        bgTask = UIBackgroundTaskInvalid;
    }];

    // Start the long-running task 
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

    while (1) {
        NSLog(@"BGTime left: %f", [UIApplication sharedApplication].backgroundTimeRemaining);
           [self doSomething];
        sleep(1);
    }   
});     

- (void)applicationDidEnterBackground:(UIApplication *)application {

    BOOL backgroundAccepted = [[UIApplication sharedApplication] setKeepAliveTimeout:600 handler:^{ [self backgroundHandler]; }];
    if (backgroundAccepted)
    {
        NSLog(@"VOIP backgrounding accepted");
    }

    UIApplication*    app = [UIApplication sharedApplication];

    bgTask = [app beginBackgroundTaskWithExpirationHandler:^{
        [app endBackgroundTask:bgTask];
        bgTask = UIBackgroundTaskInvalid;
    }];


    // Start the long-running task
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

        while (1) {
            NSLog(@"BGTime left: %f", [UIApplication sharedApplication].backgroundTimeRemaining);
           [self doSomething];
           sleep(1);
        }    
    }); 
}

当你的应用程序超时后,VoIP expirationHandler将被调用,在那里你只需重新启动一个长时间运行的任务。此任务将在600秒后终止。但是会再次调用到到期处理程序,它会启动另一个长时间运行的任务,以此类推。现在你只需要检查应用程序是否返回前台。然后关闭bgTask,完成操作。也许你可以在长时间运行的任务中的expirationHandler内执行这样的操作。试一下吧。使用控制台查看发生了什么...玩得开心!

更新2:

有时候简化事情会有所帮助。我的新方法是这样的:

- (void)applicationDidEnterBackground:(UIApplication *)application {

    UIApplication*    app = [UIApplication sharedApplication];

    // it's better to move "dispatch_block_t expirationHandler"
    // into your headerfile and initialize the code somewhere else
    // i.e. 
    // - (void)applicationDidFinishLaunching:(UIApplication *)application {
//
// expirationHandler = ^{ ... } }
    // because your app may crash if you initialize expirationHandler twice.
    dispatch_block_t expirationHandler;
    expirationHandler = ^{

        [app endBackgroundTask:bgTask];
        bgTask = UIBackgroundTaskInvalid;


        bgTask = [app beginBackgroundTaskWithExpirationHandler:expirationHandler];
    };

    bgTask = [app beginBackgroundTaskWithExpirationHandler:expirationHandler];


    // Start the long-running task and return immediately.
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

        // inform others to stop tasks, if you like
        [[NSNotificationCenter defaultCenter] postNotificationName:@"MyApplicationEntersBackground" object:self];

        // do your background work here     
    }); 
}

这是在没有VoIP黑客的情况下工作的。根据文档,如果执行时间超过限制,则会执行到期处理程序(在本例中为我的“expirationHandler”块)。通过将块定义为块变量,可以在到期处理程序内递归地重新启动长时间运行的任务。这也会导致无限执行。

请注意,在应用程序再次进入前台时终止任务。如果不再需要任务,请终止任务。

根据我自己的经验,我测量了一些东西。使用启用GPS电台的位置回调会非常快地耗尽电池。使用我在更新2中发布的方法几乎不消耗能量。从“用户体验”角度来看,这是一种更好的方法。也许其他应用程序也是这样工作的,将其行为隐藏在GPS功能背后...


2
嗨,这取决于你的编码。看看最后一个例子。如果您不终止bgtask,则在10秒后框架将终止该任务。您的bghandler(提供的块)将终止bgtask。在块内启动了一个新的bgtask。在我的应用程序中,它正在工作。我发现,您应该在headerfile中使块变量“全局”,并在启动时对其进行初始化。如果您两次初始化块变量,则应用程序将被终止。 - JackPearse
1
附加说明:不要将VoIP回调与后台处理程序方法一起使用。启动两次bgtask可能会导致终止。也就是说:仅使用“更新2”部分中的示例。它应该可以工作。 - JackPearse
1
这个原理上是可行的,但是在本帖中的任何示例代码中都不能正常运行。我建议将“expirationHandler”块中的代码移动到您类中的独立方法中。然后,每当您调用“beginBackgroundTaskWithExpirationHandler:”时,只需将“^{[self expirationHandler];}”作为参数传递即可。这避免了一些似乎由于块在执行时引用自身而引起的问题。 - aroth
1
你需要在块变量expirationHandler中添加__block,以便它可以引用自身。这样它就可以看到变量的更新。否则,它只会捕获块创建时变量的值(nil)。 - Christopher Rogers
4
这个解决方案是间歇性的,并且取决于每个iOS版本如何实现挂起后台应用程序。例如,我相信它在iOS 6.0中可以工作,但在6.1版本中停止工作。同样地,我曾经让这个解决方案在7.0中工作过,但在7.1版本中再次崩溃了,因为苹果重新加强了对电池耗尽的管理。因此,简而言之,它不可靠。 - Nate
显示剩余21条评论

17

哪些是有效的,哪些是无效的

目前还不清楚这些答案中哪些能起作用,我已经浪费了很多时间尝试它们。以下是我对每种策略的经验总结:

  1. VOIP hack - 能够起作用,但如果你不是 VOIP 应用程序,则会被拒绝。
  2. 递归的 beginBackgroundTask... - 无效。它将在10分钟后退出。即使您尝试了评论中的修复方法(至少截至2012年11月30日的评论)。
  3. 静默音频 - 能够起作用,但有些应用因此而被拒绝。
  4. 本地/推送通知 - 在您的应用程序被唤醒之前需要用户交互。
  5. 使用后台定位 - 能够起作用。以下是详细信息:

基本上,您可以使用“位置”后台模式来让您的应用程序在后台运行。即使用户禁止了位置更新,它也会起作用。即使用户按下主页按钮并启动另一个应用程序,您的应用程序仍将在后台运行。但它会耗电,并且如果您的应用程序与位置无关,审核过程可能会有点棘手。但据我所知,这是唯一一个被批准的解决方案。

以下是它的工作原理:

在您的 plist 中设置:

  • Application does not run in background: NO
  • Required background modes: location

然后引用 CoreLocation 框架(在 Build Phases 中),并在应用程序进入后台之前的某个位置添加此代码:

#import <CoreLocation/CoreLocation.h>

CLLocationManager* locationManager = [[CLLocationManager alloc] init];
[locationManager startUpdatingLocation];

注意: startMonitoringSignificantLocationChanges 不会起作用。

值得一提的是,如果您的应用程序崩溃,则iOS将不会将其恢复。 VOIP hack是唯一可以将其恢复的方法。


为什么要说“注意:startMonitoringSignificantLocationChanges不会起作用”? 因为当应用在后台运行时,该模式得到支持,并且不会像其他模式那样耗用电池电量。 - Lolo
2
在我的测试中,startMonitoringSignificantLocationChanges会在后台运行,但不能防止应用程序在一段时间后进入睡眠状态。 - bendytree
很奇怪,因为您还指定用户不需要允许位置更新,这意味着它与更新的频率无关。也许这与应用程序需要保持某些硬件组件运行有关?无论如何,感谢澄清。 - Lolo

6

还有一种技术可以永久停留在后台 - 在您的后台任务中启动/停止位置管理器,当调用didUpdateToLocation:时,会重置后台计时器。

我不知道为什么它有效,但我认为didUpdateToLocation也被称为任务,从而重置了计时器。

根据测试,我相信DataMan Pro正在使用这个技术。

请参阅此帖子https://dev59.com/8mw15IYBdhLWcg3w3vle#6465280,其中我学到了这个技巧。

以下是我们应用程序的一些结果:

2012-02-06 15:21:01.520 **[1166:4027] BGTime left: 598.614497 
2012-02-06 15:21:02.897 **[1166:4027] BGTime left: 597.237567 
2012-02-06 15:21:04.106 **[1166:4027] BGTime left: 596.028215 
2012-02-06 15:21:05.306 **[1166:4027] BGTime left: 594.828474 
2012-02-06 15:21:06.515 **[1166:4027] BGTime left: 593.619191
2012-02-06 15:21:07.739 **[1166:4027] BGTime left: 592.395392 
2012-02-06 15:21:08.941 **[1166:4027] BGTime left: 591.193865 
2012-02-06 15:21:10.134 **[1166:4027] BGTime left: 590.001071
2012-02-06 15:21:11.339 **[1166:4027] BGTime left: 588.795573
2012-02-06 15:21:11.351 **[1166:707] startUpdatingLocation
2012-02-06 15:21:11.543 **[1166:707] didUpdateToLocation
2012-02-06 15:21:11.623 **[1166:707] stopUpdatingLocation
2012-02-06 15:21:13.050 **[1166:4027] BGTime left: 599.701993
2012-02-06 15:21:14.286 **[1166:4027] BGTime left: 598.465553

这个很好用,但我不认为DataMan在使用它,因为我不记得曾经被要求允许位置权限。 - htafoya

1
如果不是GPS,我认为另一种方法就是背景音乐功能,即在启用时始终播放4'33"。这两种方法听起来都有点滥用后台处理API,并且可能受审查过程的诉求支配。

7
“playing 4'33" all the time it's enabled" 这是我今天读到的最棒的东西。 (翻译:将“playing 4'33" all the time it's enabled”翻译成中文,意为“一直演奏《4分33秒》,只要启用了它”,并将“Best thing I've read all day.”翻译成“这是我今天读到的最棒的东西。”) - BoltClock
1
我查看了plist文件,只有将UIBackgroundModes中的“location”启用,所以不是音频。我想知道他是否已经找到了隐藏位置指示器的方法。 - NeilInglis

1

我尝试了更新2,但它根本不起作用。当到期处理程序被调用时,它会结束后台任务。然后启动新的后台任务只会再次强制立即调用到期处理程序(计时器未重置且仍处于过期状态)。因此,在应用程序被挂起之前,我得到了43次后台任务的启动/停止。


是的,对我也不起作用,除非我通过gdb连接到后台,否则backgroundTimeRemaining似乎很高兴地变成负数。否则它会被看门狗杀掉。我尝试了许多变化,但没有一个能够生效。 - NeilInglis
你好。感谢您的评论。现在我已经看到,使用调试模式或ADHoc版本会有所不同。我将继续努力解决这个问题。如果有任何干净的解决方案,我会立即在这里发布。实际上,我正在等待VOIP超时来进行“短暂”的后台处理。但这将每10分钟发生一次。 - JackPearse
我使用VoIP和GPS(我的应用程序需要GPS),并且它可以正常工作(启用GPS可以通过看门狗完成)。我每秒执行一次小处理。当然,这个应用程序没有任何机会被App Store批准。但是,使用企业分发时没有问题。 - Tauri
同样的问题。我启动我的应用程序,调用beginBackgroundTaskWithExpirationHandler,注册保持活动处理程序并将其移至后台,一切正常,backgroundTimeRemaining为600。 600秒后应用程序暂停,然后在几秒钟后调用保持活动处理程序。 我再次调用beginBackgroundTaskWithExpirationHandler,但backgroundTimeRemaining仅为40〜60秒。 - s.maks

0

这是一个相当老的问题,但现在正确的方法是通过后台应用程序刷新来完成,该方法得到了操作系统的全面支持,不需要任何黑客技巧。

请注意,您仍然取决于操作系统何时实际进行后台刷新。您可以设置最小轮询时间,但不能保证它将以该频率被调用,因为操作系统将应用其自己的逻辑来优化电池寿命和无线电使用。

首先,您需要在“功能”选项卡中配置项目以支持后台刷新。这将向您的Info.plist添加必要的键:

enter image description here

然后你需要在你的AppDelegate中添加一些内容,它需要实现URLSessionDelegateURLSessionDownloadDelegate

private var _backgroundCompletionHandler: ((UIBackgroundFetchResult) -> Void)?

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    ...

    application.setMinimumBackgroundFetchInterval(5 * 60) // set preferred minimum background poll time (no guarantee of this interval)

    ...
}

func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
    _backgroundCompletionHandler = completionHandler

    let sessionConfig = URLSessionConfiguration.background(withIdentifier: "com.yourapp.backgroundfetch")
    sessionConfig.sessionSendsLaunchEvents = true
    sessionConfig.isDiscretionary = true

    let session = URLSession(configuration: sessionConfig, delegate: self, delegateQueue: OperationQueue.main)
    let task = session.downloadTask(with: url)
    task.resume()
}

func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) {
    NSLog("URLSession error: \(error.localizedDescription)")
    _backgroundCompletionHandler?(.failed)
    _backgroundCompletionHandler = nil
}

func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
    NSLog("didFinishDownloading \(downloadTask.countOfBytesReceived) bytes to \(location)")
    var result = UIBackgroundFetchResult.noData

    do {
        let data = try Data(contentsOf: location)
        let json = try JSONSerialization.jsonObject(with: data, options: .mutableContainers)

        // process the fetched data and determine if there are new changes
        if changes {
            result = .newData

            // handle the changes here
        }
    } catch {
        result = .failed
        print("\(error.localizedDescription)")
    }

    _backgroundCompletionHandler?(result)
    _backgroundCompletionHandler = nil
}

0
在我的iOS5测试中,我发现通过启动CoreLocation的监控(通过startMonitoringForLocationChangeEvents而不是SignificantLocationChange)有助于提高性能,精度并不重要,即使在iPod上也是如此。如果我这样做 - backgroundTimeRemaining永远不会下降。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接