4. 同步操作的ANR风险:为什么同步API会导致ANR、典型场景归纳
说真的,同步API是ANR的头号元凶。你想想看,主线程本来就跟高速公路似的,你把它堵了,后面的车全得停。那我今天就把这事掰开揉碎,讲讲到底哪些地方容易踩雷。
4.1 同步API的ANR根源:主线程阻塞
Android的主线程(也叫UI线程)就一个职责:处理用户交互和界面刷新。它里面跑着一个Looper循环,不停地从消息队列里取消息来执行。一旦某个消息的执行时间超过5秒(输入事件超时是5秒,BroadcastReceiver是10秒,Service是20秒),系统就会判定ANR。
而同步API呢?调用方等它返回时,整个线程就卡住了。说白了,如果在主线程里调了一个InputStream.read(),那在文件读完之前,页面点啥都不反应。我在项目里遇到过,一个同事在onClick里直接执行了网络请求,结果每次点按钮都卡好几秒,用户差点把手机摔了。
4.2 典型ANR场景一:I/O操作
I/O操作是最常见的坑。文件读写、数据库查询、网络请求——这些操作的时间完全不可控。比如:
- 文件读写:读取大文件或频繁写入,SD卡慢的时候等个几秒很正常。
- 网络请求:
HttpURLConnection.connect()同步调用,网络差时十几秒都有可能。 - 数据库操作:Room或SQLite的同步查询,数据量大时轻松超时。
我印象特别深的一次,用户反馈App在“我的订单”页面老是闪退。查了ANR日志,发现是在Activity.onCreate()里做了一次全表查询,同步执行的。表里有几万条数据,查询花了8秒,直接ANR。改成了异步协程后,问题再也没有出现。
read()、write()、query()、execute()都标为高危,代码Review时必须改成异步。
4.3 典型ANR场景二:长计算或复杂任务
除了I/O,纯CPU密集的同步操作也能堵死主线程。比如:
- 大量图片的Bitmap处理(特别是解码大图)
- JSON/XML的解析(数据量大时)
- 加密解密操作(例如RSA签名生成)
- 动画计算或视图布局的复杂运算
举个例子,BitmapFactory.decodeStream()在解析超清图片时,耗时可能超过几百毫秒。如果是在列表滑动场景里频繁调用,那卡顿感非常明显,偶尔直接ANR。我之前做过一个图片滤镜App,用户拍照后直接在主线程做高斯模糊,结果低端机上直接ANR,还以为是手机坏了。
AsyncTask、HandlerThread或Coroutine里。如果非要同步调用,至少用StrictMode提前发现。
4.4 典型ANR场景三:锁竞争与死锁
多线程编程里最常见的问题之一。主线程持有一个锁,然后在等待另一个线程释放锁——如果另一个线程也持有了主线程需要的锁,就死锁了。或者更常见:主线程拿了一个锁,然后在锁里做耗时操作(比如网络请求),那么其他需要这个锁的线程全部卡住。
我记得有一次,开发同学在SharePreferences的apply()和commit()混用,导致主线程在commit()里阻塞等待磁盘写入,而apply()的写入线程也卡在同一个锁上。好家伙,双向阻塞,ANR日志里全是"WAITING"状态。那之后我们统一禁止在主线程调用commit(),全部换成apply()或异步存储。
| 锁类型 | 常见ANR原因 | 建议 |
|---|---|---|
| synchronized | 在锁块内执行耗时操作 | 锁内只做轻量操作,不要做I/O |
| ReentrantLock | lock()超时等待 | 使用tryLock(timeout)避免无限等待 |
| Binder线程池锁 | 主线程等待Binder响应 | Binder调用也建议异步化 |
4.5 典型ANR场景四:Binder调用等待
Binder是Android进程间通信的机制。很多系统服务(如ActivityManager、WindowManager、PackageManager)都是通过Binder调用的。如果你在主线程调用了某个Binder方法,而服务端进程繁忙或者死锁,那你的主线程就会一直等下去,直到超时ANR。
常见场景:
ActivityManager.getProcessMemoryInfo()PackageManager.getInstalledApplications()PowerManager.isDeviceIdleMode()等系统服务调用
我亲自处理过一个真实案例:用户在启动页时检查“是否处于省电模式”,调用了PowerManager.isPowerSaveMode()。这个调用在后台可能几十毫秒就返回,但当时系统服务在忙,结果等待了6秒!用户直接卡在启动页。我们的解决方案是把这类Binder调用放到View.post()里延迟执行,或者用ListenableFuture异步处理。
4.6 实践检查清单
下面这个表是我在实际项目中用来做code review的快速检查清单,你可以在开发时对照着看。
| 场景 | 典型API | 检查要点 |
|---|---|---|
| 文件I/O | FileInputStream.read() | 是否在子线程?文件是否可能很大? |
| 网络请求 | HttpURLConnection.connect() | 是否有异步wrapper?是否有超时设置? |
| 数据库 | SQLiteDatabase.rawQuery() | 是否用了Room的异步写法?是否有索引? |
| 图片解码 | BitmapFactory.decodeFile() | 是否使用inSampleSize?是否在子线程? |
| Binder | ActivityManager.getRunningAppProcesses() | 是否有异步fallback?是否容忍失败? |
| 锁 | synchronized (lock) | 锁内是否包含I/O或网络调用? |
好了,这些就是我最常遇到的四类ANR场景。你想想看,只要能避开主线程直接跑同步API,90%的ANR都能解决。下一节我们聊聊异步化的具体实现方式——不过那是下一章的内容了,今天先消化这些。