Android 开发中,协程几乎已经成为异步编程的默认选择。但"使用协程"并不等于项目天然具备一致、可靠的异步模型。
很多项目在长期演进过程中都会出现类似情况:新的代码使用 suspend 和 Flow,旧模块仍然依赖 Callback、LiveData、RxJava 或 Java Future;有些人使用 Mutex 管理共享状态,有些人继续使用 synchronized 或 ReentrantLock;方法虽然标记为 suspend,内部却调用 Thread.sleep()。 之前写过Android Data 层设计的四条红线:为什么必须坚持、如何落地 没有看过的,可以先看看这个。
RedLine 就是围绕这一问题构建的一个 Android 示例项目。
我不试图提供完整业务功能,而是通过自定义 Detekt 规则,将团队关于数据层异步与并发的约定转化为自动执行的构建规则。
它的目标是:
让不符合团队约定的代码无法通过静态检查。
一、项目定位:从口头规范到自动化约束
团队规范经常以文档、Code Review 意见或者经验传承的形式存在。
例如:
- Repository 应该使用协程接口;
- 不要在协程中阻塞线程;
- 不要在新代码中使用 Java 锁;
- 不要在数据层返回
LiveData; - 不要开启 Room 主线程查询。
这些约定最大的问题是:依赖人工记忆和人工审查。
随着团队扩大、需求变快、模块增多,代码 Review 很难保证每一次都能识别全部风险。
RedLine 选择了另一种方式:
把规范直接编码成 Detekt Rule。
Detekt 是 Kotlin 生态中的静态分析工具。通过扩展 Detekt,可以编写自定义 Rule,对 Kotlin 源码进行扫描。
只要发现不符合团队规范的代码,就产生问题;再配合:
yaml
build:
maxIssues: 0
就可以让违规代码直接导致构建失败。
于是:
text
团队规范
↓
Detekt Rule
↓
静态分析
↓
发现违规
↓
构建失败
规范就从"建议"变成了可执行的工程约束。
二、整体架构(Demo)
项目主要包含两个模块:
text
RedLine
├── app
│ ├── MainActivity
│ ├── CorrectRepository
│ └── SampleRepository
│
├── detekt-rules
│ ├── RepositoryPublicApiSuspendOrFlowRule
│ ├── ForbiddenBlockingQueueApiRule
│ ├── ForbiddenJavaLockApiRule
│ ├── ForbiddenPseudoAsyncRule
│ └── RepositorySuspendOrFlowRuleSetProvider
│
└── config
└── detekt
└── detekt.yml
app 模块是规则的使用方。
其中包含:
- 合规的
CorrectRepository; - 故意违规的
SampleRepository。
detekt-rules 则是规则实现模块。
它使用 Detekt API 定义自定义 Rule,并通过 RuleSetProvider 将多个规则注册成:
text
repository-rules
应用模块通过:
kotlin
dependencies {
detektPlugins(project(":detekt-rules"))
}
加载自定义规则。
因此执行 Detekt 时,不仅会执行官方规则,还会执行项目自己的架构规则。
三、第一条红线:Repository 公开接口必须是 suspend 或 Flow
Repository 是 Data 层对上层暴露能力的边界。
这个边界一旦不统一,调用层就必须不断适配不同的异步模型。
例如:
kotlin
fun getUser(): User
fun getUserAsync(callback: (User) -> Unit)
fun getUserFuture(): CompletableFuture<User>
fun getUserLiveData(): LiveData<User>
suspend fun getUserById(id: String): User
一个 Repository 同时存在这么多异步模型,会导致:
- 调用方需要记住不同 API 的调用方式;
- 异常传播机制不一致;
- 取消机制不一致;
- ViewModel 被迫承担适配职责;
- 后续架构迁移成本增加。
因此,RedLine 的第一条规则是:
Repository 的公开方法必须是
suspend,或者返回Flow。
例如:
kotlin
class UserRepository {
fun getUser(): User = TODO()
}
这个方法会被判定为违规。
正确写法:
kotlin
class UserRepository {
suspend fun getUser(): User = TODO()
}
或者:
kotlin
class UserRepository {
fun observeUser(): Flow<User> = TODO()
}
规则同时识别这些不建议直接从 Data 层暴露的类型:
kotlin
LiveData
CompletableFuture
Single
Observable
Callback
例如:
kotlin
fun getUser(): LiveData<User>
或者:
kotlin
fun getUser(): CompletableFuture<User>
都会被拦截。
真正的设计意图是:
统一 Data 层的异步模型。
一个比较清晰的职责划分是:
text
Data
↓
suspend / Flow
↓
Domain
↓
suspend / Flow
↓
ViewModel
↓
StateFlow / UI State
↓
UI
↓
Compose State
LiveData 本身带有 Android 生命周期语义,更适合 ViewModel 与 UI 的边界,而不是进入纯数据层。
四、第二条红线:禁止 BlockingQueue
Java 的 BlockingQueue 体系包括:
text
BlockingQueue
LinkedBlockingQueue
ArrayBlockingQueue
SynchronousQueue
它们的核心特点是:
当操作无法立即完成时,调用线程可能被阻塞。
例如:
kotlin
private val queue = LinkedBlockingQueue<String>()
fun produce() {
queue.put("message")
}
如果队列已满,put() 就可能阻塞当前线程。
在以协程为主要异步模型的 Android 项目中,这通常不是理想的设计。
因此 RedLine 会扫描这些阻塞队列类型,并提示使用协程模型下的替代方案。
例如:
kotlin
private val channel = Channel<String>()
suspend fun produce(message: String) {
channel.send(message)
}
suspend fun consume(): String {
return channel.receive()
}
当 send() 或 receive() 暂时无法继续时,协程可以挂起,而不是让线程一直等待。
如果需求本质上是状态或数据流,也可以使用:
kotlin
private val events = MutableSharedFlow<String>()
suspend fun emitEvent(event: String) {
events.emit(event)
}
fun observeEvents(): Flow<String> = events
当然:
text
Channel
StateFlow
SharedFlow
Flow
并不是完全等价的。
可以简单理解为:
text
Channel
→ 协程消息传递
StateFlow
→ 当前状态
SharedFlow
→ 多订阅者事件/数据广播
Flow
→ 数据流抽象
所以 RedLine 的目标不是机械地要求:
BlockingQueue 一律替换成 Channel。
而是:
禁止阻塞式队列进入以协程为核心的异步架构。
具体使用 Channel、StateFlow、SharedFlow 还是 Flow,应由业务语义决定。
五、第三条红线:禁止 Java 锁和阻塞同步原语
RedLine 禁止以下典型 Java 并发 API:
text
ReentrantLock
Lock
Condition
CountDownLatch
Semaphore
CyclicBarrier
synchronized
@Synchronized
wait
notify
notifyAll
例如:
kotlin
private val lock = ReentrantLock()
fun updateCache() {
lock.withLock {
// 修改共享状态
}
}
或者:
kotlin
@Synchronized
fun updateCache() {
// 修改共享状态
}
这些 API 在传统 Java 多线程模型中非常常见。
但在协程模型下,需要特别关注一个问题:
等待同步资源时,是否阻塞了底层线程?
协程环境中可以使用:
kotlin
private val mutex = Mutex()
suspend fun updateCache() {
mutex.withLock {
// 修改共享状态
}
}
当协程暂时无法获取 Mutex 时,它可以挂起,而不是持续占用线程等待。
六、第四条红线:禁止伪异步
这是 RedLine 中非常重要的一条规则。
很多代码看起来已经使用协程:
kotlin
suspend fun refresh()
但内部却仍然是阻塞操作:
kotlin
suspend fun refresh() {
Thread.sleep(2_000)
}
问题在于:
text
suspend
并不意味着:
text
不会阻塞线程
suspend 只是允许函数挂起。
如果函数内部执行:
kotlin
Thread.sleep()
当前线程仍然会被阻塞。
如果运行在主线程,会导致 UI 卡顿甚至 ANR。
如果运行在线程池,也会降低线程池吞吐能力。
因此:
kotlin
suspend fun refresh() {
delay(2_000)
}
才符合协程模型。
delay() 会挂起协程,并把线程让出来。
所以 RedLine 将:
kotlin
Thread.sleep()
定义为伪异步红线。
八、Detekt 规则注册
自定义 Detekt Rule 需要经过规则注册才能真正参与分析。
RedLine 使用 RuleSetProvider 将多个 Rule 组织成一个规则集:
kotlin
class RepositorySuspendOrFlowRuleSetProvider : RuleSetProvider {
override val ruleSetId: String = "repository-rules"
override fun instance(config: Config): RuleSet = RuleSet(
ruleSetId,
listOf(
RepositoryPublicApiSuspendOrFlowRule(config),
ForbiddenBlockingQueueApiRule(config),
ForbiddenJavaLockApiRule(config),
ForbiddenPseudoAsyncRule(config)
)
)
}
然后在 detekt.yml 中启用:
yaml
repository-rules:
active: true
RepositoryPublicApiSuspendOrFlow:
active: true
ForbiddenBlockingQueueApi:
active: true
ForbiddenJavaLockApi:
active: true
ForbiddenPseudoAsync:
active: true
同时设置:
yaml
build:
maxIssues: 0
这意味着:
只要发现一条红线违规,Detekt 就失败。
对于普通代码质量规则,可以接受一定数量的问题。
但对于团队已经明确认定的架构红线:
text
0
反而是最合理的配置。
九、一次真实的 Detekt 执行结果
bash
./gradlew detekt
Gradle 会分析 app 模块中的 Kotlin 文件,同时加载 detekt-rules 中定义的 repository-rules。
本次执行共分析:
go
RepositoryPublicApiSuspendOrFlow - [红线一违规:Repository 公开方法 `getRawData` 必须是 `suspend` 或返回 `Flow`。] at RedLine/app/src/main/java/com/sample/redline/data/violations/SampleRepository.kt:22:5
RepositoryPublicApiSuspendOrFlow - [红线一违规:`getLiveData` 禁止在 Data 层返回 LiveData/Rx/Future 类型。] at RedLine/app/src/main/java/com/sample/redline/data/violations/SampleRepository.kt:25:5
RepositoryPublicApiSuspendOrFlow - [红线一违规:`getFutureData` 禁止在 Data 层返回 LiveData/Rx/Future 类型。]
例如,对于同步 Repository 方法,规则会给出类似提示:
text
红线一违规:Repository 公开方法 getRawData
必须是 suspend 或返回 Flow。
如果数据层直接返回 LiveData:
text
红线一违规:getLiveData
禁止在 Data 层返回 LiveData/Rx/Future 类型。
如果使用:
kotlin
LinkedBlockingQueue
则会提示:
text
红线二违规:检测到阻塞队列 LinkedBlockingQueue。
请替换为 Channel 或 Flow。
如果使用:
kotlin
ReentrantLock
则会提示:
text
红线三违规:检测到 Java 锁 API ReentrantLock。
请替换为 kotlinx.coroutines.sync.Mutex。
而对于:
kotlin
suspend fun pseudoAsync() {
Thread.sleep(2000)
}
则会提示:
text
红线四违规:禁止在 suspend 函数中使用 Thread.sleep,
请使用 delay。
读者请自行运行测试
十、合规示例与违规示例
项目中包含两个非常重要的 Repository。
CorrectRepository
用于展示符合规范的代码:
kotlin
class CorrectRepository {
private val mutex = Mutex()
private val channel = Channel<String>()
suspend fun fetchData(): String {
delay(100)
return "Good Data"
}
fun observeData(): Flow<String> = flow {
emit("Good Flow Data")
}
suspend fun produceItem(item: String) {
channel.send(item)
}
suspend fun safeOperation() {
mutex.withLock {
// 协程安全的临界区
}
}
}
它体现了四条红线对应的替代方案:
text
同步 API
↓
suspend
阻塞队列
↓
Channel / Flow
Java Lock
↓
Mutex
Thread.sleep
↓
delay
SampleRepository
而 SampleRepository 则是一个故意设计出来的错误示例。
例如:
kotlin
fun getRawData(): String = "Bad"
fun getLiveData(): LiveData<String> =
MutableLiveData()
fun getFutureData(): CompletableFuture<String> =
CompletableFuture.completedFuture("Bad")
以及:
kotlin
suspend fun pseudoAsync() {
Thread.sleep(2000)
}
它的存在并不是为了成为真正的业务代码,而是为了验证:
规则能不能真正抓到这些问题。
所以直接执行:
bash
./gradlew :app:detekt
失败是预期结果。
十一、为什么这种方式比 Code Review 更可靠?
Code Review 仍然非常重要。
但 Code Review 有一个天然限制:
人会遗漏问题。
例如一个团队已经规定:
text
Repository 不允许 LiveData
那么每一次 Review 都可能出现:
text
Reviewer A:这个 LiveData 不应该放 Repository。
Developer:好的,我改掉。
几个月后,又出现:
text
Reviewer B:这里为什么又用了 LiveData?
这种重复讨论本质上是在浪费工程资源。
如果把它写成 Detekt Rule:
text
Repository
+
LiveData
↓
直接失败
开发者第一次提交代码就会收到反馈。
于是 Code Review 的关注点可以从:
text
"你这里为什么用了 LiveData?"
提升到:
text
"这个 Repository 的职责边界应该怎么设计?"
也就是说:
静态检查负责守住机械规则,Code Review 负责讨论真正的设计问题。
十二、如何在真实项目中落地
如果要把 RedLine 的思想引入真实 Android 项目,可以按照几个阶段推进。
第一阶段:确定少量红线
不要一开始就增加几十条规则。
优先选择:
text
线程阻塞
并发错误
架构边界
高风险 API
例如:
text
Repository 不允许同步 API
Repository 不允许 LiveData
禁止 Thread.sleep
禁止 BlockingQueue
禁止 Java Lock
禁止 allowMainThreadQueries
这些规则的收益非常明确。
第二阶段:独立规则模块
将:
text
detekt-rules
独立出来。
这样可以让多个 Android 项目共享:
text
company-detekt-rules
例如:
text
Project A
↓
company-detekt-rules
Project B
↓
company-detekt-rules
Project C
↓
company-detekt-rules
最终形成团队统一的工程规范。
第三阶段:为 Rule 编写测试
Detekt Rule 本身也是代码。
因此必须测试:
text
应该报错
↓
必须报错
不应该报错
↓
不能误报
否则静态检查工具本身就可能制造大量噪声。
第五阶段:治理历史代码
大型项目通常存在大量遗留代码。
不应该直接要求:
text
今天开始
所有历史代码必须全部符合规则
更现实的方案是:
text
旧代码
↓
逐模块治理
新代码
↓
立即执行红线
最终逐渐把历史技术债清理掉。
失败是必然的。
对于真实项目,更合理的方式是将违规示例放入:
text
detekt-rules-test
或者:
text
lint-fixtures
或者独立 Demo 模块。
这样既可以验证规则,又不会让正常应用的 CI 永远处于失败状态。
十三、RedLine 真正解决的是什么问题?
表面上看,RedLine 解决的是:
text
禁止 LiveData
禁止 BlockingQueue
禁止 Java Lock
禁止 Thread.sleep
但更深层的问题其实是:
统一团队的异步模型和并发模型。
一个没有约束的 Android 项目,很容易逐渐变成:
text
Callback
+
LiveData
+
RxJava
+
Future
+
suspend
+
Flow
+
BlockingQueue
+
ReentrantLock
+
synchronized
+
Mutex
每一种方案单独看都可能有合理的使用场景。
但放在同一个 Data Layer 中,最终会形成:
text
异步模型不统一
↓
线程语义不统一
↓
异常模型不统一
↓
取消模型不统一
↓
测试方式不统一
↓
维护成本不断增加
RedLine 做的事情,就是在架构边界上建立一道防线:
text
┌──────────────┐
│ UI 层 │
└──────┬───────┘
↓
┌──────────────┐
│ ViewModel │
│ StateFlow │
└──────┬───────┘
↓
┌──────────────┐
│ Domain │
│ suspend/Flow │
└──────┬───────┘
↓
┌────────────────────┐
│ Data │
│ suspend/Flow │
└─────────┬──────────┘
↓
┌──────────────┐
│ RedLine │
│ Detekt │
└──────────────┘
一旦出现:
text
LiveData
Callback
Future
BlockingQueue
ReentrantLock
Thread.sleep
allowMainThreadQueries
就由静态检查直接拦截。
十四、结语
RedLine 展示的并不是某一个 Kotlin API 的使用技巧,而是一种更重要的工程治理方式:
把架构原则固化成可执行规则。
当 Repository 的异步模型统一为:
text
suspend + Flow
当:
text
BlockingQueue
Java Lock
Thread.sleep
allowMainThreadQueries
在提交阶段就被自动拦截时,团队就不需要在每一次 Code Review 中重复讨论同一类基础问题。
更重要的是,RedLine 证明了一件事情:
text
架构规范
↓
代码化
↓
静态分析
↓
自动阻断
这条链路是可以真正落地的。
源码: RedLine