Swift 并发:任务取消机制详解,避免不必要的性能浪费!

在这里插入图片描述

网罗开发 (小红书、快手、视频号同名)

  大家好,我是 展菲,目前在上市企业从事人工智能项目研发管理工作,平时热衷于分享各种编程领域的软硬技能知识以及前沿技术,包括iOS、前端、Harmony OS、Java、Python等方向。在移动端开发、鸿蒙开发、物联网、嵌入式、云原生、开源等领域有深厚造诣。

图书作者:《ESP32-C3 物联网工程开发实战》
图书作者:《SwiftUI 入门,进阶与实战》
超级个体:COC上海社区主理人
特约讲师:大学讲师,谷歌亚马逊分享嘉宾
科技博主:华为HDE/HDG

我的博客内容涵盖广泛,主要分享技术教程、Bug解决方案、开发工具使用、前沿科技资讯、产品评测与使用体验。我特别关注云服务产品评测、AI 产品对比、开发板性能测试以及技术报告,同时也会提供产品优缺点分析、横向对比,并分享技术沙龙与行业大会的参会体验。我的目标是为读者提供有深度、有实用价值的技术洞察与分析。

展菲:您的前沿技术领航员
👋 大家好,我是展菲!
📱 全网搜索“展菲”,即可纵览我在各大平台的知识足迹。
📣 公众号“Swift社区”,每周定时推送干货满满的技术长文,从新兴框架的剖析到运维实战的复盘,助您技术进阶之路畅通无阻。
💬 微信端添加好友“fzhanfei”,与我直接交流,不管是项目瓶颈的求助,还是行业趋势的探讨,随时畅所欲言。
📅 最新动态:2025 年 3 月 17 日
快来加入技术社区,一起挖掘技术的无限潜能,携手迈向数字化新征程!


如何避免不必要的计算,提升用户体验?

在开发中,我们经常遇到这样的问题:

「用户快速输入搜索关键字,但上一次搜索还没结束,新的一次请求已经发出,导致多个请求同时进行,造成性能浪费。」

「用户在数据加载过程中切换页面,后台任务还在继续运行,白白浪费了资源。」

这类问题的本质,就是 没有正确处理任务的取消。在 Swift 并发(Swift Concurrency)中,任务取消采用 协作式模型(cooperative cancellation),也就是说 系统不会强行停止你的任务,而是提供取消信息,至于如何处理,完全由你自己决定

如果任务不主动检查 取消状态,即使已经被标记为“已取消”,它仍然会继续运行,造成资源浪费,甚至影响用户体验。

今天我们就来聊聊,Swift 并发中的任务取消该如何正确处理,避免性能浪费,提升 App 的流畅度。

任务取消的痛点分析

多次搜索导致的性能浪费

假设你在做一个搜索功能,用户每输入一个字母,都会触发一次网络请求。如果不取消之前的请求,就会造成多个请求同时运行,白白浪费网络和 CPU 资源。

表现:

  • 用户搜索 apple,输入 aapappapplapple,触发 5 次搜索请求
  • 5 个请求并发执行,即使 apple 的请求返回了,aap 等请求依然会占用 CPU 资源

页面切换后,后台任务仍然在跑

比如一个健康数据查询功能,用户打开页面触发了一个异步查询,但在查询完成之前,用户已经返回了上一页。如果这个任务没有正确处理取消,它还是会继续运行,占用系统资源。

表现:

  • 用户进入「健康数据」页面,触发查询任务
  • 任务运行中,用户立刻返回主页,但查询任务仍然继续执行

用户手动取消任务后,任务仍然继续执行

有时候,我们会提供一个「取消」按钮,比如用户在下载一个大文件时,想要手动取消下载。如果我们只是标记任务为“已取消”,但没有在任务内部做检查,任务还是会继续下载,占用带宽和存储空间。

表现:

  • 用户点击「开始下载」,触发一个任务
  • 下载过程中,用户点击「取消」,但任务仍然继续下载

SwiftUI 任务取消的最佳实践

在 SwiftUI 中,我们可以使用 task(id:) 让任务在 id 变化时自动取消上一个任务,从而解决 搜索请求重复执行的问题

