GCD and callbacks - concurrency issue
Asked Answered
H

7

6

I have a callback handler registered that listens to changes in the iOS Address Book. Due to some strange reason (for which a bug has been filed), this callback can sometimes be called more than once when the app returns from the background. I want my callback handler to run it's logic only once, even in cases the callback is called multiple times. This is how I register the callback:

ABAddressBookRegisterExternalChangeCallback(address_book, adressBookChanged, self);

This is how I structured my callback handler to take advantage of GCD to handle this. Unfortunately, it's not working, and GCD doesn't prevent the internal logic to be called twice...

void adressBookChanged(ABAddressBookRef ab, CFDictionaryRef info, void 
                       *context) 
{ 
    NSLog(@"** IN addressBookChanged callback!");

    ABAddressBookUnregisterExternalChangeCallback (ab, adressBookChanged, context);

    __block BOOL fireOnce = FALSE;
    dispatch_queue_t queue;
    queue = dispatch_queue_create("com.myapp.abcallback", NULL);

    dispatch_async(queue, ^{

        if (fireOnce == FALSE) {

            fireOnce = TRUE;

            dispatch_queue_t queueInternal;
            queueInternal = dispatch_queue_create("com.myapp.abcallbackInternal", NULL);
            dispatch_async (queueInternal, ^{
               NSLog(@"do internal logic");

            });

            dispatch_release(queueInternal);
        }
    });
    dispatch_release(queue);
}

I'm pretty sure this code works for receiving multiple notifications, so are callbacks different? Do they spawn different threads automatically, making the fireOnce value to be FALSE each time? How should I write this code to prevent multiple callbacks from calling the internal logic more than once? I suppose I could use locks and/or synchronized blocks to achieve this, but GCD seemed like a cleaner way to achieve this.

Hitormiss answered 19/8, 2011 at 4:30 Comment(3)
Do you have a reference to the bug that has been filed about getting the callback several times?Chlortetracycline
9301976. It was closed a while ago due to "insufficient information", which basically means that they asked for a sample project to reproduce the issue and it's not something that I can reproduce for them at will ... it happens with some Exchange misfiring these alerts.Hitormiss
Ok, I might arrange and send a sample application as I have the error every time I run. Thanks!Chlortetracycline
H
2

I ended up using NSTimers instead of GCD to prevent the duplicate callbacks from firing my critical method. Much simpler, and works quite well!

[self.changeTimer invalidate];
self.changeTimer = nil;
self.changeTimer = [NSTimer scheduledTimerWithTimeInterval:3.0
                                                            target:self
                                                          selector:@selector(handleAdressBookExternalCallbackBackground)
                                                          userInfo:nil
                                                           repeats:NO];
Hitormiss answered 16/5, 2012 at 19:58 Comment(0)
V
3

The cause of multiple callbacks is due to the phone book iCloud background synchronization. Usually, if you have multiple devices logged in a same iCloud account, the synchronization will propagate to all devices, and echoed back to your testing device from where the change originated, thus, causes the callback to be invoked multiple times.

By the way, using a timer to constraint the duplicated invocations won't help resolve this issue completely, because you don't know when the next callback will be called depending on your network condition. You should instead program the logic to handle these duplicated invocations.

Vesicate answered 18/7, 2013 at 14:45 Comment(1)
It also appears that if a device is synced with iCloud, the callback is called at least once every time the app is brought back from the background, even if no changes to the AB have been made, so the timer can prevent multiple concurrent calls but can't prevent this "phantom" call from occurring every time the app comes back up.Actinon
H
2

I ended up using NSTimers instead of GCD to prevent the duplicate callbacks from firing my critical method. Much simpler, and works quite well!

[self.changeTimer invalidate];
self.changeTimer = nil;
self.changeTimer = [NSTimer scheduledTimerWithTimeInterval:3.0
                                                            target:self
                                                          selector:@selector(handleAdressBookExternalCallbackBackground)
                                                          userInfo:nil
                                                           repeats:NO];
