Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

perf(messaging, ios): Improve time to delivery of background messages on iOS #5547

Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,16 @@ NS_ASSUME_NONNULL_BEGIN

@property _Nullable RCTPromiseRejectBlock registerPromiseRejecter;
@property _Nullable RCTPromiseResolveBlock registerPromiseResolver;
@property (nonatomic, strong) NSCondition *conditionBackgroundMessageHandlerSet;
@property (nonatomic) BOOL backgroundMessageHandlerSet;


+ (_Nonnull instancetype)sharedInstance;

- (void)observe;

- (void)signalBackgroundMessageHandlerSet;

- (void)setPromiseResolve:(RCTPromiseResolveBlock)resolve andPromiseReject:(RCTPromiseRejectBlock)reject;

- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken;
Expand Down
62 changes: 55 additions & 7 deletions packages/messaging/ios/RNFBMessaging/RNFBMessaging+AppDelegate.m
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ + (instancetype)sharedInstance {
__strong static RNFBMessagingAppDelegate *sharedInstance;
dispatch_once(&once, ^{
sharedInstance = [[RNFBMessagingAppDelegate alloc] init];
sharedInstance.conditionBackgroundMessageHandlerSet = [[NSCondition alloc] init];
sharedInstance.backgroundMessageHandlerSet = NO;
});
return sharedInstance;
}
Expand Down Expand Up @@ -66,6 +68,16 @@ - (void)observe {
});
}

// used to signal that a javascript handler for background messages is set
- (void)signalBackgroundMessageHandlerSet {
RNFBMessagingAppDelegate *sharedInstance = [RNFBMessagingAppDelegate sharedInstance];
[sharedInstance.conditionBackgroundMessageHandlerSet lock];
DLog(@"signalBackgroundMessageHandlerSet sharedInstance.backgroundMessageHandlerSet was %@", sharedInstance.backgroundMessageHandlerSet ? @"YES" : @"NO");
sharedInstance.backgroundMessageHandlerSet = YES;
[sharedInstance.conditionBackgroundMessageHandlerSet broadcast];
[sharedInstance.conditionBackgroundMessageHandlerSet unlock];
}

