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

使用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 13:45:16