web-dev-qa-db-fra.com

iPhone - Contexte pour interroger des événements

Pendant un bon moment, je cherchais un moyen dans mon application iPhone d'interroger toutes les X minutes pour vérifier les compteurs de données. Après avoir longuement lu la documentation sur l'exécution en arrière-plan et quelques applications d'essai, j'avais considéré que c'était impossible sans abuser des API d'arrière-plan.

La semaine dernière, j'ai trouvé cette application qui fait exactement cela. http://iTunes.Apple.com/us/app/dataman-real-time-data-usage/id393282873?mt=8

Il fonctionne en arrière-plan et assure le suivi du nombre de données cellulaires/WiFi que vous avez utilisées. Je soupçonne que le développeur enregistre son application en tant que suivi des modifications d'emplacement, mais que l'icône des services de localisation n'est pas visible pendant l'exécution de l'application, ce qui, à mon avis, était une exigence.

Est-ce que quelqu'un a des indices sur la façon dont cela peut être accompli? 

65
NeilInglis

J'ai vu ce comportement aussi. Après avoir beaucoup essayé, j'ai découvert deux choses qui pourraient aider. Mais je ne sais toujours pas en quoi cela pourrait influencer le processus d’examen.

Si vous utilisez l'une des fonctionnalités d'arrière-plan, l'application sera lancée par iOS en arrière-plan une fois qu'elle aura été fermée (par le système). Nous en abuserons plus tard.

Dans mon cas, j'ai utilisé un fond VoIP activé dans mon plist. Tout le code est fait ici dans votre 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];
    }
}

et l'enregistrement se fait en

- (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");
    }
}

Maintenant, la magie opère: je n'utilise même pas les sockets VoIP. Mais ce rappel de 10 minutes fournit un effet secondaire intéressant: après 10 minutes (parfois plus tôt), j'ai découvert que mes minuteries et les marches en cours d'exécution précédentes étaient exécutées pendant un court instant. Vous pouvez le voir si vous insérez du NSLog (..) dans votre code. Cela signifie que ce court "réveil" exécute le code pendant un moment. Selon Apple, il nous reste 30 secondes d'exécution. Je suppose que ce code d'arrière-plan, comme les threads, est exécuté pendant près de 30 secondes. Ce code est utile si vous devez "parfois" vérifier quelque chose.

La doc dit que toutes les tâches en arrière-plan (VoIP, audio, mises à jour d'emplacement) seront automatiquement redémarrées en arrière-plan si l'application était fermée. Les applications VoIP seront lancées automatiquement en arrière-plan après le démarrage!

En abusant de ce comportement, vous pouvez donner l’apparence de votre application à une exécution «éternelle»… .. Inscrivez-vous à un processus en arrière-plan (c'est-à-dire la VoIP). Cela entraînera le redémarrage de votre application après la résiliation.

Maintenant, écrivez du code "La tâche doit être terminée". Selon Apple, il vous reste un peu de temps (5 secondes?) Pour terminer les tâches. J'ai découvert que cela devait prendre du temps CPU. Cela signifie donc que si vous ne faites rien, votre application est toujours en cours d'exécution! Apple suggère d'appeler un gestionnaire d'expiration si votre travail est terminé. Dans le code ci-dessous, vous pouvez voir que j'ai un commentaire à expirationHandler. Votre application sera alors exécutée tant que le système le permettra. Tous les minuteurs et les threads continuent à fonctionner jusqu'à ce qu'iOS ait terminé votre application.

- (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;

    }); 
}

Soyez très disponible avec CPU-Time ici, et votre application fonctionne plus longtemps! Mais une chose est sûre: votre application sera résiliée au bout d'un moment. Mais comme vous avez enregistré votre application en tant que VoIP ou l'une des autres, le système redémarre l'application en arrière-plan, ce qui redémarrera votre processus d'arrière-plan; -) Avec ce PingPong, je peux faire beaucoup de fond. mais souvenez-vous d’être très disponible avec le temps de calcul. Et enregistrez toutes les données, pour restaurer vos vues - votre application sera terminée quelque temps plus tard. Pour le faire paraître en cours d'exécution, vous devez revenir dans votre dernier "état" après le réveil.

Je ne sais pas si c'est l'approche des applications que vous avez mentionnées précédemment, mais cela fonctionne pour moi.

J'espère pouvoir aider

Mettre à jour:

Après avoir mesuré le temps de la tâche BG, il y avait une surprise. La tâche BG est limitée à 600 secondes. Il s'agit de l'heure minimale exacte de l'heure minimale de la VoIP (setKeepAliveTimeout: 600).

Donc, ce code mène à une exécution "infinie" en arrière-plan:

Entête:

UIBackgroundTaskIdentifier bgTask; 

Code:

// 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);
        }    
    }); 
}

Une fois que votre application a expiré, le gestionnaire ExpirationHandler de VoIP est appelé. Vous redémarrez simplement une tâche de longue durée. Cette tâche sera terminée après 600 secondes. Mais il y aura à nouveau un appel au gestionnaire d'expiration, qui lancera une autre tâche longue, etc. Il ne vous reste plus qu'à vérifier si l'application revient à l'avant-plan. Fermez ensuite la tâche bg et vous avez terminé. Peut-être qu'on peut faire ça. comme ceci dans expirationHandler de la longue tâche en cours d'exécution. Juste l'essayer. Utilisez votre console pour voir ce qui se passe ... Amusez-vous!

Mise à jour 2:

Parfois, simplifier les choses aide. Ma nouvelle approche est celle-ci:

- (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     
    }); 
}

