Getting state for system wide notifications in iOS

2019-01-22 23:54发布

I am trying to write a code which will handle turning on/off screen on iOS (You can take a look at this question where similar problem is discussed). I included OSX tag for this question, because OSX has the same system wide notification facility. And the problem described below is inherit to the notifications facility (vs to iOS or OSX).

There is well known method of registering for system wide notification com.apple.springboard.hasBlankedScreen to receive notifications when the screen is turned off or on.

Just for references (here are the API which are used for registering for system wide notifications):

  • notify_post, notify_check_ notify_get_state and friends
  • CFNotificationCenterPostNotification, CFNotificationCenterAddObserver and friends (which uses notify_post and etc internally)

However, there are two interrelated problems with this approach:

  • Notifications for both screen turning off and on come with the same name (com.apple.springboard.hasBlankedScreen)
  • Observer doesn't receive a state as part of the notification.

So, as result we need to implement some solution which will differ screen turning on and off (since the same notification callback will be called and none of parameters will have a state).

Generally speaking, the core problem that the state is decoupled from notification callback. I don't see how to handle this gracefully.

I come up with two straightforward approaches (each of them are flawed). And looking for ideas of another approaches or improvements over this approach.

Counting solution

We can implement a counter which counts how many notification we already received and based on this information we will know whether it's notification for turning screen on or off (based on oddity of our counter).

However, it has two downsides:

1) In the case, if the system (for unknown in design time reason) will send additional notifications with the same name, our logic will be screwed, because it will break oddity check.

2) Also, we need to set initial state correctly. So somewhere in the code we will have something like that:

counter = getInitialState(); 
registerForNotification();

In this case we have one race condition. If system will send notification and change the state after we did getInitialState(), but before registerForNotification() we will end up with wrong counter value.

If we will do following code:

registerForNotification();
counter = getInitialState(); 

In this case we have another race condition. If system will send notification and change the state after we did registerForNotification(), but before getInitialState(), we will get a counter, will enter notification callback and will increase a counter (which will make it wrong).

Determining state when notification received solution

In this case, we don't store any counter, but rather use API notify_get_state in notification callback to get current state.

This has it's own problem:

1) Notification delivered to the application asynchronously. So, as result if you turn off and on screen really fast, you can receive both notifications when the screen was already on. So, notify_check will get a current state (vs the state at the moment when notification was send).

As result, when the application will use notify_get_state in notification callback it will determine that there was two notification "screen was turned on", instead of one notification "screen was turned off" and another "screen was turned on".

P.S. Generally speaking, all described problems aren't specific to screen on/off case. They are actual for any system wide notifications which has distinctive states and send with the same notification name.


Update 1

I didn't test exactly scenario with turning screen on/off very fast and getting the same results from notify_get_state().

However, I have kind-of similar scenario when I received two notifications com.apple.springboard.lockstate (subscribed through CFNotificationCenterAddObserver) and I used another API to get a current device locked status and received the same values for both notifications.

So it's only my assumption that notify_get_state would return same values too. However, I think it's educated guess. The input parameter for notify_get_state will be the same for two calls (it doesn't change). And I don't think that system stores FIFO queue of states which should be returned by notify_get_state.

1条回答
疯言疯语
2楼-- · 2019-01-23 00:14

So, I built a very simple experiment. I ran this on a jailbroken iOS 6.1 iPhone 5, outside the debugger.

Code

I built a consumer app, with the following code:

#define EVENT "com.mycompany.bs"

- (void)registerForNotifications {
    int result = notify_register_dispatch(EVENT,
                                          &notifyToken,
                                          dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0l),
                                          ^(int info) {
                                              uint64_t state;
                                              notify_get_state(notifyToken, &state);
                                              NSLog(@"notify_register_dispatch() : %d", (int)state);
                                          });
    if (result != NOTIFY_STATUS_OK) {
        NSLog(@"register failure = %d", result);
    }
    CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(), //center
                                    NULL, // observer
                                    notifyCallback, // callback
                                    CFSTR(EVENT), // event name
                                    NULL, // object
                                    CFNotificationSuspensionBehaviorDeliverImmediately);
}

static void notifyCallback(CFNotificationCenterRef center, void *observer, CFStringRef name, const void *object, CFDictionaryRef userInfo) {
    uint64_t state;
    notify_get_state(notifyToken, &state);
    NSLog(@"notifyCallback(): %d", (int)state);
}

