一、ANR概念入门
说到ANR,很多新手第一反应就是“卡住了”。没错,ANR就是Application Not Responding,应用无响应。系统在监控你的应用时,如果主线程被阻塞了超过5秒(对输入事件)或者10秒(对广播接收),就会弹一个对话框问用户“是继续等待还是直接关闭”。
我刚入行时觉得这玩意儿离我很远,直到有一次在线上版本里,用户反馈“打开App就卡死”。我查了半天日志,发现就是因为在主线程里做了文件加密。嗯,从那以后我再也不敢在主线程搞加解密了。
1.1 什么是ANR?
ANR是Android系统提供的一种保护机制。它告诉你:你的应用已经失去响应了,用户正在不耐烦地戳屏幕,而你却什么都没做。
系统判断ANR的标准如下:
| 场景 | 超时时间 | 触发条件 |
|---|---|---|
| Activity输入事件 | 5秒 | 主线程未处理事件 |
| BroadcastReceiver | 10秒 | onReceive未返回 |
| Service | 20秒 | onStartCommand等超时 |
| ContentProvider | 10秒 | query等操作超时 |
说白了,主线程被堵住了,系统就会“亮红灯”。我当时在一个IM项目里,因为数据库的加解密放在主线程,结果每次消息解密都要几百毫秒,连续几条消息堆起来就直接ANR了。惨痛教训啊。
核心要点:ANR不是bug,是症状。你需要找到是什么操作阻塞了主线程。
1.2 主线程与子线程
Android的主线程也叫UI线程。它负责绘制界面、处理触摸事件、执行Activity生命周期。你想想看,如果主线程忙得没空画界面,用户看到的画面就是上一次快照,点击也没反应——这就是ANR的前兆。
子线程是干活的,比如网络请求、数据库读写、图片解码、还有我们最关心的加解密。为什么加解密必须放子线程?
- 加解密是CPU密集型操作:AES-256一次解密几MB的数据,在低端手机上可能要几百毫秒甚至几秒。
- 主线程每毫秒都很珍贵:16ms内要完成一帧的绘制,你耽误了100ms就意味着掉帧6帧,用户能明显感觉到卡顿。
- 系统监控一刀切:不管你在主线程里做什么,只要超时就弹ANR对话框。没人会原谅你的。
我的习惯:任何超过5ms的操作我都怀疑它是不是该放子线程。加解密、IO、网络、Bitmap解码,默认全部异步。
你可能会问:“那我怎么在子线程处理完加解密后更新UI?” 答案是用Handler、AsyncTask、或者现在的协程/ RxJava。后面章节我会详细讲这些方案,但核心原理是一样的——子线程做完苦力,通过消息队列把结果发回主线程,主线程快速更新界面。
1.3 加解密操作概述
加解密是什么?简单说就是把数据变成别人看不懂的密文,再变回原本的明文。在移动端,我们常碰到三种场景:
- 数据存储加密:SharedPreferences、SQLite、文件内容加密。
- 网络传输加密:请求参数、响应体的加解密。
- 密钥保护:使用Android Keystore、TEE等。
常见的对称加密算法有AES、DES、3DES(现在基本不用DES了),非对称的有RSA、ECC。对了,还有国密SM系列。
为什么加解密会导致ANR?我来给你算一笔账:
| 操作 | 数据量 | 耗时(中低端手机) | 风险 |
|---|---|---|---|
| AES-256 CBC 加密 | 1MB | 30~80ms | 轻度卡顿 |
| AES-256 CBC 解密 | 10MB | 300~800ms | 明显掉帧 |
| RSA-2048 签名 | 256字节 | 50~200ms | 可能导致卡顿 |
| 多次循环加密(如数据库逐行) | 1000行 | 2~5秒 | 必然ANR |
我在一个银行项目里遇到过:用户登录时,后端返回了加密的用户信息,大约2MB。开发小哥直接在onCreate里调用AES解密,结果在红米Note上稳稳地ANR。后来改成子线程异步解密,问题解决。
警告:永远不要在主线程做任何加解密操作。哪怕你只是加密一个字符串“hello”,在极端卡顿的设备上也可能触发ANR。我见过更离谱的:有人用了循环加解密,直接卡死了。
1.4 知识体系总览
为了让你对本章内容有个整体印象,我画了一张图。它展示了ANR、主线程、子线程、加解密之间的关系。
从图中你能看到:ANR的根源在主线程被阻塞,而加解密操作是常见的阻塞源之一。解决方案就是使用子线程处理加解密,并通过Handler等机制把结果传回主线程。
1.5 加解密操作到底有多“重”?
你可能觉得加解密不就是调个库函数吗?我告诉你,AES在CBC模式下需要逐块处理,每次迭代都要进行异或、替换、行移位等操作。数据量一大,CPU负担就很重。而且Android设备性能参差不齐,低端机上的Java实现尤其慢。
一个真实案例:我参与过一个社交App,用户头像用AES加密存储。每次显示头像时都在主线程解密,结果相册列表一屏20个头像,每个解密耗时30ms,总共600ms,加上UI绘制时间,妥妥地卡成PPT。后来用了Glide的自定义ModelLoader异步加载,再也没卡过。
所以在后续章节,我会带你一步步实现:
- 如何在子线程中安全地进行AES加解密
- 如何用Handler将解密结果同步到UI线程
- 如何用协程或RxJava写更简洁的异步加解密
- 如何避免加解密过程中的常见陷阱(比如数据过大导致OOM)
小技巧:如果你发现某个加解密操作无论如何都要占用一定时间,可以考虑在用户手指离开屏幕时再开始执行,或者预先把数据解密好缓存起来。我曾经在启动页就预加载并解密了用户最可能点击的几条数据,体验就好很多。
好了,这一章的核心就是让你建立起“加解密 → 主线程阻塞 → ANR”这条警戒线。记住:主线程是皇帝,你不能让皇帝去搬砖。加解密是重体力活,交给子线程去干。后面我们就会深入代码层面,真正写出防ANR的加解密代码。