1、课程介绍:数据加密与ANR的关系
数据加密,说白了就是给数据加把锁。App 里敏感信息存本地、传网络,都得先加密。但问题是——加密很吃 CPU,一旦跑在主线程,直接卡死界面。你想想看,用户点个按钮,结果界面僵住 5 秒,系统弹出「无响应」对话框……那就是 ANR(Application Not Responding)。
我个人习惯,每次做加密功能时都会先问自己一句:这个操作会不会阻塞 UI?如果回答“是”,那就必须放到后台线程。这不是小题大做——Android 系统对于主线程超时(触摸事件 5s,广播 10s)会直接杀进程,用户体验直接归零。
加密操作为什么容易引发 ANR?
加密不是瞬间完成的。常见的几种加密操作,耗时差异很大:
| 加密类型 | 典型耗时(1MB 数据) | 是否适合主线程 |
|---|---|---|
| AES-CBC 加解密 | ~50ms | ❌ 不安全,可能卡顿 |
| RSA 2048 密钥生成 | ~300ms | ❌ 必 ANR |
| RSA 公钥加密(小数据) | ~5ms | ⚠️ 临界,建议异步 |
| PBKDF2 密钥派生(10000 轮) | ~80ms | ❌ 不适合 |
| HMAC-SHA256 | ~1ms | ✅ 极快,但仍避免高频 |
嗯,这里要注意:上面只是参考值,不同设备、不同数据量差异很大。我在项目中遇到过一台低端机,RSA 密钥生成跑了接近 1 秒——要是放在主线程,ANR 妥妥的。
Compose UI 中的特殊风险
Compose 是声明式 UI,重组(Recomposition)是常态。你写了一个加密函数,本意是用户操作时才调用,但如果不小心把加密逻辑写进了 remember 或 LaunchedEffect 的错误地方,重组时会反复执行。我在一个线上项目里就吃过这个亏——
@Composable 函数里,结果列表滚动时每帧都重新解密……界面掉帧严重,偶尔 ANR。后来改成后台协程 + 缓存才解决。
为什么会这样?因为 Compose 的 @Composable 函数可能被频繁调用(重组)。你必须在重组中避免任何耗时操作,包括加密。Android 主线程只有一个 Looper,加密霸占它,布局刷新和点击事件就得排队。
核心矛盾:加密安全 vs UI 流畅
加密需要安全,UI 需要流畅。两者本身不冲突,冲突的是你在哪个线程做。下面这张图展示了两种场景的对比:
Android 加密 API 的线程安全
常见的加密 API 如 javax.crypto.Cipher、java.security.KeyStore、AndroidKeyStore 等,本身是线程安全的(实例可以被多个线程共享),但内部同步锁会带来额外阻塞。比如 Cipher.getInstance() 第一次调用时会加载 JCE 提供者,耗时可能达到几百毫秒——这要是放在主线程,直接卡 UI。所以即使你不做实际加密,单单获取 Cipher 实例也建议放到后台。
AES/GCM/NoPadding 的 Cipher 对象,提前在后台线程初始化好。这样用户真正需要加密时,拿到的已经是预热过的实例,性能好很多。
Compose 中的典型错误模式
以下代码在我审查过的项目中至少出现过三次:
// ❌ 错误示例:加密直接写在 Composable 中
@Composable
fun SecureContent(encryptedData: ByteArray) {
val plainData = remember(encryptedData) {
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(Cipher.DECRYPT_MODE, secretKey)
cipher.doFinal(encryptedData) // 主线程执行耗时操作!
}
Text(text = String(plainData))
}
你会说:这不是用 remember 缓存了吗?但它的问题在于:remember 的 key 一旦变化(比如数据更新),解密又在主线程重新执行。而如果解密耗时较长,重组时就会卡 UI。代码看起来没问题,实际上踩了 ANR 的雷。
remember 里。因为消息列表会频繁刷新(新消息导致重组),每次都要重新解密所有未缓存的消息。结果在旧手机上,列表滑动掉帧严重,偶尔 ANR。后来改为:在 ViewModel 中用 viewModelScope.launch(Dispatchers.IO) 解密,结果以 State 形式暴露给 Compose。
正确的做法:协程 + State
用 Compose 的 LaunchedEffect 或 ViewModel 协程,把加密操作调度到 Dispatchers.IO 上:
// ✅ 正确示例:后台解密 + Compose State
@Composable
fun SecureContent(encryptedData: ByteArray) {
var plainText by remember { mutableStateOf("") }
LaunchedEffect(encryptedData) {
withContext(Dispatchers.IO) {
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
// ... 解密
val decrypted = cipher.doFinal(encryptedData)
withContext(Dispatchers.Main) {
plainText = String(decrypted)
}
}
}
Text(text = plainText)
}
这样加密跑在 IO 线程,主线程只负责更新 State 触发重组。重组本身只做简单的赋值和 UI 绘制,不会卡。
总结几个关键点
- 任何耗时操作(>5ms)都不应出现在主线程,加密尤甚。
- Compose 的重组是隐式的,容易「不小心」把加密写进
@Composable。 - 使用
Dispatchers.IO或Dispatchers.Default做加密,通过mutableStateOf回传结果。 - 预加载 Cipher 实例到后台,避免首次调用卡顿。
- 在低端机上尤其要警惕 RSA 密钥生成、PBKDF2 这类重型操作。