So, as you see, it uses two different methods to register for the same custom event. I startup this app, let it register for the event, then put it into the background (home button press).

Then, the producer app, which lets me generate the event with a button press:

double delayInSeconds = 0.001;

dispatch_queue_t q = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0l);
dispatch_async(q, ^(void) {
    notify_set_state(notifyToken, 2);
    notify_post(EVENT);        
});

dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, q, ^(void){
    notify_set_state(notifyToken, 3);
    notify_post(EVENT);
}); 

Results

I then ran the producer app, manually generating a pair of events about every two seconds. As you can see, the producer quickly posts the event with a state of 2, and then immediately posts another event with a state of 3. So, the consumer should print out 2 then 3, for both callback methods, if this is working perfectly. It does not (as you feared):

Feb 13 21:46:12 iPhone5 MyApp[1971] <Warning>: notifyCallback(): 3
Feb 13 21:46:12 iPhone5 MyApp[1971] <Warning>: notify_register_dispatch() : 3
Feb 13 21:46:12 iPhone5 MyApp[1971] <Warning>: notify_register_dispatch() : 3
Feb 13 21:46:12 iPhone5 MyApp[1971] <Warning>: notifyCallback(): 3

Feb 13 21:46:18 iPhone5 MyApp[1971] <Warning>: notifyCallback(): 3
Feb 13 21:46:18 iPhone5 MyApp[1971] <Warning>: notify_register_dispatch() : 3
Feb 13 21:46:18 iPhone5 MyApp[1971] <Warning>: notify_register_dispatch() : 3
Feb 13 21:46:18 iPhone5 MyApp[1971] <Warning>: notifyCallback(): 3

Feb 13 21:46:22 iPhone5 MyApp[1971] <Warning>: notifyCallback(): 3
Feb 13 21:46:22 iPhone5 MyApp[1971] <Warning>: notify_register_dispatch() : 3
Feb 13 21:46:22 iPhone5 MyApp[1971] <Warning>: notify_register_dispatch() : 3
Feb 13 21:46:22 iPhone5 MyApp[1971] <Warning>: notifyCallback(): 3

Feb 13 21:46:26 iPhone5 MyApp[1971] <Warning>: notifyCallback(): 2
Feb 13 21:46:26 iPhone5 MyApp[1971] <Warning>: notify_register_dispatch() : 3
Feb 13 21:46:26 iPhone5 MyApp[1971] <Warning>: notify_register_dispatch() : 3
Feb 13 21:46:26 iPhone5 MyApp[1971] <Warning>: notifyCallback(): 3

Feb 13 21:46:30 iPhone5 MyApp[1971] <Warning>: notifyCallback(): 3
Feb 13 21:46:30 iPhone5 MyApp[1971] <Warning>: notify_register_dispatch() : 3
Feb 13 21:46:30 iPhone5 MyApp[1971] <Warning>: notify_register_dispatch() : 3
Feb 13 21:46:30 iPhone5 MyApp[1971] <Warning>: notifyCallback(): 3

Feb 13 21:46:33 iPhone5 MyApp[1971] <Warning>: notifyCallback(): 3
Feb 13 21:46:33 iPhone5 MyApp[1971] <Warning>: notify_register_dispatch() : 3
Feb 13 21:46:33 iPhone5 MyApp[1971] <Warning>: notify_register_dispatch() : 3
Feb 13 21:46:33 iPhone5 MyApp[1971] <Warning>: notifyCallback(): 3

Feb 13 21:46:36 iPhone5 MyApp[1971] <Warning>: notifyCallback(): 2
Feb 13 21:46:36 iPhone5 MyApp[1971] <Warning>: notify_register_dispatch() : 3
Feb 13 21:46:36 iPhone5 MyApp[1971] <Warning>: notify_register_dispatch() : 3
Feb 13 21:46:36 iPhone5 MyApp[1971] <Warning>: notifyCallback(): 3

I tried changing one consumer registration method to use CFNotificationSuspensionBehaviorCoalesce (instead of delivering immediately). Results:

Feb 13 21:48:17 iPhone5 MyApp[1990] <Warning>: notifyCallback(): 3
Feb 13 21:48:17 iPhone5 MyApp[1990] <Warning>: notify_register_dispatch() : 3
Feb 13 21:48:17 iPhone5 MyApp[1990] <Warning>: notify_register_dispatch() : 3
Feb 13 21:48:17 iPhone5 MyApp[1990] <Warning>: notifyCallback(): 3

