Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help


title: 线上存储、I/O 与 SQLite 可观测性 chapter: '26.9' section: '26.9' status: finalized applicable_versions: Android 8 (API 26) - Android 17 (API 37) last_verified: '2026-08-15' last_source_verified_at: '2026-08-15' last_verified_against: Android 17 android-17.0.0_r1 SQLite and libcore sources; AndroidX SQLite 2.7.0 and Room 2.8.4 release notes; current Android SQLite performance, StrictMode, Room API, and SQLite EXPLAIN QUERY PLAN docs, retrieved 2026-08-15 confidence: high sources:

  • type: clipping path: Clippings/线上疑难问题该如何排查和跟踪?-Android开发高手课-极客时间 14.md
  • type: clipping path: Clippings/线上疑难问题该如何排查和跟踪?-Android开发高手课-极客时间 16.md
  • type: clipping path: Clippings/线上疑难问题该如何排查和跟踪?-Android开发高手课-极客时间 17.md
  • type: official path: https://developer.android.com/topic/performance/sqlite-performance-best-practices
  • type: official path: https://developer.android.com/reference/android/os/StrictMode
  • type: official path: https://developer.android.com/reference/androidx/room/RoomDatabase.QueryCallback
  • type: official path: https://developer.android.com/jetpack/androidx/releases/sqlite
  • type: official path: https://developer.android.com/jetpack/androidx/releases/room
  • type: official path: https://developer.android.com/reference/android/database/sqlite/SQLiteDatabase.OpenParams.Builder
  • type: official path: https://www.sqlite.org/eqp.html
  • type: legacy-reference-preserved path: frameworks/base/core/java/android/database/sqlite/SQLiteConnection.java
  • type: legacy-reference-preserved path: libcore/luni/src/main/java/libcore/io/BlockGuardOs.java
  • type: legacy-reference-preserved path: libcore/dalvik/src/main/java/dalvik/system/CloseGuard.java
  • type: aosp path: https://android.googlesource.com/platform/frameworks/base/+/refs/tags/android-17.0.0_r1/core/java/android/database/sqlite/SQLiteConnection.java
  • type: aosp path: https://android.googlesource.com/platform/frameworks/base/+/refs/tags/android-17.0.0_r1/core/java/android/database/sqlite/SQLiteConnectionPool.java
  • type: aosp path: https://android.googlesource.com/platform/frameworks/base/+/refs/tags/android-17.0.0_r1/core/java/android/database/sqlite/SQLiteDatabaseConfiguration.java
  • type: aosp path: https://android.googlesource.com/platform/frameworks/base/+/refs/tags/android-17.0.0_r1/core/java/android/database/sqlite/SQLiteDatabase.java
  • type: aosp path: https://android.googlesource.com/platform/frameworks/base/+/refs/tags/android-17.0.0_r1/core/java/android/database/sqlite/SQLiteDebug.java
  • type: aosp path: https://android.googlesource.com/platform/libcore/+/refs/tags/android-17.0.0_r1/luni/src/main/java/libcore/io/BlockGuardOs.java
  • type: aosp path: https://android.googlesource.com/platform/libcore/+/refs/tags/android-17.0.0_r1/dalvik/src/main/java/dalvik/system/CloseGuard.java tags:
  • observability
  • storage
  • io
  • sqlite
  • matrix related_chapters:
  • '24.1'
  • '24.2'
  • '24.4'
  • '26.1'
  • '26.3'
  • '26.6' pipeline_stage: ready-to-publish task2b_state: fixed task6_state: reviewed task9_state: reviewed last_draft_polish_at: '2026-08-15T21:07:07+08:00' last_draft_polish_run_id: 20260815-210707-gracker-writing-474 last_review_finalize_at: '2026-08-15T21:07:07+08:00' last_review_finalize_run_id: 20260815-210707-gracker-writing-474 last_rework_at: '2026-08-15T21:07:07+08:00' last_rework_run_id: 20260815-210707-gracker-writing-474

线上存储、I/O 与 SQLite 可观测性

