29、Content Provider 常见问题与调试:ANR 问题、死锁、日志分析、StrictMode
Content Provider 这东西,平时用着挺顺手。但一旦出问题,往往就是大问题——ANR、死锁、数据错乱。我这些年排查过的 Provider 故障,少说也有几十起了。今天就把这些坑和经验,一次性说清楚。
核心要点:Content Provider 的稳定性,本质上取决于你对跨进程通信和线程模型的理解。不懂这些,调试就是瞎蒙。
29.1 ANR 问题:Provider 的“死罪”
ANR 是 Content Provider 最常见的死法。为什么?因为 Provider 的操作默认跑在 UI 线程上。你想想看,一个数据库查询如果耗时超过 5 秒,系统直接给你弹 ANR 对话框。
我个人习惯,在写 Provider 时第一件事就是确认:这个操作会不会阻塞主线程?
29.1.1 典型 ANR 场景
- 大表查询:没有加索引,全表扫描几万条数据
- 跨进程大数据传输:Cursor 返回太多列,Binder 缓冲区撑爆
- 同步操作网络请求:在 query/insert 里直接调网络 API
- 死锁等待:两个线程互相等锁,谁也动不了
我曾经接手过一个项目,用户反馈“打开联系人列表就卡死”。查了半天,发现 Provider 的 query 方法里,居然用了一个同步的 HTTP 请求去拉头像。嗯,这设计,只能说勇气可嘉。
29.1.2 如何定位 ANR
ANR 发生时,系统会在 /data/anr/traces.txt 里留下痕迹。这是第一手证据。
// 查看 ANR 日志
adb pull /data/anr/traces.txt .
// 搜索你的进程名
grep "com.your.app" traces.txt
// 重点关注 "main" 线程的堆栈
"main" prio=5 tid=1 Blocked
at com.your.app.provider.MyProvider.query(MyProvider.java:45)
- waiting to lock <0x0c4f1e20> (a android.database.sqlite.SQLiteDatabase)
at android.content.ContentProvider$Transport.query(ContentProvider.java:240)
...
看到 Blocked 或者 waiting to lock 了吗?这就是死锁或者长时间等待的迹象。
我的调试技巧:不要等到 ANR 发生才去看 traces.txt。我习惯在开发阶段就定期检查这个文件,特别是做压力测试的时候。提前发现,比事后补救强一百倍。
29.2 死锁:两个线程的“互相伤害”
死锁在 Content Provider 里很隐蔽。为什么?因为 Provider 的 CRUD 方法可能被多个线程同时调用,而 SQLite 本身又有锁机制。
说白了,就是线程 A 拿着锁 1 等锁 2,线程 B 拿着锁 2 等锁 1。两个人都等着对方放手,结果谁也动不了。
29.2.1 常见死锁模式
| 场景 | 说明 |
|---|---|
| 嵌套事务 | 在事务 A 中开启事务 B,且 B 等待 A 释放 |
| 读写冲突 | 读线程持有共享锁,写线程等待独占锁;写线程又阻塞了后续读线程 |
| 跨 Provider 调用 | Provider A 调用 Provider B,B 又回调 A |
我记得有一次,团队里一个同事写了个功能:在 ContentProvider.insert() 里调用了另一个 ContentResolver.query()。结果那个 query 又触发了同一个 Provider 的 insert。好家伙,直接死锁,整个应用卡死。这就是典型的“自己调用自己”。
29.2.2 死锁的预防
- 统一锁顺序:所有线程按相同顺序获取锁
- 避免嵌套事务:能用单事务就别用嵌套
- 设置超时:SQLite 支持设置 busy timeout
- 使用 WAL 模式:Write-Ahead Logging 能大幅减少读写冲突
// 设置 SQLite 忙等待超时
SQLiteDatabase db = mDbHelper.getWritableDatabase();
db.execSQL("PRAGMA busy_timeout = 5000"); // 5秒超时
// 启用 WAL 模式
db.execSQL("PRAGMA journal_mode=WAL");
注意:WAL 模式虽然好,但也不是银弹。它在高并发写入时,checkpoint 操作可能会引起短暂的卡顿。我建议在正式环境前,先做一轮压力测试。
29.3 日志分析:从日志里“破案”
日志是调试 Content Provider 的利器。但前提是——你得知道看什么。
29.3.1 关键日志标签
- ActivityManager:ANR、进程死亡、Provider 注册信息
- SQLiteLog:数据库锁、事务、错误信息
- CursorWindow:Cursor 分配、泄漏、大小超限
- ContentProvider:Provider 的 CRUD 调用记录
// 过滤 Provider 相关日志
adb logcat -s ActivityManager SQLiteLog CursorWindow ContentProvider
// 更精细的过滤,只看你的包名
adb logcat | grep "com.your.app"
我一般会写一个日志辅助类,在 Provider 的每个方法入口和出口都打日志。这样一旦出问题,我能立刻知道是哪个操作卡住了。
public class LoggingProvider extends ContentProvider {
private static final String TAG = "MyProvider";
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
Log.d(TAG, "query start: " + uri);
long start = System.currentTimeMillis();
try {
Cursor cursor = super.query(uri, projection, selection, selectionArgs, sortOrder);
Log.d(TAG, "query end: " + (System.currentTimeMillis() - start) + "ms");
return cursor;
} catch (Exception e) {
Log.e(TAG, "query failed: " + uri, e);
throw e;
}
}
}
29.3.2 日志分析实战
假设你看到这样的日志:
SQLiteLog: (284) automatic index on table 'users' for 'SELECT ...'
CursorWindow: Window is full. Requested row 100, last row 99
第一行告诉你:查询没有走索引,SQLite 自动建了个临时索引。这说明你的表缺索引,查询效率堪忧。
第二行告诉你:CursorWindow 满了。默认的 CursorWindow 大小是 2MB,如果你一次查询返回太多数据,就会触发这个警告。解决方案?分页查询,别一次拉太多。
我的习惯:每次发布前,我都会跑一遍全量功能,然后 grep 一下日志里有没有 "automatic index" 和 "Window is full"。这两个警告,基本就是性能问题的前兆。
29.4 StrictMode:你的“代码警察”
StrictMode 是 Android 提供的一个开发工具。它能帮你抓到那些“不该在主线程做的事”。说白了,就是你的代码警察。
29.4.1 如何开启 StrictMode
// 在 Application 的 onCreate 中开启
public class MyApp extends Application {
@Override
public void onCreate() {
super.onCreate();
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
.detectDiskReads() // 检测主线程读磁盘
.detectDiskWrites() // 检测主线程写磁盘
.detectNetwork() // 检测主线程网络请求
.penaltyLog() // 违规时打印日志
.penaltyDeath() // 违规时直接崩溃(开发阶段用)
.build());
StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
.detectLeakedSqlLiteObjects() // 检测 SQLite 对象泄漏
.detectLeakedClosableObjects() // 检测 Closeable 对象泄漏
.detectActivityLeaks() // 检测 Activity 泄漏
.penaltyLog()
.build());
}
}
我个人建议,在开发阶段把 penaltyDeath() 打开。虽然会频繁崩溃,但能逼着你把问题修掉。等上线前再关掉,只保留 penaltyLog()。
29.4.2 StrictMode 在 Provider 中的应用
Content Provider 的 CRUD 方法默认跑在调用者的线程上。如果调用者是 UI 线程,那你的 Provider 操作就相当于在主线程执行。
开启 StrictMode 后,一旦你在 Provider 里做了磁盘 I/O 或者网络请求,日志里会立刻出现:
StrictMode: StrictMode policy violation; ~duration=320 ms:
android.os.StrictMode$StrictModeDiskReadViolation:
policy=31 violation=2
at com.your.app.provider.MyProvider.query(MyProvider.java:42)
...
看到这个,你就知道:哦,这里不该在主线程跑。
注意:StrictMode 只在 debug 版本生效。release 版本会自动忽略这些策略。所以放心大胆地在开发阶段用,不会影响线上用户。
29.5 知识体系总览
下面这张图,是我对 Content Provider 调试体系的总结。你可以把它当作一个检查清单,遇到问题时对照着排查。
这张图把 Content Provider 的调试分成了四个维度。ANR 和死锁是“症状”,日志分析和 StrictMode 是“诊断工具”。你想想看,如果能在开发阶段就用 StrictMode 抓到问题,何必等到线上用户反馈 ANR 呢?
好了,关于 Content Provider 的调试,我就讲这么多。记住一句话:好的调试习惯,比任何高级技巧都重要。每次写完 Provider 代码,跑一遍 StrictMode,看一眼日志,花不了几分钟,但能省下你后面几天的排查时间。