// used to temporarily store a promise instance to resolve calls to `registerForRemoteNotifications`
- (void)setPromiseResolve:(RCTPromiseResolveBlock)resolve andPromiseReject:(RCTPromiseRejectBlock)reject {
_registerPromiseResolver = resolve;
Expand Down Expand Up @@ -102,6 +114,7 @@ - (void)application:(UIApplication *)application didFailToRegisterForRemoteNotif
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler {
#if __has_include(<FirebaseAuth/FirebaseAuth.h>)
if ([[FIRAuth auth] canHandleNotification:userInfo]) {
DLog(@"didReceiveRemoteNotification Firebase Auth handeled the notification");
completionHandler(UIBackgroundFetchResultNoData);
return;
}
Expand All @@ -110,6 +123,8 @@ - (void)application:(UIApplication *)application didReceiveRemoteNotification:(N
[[NSNotificationCenter defaultCenter] postNotificationName:@"RNFBMessagingDidReceiveRemoteNotification" object:userInfo];

if (userInfo[@"gcm.message_id"]) {
DLog(@"didReceiveRemoteNotification gcm.message_id was present %@", userInfo);

if ([UIApplication sharedApplication].applicationState == UIApplicationStateBackground) {
// If app is in background state, register background task to guarantee async queues aren't frozen.
UIBackgroundTaskIdentifier __block backgroundTaskId = [application beginBackgroundTaskWithExpirationHandler:^{
Expand All @@ -129,13 +144,46 @@ - (void)application:(UIApplication *)application didReceiveRemoteNotification:(N
}
});

// TODO investigate later - RN bridge gets invalidated at start when in background and a new bridge created - losing all events
// TODO so we just delay sending the event for a few seconds as a workaround
// TODO most likely Remote Debugging causing bridge to be invalidated
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t) (8 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[[RNFBRCTEventEmitter shared] sendEventWithName:@"messaging_message_received_background" body:[RNFBMessagingSerializer remoteMessageUserInfoToDict:userInfo]];
});
} else {
RNFBMessagingAppDelegate *sharedInstance = [RNFBMessagingAppDelegate sharedInstance];
[sharedInstance.conditionBackgroundMessageHandlerSet lock];
@try {
DLog(@"didReceiveRemoteNotification sharedInstance.backgroundMessageHandlerSet = %@", sharedInstance.backgroundMessageHandlerSet ? @"YES" : @"NO");
if (sharedInstance.backgroundMessageHandlerSet) {
// Normal path, backgroundMessageHandlerSet has already been set, queue the notification for immediate delivery
dispatch_async(dispatch_get_main_queue(), ^{
[[RNFBRCTEventEmitter shared] sendEventWithName:@"messaging_message_received_background" body:[RNFBMessagingSerializer remoteMessageUserInfoToDict:userInfo]];
});
DLog(@"didReceiveRemoteNotification without waiting for backgroundMessageHandlerSet to be set");
} else {
// This spin needs to be on a background/concurrent queue to await the setup of backgroundMessageHandlerSet and not block the main thread
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// Reaquire the lock in this new closure
[sharedInstance.conditionBackgroundMessageHandlerSet lock];
@try {
// Spin/wait until backgroundMessageHandlerSet
// NB it is possible while this closure was being scheduled that backgroundMessageHandlerSet is already set and this loop is skipped
while (!sharedInstance.backgroundMessageHandlerSet) {
DLog(@"didReceiveRemoteNotification waiting for sharedInstance.backgroundMessageHandlerSet %@", sharedInstance.backgroundMessageHandlerSet ? @"YES" : @"NO");
if(![sharedInstance.conditionBackgroundMessageHandlerSet waitUntilDate:[NSDate dateWithTimeIntervalSinceNow:25]]) {
// If after 25 seconds the client hasn't called backgroundMessageHandlerSet, give up on this notification
ELog(@"didReceiveRemoteNotification timed out waiting for sharedInstance.backgroundMessageHandlerSet");
return;
}
}
dispatch_async(dispatch_get_main_queue(), ^{
[[RNFBRCTEventEmitter shared] sendEventWithName:@"messaging_message_received_background" body:[RNFBMessagingSerializer remoteMessageUserInfoToDict:userInfo]];
});
DLog(@"didReceiveRemoteNotification after waiting for backgroundMessageHandlerSet");
} @finally {
[sharedInstance.conditionBackgroundMessageHandlerSet unlock];
}
});
}
} @finally {
[sharedInstance.conditionBackgroundMessageHandlerSet unlock];
}
} else {
DLog(@"didReceiveRemoteNotification while app was in foreground");
[[RNFBRCTEventEmitter shared] sendEventWithName:@"messaging_message_received" body:[RNFBMessagingSerializer remoteMessageUserInfoToDict:userInfo]];
completionHandler(UIBackgroundFetchResultNoData);
}
Expand Down
9 changes: 9 additions & 0 deletions packages/messaging/ios/RNFBMessaging/RNFBMessagingModule.m
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,15 @@ - (NSDictionary *)constantsToExport {
return resolve([NSNull null]);
}

RCT_EXPORT_METHOD(signalBackgroundMessageHandlerSet) {
DLog(@"signalBackgroundMessageHandlerSet called");
@try {
[[RNFBMessagingAppDelegate sharedInstance] signalBackgroundMessageHandlerSet];
} @catch (NSException *exception) {
ELog(@"signalBackgroundMessageHandlerSet failed");
}
}

RCT_EXPORT_METHOD(getToken:
(RCTPromiseResolveBlock) resolve
:(RCTPromiseRejectBlock) reject
Expand Down
8 changes: 7 additions & 1 deletion packages/messaging/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,10 @@ class FirebaseMessagingModule extends FirebaseModule {
}

/**
* @platform android
* Set a handler that will be called when a message is received while the app is in the background.
* Should be called before the app is registered in `AppRegistry`, for example in `index.js`.
* An app is considered to be in the background if no active window is displayed.
* @param handler called with an argument of type messaging.RemoteMessage that must be async and return a Promise
*/
setBackgroundMessageHandler(handler) {
if (!isFunction(handler)) {
Expand All @@ -298,6 +301,9 @@ class FirebaseMessagingModule extends FirebaseModule {
}

backgroundMessageHandler = handler;
if (isIOS) {
this.native.signalBackgroundMessageHandlerSet();
}
}

sendMessage(remoteMessage) {
Expand Down
Loading