存储故障很少只有一个症状。一次“打开页面卡住”可能包含主线程读文件、SQLite 连接等待和目录扫描;一次“数据丢了”可能来自磁盘空间不足、损坏恢复策略或尚未持久化的写入。这里关注如何留下足以区分这些路径的线上证据。文件 I/O(input/output,输入与输出)和 SQLite 的优化方法分别见 24.1、24.2,通用采集预算见 26.1。

平台源码以 android-17.0.0_r1 为准,涉及内核 tracepoint(内核预先定义的追踪事件)时以 android17-6.18-2026-06_r6 为准。AndroidX SQLite、Room 和 Matrix(腾讯开源的 Android 性能诊断工具集)并不随 API 37 固定版本:应用使用的 driver(承接 SQL 执行与连接管理的数据库驱动)、库版本和 Hook(运行时拦截目标函数调用)实现,必须作为事件字段保存。

观测对象边界

存储指标应先按失败层次分类。单一的 storage_slow 只能说明耗时上升,无法判断应检查文件调用、数据库并发还是设备空间。下表中的 wall time 是操作从开始到结束经过的自然时间,CPU time 是线程实际占用处理器的时间,scope 表示计时覆盖的起止边界。

问题建议事件最小字段诊断目的
文件调用慢storage_io_operation操作类别、路径类别、线程、wall time、CPU time、字节数、调用点、场景区分调用方耗时、线程被抢占和疑似 I/O 等待
不良访问形态storage_io_pattern规则、窗口内次数、累计字节、累计耗时、文件指纹、调用点找到主线程访问、细碎调用、重复读取和大目录遍历
SQLite 执行慢sqlite_operationscope、SQL 指纹、操作类型、线程、事务状态、wall time、driver区分查询本身、排队、事务和连接等待
SQLite 错误sqlite_error异常类、主错误码/扩展码(若可得)、数据库别名、事务状态、恢复动作分开处理 busy/locked、I/O error、full、corrupt 和约束错误
空间增长storage_snapshot目录类别、总字节数、文件数、年龄桶、扫描覆盖率找出增长来源,并判断快照是否完整
业务文件损坏file_validation_failure格式版本、校验阶段、校验类型、文件类别、最近写入状态、恢复结果区分下载不完整、写入中断、版本不兼容和内容损坏
资源未关闭resource_leak_signal资源类型、创建调用点、进程 fd(file descriptor,文件描述符)数量、发现方式定位 Cursor、stream、ParcelFileDescriptor 等生命周期问题

两种时间必须分开记录。wall time 很长而当前线程 CPU time 很短,可能是调度、锁或 I/O 等待;两者都高则更像应用代码中的计算。这个判断只能用于选择下一步证据,不能仅凭两个时长认定内核 I/O 是原因。

事件还要标注观测范围。围绕 DAO(data access object,数据访问对象)方法计时得到的是调用端到端时长,可能包含协程调度和连接等待;在 driver 执行边界计时更接近 SQLite 操作;Native(本地 C/C++ 层)read()/write() Hook 看到的是系统调用,无法覆盖所有页缓存回写。页缓存是内核用于暂存文件页的内存区域。若字段里只写 duration,后台会把不同 scope 的数据错误合并。

路径、SQL 和文件内容都可能包含用户数据。默认只采集稳定类别、用于聚合同类对象的受控指纹和应用调用点;需要原始证据时,应走 26.3 的受限诊断流程,常规事件仍维持较低的敏感数据采集范围。

I/O 采集路径选择

采集路径决定了覆盖面,也决定了升级风险。优先选择公开 API 和应用可控边界,只有确认缺口值得引入额外兼容成本时,才使用 Hook。

