详情页点开一张图片,应用要读取文件、解码缩略图,再把结果显示出来。把这些工作直接放在点击回调里,主线程可能卡住;于是有人写
Thread { ... }.start(),也有人换成Handler、协程或 WorkManager。它们看起来都能“把活放到后台”,但并不在做同一件事。
一句话结论:Thread和HandlerThread会创建具体线程,线程池负责复用线程;协程与 WorkManager 主要管理任务的调度和生命周期,不能简单理解成“另一种 new Thread”。
本文讨论 Android 应用侧的常见选择,不追踪 ART 和 Linux 内核如何实现线程。示例以 Kotlin 和现代 AndroidX 的概念为主;AndroidX 库的具体 API、依赖版本要以项目配置为准。AsyncTask从 Android 11(API 30)起已弃用,只作为旧代码辨认对象。
1. 先分清:创建线程,还是提交任务?
Android 主线程负责事件分发、界面更新和绘制。耗时工作占住它,输入与绘制就要等待。真正要设计的是:谁执行任务、任务完成后结果交给谁、页面离开时谁取消或清理。
| 方式 | 会不会为这次任务新建一条线程 | 更适合的工作 | 生命周期要点 |
|---|---|---|---|
Thread | 调用start()后创建一条线程 | 很少量、一次性的简单后台工作或教学实验 | 自己处理结果回传、取消和异常。 |
ExecutorService/ThreadPoolExecutor | 按池配置创建并复用工作线程 | 多个短任务、需要限制并发的工作 | 明确队列容量、拒绝策略与关闭时机。 |
HandlerThread | 启动一条自带Looper的线程 | 需要串行处理消息、依赖Handler回调的工作 | 停用时调用quitSafely()或相应清理。 |
| Kotlin 协程 | 一个协程通常不对应一条专属线程 | 随页面或 ViewModel 生命周期运行的异步任务 | 选择合适的CoroutineScope和Dispatcher,配合取消。 |
| WorkManager | 不承诺立刻创建线程,也不承诺马上执行 | 可延后、需要跨进程退出继续安排的后台任务 | 由系统条件和调度决定执行时间,任务需可重试。 |
表里还有几个经常被误叫成“开启线程”的东西。Runnable、Callable是任务接口,FutureTask是可承载结果的任务包装;它们交给Thread或执行器后才会运行。Handler.post()则把任务投递到它绑定的Looper所在的线程:绑定主Looper就在主线程执行,绑定HandlerThread.looper才会在那条后台线程执行。
2.Thread与线程池:从能跑到能控制
直接创建线程最容易看懂:
valmainHandler=Handler(Looper.getMainLooper())Thread({valresult=decodeThumbnail()// 示例:阻塞式读取和解码mainHandler.post{showThumbnail(result)}},"thumbnail-worker").start()调用start()后,新线程执行decodeThumbnail();它完成后向主线程消息队列投递更新。这里的decodeThumbnail()和showThumbnail()是业务占位函数,示例省略了异常处理、页面销毁检查与取消逻辑。Thread(...).run()只会在当前线程直接执行run(),不会开启新线程。
每次点击都创建一条原始线程,任务多时会遇到并发失控、资源消耗和难以统一取消的问题。线程池把“提交工作”和“创建多少线程”分开:业务调用execute()或submit(),池决定复用哪条工作线程、是否排队以及队列满时怎样拒绝。
在生产项目里,我会先问池由谁持有、最多同时跑几个任务、最多排队多少个、拒绝时如何反馈。Executors.newFixedThreadPool()适合展示复用概念,但默认队列可以持续增长;有明确流量上限的业务可直接配置有界队列的ThreadPoolExecutor。任务执行中抛异常、Future.cancel()的协作式取消、shutdown()的时机也要按持有者设计。不要在主线程上调用Future.get()等待结果,它可能把界面卡住。
3.HandlerThread为什么不等于普通的Thread?
普通Thread的run()结束,线程就结束。HandlerThread会为自己的线程准备Looper和消息队列,方便外部通过Handler连续投递任务。下面是一个串行工作者的最小结构:
importandroid.os.Handlerimportandroid.os.HandlerThreadimportjava.io.CloseableclassSerialWorker:Closeable{privatevalthread=HandlerThread("serial-worker").apply{start()}privatevalhandler=Handler(thread.looper)funsubmit(task:Runnable):Boolean=handler.post(task)overridefunclose(){thread.quitSafely()}}创建SerialWorker时启动一条线程;submit()把任务放进它的消息队列,按该线程的消息循环执行。close()应由持有者在结束使用时调用;之后post()可能返回false,调用方不能假设任务已经排上。若只是要串行执行普通工作、完全不需要Looper,单线程执行器通常更直接。
Handler(Looper.getMainLooper())与上面的Handler(thread.looper)用的是同一种投递 API,目标线程却不同。排查“为什么 UI 仍然卡”时,先看 Handler 绑定了哪个Looper,不要只看见post()就认定工作已到后台。
4. 协程与 WorkManager:重点在任务归属
Kotlin 协程能让异步流程按顺序写,但suspend函数本身不会自动创建线程。以页面详情数据为例,viewModelScope.launch默认从主线程上下文启动,遇到真正的阻塞式读取时再切到Dispatchers.IO:
funloadDetail(){viewModelScope.launch{try{valdetail=withContext(Dispatchers.IO){repository.readBlocking()}uiState.value=DetailState.Success(detail)}catch(cancelled:CancellationException){throwcancelled}catch(error:IOException){uiState.value=DetailState.Error(error)}}}这是 ViewModel 内的可改造片段:repository、uiState和DetailState由具体业务提供。阻塞读取进入共享的 IO 调度器,结果回到原协程上下文后更新 UI 状态。viewModelScope随 ViewModel 清理而取消;取消通常需要协作,阻塞 I/O 可能还要关闭底层资源。若调用的库已经提供正确实现的挂起 API,不必机械地为每次调用再包一层withContext(Dispatchers.IO)。
如果任务必须在用户离开页面后继续安排,例如稍后同步离线记录、满足网络条件后上传日志,就应看 WorkManager。它把任务交给 Android 的后台调度体系,支持约束、重试和持久安排;它不适合“用户点按钮后必须立刻拿到结果并刷新当前界面”的工作。即使用CoroutineWorker,也不意味着“一次工作对应一条新线程”,执行时机仍受系统调度与约束影响。
lifecycleScope适合跟 Activity 或 Fragment 生命周期绑定的工作,viewModelScope适合跟 ViewModel 绑定的工作;WorkManager 的范围更长。选它们时,先决定工作应该活多久,再决定在哪个执行器或调度器上运行。
5. 具体怎么选,哪些坑要避开?
| 遇到的任务 | 我会先选 | 理由 |
|---|---|---|
| 页面内一次请求或文件读取 | 生命周期明确的协程;阻塞调用切到合适的 Dispatcher | 结果、异常和取消能跟页面或 ViewModel 对齐。 |
| 多个独立的 Java/SDK 阻塞任务 | 有并发与队列上限的ExecutorService | 集中管理线程数量和拒绝。 |
串行的消息或依赖Looper的回调 | HandlerThread | 需要一条可持续投递消息的专用线程。 |
| 退出页面后仍需按条件执行 | WorkManager | 工作由持久调度管理,不依赖当前界面存活。 |
| 只把结果交回 UI | 主线程Handler或协程回主调度器 | 这是切回主线程,不是在创建后台线程。 |
旧教程里的AsyncTask也做“后台执行后回调主线程”,但它从 API 30 起已弃用,新代码不应围绕它设计。RxJava 的Scheduler同样是调度任务的抽象;subscribeOn(Schedulers.io())不能简单读成“每次订阅 new 一条线程”。
最容易出错的不是忘了写start(),而是后台工作已经完成,原页面却已经不存在。把任务交给正确的所有者,并在回传结果时检查它是否仍有效;网络错误、取消、队列拒绝和资源释放也要有明确路径。这样选出的“开启线程方式”,才真正服务于 Android 的生命周期。
参考资料
- Android 官方:Better performance through threading
- Android API:HandlerThread
- Android 官方:Kotlin coroutines on Android
- Android 官方:WorkManager
- Android API:AsyncTask
- 站内延伸:Java 并发、线程池与 Handler