本节摘要:前七章的纪律如果只靠 review 维持,会随人员流动而稀释;最后一道防线是机器:StrictMode 在运行时揪出主线程违规,Detekt 在提交前拦截
!!、可变数据类、GlobalScope 等模式,协程测试用虚拟时间让异步逻辑变成确定性断言。本节配置这三道防线,并给出启动路径与 ANR 的治理要点,收束全书的"安全消除术"。
StrictMode 是安卓内置的"运行时督察",开发构建里配置后,主线程上的网络、磁盘读写、Activity 泄漏当场抛出或打日志:
class SafeApp : Application() { override fun onCreate() { if (BuildConfig.DEBUG) { StrictMode.setThreadPolicy( StrictMode.ThreadPolicy.Builder() .detectNetwork() // 主线程网络 .detectDiskReads().detectDiskWrites() // 主线程磁盘 .detectCustomSlowCalls() // 自定义慢调用 .penaltyDeathOnNetwork() // 主线程网络直接崩 开发期零容忍 .penaltyLog() // 其余打日志 .build() ) StrictMode.setVmPolicy( StrictMode.VmPolicy.Builder() .detectActivityLeaks() // Activity 泄漏 .detectLeakedClosableObjects() // 流与游标未关闭 .detectLeakedSqlLiteObjects() .penaltyLog() .build() ) } super.onCreate() } }
第 5 章说"Main 上超过 50 毫秒的活必须挪走",StrictMode 就是这条纪律的自动执行者——它抓的是第 5.3 节"三种死法"里最隐蔽的那种:不崩、只慢。VmPolicy 抓的泄漏(Cursor 没关、Closable 流悬空)则对应第 6 章 use 扩展的用武场景:inputStream.use { ... } 在块结束时自动关闭,泄漏类事故从"记得关"变成"结构保证关"。
Detekt 是 Kotlin 的静态分析器,它的价值在于把本册散落各章的工程纪律固化为 CI 门槛。配置示例(规则名对应正文里的纪律):
detekt { config.setFrom(files("config/detekt/detekt.yml")) buildUponDefaultConfig = true } dependencies { detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:1.23.6") }
yaml 里重点开启的规则,每条都能在正文里找到出处:
| 规则 | 拦截的内容 | 对应章节 |
|---|---|---|
| UnsafeCallOnNullableType | 可空类型上用双感叹号 | 第 2 章 |
| GlobalCoroutineUsage | 使用 GlobalScope | 第 5 章 |
| WaitInCoroutineWithinRunBlocking | 协程里阻塞等待 | 第 5 章 |
| MutableDataClass | 数据类含 var 字段 | 第 3 章 |
| DataClassContainsFunctions | 数据类塞业务方法 | 第 3 章 |
| SwallowedException | 空 catch 吞异常 | 第 2 章 |
| TooManyFunctions | 单文件函数过多(工具类复发信号) | 第 6 章 |
| LongParameterList | 构造参数失控 | 第 6 章 |
| MagicNumber | 魔法数字(未提为 dp 扩展或常量) | 第 6 章 |
配套动作有二。基线化存量:detektBaseline 任务把存量问题记入基线文件,新代码零容忍、旧代码分批清——这延续了第 7 章渐进迁移的哲学。ktlint 管格式:代码风格(缩进、导入排序)交给 ktlint 与官方风格指南,与 Detekt 分工(格式与语义分层),格式问题在提交前被自动修复。

异步代码的可测性是它能否被称为"工程"的试金石。kotlinx-coroutines-test 的 runTest 用虚拟时间把 delay 变成零耗时跳转:
class OrderHistoryVMTest { @Test fun `刷新失败降级到缓存并提示`() = runTest { val repo = FakeOrderRepo(failWith = java.io.IOException("断网")) val saved = SavedStateHandle(mapOf("last_orders" to listOf(testOrder()))) val vm = OrderHistoryVM(repo, saved) vm.state.test { // turbine 或 kotlinx 的 flow 测试工具 assertEquals(OrderHistoryVM.UiState.Loading, awaitItem()) val state = awaitItem() assertTrue(state is OrderHistoryVM.UiState.Data && state.orders.isNotEmpty()) cancelAndIgnoreRemainingEvents() } vm.events.test { val e = awaitItem() assertTrue(e is OrderHistoryVM.Event.Toast) } } } class FakeOrderRepo(private val failWith: Exception? = null) : OrderRepo { override suspend fun fetchOrders(): List<Order> { failWith?.let { throw it } return listOf(testOrder()) } }
三个关键件。runTest:测试体内 delay(2000) 立即跳过,超时逻辑(withTimeout)也能在虚拟时间里确定性地走到——真实等待的测试既慢又飘。TestDispatcher 注入:第 5 章说"调度器当依赖注入",兑现处在这里——ViewModel 的调度器换成 TestDispatcher 后,launch 的执行时机完全可控(advanceUntilIdle 让所有排队协程跑完),并发代码的断言从"碰运气"变成"排队执行"。Fake 而非 Mock:协程代码用接口加假实现(FakeOrderRepo)比字节码 mock 框架稳定得多——挂起函数的 mock 语义各家实现不一,假实现就是普通 Kotlin 类。
这套测试直接检验第 8.2 节模板的核心行为:失败降级、缓存读取、事件发射。跑一次几百毫秒,进 CI 每次提交都执行——护航端的日常。
静态检查管不到的运行时问题,集中在启动与主线程耗时。启动瘦身三原则:Application 的 onCreate 只做必要初始化(树根、崩溃平台),重初始化(图片库、推送 SDK)挪到首帧后或用 androidx.startup 与懒加载(第 2 章的 by lazy 在这里的正确用法是"纯计算加确定线程");首页数据走"缓存快照先行、网络异步补"——8.2 的降级策略在启动场景的复用;启动期的同步反序列化(读大配置文件建对象图)是慢启动最大公约数,要么砍体积要么异步化。ANR 治理回到第 5 章的毫秒预算:StrictMode 抓违规、主线程方法采样(线上性能平台)找热点、锁竞争(主线程等后台锁)与广播超时是三大高发源——协程化改造后前两类通常显著下降,因为阻塞调用被 withContext 结构性地挪走了。
崩溃平台做最终验收:本册体系跑三个月后,NPE 占比应显著下滑、剩余集中在平台类型边界(回看第 7 章收容纪律);ANR 率随协程化推进下降;新增状态类 bug(穷举遗漏)应接近零——编译器替你拦了。数字是安全消除术的最终证词。
全册至此收束。第 1 章的骨架配好了防线默认值;第 2 到第 6 章立起五根支柱;第 7 章封住边界的缺口;本章把人的纪律换成机器的门槛。回头看这套体系的本质:Kotlin 没有让写代码变容易,它让写错代码变难——可空性要处理、分支要穷举、可变要签字、并发要归树、逻辑要归位。每一条约束在写下时都多花了十秒,在维护时省下的是十小时。当团队的新人第一次提交就被 Detekt 拦下 !!,当他看到新增密封子类后的全网标红,这套体系就完成了自我复制——安全消除术的最后一课是:防线自己会长脚。
全册完。下一步的延伸方向:Jetpack Compose 把单向数据流推向声明式 UI(本章机制全部可迁移);Kotlin Multiplatform 把领域层带到 iOS 侧(第 1 章骨架的纯 Kotlin domain 层正是为此留的门)。带着"这个机制防住了什么"的问题继续读下去,你会比大多数人学得快。