Skip to content

Commit 6f6efed

Browse files
christophpurrermeta-codesync[bot]
authored andcommitted
Fix main-thread watchdog hang in PushNotificationIOS.setApplicationIconBadgeNumber (#57707)
Summary: Pull Request resolved: #57707 Adopts the non-blocking `-[UNUserNotificationCenter setBadgeCount:withCompletionHandler:]` API (iOS 16+) to fix a chronic main-thread hang in `PushNotificationIOS.setApplicationIconBadgeNumber`. The deprecated `applicationIconBadgeNumber` setter performs a synchronous XPC round-trip to `usernotificationsd` on the calling thread, which triggers `0x8badf00d` watchdog kills when the daemon is slow. The async API returns immediately and invokes a completion handler later, keeping the main thread responsive. Also adds an RNTester example screen with a "Rapid-fire 50x set" stress test to validate the fix. API Documentation say: ``` property(nonatomic) NSInteger applicationIconBadgeNumber API_DEPRECATED("Use -[UNUserNotificationCenter setBadgeCount:withCompletionHandler:] instead.", ios(2.0, 17.0)) API_UNAVAILABLE(watchos); ``` Changelog: [iOS][Fixed] Fix main-thread watchdog hang in PushNotificationIOS.setApplicationIconBadgeNumber Reviewed By: fkgozali Differential Revision: D113810036 fbshipit-source-id: e538d6519389cadfdb0eefbfc6ec7fa8cffc4579
1 parent 4501979 commit 6f6efed

3 files changed

Lines changed: 115 additions & 1 deletion

File tree

packages/react-native/Libraries/PushNotificationIOS/RCTPushNotificationManager.mm

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,18 @@ - (void)handleRemoteNotificationRegistrationError:(NSNotification *)notification
322322
*/
323323
RCT_EXPORT_METHOD(setApplicationIconBadgeNumber : (double)number)
324324
{
325-
RCTSharedApplication().applicationIconBadgeNumber = number;
325+
NSInteger badgeCount = (NSInteger)lround(number);
326+
if (@available(iOS 16.0, *)) {
327+
[UNUserNotificationCenter.currentNotificationCenter
328+
setBadgeCount:badgeCount
329+
withCompletionHandler:^(NSError *_Nullable error) {
330+
if (error != nil) {
331+
RCTLogWarn(@"Failed to set application icon badge number: %@", error);
332+
}
333+
}];
334+
} else {
335+
RCTSharedApplication().applicationIconBadgeNumber = badgeCount;
336+
}
326337
}
327338

328339
/**
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow strict-local
8+
* @format
9+
*/
10+
11+
'use strict';
12+
13+
import type {RNTesterModuleExample} from '../../types/RNTesterTypes';
14+
15+
import RNTesterText from '../../components/RNTesterText';
16+
import * as React from 'react';
17+
import {useCallback, useState} from 'react';
18+
import {Button, StyleSheet, View} from 'react-native';
19+
import PushNotificationIOS from 'react-native/Libraries/PushNotificationIOS/PushNotificationIOS';
20+
21+
// Fires setApplicationIconBadgeNumber many times back-to-back.
22+
const RAPID_FIRE_COUNT = 50;
23+
24+
function BadgeExample(): React.Node {
25+
const [reported, setReported] = useState<?number>(null);
26+
27+
const updateBadge = useCallback((n: number) => {
28+
PushNotificationIOS.setApplicationIconBadgeNumber(n);
29+
}, []);
30+
31+
const readBadge = useCallback(() => {
32+
PushNotificationIOS.getApplicationIconBadgeNumber((n: number) => {
33+
setReported(n);
34+
});
35+
}, []);
36+
37+
const rapidFire = useCallback(() => {
38+
for (let i = 1; i <= RAPID_FIRE_COUNT; i++) {
39+
PushNotificationIOS.setApplicationIconBadgeNumber(i);
40+
}
41+
}, []);
42+
43+
return (
44+
<View style={styles.wrapper}>
45+
<RNTesterText style={styles.text}>
46+
{reported == null
47+
? 'Tap "Read current badge" to query the badge number.'
48+
: `Last read badge number: ${reported}`}
49+
</RNTesterText>
50+
<View style={styles.button}>
51+
<Button title="Set badge to 5" onPress={() => updateBadge(5)} />
52+
</View>
53+
<View style={styles.button}>
54+
<Button title="Clear badge (set 0)" onPress={() => updateBadge(0)} />
55+
</View>
56+
<View style={styles.button}>
57+
<Button
58+
title={`Rapid-fire ${RAPID_FIRE_COUNT}x set`}
59+
onPress={rapidFire}
60+
/>
61+
</View>
62+
<View style={styles.button}>
63+
<Button title="Read current badge" onPress={readBadge} />
64+
</View>
65+
</View>
66+
);
67+
}
68+
69+
const styles = StyleSheet.create({
70+
wrapper: {
71+
padding: 10,
72+
},
73+
text: {
74+
marginBottom: 10,
75+
},
76+
button: {
77+
marginVertical: 4,
78+
},
79+
});
80+
81+
exports.framework = 'React';
82+
exports.title = 'PushNotificationIOS';
83+
exports.category = 'iOS';
84+
exports.documentationURL = 'https://reactnative.dev/docs/pushnotificationios';
85+
exports.description = 'Application icon badge number via UserNotifications.';
86+
87+
exports.examples = [
88+
{
89+
title: 'Application icon badge number',
90+
description:
91+
'Set/clear/read the app icon badge. "Rapid-fire" stresses the setter ' +
92+
'to confirm the main thread never blocks on the UserNotifications XPC ' +
93+
'round-trip.',
94+
render(): React.Node {
95+
return <BadgeExample />;
96+
},
97+
},
98+
] as Array<RNTesterModuleExample>;

packages/rn-tester/js/utils/RNTesterList.ios.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,11 @@ const APIs: Array<RNTesterModuleInfo> = (
277277
key: 'PointerEventsExample',
278278
module: require('../examples/PointerEvents/PointerEventsExample'),
279279
},
280+
{
281+
key: 'PushNotificationIOSExample',
282+
module: require('../examples/PushNotificationIOS/PushNotificationIOSExample'),
283+
category: 'iOS',
284+
},
280285
{
281286
key: 'RCTRootViewIOSExample',
282287
module: require('../examples/RCTRootView/RCTRootViewIOSExample'),

0 commit comments

Comments
 (0)