在 Android 架构设计里,"main-safe" 经常被提到,但大多数人只把它理解成一句简单的话:
"Repository 要切线程,UI 层不用关心 IO。"
但如果只停留在这个理解,其实还远远没抓住它的本质。
main-safe 并不仅仅是线程规范,它的本质是一种架构契约——Data 层对调用方的承诺,也正因如此,它成了衡量架构是否真正分层的一道分水岭。
一、什么是 main-safe?
main-safe 的意思是:
调用方可以始终在 Main 线程调用它,而不需要关心内部是否涉及 IO / 网络 / 数据库 / CPU 计算。
例如:
viewModelScope.launch {
val user = getUserUseCase()
}
调用方不需要写:
withContext(Dispatchers.IO)
也不需要知道:
- API 是否阻塞
- DB 是否耗时
- 是否需要切线程
二、main-safe 的本质:复杂度是否扩散
main-safe 真正解决的不仅仅是线程问题,而是一个更深层的问题:
复杂度是否扩散到调用方。
我们对比两种架构。
❌ 非 main-safe 架构(复杂度暴露)
viewModelScope.launch(Dispatchers.IO) {
val user = api.getUser()
}
或者:
viewModelScope.launch {
withContext(Dispatchers.IO) {
api.getUser()
}
}
表面问题是线程,但本质问题是:UI 层必须知道哪些是 IO、哪些是 CPU、什么时候切线程。
👉 UI 变成了"调度中心"。
线程逻辑开始到处出现:ViewModel 管线程,UseCase 管线程,Repository 也可能管线程。最终结果是同一件事在三层重复解决。
✅ main-safe 架构(复杂度收敛)
UI 层只表达意图:
viewModelScope.launch {
val user = getUserUseCase()
}
Repository 内部处理 IO:
class UserRepository(
private val api: Api
) {
suspend fun getUser(): User =
withContext(Dispatchers.IO) {
api.getUser()
}
}
如果 上面的api是Retrofit,其实是不用withContext(Dispatchers.IO)的。
| 对比维度 | 非 main-safe | main-safe |
|---|---|---|
| UI 是否关心线程 | 是 | 否 |
| IO 逻辑位置 | 到处都是 | Data 层统一 |
| 调用复杂度 | 高 | 低 |
| 心智负担 | 重 | 轻 |
三、main-safe 是什么的契约?——四个"不泄漏"
理解 main-safe,不只是"Repository 用 withContext(IO)"这么简单。它本质上是 Data 层对 UI 层承诺的一套调用契约,具体体现在四个维度的"不泄漏"。
1. 不泄漏线程模型
UI 不需要知道:
- IO 线程池的存在
- Default 调度器的用途
- 是否并发执行
调用方不应该看到 Dispatchers.IO、Dispatchers.Default 这些词出现在 ViewModel 或 UseCase 里——这是 Data 层的内部实现,不是 UI 层的责任。
2. 不泄漏阻塞风险
UI 不需要担心:
- Room 查询是不是 blocking
- 文件 IO 会不会卡主线程
- 网络请求是否 suspend-safe
调用方只需要 suspend fun,至于它背后是 Retrofit 的协程适配器、Room 的 suspend 查询,还是手动 withContext,都不该暴露出来。
3. 不泄漏并发语义
UI 不需要知道:
- 是否多线程并发访问同一数据
- 是否有 mutex / lock 保护
- 是否存在 race condition
这些并发细节属于 Data 层的内部状态管理。一旦 UI 层需要理解并发语义才能正确调用,就说明边界已经被打破了。
4. 不泄漏调用成本
UI 不需要猜:
- 这个函数是不是"很重"
- 会不会引发 ANR
- 要不要包一层
withContext(Dispatchers.IO)
调用方看到一个 suspend fun,就应该可以放心地在 viewModelScope.launch {} 里调用,而不需要事先去翻 Repository 的实现,确认它有没有切线程。
总结: main-safe 不是在描述"Repository 怎么写",而是在描述"UI 层不需要知道什么"。这四个"不泄漏",是 Data 层对上层的承诺,也是架构边界能否真正落地的检验标准。
四、为什么 main-safe 是架构分水岭?
因为它改变了一个关键问题:
谁在控制复杂度?
🟥 非 main-safe:UI 在控制复杂度
UI 必须知道 IO、网络、DB、并发——UI 变成了执行层,而不是表达层。
🟩 main-safe:Data 层控制复杂度
UI 只做一件事:发出请求 + 展示状态。
复杂度被收敛到 Data 层:IO、线程、调度、数据源切换,全在那里解决。
👉 UI 不再拥有"执行权"
UI 只拥有:
- Intent(我要什么)
- State(我展示什么)
👉 Data 层拥有"执行权"
Data 层负责:
- 如何拿数据
- 从哪里拿
- 是否缓存
- 是否 IO
- 是否并发
五、main-safe 之后更深的一层问题
很多项目做到 main-safe 后,仍然会变乱。原因是:
main-safe 只解决"线程外溢",没有解决"业务外溢"。
❌ 错误结构:Repository 做业务决策
class UserRepository {
suspend fun getUser(): User {
val user = api.getUser()
if (user.level > 10) {
cache.save(user)
}
return user
}
}
Repository 开始做业务决策,数据层变成"上帝对象",业务逻辑无法复用。
✅ 正确结构:职责收敛
Repository 只负责数据:
class UserRepository {
suspend fun getUser(): User = api.getUser()
}
UseCase 负责业务决策:
class GetUserUseCase(
private val repo: UserRepository,
private val cache: Cache
) {
suspend operator fun invoke(): User {
val user = repo.getUser()
if (user.level > 10) {
cache.save(user)
}
return user
}
}
六、main-safe 的三个层级理解
| 层级 | 含义 | 核心问题 |
|---|---|---|
| Level 1:线程安全 | UI 不负责 IO | 谁切线程? |
| Level 2:职责安全 | Data 不负责业务判断 | 谁做决策? |
| Level 3:结构安全 | 每一层只做一件事,不越界 | 边界在哪里? |
七、总结
main-safe 的本质不仅仅是线程切换,而是复杂度是否收敛在 Data 层。
它通过四个"不泄漏"——不泄漏线程模型、不泄漏阻塞风险、不泄漏并发语义、不泄漏调用成本——构建起 UI 层与 Data 层之间真正的架构边界。
如果 Data 层无法做到 main-safe,复杂度就仍然分散在调用链中,所谓"分层架构"也只是形式上的结构,而非真正的边界设计。