Feb 13 21:48:20 iPhone5 MyApp[1990] <Warning>: notifyCallback(): 2
Feb 13 21:48:20 iPhone5 MyApp[1990] <Warning>: notify_register_dispatch() : 3
Feb 13 21:48:20 iPhone5 MyApp[1990] <Warning>: notify_register_dispatch() : 3
Feb 13 21:48:20 iPhone5 MyApp[1990] <Warning>: notifyCallback(): 3

Feb 13 21:48:24 iPhone5 MyApp[1990] <Warning>: notifyCallback(): 2
Feb 13 21:48:24 iPhone5 MyApp[1990] <Warning>: notify_register_dispatch() : 2
Feb 13 21:48:24 iPhone5 MyApp[1990] <Warning>: notify_register_dispatch() : 3
Feb 13 21:48:24 iPhone5 MyApp[1990] <Warning>: notifyCallback(): 3

Feb 13 21:48:29 iPhone5 MyApp[1990] <Warning>: notifyCallback(): 2
Feb 13 21:48:29 iPhone5 MyApp[1990] <Warning>: notify_register_dispatch() : 3
Feb 13 21:48:29 iPhone5 MyApp[1990] <Warning>: notify_register_dispatch() : 3
Feb 13 21:48:29 iPhone5 MyApp[1990] <Warning>: notifyCallback(): 3

Feb 13 21:48:32 iPhone5 MyApp[1990] <Warning>: notifyCallback(): 2
Feb 13 21:48:32 iPhone5 MyApp[1990] <Warning>: notify_register_dispatch() : 2
Feb 13 21:48:32 iPhone5 MyApp[1990] <Warning>: notify_register_dispatch() : 3
Feb 13 21:48:32 iPhone5 MyApp[1990] <Warning>: notifyCallback(): 3

Feb 13 21:48:35 iPhone5 MyApp[1990] <Warning>: notifyCallback(): 2
Feb 13 21:48:35 iPhone5 MyApp[1990] <Warning>: notify_register_dispatch() : 2
Feb 13 21:48:35 iPhone5 MyApp[1990] <Warning>: notify_register_dispatch() : 3
Feb 13 21:48:35 iPhone5 MyApp[1990] <Warning>: notifyCallback(): 3

Feb 13 21:48:38 iPhone5 MyApp[1990] <Warning>: notifyCallback(): 2
Feb 13 21:48:38 iPhone5 MyApp[1990] <Warning>: notify_register_dispatch() : 2
Feb 13 21:48:38 iPhone5 MyApp[1990] <Warning>: notify_register_dispatch() : 3
Feb 13 21:48:38 iPhone5 MyApp[1990] <Warning>: notifyCallback(): 3

Feb 13 21:48:39 iPhone5 MyApp[1990] <Warning>: notifyCallback(): 2
Feb 13 21:48:39 iPhone5 MyApp[1990] <Warning>: notify_register_dispatch() : 3
Feb 13 21:48:39 iPhone5 MyApp[1990] <Warning>: notify_register_dispatch() : 3
Feb 13 21:48:39 iPhone5 MyApp[1990] <Warning>: notifyCallback(): 3

I then tried changing the queue priority of the notify_register_dispatch() consumer to high, instead of background priority. Results:

Feb 13 21:49:51 iPhone5 MyApp[2006] <Warning>: notifyCallback(): 3
Feb 13 21:49:51 iPhone5 MyApp[2006] <Warning>: notify_register_dispatch() : 3
Feb 13 21:49:51 iPhone5 MyApp[2006] <Warning>: notify_register_dispatch() : 3
Feb 13 21:49:51 iPhone5 MyApp[2006] <Warning>: notifyCallback(): 3

Feb 13 21:49:53 iPhone5 MyApp[2006] <Warning>: notifyCallback(): 2
Feb 13 21:49:53 iPhone5 MyApp[2006] <Warning>: notify_register_dispatch() : 2
Feb 13 21:49:53 iPhone5 MyApp[2006] <Warning>: notifyCallback(): 3
Feb 13 21:49:53 iPhone5 MyApp[2006] <Warning>: notify_register_dispatch() : 3

Feb 13 21:49:55 iPhone5 MyApp[2006] <Warning>: notifyCallback(): 2
Feb 13 21:49:55 iPhone5 MyApp[2006] <Warning>: notify_register_dispatch() : 2
Feb 13 21:49:55 iPhone5 MyApp[2006] <Warning>: notify_register_dispatch() : 3
Feb 13 21:49:55 iPhone5 MyApp[2006] <Warning>: notifyCallback(): 3

