ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Android Settings值被改?dumpsys+Binder日志定位写入者实战

Android Settings值被改?dumpsys+Binder日志定位写入者实战 做系统定制和兼容性测试的朋友应该都遇到过这种场景某一天你打开adb shell settings get system一看某个值已经不是昨天设置的值了。代码里搜一圈 write/put 的调用点一个都不在触发路径上。settings 不像普通文件有 inotify也不像网络请求有现成的抓包入口它是一笔走 Binder 进到 system_server 之后写进 XML 的账。这篇文章把我排查 settings 被谁更新时用到的 dumpsys 命令和配套思路完整拆开从怎么读输出、怎么定位进程到 root 环境下怎么拿实锤适合做系统级调试、ROM 定制、自动化测试的 Android 工程师参考。1. 先搞清楚你追的“settings”到底是一个什么东西1.1 settings 家族三兄弟system、secure、globalAndroid 的 SettingsProvider 对外提供三类设置项如果你不先分清这三类后面查起来很容易被误导。Settings.System最普通的系统设置比如屏幕亮度、音量、飞行模式开关。第三方 App 只要声明WRITE_SETTINGS权限就能改这也是大多数“乱改设置”问题的来源。Settings.Secure安全类设置比如用户锁屏、定位开关、无障碍状态。调用方必须持有WRITE_SECURE_SETTINGS这个权限一般只授予系统应用和 adb shell。Settings.Global全局设置跨用户共享同样只有系统应用和 shell 能改。这三类设置最终落在/data/system/users/0/下的settings_system.xml、settings_secure.xml、settings_global.xml。Android 7.0 之前是 SQLite之后改成了 XML 文件每次写入都会触发一次落盘。这个信息对你后续定位很有用因为只要监控这几个文件的变化时间点再对应到系统日志和 Binder 踪迹就能把时间窗口缩到很小。1.2 一次更新请求的完整调用链你看到的变化只是表象真正要追的是一条跨进程调用链。以改亮度为例// App 进程 Settings.System.putInt(context.getContentResolver(), Settings.System.SCREEN_BRIGHTNESS, 128);这段代码会通过 ContentResolver 发起一次update调用内部走IContentProvider的 Binder 接口把请求跨进程发给运行在 system_server 里的 SettingsProvider。SettingsProvider 更新内存中的 SettingsState再调用持久化逻辑把新值写进 XML。整个过程发生在三处App 进程里发起 Binder 调用的调用线程system_server 进程里对应 Binder 线程也就是真正执行 update 的地方文件系统层面settings_system.xml 的修改时间戳注意第二处很关键即使 App 进程被杀system_server 里那次更新已经完成了。所以你用ps -A | grep 包名看进程是否存在来判断“谁干的”经常不靠谱因为写入者可能在写入后立刻退出。1.3 为什么常规手段查不到写入者平时排查普通文件还能看看 owner、inotify但 settings 的问题在于跨进程调用只暴露给 ContentProvider 接口Provider 内部不会自动记录“哪个包调用了 update”dumpsys settings默认只输出当前值、默认值、以及注册的观察者不会输出历史上谁写过Binder 调用本身可以通过 transaction log 看到但默认系统不开这个日志而且输出极其庞大所以要想定位必须组合使用“日志 进程观察 调用追踪”三件套。下面逐个说。2. dumpsys 命令家族能提供哪些线索2.1 dumpsys settings先看现场再找观察者第一件事永远是把当前状态完整拉出来命令就是adb shell dumpsys settings输出大致包含三类服务器的当前值以及末尾的注册观察者列表。不同 Android 版本格式有些差异但核心字段思路一致。我拿一个简化后的片段说明SETTINGS_SERVICE (settings) User 0: system ... screen_brightness128 ... User 0: secure ... User 0: global ... Registered observers: Observer 0: uricontent://settings/system/screen_brightness pid1234 uid10036这个列表里的 pid 和 uid 是“对该 uri 注册了 ContentObserver 的进程”而不是写入者。但是它的价值非常大正常情况一个设置项不会有一堆观察者如果某个第三方 App 对亮度、音量这类 key 注册了 observer那它大概率在实时监控并可能回写这个值。拿到 pid 之后立刻用adb shell ps -A | grep pid或者更直接的方式确认对应进程adb shell cat /proc/pid/cmdline这样就能把观察者列表变成一张“嫌疑进程表”。2.2 dumpsys activity providers查 ContentProvider 的进程绑定关系SettingsProvider 本身也是个 ContentProvider所以可以用dumpsys activity providers查它的实例状态。这个命令能看到哪些进程当前持有 SettingsProvider 的客户端引用。adb shell dumpsys activity providers | grep -A 50 com.android.providers.settings/.SettingsProvider输出里一般会有clients:列表列出当前绑定到这个 Provider 的进程记录。如果一个进程正在监听或持有 cursor它会出现在这里。但这里的“绑定”不代表“写入”。它只能帮你确认当前进程集合里谁和 settings provider 有活跃连接结合 2.1 的观察者列表交叉比对基本能锁定重点排查对象。2.3 辅助命令dumpsys package 与 dumpsys activity processes光知道 pid 还不够我们通常还要确认这个 pid 对应的包名、uid、以及 AppOps 状态。组合命令如下# 查看 pid 归属 adb shell ps -A | grep pid # 查看 uid 对应的包名 adb shell dumpsys package | grep -A 5 userIduid # 查看这个进程当前在干什么前台 Activity 是否相关 adb shell dumpsys activity processes | grep -B 3 -A 10 pidpid经验之谈很多“偷改设置”的进程不会一直活着而是被别的 App 调起后再改。所以 pid 是动态的你要尽量在复现窗口内多次抓取不要一次性抓完就下结论。3. 实战从“值被改”到“进程落地”的四板斧3.1 第一板斧建立状态基线做 before/after 差分追查任何一次设置变更第一步永远是确认变化范围和变化时间。不要凭记忆觉得“亮度变了”要量化。我建议在还没开始操作前先导出一份 settings 快照# before adb shell settings list system /tmp/settings_system_before.txt adb shell settings list secure /tmp/settings_secure_before.txt adb shell settings list global /tmp/settings_global_before.txt # after 复现后 adb shell settings list system /tmp/settings_system_after.txt # diff diff /tmp/settings_system_before.txt /tmp/settings_system_after.txt这一步看起来简单但很多新人会跳过。没有差分后面你连到底改了哪个 key 都不知道更别提定位进程了。同时要看 XML 文件的修改时间adb shell ls -l /data/system/users/0/settings_system.xml如果时间戳和复现时间吻合说明这次变更确实通过 SettingsProvider 正常落盘了。如果时间戳没变但值变了那说明可能是内存态被别的方式干扰思路又要调整。绝大多数情况是前者。3.2 第二板斧写一个最小 ContentObserver 监听器缩小时间窗口dumpsys 只能看静态快照如果要动态捕捉写入动作必须借助 ContentObserver 监听变化。你不需要特殊权限普通 App 就能监听 settings 的 uri 变化。核心代码非常简单public class SettingsSpy extends Application { Override public void onCreate() { super.onCreate(); ContentResolver resolver getContentResolver(); Uri target Settings.System.getUriFor(Settings.System.SCREEN_BRIGHTNESS); resolver.registerContentObserver(target, false, new ContentObserver(new Handler(Looper.getMainLooper())) { Override public void onChange(boolean selfChange, Uri uri) { super.onChange(selfChange, uri); long now System.currentTimeMillis(); Log.w(SettingsSpy, onChange: uri uri time now bright Settings.System.getInt( getContentResolver(), Settings.System.SCREEN_BRIGHTNESS, -1)); } }); } }跑起来之后让测试环境按原路径复现问题。observer 的 logcat 时间戳会精确告诉你“哪个时间点 settings 变了”。然后你拿着这个时间点去看系统 logcat 里这个时间点附近有哪些进程在活动dumpsys activity processes里哪个 App 刚好是前台或者刚被拉起如果这台机器有 root还可以看/proc/pid/stack或 netlink 审计日志这板斧的局限是拿不到写入者的 pid但它能极大缩小排查窗口。我实测下来大多数场景到这一步就能从 logcat 里看到某第三方 App 的 Binder 调用痕迹了。3.3 第三板斧root 设备上开 Binder transaction 日志拿直接实锤如果你的测试机能 root就能从 Binder 层直接看到“哪个进程在某一时刻调用了 SettingsProvider”。这算是比较硬的证据。Android 内核的 binder 驱动提供了 transaction log前提是内核开启相关配置而且 debugfs/binderfs 已挂载。不同内核路径略有差异常见的是adb root adb shell cat /sys/kernel/debug/binder/transaction_log 2/dev/null如果上面路径不存在可以试试 binderfs 挂载后的路径adb shell ls /dev/binderfs/binder_logs/ 2/dev/nulltransaction_log 恰恰会把发起进程、目标进程、code 打印出来。你需要做的是先清掉日志再复现问题最后 grep 出与 SettingsProvider 相关的条目。adb shell cat /sys/kernel/debug/binder/transaction_log | grep -E settings|provider这个方案的痛点是日志量巨大几十秒就能刷出几万行。我自己的做法是先加时间过滤记住复现的时间点然后只 grep 那个时间窗口的内容避免被无关 Binder 事务淹没。不过要提醒一句不是所有 root 机都能看到 binder 日志厂商内核经常把 debugfs 关掉。如果你遇到的机器看不到别死磕直接跳第四板斧。3.4 第四板斧定制 ROM 插桩 SettingsProvider打印调用栈如果你能拿到系统签名或者本来就在做 ROM 定制最直接的办法是在 SettingsProvider 里插桩把调用者的 pid、uid、包名、调用栈全打出来。改动位置在packages/providers/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java在 update、insert、delete、bulkInsert 等方法入口加一段临时日志。一种不会丢信息的写法Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { int callingUid Binder.getCallingUid(); int callingPid Binder.getCallingPid(); String caller getCallingPackage() ! null ? getCallingPackage() : unknown; Log.w(SettingsDebug, String.format(update uri%s uid%d pid%d caller%s, uri, callingUid, callingPid, caller), new Throwable(caller stack)); return super.update(uri, values, selection, selectionArgs); }new Throwable(caller stack)比Log.getStackTraceString更省事它会自动把当前线程的完整 Java 栈打进 logcat。因为 SettingsProvider 运行在 system_server 里这个栈会显示 Binder 线程的调用来源。注意 getCallingPackage() 只对 ContentResolver 通过 Binder 传来的 package 有效在某些 Provider 代理场景下可能返回 null。如果拿不到就靠 uid 反查adb shell dumpsys package | grep -B 2 -A 5 userId10123我当时遇到过最恶心的情况是uid 拿到了但 uid 是 sharedUserId 的框架进程根本看不出具体哪个 APK。这时候需要在 Application 初始化时给每个包做 uid 到包名的双向映射再在日志里输出。插桩后杀掉 system_server 让它重启或者直接刷机重启复现一次logcat 里就会直接出现W SettingsDebug: update uricontent://settings/system/screen_brightness uid10036 pid5123 callercom.example.batteryapp W SettingsDebug: java.lang.Throwable: caller stack at com.android.providers.settings.SettingsProvider.update(SettingsProvider.java:...) at android.content.ContentProvider$Transport.update(...) ...这一条日志就是实锤。4. 排查过程中最容易踩的坑4.1 把“监听者”当成“写入者”这是最常见的误判。ContentObserver 的注册列表里出现的进程只代表它在观察 key 的变化不代表它改了值。有些 App 是为 UI 刷新才注册 observer。如果你根据 2.1 的 dumpsys 输出就开枪很容易冤杀。我的判断标准是观察者列表只用来建立嫌疑面真正的实锤必须靠 Binder 日志或插桩日志。在没有实锤前所有嫌疑对象一律标注“待验证”。4.2 dumpsys 输出格式随版本变化别背模板Android 8 到 Android 14 的 dumpsys settings 输出差异非常大。早期版本有 SettingsProvider 内部状态后来改成了 SettingsService dump。不同厂商定制 ROM 也可能增加额外段落。所以我不建议背一个固定模板而是抓住几个常量字段Registered observers、User 0、No such setting这类。只要看到观察者列表核心线索就有了。4.3 高频写入下的日志风暴某些设置项会被系统服务频繁刷新比如亮度、电池温度。如果你插桩了 update并且打印了完整调用栈产生日志的速率可能达到每秒几十条。这时候 logcat 的 ring buffer 很快被冲掉真正要抓的那条反而丢了。对策很简单给日志加条件只打目标 key。if (uri ! null uri.toString().contains(screen_brightness)) { Log.w(SettingsDebug, update ..., new Throwable(caller stack)); }另外建议把 logcat 直接落到本地文件防止 ring buffer 覆盖adb logcat -s SettingsDebug -f /data/local/tmp/settings_debug.log 4.4 system_server 内部写入Binder 日志看不到调用者最后一种坑比较隐蔽有一部分 settings 的更新不是外部进程发起的而是 system_server 内部其他系统服务直接通过SettingsProvider的本地引用调的。例如某些电源策略、网络检测服务会自己写 global 设置。这种场景下 Binder transaction log 根本不会出现对应记录因为请求根本没有跨进程。插桩打印调用栈还是能看到因为栈上会有来自其他系统服务的调用入口只是 uid 全是 system1000。你需要根据栈里的系统服务名再去定位具体模块不能简单说“系统改的”就完事要继续看栈。5. 完整案例复盘谁把我的亮度悄悄拉满下面是一个真实测过的场景我用它说明整套排查流程怎么串起来。设备Android 11用户反馈手机用一段时间后屏幕亮度自动变成 100 复现步骤清后台后锁屏等待约 5 分钟后亮度拉满第一步做差分。先导一份 before 快照锁屏等 5 分钟再导 afterdiff 结果锁定 keyscreen_brightness。再看 XML 文件时间戳确认写入发生在锁屏后 4 分 37 秒。第二步看图 2.1 的观察者列表。dumpsys settings 显示有 pid2310 的进程注册了 screen_brightness observer。用/proc/2310/cmdline查出是com.example.batterysaver一个省电类应用。第三步用最小 ContentObserver 监听器重新复现logcat 打出变化时间点附近有一条来自该应用的 Binder 调用痕迹但还不能完全确定它就是写入者。第四步root 后用 binder transaction_log grep screen_brightness 附近的内容看到发起进程 pid2310 调用目标 system_servercode 是 UPDATE。到这里基本确认写入者是它。第五步为了写进问题报告我拉了dumpsys package com.example.batterysaver确认它有WRITE_SETTINGS权限并且通过插桩日志拿到了完整调用栈证明它是通过 ContentResolver.update 写入的。结论省电应用在特定场景下自作主张调整系统亮度而且没有把权限请求流程做完整。这个案子如果只靠 dumpsys settings 根本查不到但通过“快照差分 观察者列表 动态监听 Binder 日志”四步就能锁定。6. 把这套排查能力自动化减轻重复劳动6.1 写一个循环抓 dumpsys 的脚本手动敲命令多了就会想偷懒。我后来写了个小脚本放到 PC 上跑定时抓取 settings 状态并做 diff#!/system/bin/sh KEYscreen_brightness BEFORE$(settings get system $KEY) while true; do sleep 2 AFTER$(settings get system $KEY) if [ $BEFORE ! $AFTER ]; then echo $(date) value changed: $BEFORE - $AFTER /data/local/tmp/settings_change.log ps -A /data/local/tmp/settings_change.log dumpsys settings /data/local/tmp/settings_change.log BEFORE$AFTER fi done这种脚本适合在复现环境内长时间挂机尤其是问题间隔不固定的场景。日志会自动记录 change 发生的时刻和当时进程列表比人眼盯着 logcat 强得多。6.2 在 SettingsProvider 里做一个可控开关如果 ROM 是你自己维护的可以把插桩日志做成动态开关避免平时刷屏。我通常的做法是在 SettingsProvider 里读一个Settings.Global.SETTINGS_DEBUG开关只有打开时才打印详细调用栈boolean debug android.provider.Settings.Global.getInt( resolver, settings_debug, 0) 1; if (debug uri.toString().contains(targetKey)) { Log.w(SettingsDebug, update ..., new Throwable(caller stack)); }这样线上/测试环境里平时零开销需要排查时一条命令就能开启adb shell settings put global settings_debug 1排查完再关掉。这套做法已经在我手上处理过至少三起同类问题每次都能快速定位到具体调用方省掉了“猜 App”的过程。6.3 结合自动化测试做回归拦截如果你有 UI 自动化测试框架还可以把“settings 关键值不被任意第三方改动”变成一条回归用例。做法是自动化脚本启动某个可疑 App 的完整流程然后比对核心 settings key 是否发生变化。只要发现变化就自动抓dumpsys settings和进程快照作为 bug 证据自动上传。这套体系建立后再遇到这类问题就是测试自动报单不需要人肉复现了。我个人在实际排查中最大的体会是不要迷信某一个命令能一步到位给出答案。dumpsys 告诉你的永远是一个“快照”binder 日志告诉你的是“瞬时动作”插桩日志告诉你的才是“实锤证据”。正确顺序永远是先缩小时间窗口再交叉比对进程关系最后才上重型方案。大多数问题到 ContentObserver 这一步就能破案真正需要动 Binder 日志的情况远没有想象中多。先把这几板斧练熟再复杂的 settings“迷案”也会变成一条清晰的 logcat 记录。
返回列表