使用Task仍致UI阻塞?LazyVStack滚动卡顿问题咨询
问题原因分析
你用Task包裹函数却仍出现UI卡顿,核心问题在于默认创建的Task会继承当前上下文的Actor:onScrollVisibilityChange的回调运行在MainActor(主线程)上,所以你的Task本质还是在主线程执行saveIdsWatchedTime。哪怕函数仅需几毫秒,LazyVStack滚动时会频繁触发该回调,大量主线程任务堆积就会导致滚动卡顿。
另外,函数中直接修改AppStorage变量,AppStorage的变更会立即触发视图刷新,这进一步加重了主线程的负担。
解决方案
1. 让函数在后台线程执行
把saveIdsWatchedTime移到后台Actor中,彻底避免占用主线程资源:
首先定义后台Actor:
actor WatchedTimeStorage { var watchedTimeChecked = false var gal: String var endOfMemesReached: Bool var lastTimeChecked: Int var timeRanges: [TimeRange] var latestStorageString: String { didSet { UserDefaults.standard.set(latestStorageString, forKey: "memesWatchedTimeRangesString") } } var topStorageString: String { didSet { UserDefaults.standard.set(topStorageString, forKey: "memesWatchedTimeRangesTopString") } } // 结构化存储时间范围,替代字符串解析 struct TimeRange { let newest: Int let oldest: Int init?(string: String) { let parts = string.components(separatedBy: ",") guard parts.count == 2, let newest = Int(parts[0]), let oldest = Int(parts[1]) else { return nil } self.newest = newest self.oldest = oldest } var stringValue: String { "\(newest),\(oldest)" } } init(gal: String, endOfMemesReached: Bool, lastTimeChecked: Int, initialRanges: [String], initialLatest: String, initialTop: String) { self.gal = gal self.endOfMemesReached = endOfMemesReached self.lastTimeChecked = lastTimeChecked self.timeRanges = initialRanges.compactMap { TimeRange(string: $0) } self.latestStorageString = initialLatest self.topStorageString = initialTop } func saveIdsWatchedTime(postTime: Int) { guard !watchedTimeChecked else { return } watchedTimeChecked = true guard (gal == "latest" || gal == "top") && !endOfMemesReached && postTime < lastTimeChecked else { return } guard var firstRange = timeRanges.first else { return } var oldestMemeViewed = firstRange.oldest if oldestMemeViewed > postTime { firstRange = TimeRange(newest: firstRange.newest, oldest: postTime)! timeRanges[0] = firstRange oldestMemeViewed = postTime } var filteredRanges = [TimeRange]() var endString = "" var isFirst = true for range in timeRanges { if isFirst { isFirst = false filteredRanges.append(range) endString += "\(range.stringValue)|" } else { if !(firstRange.newest > range.newest && oldestMemeViewed < range.oldest) { filteredRanges.append(range) endString += "\(range.stringValue)|" } } } if endString.hasSuffix("|") { endString = String(endString.dropLast()) } timeRanges = filteredRanges if gal == "latest" { latestStorageString = endString } else { topStorageString = endString } } }
然后在视图中初始化并调用:
@State private var watchedStorage: WatchedTimeStorage! @AppStorage("memesWatchedTimeRangesString") private var latestString = "" @AppStorage("memesWatchedTimeRangesTopString") private var topString = "" init() { _watchedStorage = State(initialValue: WatchedTimeStorage( gal: "latest", // 根据实际业务传入当前分类 endOfMemesReached: false, lastTimeChecked: 0, initialRanges: latestString.components(separatedBy: "|"), initialLatest: latestString, initialTop: topString )) } // 在LazyVStack条目上绑定回调 .onScrollVisibilityChange { isVisible in if isVisible { Task { await watchedStorage.saveIdsWatchedTime(postTime: meme.time) // 同步回AppStorage(按需执行,避免频繁刷新视图) latestString = await watchedStorage.latestStorageString topString = await watchedStorage.topStorageString } } }
2. 优化触发频率
滚动时onScrollVisibilityChange会频繁触发,可添加防抖逻辑,仅当条目稳定可见一段时间后再执行保存:
@State private var visibilityTask: Task<Void, Never>? .onScrollVisibilityChange { isVisible in visibilityTask?.cancel() if isVisible { visibilityTask = Task { try? await Task.sleep(nanoseconds: 500_000_000) // 等待500ms guard !Task.isCancelled else { return } await watchedStorage.saveIdsWatchedTime(postTime: meme.time) latestString = await watchedStorage.latestStorageString topString = await watchedStorage.topStorageString } } }
3. 移除强制解包
原代码中大量使用!强制解包,不仅有崩溃风险,还会增加不必要的运行时开销。改用上面的TimeRange结构体提前解析数据,避免重复调用字符串分割和强制转换。
内容的提问来源于stack exchange,提问作者TheGreatCornholio
相关产品推荐
相关产品推荐