Feb 13 21:49:59 iPhone5 MyApp[2006] <Warning>: notifyCallback(): 2
Feb 13 21:49:59 iPhone5 MyApp[2006] <Warning>: notify_register_dispatch() : 2
Feb 13 21:49:59 iPhone5 MyApp[2006] <Warning>: notify_register_dispatch() : 3
Feb 13 21:49:59 iPhone5 MyApp[2006] <Warning>: notifyCallback(): 3

Feb 13 21:50:01 iPhone5 MyApp[2006] <Warning>: notifyCallback(): 2
Feb 13 21:50:01 iPhone5 MyApp[2006] <Warning>: notify_register_dispatch() : 3
Feb 13 21:50:01 iPhone5 MyApp[2006] <Warning>: notify_register_dispatch() : 3
Feb 13 21:50:01 iPhone5 MyApp[2006] <Warning>: notifyCallback(): 3

Feb 13 21:50:04 iPhone5 MyApp[2006] <Warning>: notify_register_dispatch() : 2
Feb 13 21:50:04 iPhone5 MyApp[2006] <Warning>: notifyCallback(): 2
Feb 13 21:50:04 iPhone5 MyApp[2006] <Warning>: notify_register_dispatch() : 3
Feb 13 21:50:04 iPhone5 MyApp[2006] <Warning>: notifyCallback(): 3

Feb 13 21:50:06 iPhone5 MyApp[2006] <Warning>: notifyCallback(): 2
Feb 13 21:50:06 iPhone5 MyApp[2006] <Warning>: notify_register_dispatch() : 2
Feb 13 21:50:06 iPhone5 MyApp[2006] <Warning>: notify_register_dispatch() : 3
Feb 13 21:50:06 iPhone5 MyApp[2006] <Warning>: notifyCallback(): 3

Feb 13 21:50:09 iPhone5 MyApp[2006] <Warning>: notifyCallback(): 2
Feb 13 21:50:09 iPhone5 MyApp[2006] <Warning>: notify_register_dispatch() : 2
Feb 13 21:50:09 iPhone5 MyApp[2006] <Warning>: notify_register_dispatch() : 3
Feb 13 21:50:09 iPhone5 MyApp[2006] <Warning>: notifyCallback(): 3

Feb 13 21:50:10 iPhone5 MyApp[2006] <Warning>: notifyCallback(): 2
Feb 13 21:50:10 iPhone5 MyApp[2006] <Warning>: notify_register_dispatch() : 3
Feb 13 21:50:10 iPhone5 MyApp[2006] <Warning>: notify_register_dispatch() : 3
Feb 13 21:50:10 iPhone5 MyApp[2006] <Warning>: notifyCallback(): 3

Conclusions (?)

  • There is a problem as you suspected, and it's not just with the SBGetScreenLockStatus call. Sometimes, the consumer never saw the state set to 2.
  • If I increased the producer time delay to 5 msec, I never saw the problem. So, this may only be an issue for events really close in time. Screen lock / unlock probably isn't a big deal. Obviously, slower phones (iPhone < 5) will respond differently.
  • The static notifyCallback() method seemed to get called back first, unless the GCD callback block was put on the high priority queue. Even then, usually the static callback function was called first. Many times, the first method called back got the correct state (2), while the second one did not. So, if you have to live with the problem, you might choose the callback mechanism that seemed to perform best (or at least, prototype this yourself, inside your app).
  • I can't say that the suspensionBehavior parameter made much difference. That said, depending on how iOS is posting the events, they may be using a call like CFNotificationCenterPostNotification that may ignore the consumers' behavior request.
  • If you look at this Apple document, you'll see two things.

    1. First, notify_set_state was not part of the very original API
    2. Second, the very first paragraph in that document says

Darwin Notification API Reference

These routines allow processes to exchange stateless notification events.

So, maybe we're just trying to do something that's not consistent with the original design :(

  • If you also look at Apple's NotificationPoster example, you see that they don't use notify_get_state and notify_set_state to convey state. They pass it with the notification as a user info dictionary. Obviously, if you're observing Apple's iOS events, you have no control over how the events are posted. But, in apps where you get to create the producer and consumer, I'd stay away from notify_set_state.
查看更多
登录 后发表回答