Cela fonctionne sans le piratage VoIP. Selon la documentation, le gestionnaire d'expiration (dans ce cas, mon bloc 'expirationHandler') sera exécuté si le temps d'exécution est écoulé. En définissant le bloc dans une variable de bloc, il est possible de relancer de manière récursive la tâche d'exécution longue dans le gestionnaire d'expiration. Cela mène également à une exécution sans fin.

Sachez que vous devez mettre fin à la tâche si votre application entre à nouveau au premier plan. Et terminez la tâche si vous n'en avez plus besoin. 

Pour ma propre expérience, j’ai mesuré quelque chose… .. Utiliser le rappel de position avec la radio GPS allumée me permettait de vider ma batterie très rapidement. Utiliser l’approche que j’ai publiée dans Update 2 ne prend presque pas d’énergie. Selon "l'expérience utilisateur", il s'agit d'une meilleure approche. Peut-être que d'autres applications fonctionnent comme cela, cachant son comportement derrière la fonctionnalité GPS ...

91
JackPearse

Ce qui fonctionne et ce qui ne fonctionne pas

On ne sait pas très bien laquelle de ces réponses fonctionne et j'ai perdu beaucoup de temps à toutes les essayer. Alors voici mon expérience avec chaque stratégie:

  1. VoIP bidouille - fonctionne, mais va vous faire rejeter si vous n'êtes pas une application VOIP
  2. beginBackgroundTask... récursif - ne fonctionne pas. Il va cesser après 10 minutes. Même si vous essayez les correctifs dans les commentaires (au moins les commentaires jusqu'au 30 novembre 2012).
  3. Silent Audio - fonctionne, mais les gens ont été rejetés pour cela
  4. Notifications locales/push - nécessitent une interaction de l'utilisateur avant que votre application ne soit réactivée
  5. Utilisation de l'emplacement de l'arrière-plan - fonctionne. Voici les détails:

Fondamentalement, vous utilisez le mode d’arrière-plan «Emplacement» pour que votre application continue de fonctionner en arrière-plan. Cela fonctionne, même si l'utilisateur n'autorise pas les mises à jour d'emplacement. Même si l'utilisateur appuie sur le bouton d'accueil et lance une autre application, votre application sera toujours en cours d'exécution. Il s’agit également d’un vider la batterie et peut s’avérer très compliqué dans le processus d’approbation si votre application n’a rien à voir avec l’emplacement, mais à ma connaissance, c’est la seule solution qui a de bonnes chances d’être approuvée.

Voici comment ça fonctionne:

Dans votre set de plist:

  • L'application ne s'exécute pas en arrière-plan: NON
  • Modes d'arrière-plan requis: emplacement

Ensuite, référencez le framework CoreLocation (dans Build Phases) et ajoutez ce code quelque part dans votre application (avant qu'il ne soit passé en arrière-plan):

#import <CoreLocation/CoreLocation.h>

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

Remarque: startMonitoringSignificantLocationChanges va pas fonctionne. 

Il convient également de mentionner que si votre application se bloque, iOS ne la ramènera pas à la vie. Le piratage VOIP est le seul qui peut le ramener.

17
bendytree

Il existe une autre technique pour rester en permanence à l'arrière-plan: le démarrage/l'arrêt du gestionnaire d'emplacement dans votre tâche d'arrière-plan, réinitialiser le temporisateur d'arrière-plan lorsque didUpdateToLocation: est appelé.

Je ne sais pas pourquoi cela fonctionne, mais je pense que didUpdateToLocation est également appelée en tant que tâche et réinitialise ainsi le minuteur. 

D'après des tests, je pense que c'est ce que DataMan Pro utilise. 

Voir ce message https://stackoverflow.com/a/6465280 d'où je viens d'un tour.

Voici quelques résultats de notre application:

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
6
Kashif Shaikh

J'ai essayé la mise à jour 2 mais cela ne fonctionne tout simplement pas. Lorsque le gestionnaire d'expiration est appelé, il termine la tâche en arrière-plan. Puis, le démarrage d’une nouvelle tâche en arrière-plan oblige à nouveau à appeler immédiatement le gestionnaire d’expiration (le temporisateur n’est pas réinitialisé et a expiré). Ainsi, j'ai eu 43 démarrages/arrêts de tâches en arrière-plan avant la suspension de l'application.

1
iOSTester

Si ce n’est pas le GPS, je pense que la seule autre façon de le faire est la fonction de musique de fond, c’est-à-dire de jouer à 4 "33" tout le temps, elle est activée. Les deux semblent être un peu un abus des API de traitement en arrière-plan et donc potentiellement soumis aux caprices du processus de révision.

1
Stephen Darlington

C'est une question un peu ancienne, mais la bonne façon de le faire maintenant consiste à utiliser Background App Refresh , qui est entièrement pris en charge par le système d'exploitation et ne nécessite aucun piratage.

Notez que vous êtes toujours à la merci du système d'exploitation pour savoir quand votre actualisation en arrière-plan aura lieu. Vous pouvez définir un temps d'interrogation minimal, mais rien ne garantit qu'il sera appelé à cette fréquence, car le système d'exploitation appliquera sa propre logique pour optimiser la durée de vie de la batterie et l'utilisation de la radio.

La première chose à faire est de configurer votre projet pour qu'il prenne en charge l'actualisation en arrière-plan dans l'onglet Capacités. Cela ajoutera les clés nécessaires à votre Info.plist:

 enter image description here 

Ensuite, vous devez ajouter des éléments à votre AppDelegate, qui doit implémenter à la fois URLSessionDelegate et 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
}
0
devios1

dans mes tests sur iOS5, j'ai trouvé utile de lancer la surveillance de CoreLocation via startMonitoringForLocationChangeEvents (pas SignificantLocationChange), la précision .__ importe peu. diminue. 

0
dkzm