路径能看到什么看不到什么建议使用范围
应用封装或库提供的回调自有文件组件、Okio/FileSystem、缓存组件、导入导出任务绕过封装的三方库和系统内部调用生产环境长期采集的主路径
编译期插桩(构建时在字节码中加入观测代码)自有字节码中的文件入口、DAO 调用、事务方法Native、反射调用、已编译三方库和系统内部 I/O可控模块的低采样观测;升级 Android Gradle Plugin(AGP)时做字节码回归
StrictMode / BlockGuard经 libcore(Android Java 核心库)OS 层上报的线程磁盘读写,以及未缓冲 I/O、SQLite/Closable 泄漏信号三方 Native 代码直接调用 libc 的全部 I/O;精确字节数和设备耗时debug(调试版)、自动化测试、dogfood(团队内测);生产仅在评估成本后受控开启
Native interposition(函数替换)/ Hook命中的 libc(C 运行库接口)或目标库 I/O 符号,可覆盖部分 Java 与 Native 路径内联调用、直接 syscall(系统调用)、未命中符号、mmap(内存映射)后缺页、异步回写只向少量用户启用的专项灰度;必须按 API、ABI(应用二进制接口)、加载顺序和目标库验证
Android Debug Bridge(adb)+ Perfetto调度状态、频率、应用 trace、database atrace(数据库追踪事件),以及设备允许的 ftrace(内核追踪框架)、block(块设备请求)和 filemap(文件页缓存)数据没有启用或无权限的数据源;一次 trace 之外的长期分布实验室复现和开发设备诊断
ProfilingManager system trace(系统性能轨迹)Android 15+ 受系统限流的应用请求采集;Android 17 还可结合部分系统 trigger(触发条件)不保证每次请求成功,也不等同于任意 adb Perfetto 配置线上小范围诊断,版本细节见 26.6

StrictMode 能证明什么

Android 17 的 BlockGuardOsopenreadwritefsyncstatrename 等 libcore OS 操作前调用线程策略的 onReadFromDisk()onWriteToDisk()。因此 StrictMode 擅长回答“这个受监控线程是否触发磁盘操作”,它不测量存储设备完成一次请求的时间。

下面的配置用于 debug 或 dogfood 包,让主线程磁盘访问、未缓冲 I/O 和资源未关闭进入日志。它只是发现入口,生产策略应另行评估。

StrictMode.setThreadPolicy(
    StrictMode.ThreadPolicy.Builder()
        .detectDiskReads()
        .detectDiskWrites()
        .detectUnbufferedIo()
        .penaltyLog()
        .build()
)

StrictMode.setVmPolicy(
    StrictMode.VmPolicy.Builder()
        .detectLeakedSqlLiteObjects()
        .detectLeakedClosableObjects()
        .penaltyLog()
        .build()
)

setThreadPolicy() 作用于调用它的线程;若只在主线程安装,就不能据此声称覆盖全部工作线程。detectUnbufferedIo() 从 API 26 提供,detectLeakedSqlLiteObjects() 从 API 9 提供。detectResourceMismatches() 从 API 23 开始检查资源类型与读取方法是否匹配;它不检查文件句柄泄漏,也没有在 Android 17 新增。

CloseGuard.setReporter() 在 Android 17 仍标记为隐藏/System API,即普通应用没有稳定的公开调用契约。通过反射替换 reporter 会同时引入 non-SDK 限制和版本兼容风险;长期监控应优先使用公开的 StrictMode VM policy,并限制在测试与受控诊断场景。

Native Hook 的工程边界

Native Hook 属于未获 Android 平台兼容性保证的实现手段。即使某版能够拦截 openreadwriteclose,也要逐项验证:

  • 已加载和延迟加载的共享库是否都进入 Hook。
  • 32/64 位 ABI、不同 Android 版本和厂商链接器行为是否一致。
  • pread/pwritereadv/writevmmapsendfile、直接 syscall 是否在覆盖清单中。
  • Hook 内部是否有递归保护,采集和上报自身的 I/O 是否会再次触发 Hook。
  • 路径、fd 生命周期和调用栈采集是否线程安全,卸载或失败时能否回到原函数。
  • 远程关闭、采样和崩溃隔离是否在 Hook 初始化前可用。

Hook 覆盖不完整并不表示数据无用,但事件必须带 collectorcoverage_version。没有这两个字段,就无法区分业务回归与采集器升级造成的数量变化。

不良 I/O 规则模板

通用规则可以固定,阈值不能跨设备和场景照搬。闪存、文件系统、加密层、温控、后台压力和数据规模都会改变耗时。规则至少要按设备档、前后台、业务场景和 App 版本建立基线。

