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

SwiftUI计算属性引发的性能问题及优化咨询

解决SwiftUI预算应用计算属性性能问题的实践方案

1. 内存级自动缓存(替代持久化缓存)

放弃将计算结果存入SwiftData的方案,转而在模型内存层面维护缓存,通过监听交易数组变化自动清空缓存,触发重新计算。这种方式不会有数据不一致风险,且性能开销极低。

示例代码:

class MonthBudget: ObservableObject {
    @Published var transactions: [Transaction] = []
    
    // 私有缓存存储
    private var _totalSpent: Double?
    private var _spentNeedsBudget: Double?
    
    // 带缓存的计算属性
    var totalSpent: Double {
        _totalSpent ?? calculateTotalSpent()
    }
    
    var spentNeedsBudget: Double {
        _spentNeedsBudget ?? calculateSpentNeeds()
    }
    
    private func calculateTotalSpent() -> Double {
        let value = transactions.reduce(0) { $0 + $1.amount }
        _totalSpent = value
        return value
    }
    
    private func calculateSpentNeeds() -> Double {
        let value = transactions.filter { $0.category == .needs }.reduce(0) { $0 + $1.amount }
        _spentNeedsBudget = value
        return value
    }
    
    // 监听交易数组变化,清空所有缓存
    private var cancellables = Set<AnyCancellable>()
    init() {
        $transactions
            .sink { [weak self] _ in
                self?._totalSpent = nil
                self?._spentNeedsBudget = nil
            }
            .store(in: &cancellables)
    }
}

2. 后台异步计算+主线程更新

将耗时的计算逻辑移到后台队列执行,完成后切回主线程更新UI状态,避免阻塞主线程导致卡顿。配合防抖处理,还能避免频繁修改交易时重复计算。

示例代码:

class MonthBudget: ObservableObject {
    @Published var transactions: [Transaction] = []
    @Published var totalSpent: Double = 0
    @Published var spentNeedsBudget: Double = 0
    
    private var cancellables = Set<AnyCancellable>()
    init() {
        $transactions
            .debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)
            .receive(on: DispatchQueue.global(qos: .background))
            .map { transactions in
                let total = transactions.reduce(0) { $0 + $1.amount }
                let needs = transactions.filter { $0.category == .needs }.reduce(0) { $0 + $1.amount }
                return (total, needs)
            }
            .receive(on: DispatchQueue.main)
            .sink { [weak self] total, needs in
                self?.totalSpent = total
                self?.spentNeedsBudget = needs
            }
            .store(in: &cancellables)
    }
}

3. 按需计算,拆分逻辑

不要一次性计算所有统计值,而是根据当前视图展示的图表类型,只计算对应的数据。比如用户当前查看"需求类支出"图表,就仅计算spentNeedsBudget,其他统计值暂不处理。

实现方式:

  • 在视图中用@State标记当前活跃的图表类型(如.needs/.wants/.savings)
  • 模型根据传入的类型触发对应计算,避免不必要的遍历和累加操作

4. 预处理交易数据,按分类存储

在添加/删除交易时,直接将数据按预算分类(needs/wants/savings)拆分存储,而非每次计算时再过滤整个交易数组。这种方式能大幅减少计算阶段的耗时,尤其适合交易数量庞大的场景。

示例代码:

class MonthBudget: ObservableObject {
    @Published var allTransactions: [Transaction] = []
    @Published var needsTransactions: [Transaction] = []
    @Published var wantsTransactions: [Transaction] = []
    
    func addTransaction(_ transaction: Transaction) {
        allTransactions.append(transaction)
        switch transaction.category {
        case .needs: needsTransactions.append(transaction)
        case .wants: wantsTransactions.append(transaction)
        case .savings: // 对应分类存储逻辑
        }
    }
    
    func removeTransaction(_ transaction: Transaction) {
        allTransactions.removeAll { $0.id == transaction.id }
        needsTransactions.removeAll { $0.id == transaction.id }
        wantsTransactions.removeAll { $0.id == transaction.id }
    }
    
    // 直接使用分类数组计算,无需过滤
    var spentNeedsBudget: Double {
        needsTransactions.reduce(0) { $0 + $1.amount }
    }
}

5. 值类型配合自定义缓存包装器

如果MonthBudget使用值类型(struct),可以自定义@Memoized属性包装器来缓存计算结果,借助值类型的自动更新特性,在交易数组变化时自动失效缓存。

自定义包装器示例:

@propertyWrapper
struct Memoized<Value> {
    private var cache: Value?
    private let compute: () -> Value
    
    init(compute: @escaping () -> Value) {
        self.compute = compute
    }
    
    var wrappedValue: Value {
        cache ?? {
            let value = compute()
            cache = value
            return value
        }()
    }
    
    mutating func invalidate() {
        cache = nil
    }
}

模型中使用:

struct MonthBudget {
    var transactions: [Transaction] {
        didSet {
            _totalSpent.invalidate()
            _spentNeedsBudget.invalidate()
        }
    }
    
    @Memoized var totalSpent: Double {
        transactions.reduce(0) { $0 + $1.amount }
    }
    
    @Memoized var spentNeedsBudget: Double {
        transactions.filter { $0.category == .needs }.reduce(0) { $0 + $1.amount }
    }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.02 23:52:49