iPhone - фон для опроса событий

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

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

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

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

6 ответов

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

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

В моем случае я использовал 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-сокеты. Но этот 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 я могу выполнять много фоновых операций. но помните, что нужно уделять больше времени процессору. И сохраните все данные, чтобы восстановить ваши представления - ваше приложение будет закрыто через некоторое время. Чтобы он все еще работал, вы должны вернуться в свое последнее "состояние" после пробуждения.

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

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

Обновить:

После измерения времени выполнения задания 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);
        }    
    }); 
}

После истечения времени ожидания вашего приложения будет вызван expirationHandler VoIP, где вы просто перезапустите долгосрочную задачу. Эта задача будет завершена через 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...

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

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

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

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

Вот как это работает:

В вашем списке plist:

  • Приложение не работает в фоновом режиме: НЕТ
  • Требуемые фоновые режимы: местоположение

Затем обратитесь к платформе CoreLocation (в фазе сборки) и добавьте этот код где-нибудь в вашем приложении (до того, как оно перейдет в фоновый режим):

#import <CoreLocation/CoreLocation.h>

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

Замечания: startMonitoringSignificantLocationChanges не будет работать

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

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

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

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

Смотрите этот пост /questions/21934034/kak-mne-poluchat-fonovoe-obnovlenie-mestopolozheniya-kazhdyie-n-minut-v-moem-prilozhenii-ios/21934049#21934049 откуда я получил трюк.

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

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

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

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

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

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

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

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

Затем вам нужно добавить некоторые вещи к вашему 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.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
}
Другие вопросы по тегам