规则判断信号必要现场信息容易误判的情况
主线程 I/OStrictMode violation,或主线程文件调用与交互卡顿时间重叠生命周期阶段、调用点、操作类型、wall/CPU time启动框架内部一次性访问、采集器本身写日志
细碎 I/O单位字节对应的系统调用数偏高,连续窗口内多次短 read/write总字节数、调用数、buffer(缓冲区)、文件类型、调用点协议要求的小记录、设备已在页缓存中
重复读取同一文件版本和调用点在短业务窗口内重复读取,期间没有有效写入文件指纹、mtime(文件修改时间)/业务版本、调用点、场景文件被外部进程或 ContentProvider 更新
大目录遍历扫描耗时、访问文件数或主线程阻塞相对基线异常已扫描/总量估计、深度、剪枝原因、目录类别首次迁移、媒体导入、用户主动全量扫描
fd 数量持续增长/proc/self/fd 数量在稳定场景中持续上升,离开场景后不回落进程、场景、资源类型、StrictMode/CloseGuard 信号网络连接池、动态加载和诊断采集临时占用
同步写入放大单次业务提交对应多次 write/fsync,且交互时延同步升高事务、文件协议、WAL(write-ahead logging,预写式日志)状态、调用点明确要求持久性的关键数据

不要用 StatFs.getBlockSizeLong() 推导 Java buffer 的统一下限。文件系统块大小不等于存储设备页、内核合并粒度或应用最佳 buffer;小 buffer 是否有问题,应由调用次数、总字节数、CPU 开销和目标设备实验共同判断。

阈值的制定与维护可以遵循同一流程:在无回归版本采集分布,固定事件 scope 和分母;按设备档与场景建立基线;用对照版本差值和置信区间触发告警;检查样本量、采集覆盖与上报缺口;修复后验证同一分组是否恢复。业务有明确体验预算时,可以增加绝对上限,但要记录预算来源。

主线程磁盘访问可以在 debug 阶段按“发生即检查”处理,线上告警仍不应把每次访问等同于用户卡顿。调用发生时数据可能已在页缓存,耗时也可能短于一次帧预算;关联 FrameTimeline(帧时间线)、主线程 slice(带起止时间的轨迹片段)和用户操作后再确定优先级。

SQLite 耗时、损坏与查询计划

SQLite 可观测性应区分执行、事务、连接等待、错误和恢复。一个 DAO 方法慢,不代表 SQL 计划一定慢;它也可能在等待 writer(写连接)、调度到数据库线程,或在结果映射,也就是把查询行转换为业务对象的阶段消耗 CPU。

事件 scope起止位置可以解释什么不能直接解释什么
DAO end-to-end调用 DAO 到结果返回用户路径中的总等待SQLite 内部执行占比
transactionbegin 到 commit/rollback 返回锁持有窗口、批量写范围每条语句成本
driver statementprepare/step(预编译与逐步执行)到完成接近 SQL 执行和取数成本调用前排队与结果业务处理
query callbackRoom 在查询执行时通知 SQL 与 bind 参数SQL 文本、bind(绑定)参数和调用频率精确耗时;回调可能在另一个 Executor(任务执行器)上
exception/recovery捕获异常到恢复动作结束错误类别、数据可用性和恢复结果错误发生前所有磁盘状态

Room QueryCallback 无法直接测得慢查询耗时

RoomDatabase.QueryCallback#onQuery() 在查询执行时提供 SQL 和可用的绑定参数。官方文档明确提示它有少量额外成本,应按需启用,并避免在生产包中长期全量使用。回调本身没有开始时间和结束时间;在回调的 Executor 里计时,只会得到处理该回调所花的时间。

下面的片段只用于生成 SQL 指纹和参数类型,不保存参数值。实际工程还应限制队列长度并对回调自身做采样。

val callback = RoomDatabase.QueryCallback { sql, bindArgs ->
    queryObserver.record(
        sqlFingerprint = normalizer.fingerprint(sql),
        bindTypes = bindArgs.map { value -> value?.javaClass?.simpleName ?: "null" }
    )
}

Room.databaseBuilder(context, AppDatabase::class.java, "main.db")
    .setQueryCallback(callback, queryCallbackExecutor)
    .build()

