You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

应用后台切换后无响应:通知视图定时移除.task逻辑异常

通知定时移除导致应用切回后台后无响应问题排查与解决

问题现象

实现了5秒自动消失的通知视图,常规使用正常,但出现以下异常流程:

  • 用户触发操作显示通知;
  • 立即切换到其他应用,停留数秒后切回;
  • 应用出现无响应冻结。

控制台仅输出两条无关日志:

-[RTIInputSystemClient remoteTextInputSessionWithID:performInputOperation:]  perform input operation requires a valid sessionID. inputModality = Keyboard, inputOperation = dismissAutoFillPanel, customInfoType = UIUserInteractionRemoteInputOperations
Snapshotting a view (0x109264080, UIKeyboardImpl) that is not in a visible window requires afterScreenUpdates:YES.

已确认问题根源:注释掉定时移除的.task逻辑后,冻结现象消失。尝试过两种实现方式,问题都存在:

  1. 定时逻辑放在addNotification方法内;
  2. 定时逻辑放在NotificationBoxModifier的.task修饰符中。

相关代码

AppState 实现

class AppState: ObservableObject {

    static let shared = AppState()

    @Published var notificationArray: [NotificationItem] = []

    func addNotification(_ notification: NotificationItem) {
        notificationBoxArray.append(notification)
    }
    
    func removeNotification(_ notification: NotificationItem) {
        self.notificationBoxArray.removeAll { $0.id == notification.id }
    }

}

NotificationBoxModifier 实现

struct NotificationBoxModifier: ViewModifier {
    func body(content: Content) -> some View {
        content
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            .overlay(
                ZStack {
                    mainNotificationBoxView()

                }.animation(.spring(), value: AppState.shared.notificationBoxArray)
            )
        .enableInjection()
    }

    #if DEBUG
    @ObserveInjection var forceRedraw
    #endif

    @ViewBuilder func mainNotificationBoxView() -> some View {
        if AppState.shared.notificationArray.count > 0 {
            VStack {
                ForEach(AppState.shared.notificationArray){ notification in
                    NotificationBoxView(notificationText: notification.message, notificationType: notification.type) {}
                        .padding(.bottom,16)
                        .task {
                            do {
                                try await Task.sleep(seconds: 5)
                                await MainActor.run {
                                    AppState.shared.removeNotification(notification)
                                }
                            } catch {
                                logger.error("Task.sleep error \(error.localizedDescription)")
                            }
                        }
                }
                Spacer()
            }
            .transition(.move(edge: .top))
        }
    }
}

之前的 addNotification 实现

func addNotification(_ notification: NotificationItem) {
    notificationBoxArray.append(notification)
    Task {
        do {
            try await Task.sleep(seconds: 5)
            await MainActor.run {
            AppState.shared.removeNotification(notification)
            }
        } catch {
            logger.error("Task.sleep error \(error.localizedDescription)")
        }
    }
}

问题原因

核心问题在于应用进入后台时,Task.sleep或定时器仍在执行,切回前台时任务尝试修改UI状态,但此时应用的UI线程可能处于不稳定状态,或者任务持有了已失效的视图/数据引用,导致UI更新时出现线程冲突或状态不一致。

另外,.task修饰符的生命周期和视图绑定,当应用进入后台,视图的活跃状态发生变化,但任务不会自动暂停,切回前台后任务继续执行并触发@Published数组更新,进而触发动画,此时UI线程可能无法正常处理这些更新,最终导致应用冻结。

解决方案

方案1:使用定时器+应用生命周期监听(推荐)

改用Timer实现定时移除,并监听应用前后台状态,后台时暂停定时器,前台时恢复剩余计时:

import UIKit
import Combine

class AppState: ObservableObject {
    static let shared = AppState()
    
    @Published var notificationArray: [NotificationItem] = []
    private var timers: [UUID: Timer] = [:]
    private var cancellables = Set<AnyCancellable>()
    
    private init() {
        // 监听应用退到后台事件
        NotificationCenter.default.publisher(for: UIApplication.willResignActiveNotification)
            .sink { [weak self] _ in
                self?.pauseAllTimers()
            }
            .store(in: &cancellables)
        
        // 监听应用回到前台事件
        NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)
            .sink { [weak self] _ in
                self?.resumeAllTimers()
            }
            .store(in: &cancellables)
    }
    
    func addNotification(_ notification: NotificationItem) {
        notificationArray.append(notification)
        // 创建5秒后执行的定时器
        let timer = Timer.scheduledTimer(withTimeInterval: 5, repeats: false) { [weak self] _ in
            guard let self = self else { return }
            self.removeNotification(notification)
            self.timers.removeValue(forKey: notification.id)
        }
        timers[notification.id] = timer
    }
    
    func removeNotification(_ notification: NotificationItem) {
        notificationArray.removeAll { $0.id == notification.id }
        // 移除对应定时器,避免内存泄漏
        if let timer = timers.removeValue(forKey: notification.id) {
            timer.invalidate()
        }
    }
    
    private func pauseAllTimers() {
        // 将定时器触发时间设为遥远未来,实现暂停
        for timer in timers.values {
            timer.fireDate = Date.distantFuture
        }
    }
    
    private func resumeAllTimers() {
        // 计算剩余时间,恢复定时器
        for (id, timer) in timers {
            let elapsed = Date().timeIntervalSince(timer.fireDate.addingTimeInterval(-5))
            let remaining = max(0, 5 - elapsed)
            timer.fireDate = Date().addingTimeInterval(remaining)
        }
    }
}

方案2:优化Task实现,绑定视图与应用生命周期

如果坚持使用Task,需要在视图消失或应用后台时主动取消任务:

struct NotificationBoxView: View {
    let notification: NotificationItem
    let onTap: () -> Void
    @State private var autoRemoveTask: Task<Void, Error>?
    
    var body: some View {
        // 你的通知视图UI
        HStack {
            Text(notification.message)
            // 其他UI元素
        }
        .onAppear {
            autoRemoveTask = Task {
                do {
                    try await Task.sleep(seconds: 5)
                    await MainActor.run {
                        AppState.shared.removeNotification(notification)
                    }
                } catch is CancellationError {
                    // 任务被取消,无需处理
                } catch {
                    logger.error("Notification auto-remove task error: \(error.localizedDescription)")
                }
            }
        }
        .onDisappear {
            // 视图消失时取消任务
            autoRemoveTask?.cancel()
        }
        .onReceive(NotificationCenter.default.publisher(for: UIApplication.willResignActiveNotification)) { _ in
            // 应用退后台时取消任务
            autoRemoveTask?.cancel()
        }
    }
}

额外注意事项

  • 确保NotificationItem遵循Identifiable协议,id为唯一值;
  • 所有修改@Published数组的操作必须在主线程执行(用MainActor.run包裹);
  • 避免在任务中持有外部对象的强引用,使用[weak self]防止内存泄漏;
  • 动画触发条件尽量精准,避免不必要的UI重绘。

内容的提问来源于stack exchange,提问作者smekers

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.20 10:54:55