别再靠 Code Review 守底线:我做了一个静态分析项目 RedLine

Android 开发中,协程几乎已经成为异步编程的默认选择。但"使用协程"并不等于项目天然具备一致、可靠的异步模型。

很多项目在长期演进过程中都会出现类似情况:新的代码使用 suspendFlow,旧模块仍然依赖 Callback、LiveData、RxJava 或 Java Future;有些人使用 Mutex 管理共享状态,有些人继续使用 synchronizedReentrantLock;方法虽然标记为 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

相关推荐
y = xⁿ10 小时前
DeepSeek Harness 学习日记:关于Agent接口,工具调用的底层实现
android·java·学习
2601_9620664912 小时前
【Sql Server】Update中的From语句,以及常见更新操作方式
android·java·数据库
用户693717500138417 小时前
曾经安卓开发人手一个的 EventBus,为什么现在没人爱用了?
android·android studio
九皇叔叔19 小时前
MySQL ORDER BY 性能优化详解:Using filesort、联合索引、ASC/DESC 与覆盖索引
android·adb
OriginCoding20 小时前
用 AI Agent 协作完成一个 Android TOTP 应用:从需求边界到 v1.0.0
android·ai编程
杉氧1 天前
React 灵魂:深入剖析 Hooks 与副作用(Side Effects)陷阱
android·前端·react native
又见情义1 天前
RK3568 + RTL8211F 网络唤醒(WOL)功能适配全记录
android·网络·驱动开发
用户0934077735141 天前
HarmonyOS WPS Open SDK 实践:registerApp 鉴权与就绪门禁
android·typescript·harmonyos
jike_20261 天前
会议离线录音APP:无网络记录能力对比
android·智能手机·语音识别