Skip to content

Commit 58fd138

Browse files
Fix main-thread watchdog hang in PushNotificationIOS.setApplicationIconBadgeNumber
Summary: 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); ``` Differential Revision: D113810036
1 parent 5e40a17 commit 58fd138

3 files changed

Lines changed: 115 additions & 1 deletion

File tree

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

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

328338
/**
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
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 setBadge = 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+
PushNotificationIOS.setApplicationIconBadgeNumber(RAPID_FIRE_COUNT);
42+
}, []);
43+
44+
return (
45+
<View style={styles.wrapper}>
46+
<RNTesterText style={styles.text}>
47+
{reported == null
48+
? 'Tap "Read current badge" to query the badge number.'
49+
: `Last read badge number: ${reported}`}
50+
</RNTesterText>
51+
<View style={styles.button}>
52+
<Button title="Set badge to 5" onPress={() => setBadge(5)} />
53+
</View>
54+
<View style={styles.button}>
55+
<Button title="Clear badge (set 0)" onPress={() => setBadge(0)} />
56+
</View>
57+
<View style={styles.button}>
58+
<Button
59+
title={`Rapid-fire ${RAPID_FIRE_COUNT}x set`}
60+
onPress={rapidFire}
61+
/>
62+
</View>
63+
<View style={styles.button}>
64+
<Button title="Read current badge" onPress={readBadge} />
65+
</View>
66+
</View>
67+
);
68+
}
69+
70+
const styles = StyleSheet.create({
71+
wrapper: {
72+
padding: 10,
73+
},
74+
text: {
75+
marginBottom: 10,
76+
},
77+
button: {
78+
marginVertical: 4,
79+
},
80+
});
81+
82+
exports.framework = 'React';
83+
exports.title = 'PushNotificationIOS';
84+
exports.category = 'iOS';
85+
exports.documentationURL = 'https://reactnative.dev/docs/pushnotificationios';
86+
exports.description = 'Application icon badge number via UserNotifications.';
87+
88+
exports.examples = [
89+
{
90+
title: 'Application icon badge number',
91+
description:
92+
'Set/clear/read the app icon badge. "Rapid-fire" stresses the setter ' +
93+
'to confirm the main thread never blocks on the UserNotifications XPC ' +
94+
'round-trip.',
95+
render(): React.Node {
96+
return <BadgeExample />;
97+
},
98+
},
99+
] 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)