SQL 文本也可能通过字符串拼接包含账号、搜索词或 URL,不能只丢弃 bindArgsnormalizer(归一化器)应先统一 SQL 的等价写法并删除敏感字面量,再计算受控指纹。若使用 DAO 外围计时,字段名应明确写成 dao_end_to_end,避免与 driver 执行耗时合并。

Android 17 系统框架内部有哪些诊断信息

SQLiteConnection.OperationLog 是 Android 17 系统框架的私有内部类。下面两项常量说明它保留 20 条最近操作,并把严格超过 2 秒的操作放入另一组长操作记录。

private static final int MAX_RECENT_OPERATIONS = 20;
private static final long LONG_OPERATION_THRESHOLD_MS = 2_000;

这段实现只服务于 framework 调试和 dump(诊断信息输出),普通 App 没有对应的采集接口。2 秒是 framework 内部的“long operation”分类值,不应复制成业务慢查询门槛。源码还保存最近 10 条长操作,并对相关日志做限速;“20 条最近操作”“10 条长操作”和累计长操作数是三个不同概念。

Android 17 的 SQLiteConnectionPool#getStatementCacheMissRate() 从各可用连接的 prepared statement cache(预编译语句缓存)命中与未命中次数计算 miss rate(未命中率),但该方法带 @hide。每个 framework connection 的默认 cache size 在 SQLiteDatabaseConfiguration 中是 25,公开 SQLiteDatabase#setMaxSqlCacheSize() 允许的上限是 100。应用不应通过隐藏方法读取 miss rate,也不应在没有内存与命中率证据时把 cache 调到上限。

开发设备可用 adb shell dumpsys meminfo <package> 查看 SQLite 的 cache hit、miss、cache size 和部分连接统计;Android 官方 SQLite 性能文档给出了该输出。Android 17 的 POOL STATS cache size 表示缓存中的预编译语句总数;较早版本的同名字段等于 hit 与 miss 之和,不能按缓存容量解读。该命令适合复现时观察,普通应用没有对应的免权限生产 API。

SQLiteDebug.shouldLogSlowQuery() 读取 db.log.slow_query_threshold 及 UID(应用进程的用户标识)后缀的系统属性,并且相关日志还受 SQLiteSlowQueries 日志标签控制。这个隐藏的 adb/系统调试开关不能作为普通 App 动态下发慢查询阈值的方案。官方另有 log.tag.SQLiteTime 的开发设备查询计时日志,两条机制也不要混为一个开关。

Android 17 仍提供 lookaside(SQLite 的小对象内存复用区)和 idle connection timeout(空闲连接超时)配置,但不能从“可配置”推导出“应在生产环境调整”。SQLiteDatabase.OpenParams.Builder#setIdleConnectionTimeout() 已废弃,文档明确警告重建连接会清除 per-connection PRAGMA,即每条连接上的 SQLite 运行时配置;系统又没有供 App 恢复这些状态的回调。setLookasideConfig(0, 0) 可以请求禁用 lookaside,但系统可能按设备选择不同值;只有在目标 driver、内存和查询基准都证明收益时才考虑修改。

查询计划应在同一 SQLite 实现上验证

下面的 SQL 用于检查筛选与排序是否得到合适的访问计划。占位符应在同一 schema 和具有代表性数据量的副本上绑定。

EXPLAIN QUERY PLAN
SELECT id, name
FROM message
WHERE conversation_id = ?
  AND create_time >= ?
ORDER BY create_time DESC
LIMIT 50;

输出中的 SCAN 表示扫描,SEARCH ... USING INDEX 表示使用索引;USE TEMP B-TREE 说明排序、分组或去重可能需要临时 B-tree(平衡树结构)。这些计划节点不能当作自动修复指令:小表扫描可能合理,索引会增加写入和空间成本,复合索引的列顺序还要结合过滤、排序和选择性判断。

Android 官方建议用目标设备的 adb shell sqlite3,因为不同 Android 版本使用的 SQLite revision(源码修订版)不同。使用 BundledSQLiteDriver 时,应用携带自己的 SQLite 实现,设备自带 sqlite3 可能与它不同;此时应使用同一 driver 和同一 schema(数据库结构定义)的测试工具执行计划。