搜索功能中的任务取消

struct ContentView: View {
    @State private var store = Store()
    @State private var query = ""
    
    var body: some View {
        NavigationStack {
            List(store.results, id: \.self) { result in
                Text(verbatim: result)
            }
            .searchable(text: $query)
            .task(id: query) {  // 👈 任务 id 变化时,自动取消前一个任务
                await store.search(matching: query)
            }
        }
    }
}

实际效果:

  • task(id: query) 绑定了搜索内容,每次 query 变化时,都会取消上一个搜索任务并启动新的任务
  • 但是 上一个任务不会自动停止,它只是被标记为已取消,所以我们还需要在 search(matching:) 内手动检查取消状态

如何在任务内部正确处理中途取消?

Swift 提供了 Task.checkCancellation() 方法,它可以在任务被取消时抛出异常,让任务立即终止,避免不必要的计算。

在搜索任务中检查取消状态

import HealthKit

@MainActor @Observable final class Store {
    private(set) var results: [HKCorrelation] = []
    private let store = HKHealthStore()
    
    func search(matching query: String) async {
        let foodQuery = HKSampleQueryDescriptor(
            predicates: [.correlation(type: .init(.food))],
            sortDescriptors: []
        )
        
        do {
            let food = try await foodQuery.result(for: store)
            
            try Task.checkCancellation()  // 👈 任务取消时,抛出异常,立即终止
            
            results = food.filter { food in
                let title = food.metadata?["title"] as? String ?? ""
                return title.localizedStandardContains(query)
            }
        } catch {
            results = []
        }
    }
}

这样做的好处:

  • 任务取消后,不会继续执行过滤逻辑,避免多余计算
  • 直接 catch 取消异常,清空 results,防止 UI 误显示旧数据

使用 Task.isCancelled 检查任务状态

除了 checkCancellation() 抛出异常外,我们还可以使用 Task.isCancelled 更温和地处理取消逻辑

查询任务中使用 isCancelled

actor SearchService {
    private var cachedResults: [HKCorrelation] = []
    private let store = HKHealthStore()
    
    func search(matching query: String) async throws -> [HKCorrelation] {
        guard !Task.isCancelled else {
            return cachedResults  // 👈 任务被取消时,直接返回缓存结果
        }
        
        let foodQuery = HKSampleQueryDescriptor(
            predicates: [.correlation(type: .init(.food))],
            sortDescriptors: []
        )
        
        let food = try await foodQuery.result(for: store)
        
        guard !Task.isCancelled else {
            return cachedResults  // 👈 再次检查取消状态,避免无效计算
        }
        
        cachedResults = food.filter { food in
            let title = food.metadata?["title"] as? String ?? ""
            return title.localizedStandardContains(query)
        }
        
        return cachedResults
    }
}

适用场景:

  • 需要返回部分结果,比如前一次请求的缓存数据,而不是直接报错退出
  • 避免 UI 显示空数据,提升用户体验

手动取消任务的正确方式

有些场景下,我们可能需要提供一个「取消」按钮,让用户手动取消任务。

手动取消任务

struct ExampleView: View {
    @State private var store = Store()
    @State private var task: Task<Void, Never>?
    
    var body: some View {
        VStack {
            Button("开始任务") {
                task = Task {
                    await store.fetch()
                }
            }
            
            Button("取消任务") {
                task?.cancel()  // 👈 只是标记任务为“已取消”,不会主动终止
            }
        }
    }
}

但光这样是不够的,还需要在 fetch() 里面检查取消状态,否则任务仍然会继续运行!

总结

痛点回顾

  • 搜索时,输入过快导致多个请求并发执行,浪费性能
  • 用户离开页面,任务仍然在后台运行,影响系统资源
  • 手动取消任务后,任务仍然继续执行,体验不佳

解决方案

  • task(id:) 绑定任务 ID,自动取消上一个任务
  • 在任务内部使用 Task.checkCancellation()Task.isCancelled 主动检查取消状态
  • 手动任务取消时,任务内部仍需自行处理取消逻辑

如果不正确处理任务取消,轻则浪费资源,重则影响用户体验,这些细节一定要掌握好!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

网罗开发

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值