调试 Android 应用的时候,最常卡住的往往不是代码,而是一个看起来特别基础的问题:这东西到底装在哪。屏幕上跑着一个应用,你只有一个进程号;日志里冒出来一个 content:// 开头的 URI,你只有一串 authority;或者你手上就是一个包名,但你不知道它在设备上的真实安装路径。Android 应用的安装目录、包名、安装路径这三件事,单拎出来都不算难,可真在真机上敲命令的时候,坑一个接一个。Android 8.0 把/data/app下面的目录名整个随机化了,Android 11 又把Android/data上了锁,再加上各家 ROM 对 shell 权限的收紧,很多几年前还能用的命令现在直接吐空。
这篇东西我按自己的排查习惯来写,从目录体系的底层结构讲起,再到怎么查正在运行的应用包名、怎么用包名反查安装路径,最后给一份完整的分步实操和一张排查速查表。适合的人群挺广:做 Android 开发要定位自己应用的私有目录、做数据迁移要找到旧机上的残留文件、做自动化测试要动态拿当前前台包名,都能直接抄作业。文中所有命令都在真机 + adb shell 的组合下验证过,涉及权限受限的部分我会明确标注,不藏着。
1. 先把 Android 的安装目录体系摸清楚
1.1 系统应用与第三方应用:两条完全不同的落点路径
Android 的安装目录不是一个目录,是一整套按分区划分的落点体系。你得先分清一个应用属于哪一类,才能知道该往哪儿找。系统应用一般躺在/system/app、/system/priv-app这几个目录下,其中priv-app里的应用可以申请到 privileged 级别的权限,这是app目录拿不到的待遇。除此之外,Android 8.0 之后为了支持 OEM 定制和 SoC 厂商的驱动/框架集成,又增加了/product/app、/vendor/app、/system_ext/app这几个分区目录。到了 Android 10 以后,还有/apex目录承载 APEX 格式的模块化系统组件。
这些目录全部是只读的。它们跟着系统镜像一起烧写进去,挂载方式决定了你在正常状态下根本改不动它们——/system、/product、/vendor挂的是只读的镜像文件,你就算拿到 root,直接rm也会报 read-only file system,得先重新挂载成可写才行。这也是为什么系统应用的升级通常不是原地覆盖,而是在/data/app里放一份新版本,然后用 dm-verity 或者 fs-verity 做完整性校验。
第三方应用就不一样了,它们统一装在/data/app下。/data分区是可读写的独立分区,OTA 升级系统的时候这个分区会被保留,所以用户装的应用不会因为系统更新而丢失。这个设计其实挺关键:早期 Android 把用户数据和应用都放在同一个可写分区,升级时需要做复杂的备份恢复,后来分区分明之后,升级逻辑一下子就清爽了。
怎么快速判断一个包是系统应用还是第三方?两条命令就够了:
adb shell pm list packages -s # 只列系统应用 adb shell pm list packages -3 # 只列第三方应用-s是 system,-3是 third-party。这两个参数我建议常备,尤其在需要批量筛选的时候,先用-3把用户自己装的挑出来,后面的排查范围会小很多。顺带说一句,有些内置应用在部分定制 ROM 上会同时存在两个来源,-s和-3都能查到,这种情况说明系统内置了一份,用户又从应用商店更新过一份,实际生效的是/data/app里那份,优先级更高。
1.2 Android 8.0 之后 /data/app 目录名为什么变成了一串乱码
如果你翻过老教程,大概率见过这种路径写法:/data/app/com.example.app-1/base.apk。这是 Android 7.0 及以前的规则,结构非常规整——固定前缀加包名,再加一个递增的数字后缀表示第几次安装。开发者可以直接把包名拼进去拿到路径,甚至有一批工具就是靠这个硬编码逻辑做文件管理的。
Android 8.0 把这套规则推翻了。现在/data/app底下的目录长这样:
/data/app/~~k83jQwXm-Tg==/com.example.app-1A2b3C==/base.apk多了一级~~开头的随机目录,包名后面也多了一段随机串。这个改动有两个直接目的。第一是防止路径猜测:以前的应用可以算出其他应用的安装目录,进而做出一些不该做的事,随机化之后这条路基本堵死了。第二是支持多实例共存:同一个应用在同一个设备上可能有多个副本,比如工作资料里装了一份、应用克隆又复制了一份,随机串保证了它们的目录不会互相打架。
这个改动对做逆向和自动化的人来说是致命的。所有"用包名拼路径"的老代码,在 Android 8.0 以上的设备上全部失效,唯一可靠的办法就是走系统提供的查询接口——也就是后面要重点讲的pm path。我见过不止一个人在这上面浪费半天,明明包名没写错,ls /data/app/com.xxx就是不存在的,其实是规则变了。
顺便提一下目录里那两处随机串的形态:它们都是 base64 编码的随机字节,所以结尾经常会看到==或者单个=的填充符号。这也就意味着你不能用正则去匹配"包名 + 数字"这种模式,老老实实查吧。
1.3 除了 APK 本体,还有四个目录值得记在备忘录里
很多人只关心 APK 装在哪,但实际排查问题的时候,APK 本体只是入口,真正要动的东西在别处。我把这几类目录整理成一张表,方便对照:
| 目录路径 | 存放内容 | 访问门槛 |
|---|---|---|
/data/app/~~xxx==/pkg-xxx==/ | APK 本体、split APK、oat 编译产物 | pm path可直接查,直接读需 root |
/data/data/<pkg>/或/data/user/0/<pkg>/ | 内部私有数据:databases、shared_prefs、files、cache | 需 root 或run-as |
/data/user_de/0/<pkg>/ | 设备加密(Direct Boot)存储目录,Android 7.0 起 | 需 root |
/storage/emulated/0/Android/data/<pkg>/ | 外部存储私有目录,常见 files 和 cache | Android 11 起受限 |
/storage/emulated/0/Android/media/<pkg>/ | 媒体私有目录,Android 11 起的替代方案 | 通常可读 |
/data/system/packages.xml、packages.list | 系统记录的完整安装信息 | 需 root |
这里有个容易搞混的点:/data/data/<pkg>和/data/user/0/<pkg>其实是同一个东西。/data/data是一个软链接,指向/data/user/0。做多用户支持之后,Android 把每个用户的数据目录拆成了/data/user/<user_id>,0 就是机主,10 之后一般是工作资料或者其他分身。所以你在排查的时候如果发现/data/data下面找不到东西,先确认一下是不是正在看另一个用户的空间。
/data/user_de/0/<pkg>这个 DE 目录也值得单独记一下。Android 7.0 引入 Direct Boot 之后,一部分应用需要在用户还没解锁屏幕之前就能运行(比如闹钟、来电界面),这时候主数据目录还处于加密不可读状态,所以系统额外提供了一份可以在设备加密阶段访问的存储空间。如果你的应用写了开机自启相关的逻辑,数据可能被写进了 DE 目录。
2. 查询当前正在运行的应用包名
2.1 dumpsys 三件套:activity、window、package 各自的取用场景
手上没有包名、只知道屏幕上跑着某个应用的时候,dumpsys是最稳的入口。这里有三条命令,用途各有侧重,我按命中率从高到低排一下。
第一条是查当前焦点窗口:
adb shell dumpsys window | grep -E "mCurrentFocus|mFocusedApp"输出长这样:
mCurrentFocus=Window{8a3f21 u0 com.example.app/com.example.app.ui.MainActivity} mFocusedApp=ActivityRecord{... u0 com.example.app/.ui.MainActivity t1053}从com.example.app/com.example.app.ui.MainActivity里,斜杠前面那段就是包名。这是我最常用的一条,因为它反映的是"用户此刻眼睛看着的界面",语义最直接。要注意不同 Android 版本的输出字段名会有差异,Android 10 之前用dumpsys window displays更稳,11 之后直接dumpsys window就行。
第二条是查 Activity 栈顶:
adb shell dumpsys activity activities | grep -E "mResumedActivity|topResumedActivity"输出里的ActivityRecord{... u0 com.example.app/.ui.MainActivity t1053}同样能在斜杠前切出包名。这条的优势是能看到t1053这样的任务栈 ID,做多任务排查的时候有用。
第三条更简洁,直接看栈顶那条记录:
adb shell dumpsys activity top | grep ACTIVITY输出是ACTIVITY com.example.app/.ui.MainActivity pid=12345。这条命令的好处是连带把 pid 也给你了,省得再去查一次进程号,做进程级别的分析时特别顺手。
如果上面三条都拿不到(比如当前停在桌面或者系统弹窗上),可以退回查最近任务:
adb shell dumpsys activity recents | grep -E "Recent #0|baseIntent"Recent #0就是最近一个被打开过的任务,baseIntent里会带出包名。这个方法在自动化脚本里比查焦点更稳定,因为焦点窗口可能是输入法、可能是系统权限弹窗,会干扰判断,而最近任务列表相对干净。
2.2 ps 和 pidof 在高版本 Android 上的权限陷阱
ps这条命令看着最简单,实际上坑最多。Android 7.0 之后引入了一套更严格的进程可见性策略,普通应用只能看到自己的进程,用代码调ps拿到的是一个残缺列表。但adb shell是以 shell 用户身份运行的,shell 用户被授予了更高的权限,所以从电脑上敲adb shell ps通常还是能看到完整列表。这个区别让很多人产生了错觉,以为代码里也能这么干。
另外ps的实现从 Android 8.0 起换成了 toybox 版本,参数语法和以前的 busybox 版本不一样:
adb shell ps -A # 列出所有进程,含系统进程 adb shell ps -A -o PID,NAME # 只输出 PID 和进程名 adb shell ps -ef # 完整格式老设备上ps直接输出全部,ps -A反而会报错,写脚本的时候得做兼容判断。
比ps更好用的是pidof:
adb shell pidof com.example.app直接吐出一个或多个 pid,多个说明是分进程架构。拿到 pid 之后可以进一步挖:
adb shell cat /proc/12345/cmdline adb shell cat /proc/12345/commcmdline里通常是包名(有些应用会改,比如加了:remote后缀的独立进程),comm是进程的短名,最多 15 个字符,被截断的情况下用cmdline更准。如果cmdline读出来是空的(这种情况在极端受限的 ROM 上会出现),那就只能靠dumpsys了。
注意:Android 12 以后,部分系统进程和 isolated 进程的
/proc/<pid>目录对 shell 用户也不完全开放,cat会报 permission denied。这不代表命令写错了,是系统策略收紧,遇到这种情况直接换dumpsys路线。
2.3 从端口号或进程号往回倒推包名
有些场景下你手上既没有包名也没有界面,只有一个网络端口或者一个 uid。比如抓包时看到某个本地端口在通信,想找出是哪个应用;或者从日志里看到一个 uid,想知道它对应谁。
从端口推应用,第一步是查/proc/net/tcp:
adb shell cat /proc/net/tcp输出里本地地址那一列是十六进制的IP:PORT格式,端口需要倒过来读——比如1F90对应的十进制是 8080。这一行里还有一个uid字段,这就是突破口。拿到 uid 之后,反查包名有两条路:
# 路线一:遍历 pm 输出里的 uid adb shell pm list packages -U | grep "uid:10123" # 路线二:有 root 的情况下直读 adb shell su -c "cat /data/system/packages.list | grep 10123"pm list packages -U会把每个包的 uid 一起输出,格式是package:com.example.app uid:10123,grep 一下就行。这条路在 Android 11 之后可能会因为权限问题输出为空,那就需要 root 或者改走dumpsys路线。
如果起点是 pid 而不是端口,除了cmdline,还有一条少有人用的路——看进程打开的文件描述符:
adb shell ls -l /proc/12345/fd输出里会带出这个进程正在读写的文件路径,从路径反推包名非常直接,因为私有数据目录里必然包含包名。这个方法在分析某些动态加载的应用时特别好用,能直接看出它在读哪个配置文件。
3. 用包名反查安装路径的四种手段
3.1 pm path:一行命令解决 90% 的场景
有包名之后,查安装路径最正统的做法就是pm path:
adb shell pm path com.example.app输出:
package:/data/app/~~k83jQwXm-Tg==/com.example.app-1A2b3C==/base.apk如果应用用了 Split APK(Android 5.0 引入的拆分机制,把资源、so 库、不同架构的代码分成多个 APK),输出会是多行:
package:/data/app/~~k83jQwXm-Tg==/com.example.app-1A2b3C==/base.apk package:/data/app/~~k83jQwXm-Tg==/com.example.app-1A2b3C==/split_config.arm64_v8a.apk package:/data/app/~~k83jQwXm-Tg==/com.example.app-1A2b3C==/split_config.xxhdpi.apk package:/data/app/~~k83jQwXm-Tg==/com.example.app-1A2b3C==/split_config.zh.apk全部 APK 都在同一个目录里,所以只要拿第一行做dirname,就得到安装目录:
adb shell pm path com.example.app | head -n 1 | sed 's/package://;s/\/base.apk//'pm path有一个升级版写法,在 Android 8.0 之后推荐用cmd接口:
adb shell cmd package path com.example.app两者结果一致,但cmd package的定位更明确,因为在 Android 10 之后pm工具本身也变成了cmd package的一个包装脚本。另外别忘了--user参数,多用户设备上不指定用户可能会查到空:
adb shell pm path --user 0 com.example.app adb shell pm path --user 10 com.example.app3.2 pm list packages -f 与 dumpsys package 的字段对照
不想一个包一个包查的时候,pm list packages -f可以一次把所有应用的路径倒出来:
adb shell pm list packages -f | grep example输出格式是package:完整APK路径=包名,跟pm path的区别在于它是全量列表,适合批量处理或者喂给脚本。常用参数我整理成一张表:
| 参数 | 含义 | 使用场景 |
|---|---|---|
-f | 同时输出 APK 路径 | 批量建立包名到路径的映射 |
-s | 只列系统应用 | 排查预装软件 |
-3 | 只列第三方应用 | 缩小排查范围 |
-d | 只列已禁用的包 | 应用被禁用后默认不显示 |
-e | 只列已启用的包 | 与-d互补 |
-u | 包含已卸载但保留数据的包 | 卸载残留排查 |
-U | 附带输出 uid | 从 uid 反查包名 |
--user <id> | 指定用户空间 | 多用户/应用分身设备 |
另一个更全面的信息源是dumpsys package:
adb shell dumpsys package com.example.app | grep -E "codePath|resourcePath|dataDir|legacyNativeLibraryDir|primaryCpuAbi|versionName|userId"关键字段的含义我逐个说明。codePath是 APK 的安装位置,等价于pm path的结果;resourcePath通常是同一个目录,但某些 ROM 上资源被单独拆到别处;dataDir是应用私有数据目录,一般显示为/data/user/0/com.example.app;legacyNativeLibraryDir是旧版 native 库查找路径,Android 10 之后一般指向/data/app/~~xxx==/pkg-xxx==/lib/arm64;primaryCpuAbi告诉你这个应用主用哪个 ABI,排查 so 加载失败的时候必看。
dumpsys package的输出通常有上千行,全读会很痛苦,建议先 grep 出关键字段,再根据需要展开某个具体段落。我一般会把输出重定向到文件:
adb shell dumpsys package com.example.app > pkg_info.txt然后在本地慢慢看,比在终端里翻页舒服得多。
3.3 从 uid 反查 /data/app 里的真实目录
pm命令在某些深度定制的 ROM 上会被阉割,输出为空或者报错。这时候还有一条兜底路线:直接看/data/app下的目录属主。
adb shell ls -l /data/app/输出类似:
drwxr-xr-x 3 system system 4096 2024-01-15 10:23 ~~k83jQwXm-Tg==这里的属主信息在部分机型上是 system,看不出 uid,那就往下钻一层:
adb shell ls -l /data/app/~~k83jQwXm-Tg==/这一层的目录属主就是应用的 uid,比如u0_a123这种形式。u0_a123里的123加上基数 10000,就是实际的 uid 10123。另一边用
adb shell dumpsys package com.example.app | grep userId=拿到 uid 10123,两边一对,路径就确认了。这个方法看着绕,但在pm path失效的设备上是唯一的通路,我在几台定制系统上验证过,都能走通。
顺带提醒,/data/app目录本身对 shell 用户的读权限在不同 ROM 上差异很大。有些设备ls直接报 permission denied,这时候前面的路线全部作废,只能依赖pm或者 root。
3.4 老设备与 root 环境下的一套替代方案
Android 7.0 及以前的老设备其实最省事,路径规则是固定的:
/data/app/com.example.app-1/base.apk包名加上连字符和序号,序号从 1 开始递增,卸载重装会变成 2。写脚本的时候直接拼就行,不用查。但这也带来一个问题:老教程里的代码在新设备上会静默失败,你以为路径是对的,实际文件根本不存在,所以做兼容的时候一定要先判断 Android 版本。
有 root 权限的话,还有一个信息最全的来源:/data/system/packages.list。这个文件每一行记录一个包的安装信息,格式大致是包名、uid、调试标志、数据目录、SELinux 标签、版本号,各字段用空格分隔。用 awk 提取:
adb shell su -c "cat /data/system/packages.list | awk '{print \$1, \$2, \$4}'" | grep example/data/system/packages.xml信息更全,包含权限授予记录、签名摘要、安装时间等,但它是 XML,在设备上解析不方便,一般拉回本地用工具处理:
adb shell su -c "cp /data/system/packages.xml /sdcard/" adb pull /sdcard/packages.xml拿回来之后用任意 XML 解析器读,或者在 Python 里用xml.etree处理。这个文件是排查"某个权限到底给没给"这类问题的终极依据,比dumpsys的输出更原始也更完整。
4. 一次完整实操:从连接设备到拿到目标路径
4.1 环境准备与设备连接要点
工具就一套platform-tools,里面有adb。版本建议不要太旧,太旧可能识别不了新系统的某些命令特性。判断标准很简单,能正常连上设备就行。拿到之后解压,把目录加到 PATH,或者每次用全路径调用。
设备侧的准备工作有三步。第一,进设置里的关于手机,连续点版本号七次打开开发者选项;第二,在开发者选项里打开 USB 调试;第三,用数据线接上电脑,手机上会弹出授权对话框,勾选"始终允许"再确认。这第三步最容易被忽略,很多人插上就敲adb devices,看到unauthorized就以为是驱动问题,其实只是没点确认。
adb devices正常输出是:
List of devices attached ABC123456789 device如果显示unauthorized,重新插拔并确认弹窗;如果列表为空,检查数据线是否支持数据传输(有些线只能充电)。Windows 上还可能需要装厂商驱动,Linux 和 macOS 一般免驱。
Android 11 开始支持无线调试,懒得插线的可以用:
adb pair 192.168.1.100:37000 # 配对,端口和码在开发者选项里看 adb connect 192.168.1.100:5555 # 连接配对码每次都会变,注意别超时。无线调试的稳定性在局域网环境下其实挺好的,做长时间排查的时候不用被线绊着。
4.2 分步命令实录与输出解读
我按真实排查流程走一遍。假设需求是:手机上开着某个应用,我要找到它的安装目录,顺便把 APK 拉出来。
第一步,进入 shell 环境。用adb shell一次性进去比每条命令前面都加adb shell效率高很多:
adb shell第二步,拿到当前前台包名。前面讲过三条路,这里用最稳的一条:
dumpsys window | grep -E "mCurrentFocus|mFocusedApp"假设输出里含com.example.app/.ui.MainActivity,包名就是com.example.app。
第三步,查安装路径:
pm path com.example.app输出package:/data/app/~~k83jQwXm-Tg==/com.example.app-1A2b3C==/base.apk。至此安装目录已经拿到了,就是/data/app/~~k83jQwXm-Tg==/com.example.app-1A2b3C==/。
第四步,确认是不是 Split APK。如果pm path输出了多行,说明是拆分安装。这时候要把每个 APK 都拉下来,单独拉base.apk装不回设备上。拉取:
exit adb pull /data/app/~~k83jQwXm-Tg==/com.example.app-1A2b3C==/ ./app_dump/这一步有个前提:shell 用户对/data/app下的文件是否有读权限。实测下来多数设备是允许的,因为 APK 本身需要被系统读取做校验,但极少数 ROM 会拒绝。如果遇到Permission denied,走pm命令的替代方案:
adb shell pm path com.example.app | sed 's/^package://' | xargs -I {} adb pull {} ./app_dump/效果一样,但如果 shell 本身没权限,换什么命令都白搭,只能上 root。
第五步,查 uid 和数据目录:
adb shell dumpsys package com.example.app | grep -E "userId=|dataDir="输出userId=10123和dataDir=/data/user/0/com.example.app。
第六步,看外部存储私有目录:
adb shell ls -l /sdcard/Android/data/com.example.app//sdcard是/storage/emulated/0的软链接,写哪个都行。这个目录 Android 11 之后对第三方应用关闭了,但 shell 用户通常还能进,实测大部分机型可以。
第七步,如果需要一个脚本把上面步骤串起来,可以写成这样:
#!/system/bin/sh # 用途:通过当前前台窗口拿到包名并输出安装路径 pkg=$(dumpsys window | grep mCurrentFocus | awk '{ n = split($0, a, " "); for (i = 1; i <= n; i++) { if (a[i] ~ /^u[0-9]+$/) { print a[i+1]; exit } } }' | sed 's|/.*||') echo "当前前台包名: $pkg" pm path "$pkg"这段脚本里用了一个取巧的写法:mCurrentFocus后面的字段里,包名紧跟在u0这种用户标识后面,所以找到匹配u<数字>的字段,取它的下一个字段,再按斜杠切掉 Activity 名就得到包名。不同 ROM 输出格式有细微差异,脚本上线前一定要在目标设备上验证一遍,别直接拿去跑批量任务。
提示:
awk在部分精简 ROM 上可能缺失,因为 toybox 对 awk 的支持是后来才补的。如果跑不通,把 awk 换成 sed 或者干脆用pm list packages -f加 grep 做全量匹配,牺牲一点优雅换取兼容性。
4.3 content:// URI 反查真实文件路径的换算规则
前面提到的那些 content:// 开头的 URI,本质上不是文件路径,是内容提供者(ContentProvider)的寻址标识。但 Android 的FileProvider有个固定的映射规则,只要你拿到 APK 里的res/xml/file_paths.xml,就能把 URI 换算成真实路径。
映射关系是固定的一张表:
| file_paths.xml 中的标签 | 对应的方法调用 | 换算后的真实路径 |
|---|---|---|
<files-path> | getFilesDir() | /data/user/0/<pkg>/files |
<cache-path> | getCacheDir() | /data/user/0/<pkg>/cache |
<external-path> | Environment.getExternalStorageDirectory() | /storage/emulated/0 |
<external-files-path> | getExternalFilesDir(null) | /storage/emulated/0/Android/data/<pkg>/files |
<external-cache-path> | getExternalCacheDir() | /storage/emulated/0/Android/data/<pkg>/cache |
<external-media-path> | getExternalMediaDirs() | /storage/emulated/0/Android/media/<pkg> |
<root-path> | 根目录 | /(仅限特定场景,权限极高) |
换算的方法是把 URI 里 authority 之后的那一段,按第一个斜杠切开:前半段是标签名,后半段是相对路径,两者拼起来就是真实路径。举一个具体的例子,content://com.tencent.mobileqq.sharefileprovide/external_files/xxx/yyy.jpg这个 URI 里,authority 是com.tencent.mobileqq.sharefileprovide,路径段是external_files/xxx/yyy.jpg。这里的external_files不是标准标签名,是应用自己定义的,所以必须去看它的file_paths.xml才能确定它到底指向哪里。
查看这个 XML 有两条路。第一条是解包 APK 之后直接读:
unzip -o base.apk res/xml/file_paths.xml -d unpacked/XML 文件是二进制格式(AXML),直接读会乱码,需要用apktool反编译,或者用aapt2 dump xmltree解码:
aapt2 dump xmltree base.apk --file res/xml/file_paths.xml第二条路是不解包,直接在设备上找已经解压出来的资源。已安装的应用资源在 APK 里,运行时解压到/data/app/.../旁边的目录,不过这条路比较绕,还是解包更直接。
有个经验性的判断可以先做:如果路径段的前缀带external_files、external_这类字样,八成映射的是外部存储目录。Android/data/<pkg>/files这个位置最常见,因为应用往这里写文件不需要申请存储权限,写自己的私有空间天然合法。但具体是不是,还得看 XML,别靠猜。
5. 常见问题与排查速查表
5.1 命令有输出但路径不存在:三个高频原因
第一个原因是包名写错了但没报错。pm path对不存在的包名会返回空,不报错也不提示。所以拿到空结果的时候,先别怀疑设备,先确认包名。用pm list packages | grep 关键词搜一下,注意包名大小写敏感,com.Example.App和com.example.app是两个不同的包。
第二个原因是应用处于"已卸载但保留数据"状态。Android 提供了一种保留数据的卸载,卸载之后/data/data里的内容还在,但pm path查不到,因为 APK 已经删了。这种包需要加-u参数才能列出来:
adb shell pm list packages -u | grep example排查卸载残留的时候,这个参数是关键。
第三个原因是多用户空间错位。应用分身、工作资料这些功能本质上是在不同的用户空间里各装了一份。默认查的是当前用户,如果你要查的是分身里的那份,得显式指定:
adb shell pm list packages --user 10 adb shell pm path --user 10 com.example.app用户 ID 用adb shell pm list users查看,输出里会列出所有用户及其 ID。
5.2 路径拿到了却读不到内容怎么办
这是 Android 11 之后最普遍的问题。你能查到/storage/emulated/0/Android/data/<pkg>/这个路径,但在文件管理器里点进去是空的,或者应用代码里listFiles()返回 null。原因在于 Android 11 引入了分区存储的进一步收紧,Android/data和Android/obb这两个目录对第三方应用关闭了直接访问。
能绕过去的办法有几种,各有适用场景。第一种是走 adb,shell 用户的权限没有被这条策略覆盖,adb shell ls /sdcard/Android/data/<pkg>/通常还能列出内容,adb pull也能拉文件。这条路适合做单次排查和数据导出。第二种是申请MANAGE_EXTERNAL_STORAGE,也就是所谓的所有文件访问权限,用户需要在系统设置里手动授予。这个权限在上架应用商店时会受到严格审核,只适合内部工具和调试版本。第三种是走 SAF,让用户通过系统文件选择器手动授权某个目录,这是一种合规但体验较重的方案。第四种是 root,root 之后 SELinux 策略仍然可能拦截,需要配合setenforce 0(不推荐在正式设备上做)。
应用内部目录/data/data/<pkg>是另一套逻辑。没有 root 的情况下,只有一种合法通路:
adb shell run-as com.example.app ls /data/data/com.example.app/ adb shell run-as com.example.app cat /data/data/com.example.app/shared_prefs/config.xmlrun-as的原理是临时切换到应用的 uid 身份执行命令,所以只有满足两个条件才能用:应用本身是 debuggable 的(AndroidManifest里android:debuggable="true",或者系统是 userdebug/eng 版本),并且设备没有额外加固。正式发布的应用几乎都是 non-debuggable,run-as会直接报package not debuggable,这时候就只能上 root。
5.3 排查速查表
把上面这些坑整理成一张表,遇到问题先对号入座:
| 症状 | 可能原因 | 处理方式 |
|---|---|---|
pm path输出为空 | 包名错误 / 用户空间不对 / 已卸载 | 用pm list packages搜索,加--user或-u |
ls /data/app/com.xxx不存在 | 系统版本 ≥ 8.0,目录名已随机化 | 改用pm path动态查询 |
dumpsys window无mCurrentFocus | 系统版本差异或命令名变化 | 试dumpsys window displays或查 Activity 栈 |
ps只看到自己的进程 | 权限策略限制 | 改用adb shell执行,或换dumpsys |
run-as报 not debuggable | 应用是正式发布版本 | 上 root,或改用其他信息源 |
文件管理器里Android/data是空的 | Android 11 分区存储限制 | 用 adb 操作,或申请全文件访问权限 |
pidof无输出 | 应用未运行或进程名被改 | 用ps -A结合dumpsys交叉验证 |
grep不存在 | 精简 ROM 缺工具 | 用 toybox 内置命令,或拉回本地过滤 |
拉 APK 报Permission denied | shell 无读权限 | 改用 root,或pm相关命令绕行 |
这张表我自己用的频率很高,尤其是前四条,基本覆盖了日常排查的绝大部分情况。表格的价值在于缩短决策路径——不用每次都从原理开始推,看一眼症状就能直接跳到处理方式。
6. 几条踩过坑才明白的经验
6.1 把重复查询压成一条命令
排查做得多了就会发现,真正花时间的不是单次查询,而是反复在几个目录之间跳。我的做法是把高频组合封装成函数,放进 shell 的启动脚本里。比如这一段:
checkpkg() { local pkg="$1" echo "=== 包名: $pkg ===" adb shell "pm path $pkg" | sed 's/^package://' | while read p; do echo "APK: $p" done adb shell "dumpsys package $pkg" | grep -E "userId=|dataDir=|versionName=" adb shell "ls -d /sdcard/Android/data/$pkg 2>/dev/null" }调用的时候直接checkpkg com.example.app,三个最关键的路径一次全出来。这种封装的收益在批量排查时特别明显,比如要清点设备上所有第三方应用的占用情况,一条循环就能跑完全部。
另一类值得封装的是"从界面到包名"的提取逻辑。前面那个 awk 脚本虽然能用,但不同 ROM 输出格式有差异,硬写死正则迟早出问题。更稳的做法是双路验证:先用mCurrentFocus拿一次,再用dumpsys activity top拿一次,两边结果一致才采信,不一致就报出来人工确认。多花不到一秒,但能避免一整轮排查方向跑偏。
6.2 关于合规使用的边界
最后说点实在的。这套查询手段本身是中性的,用途决定性质。用在自己开发的应用上做调试,用在自己拥有的设备上做数据迁移和备份,用在授权的安全评估里做资产清点,这些都是正常且必要的。但如果用在别人的设备、别人的应用上,去读取不该读的数据,那就越界了。我的习惯是:任何一次操作之前先问自己一句"这台设备、这个应用,我有没有处置权",答案模糊就不做。
另外还有一个容易被忽略的点——不要在正式发布的设备上关 SELinux,也不要把 root 状态长期保持。这些操作会显著削弱设备的安全防护,一旦装着敏感数据的应用在这台机器上跑,风险是实打实的。要做深度排查,用一台专门的测试机,用完恢复出厂设置,比在主力机上折腾划算得多。
还有一个实操层面小建议:查到的路径、uid 这类信息,尽量随手记录下来,比如写进一个 markdown 文件。因为随机化的目录名和设备绑定,换台机器就全变了,同样的排查流程可能要重走一遍。有一份自己的记录,下次遇到同类问题能省掉大量重复劳动。我自己那张表里已经攒了两百多条,从包名到路径到踩坑原因,翻一翻比重新推一遍快得多。
注意:
/data/system/packages.xml这类文件包含设备上所有应用的完整信息,拉取和留存都要谨慎,用完及时删除,别随手扔在共享目录里。
这套东西说到底就是三个动作的循环:先确定包名,再拿路径,最后确认权限够不够读。把这三步跑顺了,剩下的大部分时间其实是在处理各家 ROM 的差异。遇到命令不生效,别急着怀疑自己写错了,先想想是不是系统版本又变了规则——这几年 Android 在权限和目录策略上的改动频率,比大多数人的教程更新速度快多了。