错误、损坏和恢复必须分开

busy/locked 说明并发或锁状态,full 说明写入空间不足,IOERR 指向 I/O 路径,CORRUPT/NOTADB 才更接近数据库内容或格式问题。只按异常 message(消息文本)搜索 “corrupt” 容易把不同错误合并。

事件至少记录数据库别名、driver、库版本、异常类、主/扩展错误码(driver 提供时)、事务状态、WAL/SHM 是否存在及大小、剩余空间、恢复动作和恢复结果。SHM 是 WAL 模式供进程协调访问的共享内存文件。异常 message 也可能带 SQL 或路径,进入日志前要做脱敏。

完整 PRAGMA integrity_check 会读取大量页面,不适合在前台或每次启动执行。需要验证时,可在维护窗口、数据库副本或用户明确发起的诊断流程中运行,并记录检查覆盖。恢复策略应区分可再生缓存、可从服务器重建的数据和唯一用户数据;默认删除数据库会让“恢复成功”掩盖数据丢失。

存储容量与文件损坏指标

目录总大小只能说明空间被占用。诊断增长来源还需要文件数、年龄、类别和扫描覆盖率。

指标采集方式需要记录的边界
空间余量StatFs 获取目标卷可用字节和总字节记录卷与采集时间;不要只看全局百分比
私有目录大小filescachedatabasesno_backup 等受控类别分别扫描记录扫描是否完成、忽略项和符号链接策略
文件数与深度有预算的增量遍历记录已访问节点、下次继续扫描的位置和耗时,避免把部分结果当全量
大文件/目录只保留受控类别与 Top-K(数值最大的 K 项)统计K 由上报预算决定,不上传原始文件名
年龄分布按业务允许的时间区间分组(分桶)mtime 可能被迁移、恢复或外部写入改变
文件格式校验magic(固定文件头标识)、版本、长度、CRC(循环冗余校验)/hash(哈希摘要)或业务结构校验校验算法必须属于文件格式协议,不能给所有文件统一加 CRC
清理效果规则版本、候选字节、删除字节、失败原因、再次扫描结果只操作明确可再生且在 allowlist(允许清理的固定名单)内的文件

全量目录扫描本身会产生 I/O 和 CPU 开销。大目录应保存扫描游标,分批进行,并在前台交互、低电量或系统压力不合适时停止。事件中的 coverage(本次覆盖范围)、visited_count(已访问节点数)和 complete(扫描是否完整)与大小值同等重要。

“目录树剪枝”,即跳过不需要继续深入的分支,再配合受控样本,比上传完整路径更安全。每层保留聚合后的最大目录类别,其他节点归入 other;若业务必须关联同一对象,可以使用服务端持有密钥的 keyed digest(带密钥摘要),并设置密钥轮换周期。普通 SHA-256 截断并不能可靠匿名化可枚举的短路径。

业务文件损坏检测需要与写入协议一起设计。每种格式应定义 magic、版本、长度和校验范围;事件记录写入阶段、临时文件状态和恢复来源。对于应用私有的可替换文件,可使用 AtomicFile,或采用“先写临时文件,成功后以 rename 替换目标”的协议;数据库文件应交给 SQLite 的事务与 journal/WAL 日志机制,不要自行拼装或远程删除 .db-wal-shm 文件。

远程清理是破坏性能力,应采用固定 allowlist、规则版本、dry-run(只统计而不执行删除)、幂等执行(重复运行结果一致)和远程关闭。未知文件、唯一用户数据、正在打开的数据库文件与现有规则无法识别的目录都不能按“疑似缓存”处理。

上报、采样与隐私边界

I/O 和 SQL 都是高频事件,采集器必须在设计阶段设定 CPU、内存、磁盘、网络和隐私预算。

数据默认保留默认丢弃放大条件
文件操作类别、调用点、scope、耗时、字节、采集器版本原始绝对路径、文件内容同调用点持续回归,且远程策略仍满足预算
SQL归一化指纹、语句类别、表集合、参数类型、driverbind 值、未处理字面量、完整结果受限诊断会话;仍不得上传业务数据
目录快照分类大小、文件数、年龄桶、coverage完整目录树和文件名用户授权的问题诊断
错误类型、错误码、恢复状态、受控 message 指纹数据库内容、原始 SQL、账号路径严重数据不可用事件,按事件限速
调用栈应用帧指纹、mapping(混淆符号映射表)版本全量系统帧、线程局部变量调用点无法归因的专项灰度