Hitormiss answered 16/5, 2012 at 19:58 Comment(0)
D
0

Whatever you are trying to use GCD for, you are negating any of its effects, since you are creating a queue for each time the callback is being called, and that queue is different from the others, so it always runs. You probably mean creating the queue outside of the callback and using it inside the callback (maybe a static global?).

Still, I don't understand how would that help you, since you would still be running each GCD block each time a callback fired. Unless your do internal logic part marks a record as has having been updated and you check this flag in your queued methods affecting the same record, you are still going to be running multiple times your code, GCD or not.

Descendant answered 23/8, 2011 at 20:11 Comment(0)
P
0

Not really a direct answer to you GCD question, but I think that every time a unique 'context' is provided when you register, this creates a new 'registration' such that you are called back for each 'context'. You may be able to avoid being called multiple times by providing the same 'context'.

Peltz answered 4/1, 2012 at 2:12 Comment(0)
J
0

I had similar problem. My solution was to save flag in NSUserDefaults, enable this flag after first addressbookChanged method, and disable it again, after my actions were done..

void MyAddressBookExternalChangeCallback (ABAddressBookRef notifyAddressBook,CFDictionaryRef info,void *context)
{
   NSLog(@"in MyAddressBook External Change Callback");

    if([[[NSUserDefaults standardUserDefaults]objectForKey:@"addressBookChanged"] boolValue] == NO)
      {
       [[NSUserDefaults standardUserDefaults] setObject:@YES forKey:@"addressBookChanged"];
       [[NSUserDefaults standardUserDefaults] synchronize];

        //we save sync status to defaults to prevent duplicate call of this method

        [[NSUserDefaults standardUserDefaults] setObject:[NSNumber numberWithBool:YES] forKey:@"addressBookSync"];
        [[NSUserDefaults standardUserDefaults]synchronize];

        [APICallWithCompletion:^(BOOL success, id object) {
            [[NSUserDefaults standardUserDefaults] setObject:@NO forKey:@"addressBookChanged"];
            [[NSUserDefaults standardUserDefaults] synchronize];
        }];
   }
}

While this may not be correct approach, it seems to be working for me, as my api call takes long enough to prevent duplicate call of this method... I guess you could replace it with

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
    [[NSUserDefaults standardUserDefaults] setObject:@NO forKey:@"addressBookChanged"];
    [[NSUserDefaults standardUserDefaults] synchronize];
});
Johnsen answered 30/4, 2015 at 17:19 Comment(0)
A
0

I spent almost 2 days behind this issues. Even I was using timer but that was creating more issues. For eg. if you set timer for 5 sec and in that time duration if you go to contacts again and make some changes and come to the app it will end up ignoring that change because 5 secs are not over yet. so for that change you will have to kill the app and rerun the application. I just did 2 steps and everything worked like magic On

- (void)applicationDidEnterBackground:(UIApplication *)application

method I am registering to external changes

    -(void) registerExternalChanges
{
    dispatch_async(dispatch_get_main_queue(), ^{
        ABAddressBookRef addressBookRef = [self takeAddressBookPermission];
        ABAddressBookRegisterExternalChangeCallback(addressBookRef, addressBookChanged , (__bridge void *)(self));
    });
}

And once you come to app after finish making changes in contacts database UnRegisterExternalChanges

 ABAddressBookUnregisterExternalChangeCallback(ntificationaddressbook, addressBookChanged,(context));

Thats it addressBookChanged method will only gets called once!!!

Anelace answered 20/8, 2015 at 13:37 Comment(0)
C
-1

To execute a piece of code exactly once with the help of GDC, you can do:

static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^
{
    a piece of code
});
Chlortetracycline answered 18/5, 2012 at 6:8 Comment(1)
That only runs the piece of code once for the lifetime of the app (till it's terminated). It doesn't help with our situation, since we want the Address Book callback to be handled repeatedly.Hitormiss

© 2022 - 2024 — McMap. All rights reserved.