每个移动应用同时执行许多任务:从网络加载数据、处理用户触摸、动画化界面和保存文件。如果所有这些代码都在一个线程中运行,那么任何网络延迟都会导致应用冻结。多线程(multithreading)和并发是关键概念,使应用保持响应和高效。本文将涵盖所有主要工具:从 Main Thread 和 RunLoop 到 Kotlin 协程和 iOS 上的 Combine。内容基于 Apple 官方 GCD 文档。
关键要点
多线程是应用同时执行多个代码片段的能力。每个片段在单独的线程(Thread)中运行 — 一个轻量级进程,拥有自己的调用栈。在移动开发中,线程分为两类:Main Thread(UI 线程)和 Background Threads(后台线程)。
操作系统本身管理线程在处理器核心上的分配。现代设备有 6–8 个核心,因此并行执行确实可以加速工作。然而,创建线程是一项昂贵的操作,因此不建议直接使用 Thread。相反,使用更高级别的抽象:DispatchQueue、OperationQueue、CoroutineDispatcher。
并发(Concurrency)是一个比多线程更广泛的概念。并发意味着任务甚至可以在单核上通过上下文切换「同时」执行。异步(Async/Await)是一种编程模型,其中任务不阻塞线程,而是在等待结果时返回控制权。现代语言(Kotlin、Swift、Dart)内置了对 Async/Await 的支持。
在 IT Sectr,我们特别关注项目启动时正确的多线程架构。早期阶段犯的错误会导致难以发现的 bug:数据竞争、死锁和负载下的应用不稳定。我们的每个项目在规划阶段都会进行并发架构审查。
Main Thread(主线程)— 移动应用中唯一可以访问 UI 的线程。在 Android 上称为UI Thread,在 iOS 上称为 Main Thread。所有界面操作 — 更改文本、动画、触摸处理 — 只在 Main Thread 上执行。如果在主线程上执行繁重操作(文件加载、JSON 解析),界面将停止响应。在 Android 上这会导致 ANR(Application Not Responding),在 iOS 上则会导致屏幕「冻结」。
Background Threads(后台线程)用于所有与 UI 无关的事情:网络请求、数据库操作、图像处理、加密。完成后,结果传递给 Main Thread 进行显示。每个平台都提供自己的工具在线程之间切换:iOS 上的 DispatchQueue.main.async,Android 上的 runOnUiThread 或 withContext(Dispatchers.Main)。
RunLoop — iOS 主线程上的事件处理循环。RunLoop 等待事件(触摸、定时器、通知)并将它们分派给适当的处理程序。在 Android 上,等效的是Looper,它与每个 Main Thread 相关联。Main Looper 无限地从队列中提取消息并将其传递给 Handler 进行处理。了解 RunLoop 和 Looper 有助于避免内存泄漏和界面「卡顿」。
Grand Central Dispatch(GCD)— Apple 用于在 C 语言级别管理多线程的库。GCD 使用 DispatchQueue(任务队列)工作。开发者不手动创建线程;GCD 管理一个线程池,将任务分配到可用的处理器核心上。DispatchQueue 有两种类型:Serial Queue(串行队列 — 任务一个接一个执行)和 Concurrent Queue(并发队列 — 任务可以同时执行)。
Main DispatchQueue — 绑定到主线程的串行队列。Global Queues — 具有不同优先级(QoS — Quality of Service)的并发队列:userInteractive、userInitiated、utility、background。选择正确的 QoS 对性能至关重要:.userInteractive — 用于影响 UI 的任务(动画、渲染);.background — 用于时间不敏感的任务(同步、缓存清理)。
OperationQueue — 在 GCD 之上的抽象,具有额外功能:取消任务、设置操作之间的依赖关系、控制最大并发操作数。操作是 Operation 类(或 BlockOperation)的对象。示例:如果需要加载图像,然后应用滤镜,然后才显示它 — 具有依赖关系的 OperationQueue 可以完美处理。在 GCD 中,您必须使用 DispatchGroup 或信号量手动同步这些步骤。
Swift 5.5+ 中的 Async/Await — 是 GCD 的现代替代方案。async 和 await 关键字使异步代码线性且可读。函数被标记为 async,调用通过 await 等待。系统本身管理上下文切换:默认情况下,async 函数在后台线程上执行,而 UI 更新在 MainActor 上执行。@MainActor — 保证代码在主线程上执行的属性。
Coroutines(协程)— JetBrains 开发的用于 Kotlin 的轻量级线程。与普通线程不同,协程不绑定到特定的 Thread。数千个协程可以在多个线程上运行而不会产生显著开销。CoroutineScope 管理协程的生命周期:viewModelScope 绑定到 ViewModel,lifecycleScope 绑定到 Activity/Fragment。当作用域被销毁时,所有子协程都会自动取消。
Dispatchers 确定协程在哪个线程池上运行:Dispatchers.Main — UI 线程;Dispatchers.IO — 用于网络请求和磁盘操作;Dispatchers.Default — 用于 CPU 密集型计算。要切换调度器,使用 withContext。协程支持结构化并发:每个协程都有一个父级,当父级被取消时,所有子协程都会被取消。这可以防止内存泄漏和挂起的任务。
Flow — 来自协程库的冷异步数据流。Flow 顺序发出值:(1)生产者生成数据,(2)操作符转换流,(3)收集器消费结果。与 LiveData 不同,Flow 支持复杂的操作符链(map、filter、flatMapConcat、catch),并且是完全线程安全的。StateFlow 和 SharedFlow — Flow 的热变体,非常适合 UI 状态和一次性事件(Snackbar、导航)。
Channel — 另一种用于在协程之间传递数据的协程抽象。Channel 像一个队列一样工作:一个发送者(send)和一个或多个接收者(receive)。缓冲通道(Channel(UNLIMITED)、Channel(BUFFERED))允许配置溢出时的行为。Channel 经常与 Flow 一起用于将基于回调的 API 桥接到协程:callbackFlow { … }。
在 IT Sectr,我们在所有 Android 项目中积极使用协程和 Flow。这允许编写看起来同步的异步代码,易于测试(runTest、TestDispatcher)且不需要手动线程管理。一个带有数据加载的简单协程示例:
class UserRepository(
private val api: UserApi,
private val dao: UserDao
) {
suspend fun getUsers(): List<User> = withContext(Dispatchers.IO) {
return@withContext try {
val users = api.fetchUsers()
dao.insertAll(users)
users
} catch (e: Exception) {
dao.getAll()
}
}
}
响应式编程 — 一种数据以异步流(Observable、Publisher)形式传播的范式。RxJava/RxKotlin — Android 上最流行的实现,从 .NET Rx 移植而来。RxSwift — 用于 iOS 的类似库。主要组件:Observable(事件源)、Observer(订阅者)、Scheduler(线程管理)、Operators(流转换)。
Combine — Apple 在 iOS 13 中引入的响应式编程框架。Combine 使用 Publisher(发布者)和 Subscriber(订阅者)协议。与 RxSwift 不同,Combine 内置于 SDK 中,并与 SwiftUI 紧密集成。Combine 中的操作符:map、filter、combineLatest、zip、debounce、throttle — 涵盖了大多数场景:从数据绑定到 UI 到搜索查询去抖。
Future 和 Promise — 用于处理单个异步结果的模式。Future 表示以后可用的值。Promise 是提供值的承诺。在 Rx 中这是 Single(一个成功响应或错误),在 Combine 中是 Future Publisher。在实践中,Future/Promise 对于单个 API 请求很方便,而 Observable/Publisher 则适用于连续流(地理位置、文本输入)。
Callback 和 Delegate — 用于异步操作的经典模式。Callback — 作为参数传递并在操作完成时调用的函数。Delegate — 实现具有事件处理程序方法的协议的对象。缺点:「回调地狱」(嵌套回调)和错误处理的复杂性。NotificationCenter(iOS)和 EventBus(Android)— 广播事件机制,适用于松散耦合的通信,但会导致隐式依赖。
多线程打开了高性能的大门,但同时也带来了难以发现的错误风险。最常见的是:Race Condition(竞态条件)、Deadlock(死锁)、Livelock(活锁)和 Starvation(线程饥饿)。理解这些问题是任何移动开发者的必备技能。
当两个或多个线程在没有同步的情况下同时读写相同的数据时,就会发生 Race Condition。结果取决于哪个线程先执行。经典示例:两个线程递增计数器。「读取 → 递增 → 写入」操作不是原子的,因此当同时执行时,一个递增会「丢失」。解决方案 — 使用原子操作(AtomicInteger、AtomicReference)或锁(Mutex、Semaphore、synchronized)。
Deadlock — 每个线程持有一个资源并等待另一个线程持有的资源的情况。没有线程可以继续。发生条件:互斥、持有并等待、无抢占、循环等待。预防:建立单一的锁获取顺序、使用带超时的 tryLock、应用无锁算法(ConcurrentHashMap、CopyOnWriteArrayList)。
Livelock — 线程未被阻塞,但不断相互「传递」资源而不做有用工作。示例:两个人在走廊相遇,都向同一方向移动让路。Starvation — 线程无法访问资源,因为其他线程不断拦截它。解决方案:公平锁(fair locks)、谨慎使用线程优先级。
为了防止多线程问题,使用同步原语:Mutex(互斥)、Semaphore(限制并发访问数)、Lock(带 tryLock 的接口)、Synchronized(JVM 级锁)、@MainActor(Swift — 保证在主线程上执行)。在 Android 上,也可通过 Executors.newFixedThreadPool、newCachedThreadPool 使用 ThreadPool。然而,手动池管理是遗留项目的特权;在新项目中,最好使用协程。
| 工具 | 平台 | 类型 | 特性 |
|---|---|---|---|
| DispatchQueue (GCD) | iOS | 任务队列 | 串行/并发、QoS 优先级、线程池由系统管理 |
| OperationQueue | iOS | 操作队列 | 依赖关系、取消、maxConcurrentOperationCount |
| Coroutines + Flow | Android | 协程 | 轻量、结构化并发、StateFlow、Channel |
| RxJava / RxKotlin | Android | 响应式流 | Observable、Schedulers、丰富的操作符集 |
| Combine | iOS | 响应式流 | Publisher/Subscriber、SwiftUI 集成 |
| Async/Await + Task | iOS / Android | 异步模型 | 线性代码、@MainActor、结构化并发 |
常见问题
Main Thread(UI 线程)负责界面渲染和触摸处理。Background Thread 执行后台任务 — 数据加载、计算、网络工作。阻塞 Main Thread 会导致界面冻结(Android 上为 ANR,iOS 上为 frozen UI)。
Race Condition — 当两个线程同时访问共享数据且结果取决于执行顺序时的竞态条件。通过同步避免:Mutex、Semaphore、Lock、Synchronized、@MainActor 或原子操作。
Coroutines 是 Android 的现代标准(JetBrains,Google 支持)。RxJava/RxKotlin 是一种具有丰富操作符集的响应式方法。Coroutines 对于异步调用更简单,RxJava 对于复杂数据流更强大。在 IT Sectr,我们对新项目使用 Coroutines + Flow。
Deadlock — 两个线程互相等待对方资源的相互阻塞。Livelock — 线程未被阻塞但不断传递资源而不做有用工作。两个问题都通过正确的锁顺序和超时来解决。
DispatchQueue 是 Grand Central Dispatch(GCD)用于线程管理的抽象。Main Queue 在主线程上执行任务,Global Queues 在后台线程上执行。Serial Queue 保证顺序执行,Concurrent Queue 保证并行执行。在现代项目中,GCD 通常被 Async/Await 和 Task 取代。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。