采样单位要写清楚。按事件采样会偏向高频调用点;按 session(一次使用会话)或 device(设备)采样更适合估算用户影响;触发后采样适合收集异常现场信息。后台聚合不能把三种采样结果当作同一分母。

上报组件还需自监控:采集耗时、被采集事件数、采样后保留数、队列深度、本地缓存字节、压缩后大小、上传结果和丢弃原因。文件 Hook 与日志写入之间必须有递归保护;空间不足时诊断缓存应优先丢弃,不能与业务数据争用剩余空间。

隐私处理要尽可能靠近数据源。Room callback 收到 bind 参数后立即转成类型并丢弃值;路径在离开调用线程前转成类别;URL、账号、搜索词或 token(身份或访问凭据)不应进入标签、文件名和 SQL 指纹。哈希只减少明文暴露,不自动满足匿名化要求。

与现有章节的分工

  • 24.1 负责文件 I/O、SharedPreferences、线程与持久化策略的优化。
  • 24.2 负责 WAL、事务、索引、连接并发和 Room 使用方式。
  • 24.4 负责 MediaStore、分区存储(Scoped Storage)、用户态文件系统(FUSE)与媒体扫描成本。
  • 26.1 负责指标分位数、采样、上报预算和劣化检测。
  • 26.3 负责远程证据包、受限诊断、灰度隔离和问题单流程。
  • 26.6 负责 ProfilingManager 与 trigger 的系统版本边界。

这些事件字段用于把问题导航到对应章节,不重复给出另一套优化规则。

扩展:Matrix I/O Canary 与 SQLiteLint

Matrix 中的 I/O Canary 和 SQLiteLint 仍适合作为规则设计参考,但历史文档不能充当 Android 17 兼容性声明。

模块可借鉴内容现代工程必须重验的部分
I/O Canary从文件调用重建主线程 I/O、细碎访问、重复读和泄漏规则Hook 符号、链接器/ABI、mmap 缺口、采集递归、当前 AGP 与打包流程
SQLiteLint基于运行时 SQL、schema 和 EXPLAIN QUERY PLAN 检查索引及临时 B-tree 等问题2019 wiki 所述 sqlite3_profile Hook、Room/SQLiteDriver 覆盖、SQLite 新版计划文本和误报

SQLiteLint 的 wiki 明确说明其系统 SQLite 路径通过 Hook 向 C 层 sqlite3_profile 注册回调。该说明的上次编辑时间为 2019 年,不能推导出它在 API 37、所有 driver 或所有 ABI 上仍有相同覆盖。使用 BundledSQLiteDriver、WCDB(腾讯开源的移动数据库框架)或自带 SQLite 时,目标库可能与系统 framework 打开的 SQLite 实现不同。

接入评审至少要求:固定 Matrix revision(源码修订号);列出支持的 API/ABI/driver;在目标构建工具中运行启动、查询、并发、进程退出和崩溃测试;验证关闭开关;对采集前后 CPU、内存、I/O 与崩溃率做对照。达不到这些条件时,可以复用规则思想,在应用封装或 Room/driver 回调上重新实现。

自动分析 EXPLAIN QUERY PLAN 也要允许例外。小表的 SCAN、联接的外层扫描和一次性迁移不一定需要索引;看到 SEARCH 也不表示结果集、排序和根据索引记录再次访问原表(回表)的成本合格。规则输出应包含 schema 版本、表行数范围、计划文本和人工处置状态。

扩展:AndroidX SQLite/Room 新版本诊断能力

截至 2026 年 8 月 15 日,AndroidX SQLite 稳定版为 2.7.0,Room 稳定版为 2.8.4。它们是独立于 Android 17 / API 37 的库版本,Room 主版本也不能从系统版本或 AndroidX SQLite 版本推导。本文讨论稳定版能力,因此使用 Room 2.8.4,不把 SQLite 2.7.0 的版本号写成 Room 版本。

