IPhone - Фоновая обработка опроса для событий

В течение некоторого времени я искал способ в своем приложении iPhone для опроса каждые X минут, чтобы проверить счетчики данных. После значительного чтения документации по фоновому исполнению и нескольких пробных приложений я отклонил это как невозможное, не злоупотребляя фоновыми API-интерфейсами.

На прошлой неделе я нашел это приложение, которое делает именно это. http://itunes.apple.com/us/app/dataman-real-time-data-usage/id393282873?mt=8

Он работает в фоновом режиме и отслеживает количество данных Cellular/WiFi, которые вы использовали. Я подозреваю, что разработчик регистрирует свое приложение в качестве изменений местоположения отслеживания, но значок службы местоположения не отображается во время работы приложения, что я считал необходимым.

Кто-нибудь знает, как это можно сделать?

Ответ 1

Я тоже видел это поведение. Пробовав много, я обнаружил две вещи, которые могли бы помочь. Но я все еще не уверен, как это может повлиять на процесс рассмотрения.

Если вы используете одну из функций фонового рисунка, приложение будет запускаться iOS в фоновом режиме снова после его выхода (системой). Это мы будем оскорблять позже.

В моем случае я использовал фоновое соединение VoIP в моем plist. Весь код здесь выполняется в 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-сокеты. Но этот ответ на 10 минут обеспечивает хороший побочный эффект: через 10 минут (иногда раньше) я обнаружил, что мои таймеры и предыдущие бегущие ступени выполняются ненадолго. Вы можете это увидеть, если поместить в свой код NSLog (..). Это означает, что этот короткий "пробуждение" выполняет код некоторое время. По словам Apple, у нас осталось 30 секунд. Я предполагаю, что фоновый код, подобный потокам, выполняется почти 30 секунд. Это полезный код, если вы должны "иногда" что-то проверить.

В документе говорится, что все фоновые задачи (VoIP, аудио, обновления местоположения) будут автоматически перезагружены в фоновом режиме, если приложение будет прекращено. Приложения VoIP будут запущены в фоновом режиме автоматически после загрузки!

С злоупотреблением этим поведением вы можете заставить ваше приложение выглядеть как "вечно". Зарегистрируйтесь для одного фонового процесса (например, VoIP). Это приведет к перезапуску вашего приложения после завершения.

Теперь напишите код "Задача должна быть закончена". По словам Apple, у вас есть время (5 секунд?) Для завершения задач. Я обнаружил, что это должно быть время процессора. Таким образом, это означает: если вы ничего не делаете, ваше приложение все еще выполняется! Apple предлагает позвонить в суд, если вы закончили свою работу. В приведенном ниже коде вы можете видеть, что у меня есть комментарий на 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-Time здесь, и ваше приложение работает дольше! Но одно можно сказать наверняка: ваше приложение будет прекращено через некоторое время. Но поскольку вы зарегистрировали свое приложение как VoIP или одно из других, система перезапустит приложение в фоновом режиме, что перезапустит ваш фоновый процесс;-) С помощью этого PingPong я могу многое сделать. но помните, что очень экономьте время процессора. И сохраните все данные, чтобы восстановить свои представления - ваше приложение будет прекращено через некоторое время. Чтобы он все еще работал, вы должны вернуться в свое последнее состояние после пробуждения.

Я не знаю, является ли это подход приложений, о которых вы говорили ранее, но он работает для меня.

Надеюсь, что смогу помочь

Update:

После измерения времени задания BG произошел сюрприз. Задача BG ограничена 600 секундами. Это минимальное время минимального времени VoIP (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 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, почти не имеет энергии. Согласно "userexperience", это лучший подход. Возможно, другие приложения работают так, скрывая свое поведение за функциональностью GPS...

Ответ 2

Что работает, а что нет

Не совсем понятно, какой из этих ответов работает, и я потратил много времени на то, чтобы пробовать их всех. Итак, вот мой опыт в каждой стратегии:

  • VOIP hack - работает, но будет получать отклонение, если вы не являетесь приложением VOIP
  • Рекурсивный beginBackgroundTask... - не работает. Он прекратит работу через 10 минут. Даже если вы попробуете исправления в комментариях (по крайней мере, до 30 ноября 2012 г.).
  • Silent Audio - работает, но люди были отклонены для этого
  • Локальные/Push-уведомления - требуется взаимодействие с пользователем до того, как ваше приложение будет разбужено.
  • Использование фонового местоположения - работает. Вот подробности:

В основном вы используете фоновый режим "location", чтобы ваше приложение работало в фоновом режиме. Он работает, даже если пользователь не разрешает обновление местоположения. Даже если пользователь нажимает кнопку "домой" и запускает другое приложение, ваше приложение все равно будет работать. Это также разрядник аккумулятора и может быть растянутым в процессе утверждения, если ваше приложение не имеет ничего общего с местоположением, но насколько я знаю, это единственное решение, которое имеет хорошие шансы на одобрение.

Здесь как это работает:

В вашем наборе plist:

  • Приложение не работает в фоновом режиме: NO
  • Необходимые фоновые режимы: расположение

Затем укажите базовую структуру CoreLocation (в разделе "Фазы сборки" ) и добавьте этот код в свое приложение (прежде чем он перейдет в фоновый режим):

#import <CoreLocation/CoreLocation.h>

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

Примечание: startMonitoringSignificantLocationChanges работает не.

Также стоит отметить, что если ваше приложение выйдет из строя, iOS не вернет его к жизни. Перехват VOIP - единственный, который может вернуть его.

Ответ 3

Существует еще один способ остаться навсегда в фоновом режиме - запуск/остановка менеджера местоположений в фоновой задаче, будет reset фоновый таймер при вызове doneUpdateToLocation:.

Я не знаю, почему это работает, но я думаю, что doUpdateToLocation также называется задачей и тем самым сбрасывает таймер.

Основываясь на тестировании, я считаю, что это то, что использует DataMan Pro.

Смотрите это сообщение fooobar.com/questions/34958/..., откуда я получил трюк.

Вот некоторые результаты из нашего приложения:

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

Ответ 4

Если это не GPS, я думаю, что единственный способ сделать это - функция фоновой музыки, т.е. играть 4 "33" все время, когда она включена. Оба звука кажутся немного злоупотребляющими API обработки фоновых изображений и поэтому потенциально подвержены прихотям процесса обзора.

Ответ 5

Я попробовал обновление 2, но он просто не работает. Когда вызывается обработчик истечения срока действия, он завершает фоновое задание. Затем запуск новой фоновой задачи снова заставляет немедленный вызов обработчику истечения срока действия (таймер не reset и по-прежнему истек). Таким образом, я получил 43 запуска/остановки фоновых задач до того, как приложение было приостановлено.

Ответ 6

в моих тестах на iOS5 я обнаружил, что он помогает запустить мониторинг CoreLocation через startMonitoringForLocationChangeEvents (not SignificantLocationChange), точность не имеет значения и даже на iPod если я это сделаю - backgroundTimeRemaining никогда не опускается.

Ответ 7

Это довольно старый вопрос, но правильный способ сделать это сейчас - это обновить фоновое приложение, которое полностью поддерживается ОС и не требует взломов.

Обратите внимание, что вы зависите от операционной системы относительно того, когда на самом деле произойдет обновление вашего фона. Вы можете установить минимальное время опроса, но нет гарантии, что он будет вызываться на этой частоте, поскольку ОС будет применять свою собственную логику для оптимизации времени работы от батареи и использования радиосвязи.

Первое, что вам нужно сделать, это настроить свой проект для поддержки фонового обновления на вкладке "Возможности". Это добавит необходимые ключи в ваш Info.plist:

enter image description here

Затем вам нужно добавить некоторые вещи в ваш AppDelegate, который должен реализовать как URLSessionDelegate и URLSessionDownloadDelegate:

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.quetzalpos.Dashboard.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)

        // handle fetched data here and determine if there are changes
        if changes {
            result = .newData
            // ...
        }
    } catch {
        result = .failed
        print("\(error.localizedDescription)")
    }

    _backgroundCompletionHandler?(result)
    _backgroundCompletionHandler = nil
}