iOS通知拦截与shared_preferences存储问题及Laravel-Flutter通信优化
iOS Notification Service Extension 异常排查与Flutter-Laravel通信同步问题
我正在开发Flutter聊天应用,核心需求是将所有消息和聊天记录本地存储在Android/iOS设备,应用启动时从本地加载数据。最初计划通过Laravel后端发送静默通知(数据消息),同步新消息接收、消息删除、群组成员变更等操作——Android设备不受严格限流和电池优化影响,消息能稳定送达;但iOS设备因系统电池优化,静默通知被限流,后台唤醒应用的次数受限,无法依赖该方案更新本地数据。
经咨询苹果工程师,调整方案为使用Notification Service Extension(NSE):该扩展会处理每条可见推送通知,只要用户未禁用通知可见性即可触发,但不会处理无视觉展示的静默通知。NSE有30秒运行时间、24MB内存限制,无法直接与主应用通信,只能通过共享容器读写数据。
基于此,我设计的流程是:发送带唯一ID的普通通知避免折叠,确保送达可靠性;通过NSE拦截通知,根据payload中的suppress字段决定是否展示通知,同时将业务数据存储到共享容器;Flutter应用启动后读取共享容器数据,转换后同步到本地数据库。目前已按要求创建并配置App Group到NSE和主应用目标,但NSE代码无法正常工作。
现有代码
Notification Service Extension 代码
import UserNotifications class NotificationService: UNNotificationServiceExtension { var contentHandler: ((UNNotificationContent) -> Void)? var bestAttemptContent: UNMutableNotificationContent? override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { print("NSE: didReceive triggered") self.contentHandler = contentHandler bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent) guard let bestAttemptContent = bestAttemptContent else { print("NSE: bestAttemptContent is nil") contentHandler(request.content) return } print("NSE: Notification Payload - \(request.content.userInfo)") if let userInfo = request.content.userInfo as? [String: Any], let suppressFlag = userInfo["suppress"] as? Bool, let cleanData = userInfo["data"] as? [String: Any] { print("NSE: Suppress flag = \(suppressFlag)") if suppressFlag { print("NSE: Suppressing notification") bestAttemptContent.title = "" bestAttemptContent.body = "" bestAttemptContent.sound = nil } else { print("NSE: Displaying notification") bestAttemptContent.title = cleanData["title"] as? String ?? "New Message" bestAttemptContent.body = cleanData["body"] as? String ?? "You have a new message." } print("NSE: Saving notification to shared container") saveNotificationToSharedContainer(data: cleanData) } print("NSE: Passing modified content to handler") contentHandler(bestAttemptContent) } override func serviceExtensionTimeWillExpire() { // Called when time runs out if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent { contentHandler(bestAttemptContent) } } private func saveNotificationToSharedContainer(data: [String: Any]) { // Access shared container let sharedDefaults = UserDefaults(suiteName: "group.com.egensolve.demo") var notifications = sharedDefaults?.array(forKey: "stored_notifications") as? [[String: Any]] ?? [] // Append the new notification data notifications.append(data) sharedDefaults?.set(notifications, forKey: "stored_notifications") sharedDefaults?.synchronize() } }
Laravel 端发送通知代码
public function sendDataMessage($deviceTokens, $data = [], $priority = "high") { $messaging = app('firebase.messaging'); $clean_data = $data; if (isset($data["type"]) && $data["type"] == "chat" && $data["action"] == "receiveMessage") { unset($clean_data["fcm_message"]); } $messages = []; foreach ($deviceTokens as $token) { $aps = []; try { if (isset($data["type"]) && $data["type"] == "chat") { $chat_message = $data["fcm_message"]; $clean_data["title"] = Chats::select("name")->find($data["chat_id"])->name; if ($data["action"] == "receiveMessage") { $clean_data["body"] = $chat_message["sender"]["name"] . ": " . ($chat_message["content"]["type"] == "Text" ? $chat_message["content"]["body"] : $chat_message["content"]["type"]); $aps = [ "alert" => [ "title" => $clean_data["title"], "body" => $clean_data["body"] ], "content-available" => 1, "mutable-content" => 1 ]; $clean_data["suppress"] = true; $aps["sound"] = "default"; } else { $clean_data["suppress"] = true; } } $messages[] = CloudMessage::withTarget('token', $token) ->withData($clean_data) ->withAndroidConfig(AndroidConfig::fromArray([ 'priority' => 'normal', // Use high priority 'ttl' => 86400 * 28, // 4 weeks in seconds ])) ->withApnsConfig(ApnsConfig::fromArray([ 'headers' => [ 'apns-collapse-id' => uniqid(), 'apns-expiration' => strval(time() + 86400 * 28), // Expiry timestamp 'apns-priority' => '10', ], 'payload' => [ 'aps' => $aps, ], ])); } catch (Exception $e) { return $e; } } try { $report = $messaging->sendAll($messages); } catch (NotFound $e) { return $e; } catch (Exception $e) { return $e; } return $report->successes()->count(); }
Flutter 读取共享容器代码
final notifications = prefs?.getStringList('stored_notifications') ?? []; print(notifications);
求助需求
需要排查以下问题,确保iOS端能稳定拦截通知并将数据存储到共享容器,实现Laravel与Flutter的可靠通信:
- NSE未触发或无法正确解析处理通知payload
- 数据无法写入共享容器,导致Flutter端读取不到内容
- 验证
suppress字段逻辑是否正常生效(值为true时隐藏通知,false时展示)
内容的提问来源于stack exchange,提问作者OKKO
相关产品推荐
相关产品推荐