与观测直接相关的变化包括:

  • AndroidX SQLite 2.6.2 为 BundledSQLiteDriver 创建的连接启用 extended error codes(扩展错误码),并用 @FastNative 降低部分 JNI(Java 与本地代码调用接口)成本。
  • AndroidX SQLite 2.7.0 已发布,但是否采用应根据该版本发布说明、目标平台和回归测试决定,不能只因它更新而替换生产环境 driver。
  • Room 2.8.4 在使用内部没有连接池的 SQLiteDriver(例如 BundledSQLiteDriver)时,为 Room 连接池增加 prepared statement cache;它与 Android 17 framework connection 自带的 cache 分属两层。
  • RoomDatabase.QueryCallback 仍会为每条执行的查询触发回调,官方继续提示其运行成本。开启前应按实际查询量测试。

线上事件建议保留 room_versionandroidx_sqlite_version、driver 类型、SQLite 是否随应用打包、journal mode(日志模式)和 schema version(数据库结构版本)。更换系统 AndroidSQLiteDriverBundledSQLiteDriver 或其他实现后,性能基线和错误码能力都可能变化,时间序列应标记切换点。

扩展:Perfetto 文件系统 trace 与线上指标对照

Android 官方 SQLite 性能文档给出的 Perfetto 配置是在 linux.ftrace 数据源中加入 atrace_categories: "database",也就是启用 Android 的 database 追踪类别,用于显示单条查询轨迹。应用还应在业务入口放置受控 Trace slice,这样才能把 DAO、事务和页面场景与 database track(数据库轨迹)对齐。

线上信号开发设备 trace 证据可以排除或确认什么
主线程文件调用 wall time 高应用 slice、sched 调度状态、CPU freq(处理器频率)、相关 syscall/ftrace线程是在运行、runnable(可运行但等待调度)、锁等待还是疑似 I/O 阻塞
SQLite end-to-end 高DAO/transaction slice、database atrace、连接等待附近线程SQL 执行、调度和事务等待各占多少
write/fsync 集中文件调用 slice、writeback(页缓存回写)与 block(块设备)事件、设备状态写入是否到达 block 层,是否伴随较长设备请求
读取慢但 block 事件少filemap/page fault(文件页缺页)、sched、CPU 和应用处理数据可能来自页缓存,或耗时发生在应用代码/锁等待
目录扫描影响交互扫描 slice、主线程、Binder 跨进程调用、GC(垃圾回收)、文件系统事件遍历、对象分配和系统调用哪个阶段占主导

可核对的内核源码 block.h 定义了 block_rq_issueblock_rq_complete 等块设备请求事件;filemap.hwriteback.h 提供缺页与回写相关事件。设备是否开放这些事件取决于内核配置、trace 配置和权限,不能假设所有量产机都可采。

应用调用与 block request(块设备请求)没有一一对应关系。页缓存可能直接满足 read,延迟回写会让 write 与 block I/O 分离,多个请求还可能合并;文件系统、dm-crypt(Linux 块设备透明加密层)和存储驱动又会增加层次。trace 用于建立时序证据,无需按相同时间戳强行配对每个 Java 调用和每个 block event。

Android 15+ 的 ProfilingManager 可以请求受控 system trace,Android 17 的 trigger 能覆盖更多系统事件,但请求受系统限流且可能没有产物。线上应上传采集请求、结果状态和 trace 关联 ID;原始 trace 的合规、保留和访问控制沿用 26.6,不在存储事件里额外保存一份。

全文小结

存储可观测性的产物应是一组定义清楚的证据:文件调用 scope、SQLite 执行层次、空间快照覆盖率、文件格式校验、资源生命周期和采集器自身成本。Android 17 framework 内部的 OperationLog、cache 统计和慢查询属性能帮助理解平台诊断,但其中多项是 private 或 @hide,不能直接变成普通 App API。

线上用公开边界和低成本字段发现分布,开发设备再用同一 SQLite 实现、EXPLAIN QUERY PLANdumpsys meminfo 和 Perfetto 验证原因。这样既保留源码依据,也不会让诊断工具依赖未经平台保证的实现细节。

参考资料