ARTICLE DETAIL

资讯详情

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

Android 应用安装目录与包名路径查询实战

Android 应用安装目录与包名路径查询实战 做 Android 逆向或者做兼容性排查的时候卡住人的第一步往往不是反编译、不是脱壳而是一个更朴素的问题这个应用到底被装到了机器上的哪个目录里包名叫什么正在跑的那个进程对应的又是谁这几个问题看着简单可真到命令行里敲的时候路径拼错了、查出来是软链接、拿到的是 content:// 开头的 URI、高版本系统上进程列表一片空白各种情况都能遇到。这篇就把应用安装目录包名安装路径这条链路完整走一遍从 adb 命令行到 PackageManager 的 API 层从 APK 落盘位置到外部存储的数据目录把每一步的来龙去脉和踩坑点都说清楚。内容偏实战命令都是可以直接复制粘贴跑的那种。适合两类人一类是刚开始接触 Android 应用结构、想搞明白文件都去哪了的开发者另一类是做逆向分析、需要在设备上定位目标 APK 和它的运行痕迹的同学。前置要求不高会开 adb、能用终端就行root 不是必须的但我会标出来哪些操作绕不开 root。1. 先搞清楚安装目录这四个字在 Android 里到底指什么很多人第一次搜Android 应用安装目录期望的答案是一个路径。但 Android 的设计里一个应用从装上到跑起来至少会牵扯到五个不同的物理位置随便拿一个出来说这就是安装目录后面一定会出问题。所以动手之前先把这几个位置的名字和职责对齐比背命令重要得多。1.1 一个应用至少对应五个物理位置我把它们列成一张表这张表我在实际排查里几乎是当字典用的位置典型路径里面装的是什么APK 本体目录/data/app/~~随机串/包名-随机串/base.apk、split 分包、lib 目录私有数据目录/data/user/0/包名/shared_prefs、databases、files、cache设备加密数据目录/data/user_de/0/包名/开机即需访问的那部分数据外部数据目录/storage/emulated/0/Android/data/包名/应用自己往公共存储写的文件外部媒体与 OBB/storage/emulated/0/Android/media/包名/、/storage/emulated/0/Android/obb/包名/媒体文件、游戏资源包系统预装应用则是另一套逻辑它们不在/data/app下而是躺在/system/app、/system/priv-app、/product/app、/vendor/app这些只读分区里。你用pm path去查一个系统应用返回的可能是/system/priv-app/XXX/XXX.apk也可能是被 OTA 更新过后的/data/app版本——这就涉及同一个包名在设备上存在两份安装体的问题后面第 6 节会专门讲。一句话总结APK 本体管程序从哪加载/data/user/0/包名管程序的数据写哪去外部目录管用户看得见的文件放哪。你要定位的是哪一种决定了该敲哪条命令。1.2 为什么 Android 8.0 之后路径变成了一串随机字符如果你在 Android 7 及以前的机器上执行pm path com.example.app得到的大概是这样清晰好认package:/data/app/com.example.app-1/base.apk但从 Android 8.0 开始同样的命令返回的东西会变得很丑package:/data/app/~~kR3n7QxLm9vA2bCdEfGh/com.example.app-xY8zWvUtSrQp/base.apk这两个~~包裹的随机串不是乱码也不是加密而是系统在安装时为每个应用生成的唯一目录名。为什么要这么干核心原因是防止应用之间互相干扰和预置攻击。旧方案里路径直接由包名推导攻击者只要拿到包名就能预测路径进而做目录抢占、符号链接替换这类操作同时包名相同就必然路径相同多个版本共存时会互相覆盖。改成随机串之后路径不可预测同名包也能安全共存比如企业工作资料里的克隆应用。这个变化对逆向分析最直接的影响是你不能再用包名去拼 APK 路径了。写脚本的时候一定要走pm path或者 PackageManager 查询把结果当作不透明字符串来用。我见过不止一个人写了/data/app/ 包名 -1/base.apk这种拼接在 Android 9 以上的机器上全军覆没。1.3 符号链接把 /data/data 藏到了 /data/user/0 后面还有一个特别容易把人绕晕的设计。你在老资料里看到的数据目录是/data/data/包名可现在查出来却是/data/user/0/包名。这俩其实是同一个地方ls -l /data/data # lrwxrwxrwx 1 root root 7 ... /data/data - /data/user/0/data/data是个符号链接指向/data/user/0而0就是主用户的 user id。Android 从 4.2 引入多用户之后每个用户的数据目录是/data/user/userId/包名主用户是 0第二个用户就是 10、11 这样递进。这里有个坑ApplicationInfo.dataDir这个字段在不同版本、不同调用方返回的字符串不完全一致有时给/data/data/包名有时给/data/user/0/包名。如果你拿它去做字符串比较或者当 Map 的 key很容易因为两种写法对不上而出错。稳妥的做法是用它做展示但比较时统一用File.getCanonicalPath()归一化之后再比。顺带说一句/data/user_de/0/包名。de是 device encrypted 的缩写直连加密。这部分数据在用户第一次解锁屏幕之前就能被访问所以系统级组件、闹钟、短信这类需要在开机早期工作的应用会把关键数据放这儿。逆向时如果只在/data/user/0里翻可能会漏东西。2. 只用 adb 和 shell最省事的路径查询链路搞清楚目录结构之后接下来就是怎么查。我的习惯是能用 adb 一条命令解决的绝不先写代码。因为命令行反馈快、可组合而且不需要装环境。这一节把常用命令按使用频次排一下。2.1 pm path一行命令拿到 APK 落盘位置这是最核心的一条命令没有之一adb shell pm path com.example.app返回内容可能是单行也可能是多行——多行说明这个应用用了 Split APKApp Bundle 分发出来的应用基本都是这种base.apk 加上若干个 config 分包package:/data/app/~~kR3n7QxLm9vA2bCdEfGh/com.example.app-xY8zWvUtSrQp/base.apk package:/data/app/~~kR3n7QxLm9vA2bCdEfGh/com.example.app-xY8zWvUtSrQp/split_config.arm64_v8a.apk package:/data/app/~~kR3n7QxLm9vA2bCdEfGh/com.example.app-xY8zWvUtSrQp/split_config.xxhdpi.apk想把整套 APK 拉下来做分析就在宿主机的 shell 里写个循环for p in $(adb shell pm path com.example.app | sed s/package://); do adb pull $p done这里有个细节值得注意pm path的输出带package:前缀而且行尾可能有\r。在 Windows 上做脚本处理时如果不处理这个回车符拼出来的路径会带一个不可见字符adb pull会报文件不存在。用tr -d \r清一下最省心。另外Android 10 以后也支持cmd package path com.example.app输出格式略有不同但功能等价。如果你的设备上pm有点异常有些定制 ROM 会裁剪可以拿cmd package试试。2.2 pm list packages 的几个过滤开关当你不知道确切包名需要先看有哪些应用时用这几个开关组合adb shell pm list packages -f # 带 APK 路径的完整列表 adb shell pm list packages -3 # 只看第三方应用 adb shell pm list packages -s # 只看系统应用 adb shell pm list packages -d # 只看被禁用的 adb shell pm list packages -e # 只看启用的 adb shell pm list packages -u # 包含已卸载但保留数据的 adb shell pm list packages | grep 关键词 # 模糊匹配-f的输出格式是package:/data/app/xxx/base.apkcom.example.app路径和包名用等号连接。这个格式很好用一次就能同时拿到路径和包名不用二次查询adb shell pm list packages -f -3 | sed s/package:// | awk -F {print $2\t$1}要提醒的是pm list packages的列表是系统视角的完整列表但它有个坑被暂停/挂起状态的包、工作资料里的包在不同 ROM 上表现不一致。有些国产 ROM 会把自家的应用商店、推送服务隐藏在标准列表之外。如果你是在做安全排查pm list packages的结果不能当作全集还得去/data/app目录本身列一遍做交叉验证这一步需要 root。2.3 不安装也能查包名aapt 与 apkanalyzer手里有一个 APK 文件但还没装到设备上怎么知道它的包名这是逆向流程里最先要做的一步。三个方法我按推荐度排第一种用aaptAndroid SDK build-tools 里自带aapt dump badging app.apk | grep ^package # package: namecom.example.app versionCode1024 versionName3.2.1第二种用aapt2更简洁aapt2 dump packagename app.apk第三种用apkanalyzerSDK cmdline-tools 里apkanalyzer manifest print app.apk | head -20它会把 AndroidManifest 解析成可读 XMLpackage属性一眼就能看到顺便还能看到applicationId和权限声明。这里顺便解释一个很多人遇到的报错您上传的 APK 包名已存在。这通常发生在往应用市场提包的时候。原因不是包名重复了而是你上传的这个包名在平台上已经被另一个开发者账号占用或者你自己之前提过同包名的应用。解决办法是改build.gradle里的applicationId改完重新签名。要注意applicationId和namespace是两回事改的时候别只改一个。而如果你的应用已经上线、有存量用户改applicationId等于做了一款新应用老用户不会收到更新这个代价要先想清楚。2.4 dumpsys package 里值得逐行看的字段pm path只给你一个路径想知道更全的信息就得靠dumpsys packageadb shell dumpsys package com.example.app输出很长几百上千行但真正值得关注的字段就那么几个。我一般这样过滤adb shell dumpsys package com.example.app | grep -E codePath|resourcePath|dataDir|primaryCpuAbi|versionName|firstInstallTime|lastUpdateTime|flags各字段含义如下codePath代码也就是 APK加载路径和pm path一致resourcePath资源路径正常和 codePath 相同做过插件化/资源热修复的可能不同dataDir私有数据目录一般形如/data/user/0/包名primaryCpuAbi主 ABIarm64-v8a、armeabi-v7a这类决定了它加载哪一套 native 库flags一大串十六进制位标记能看到是不是 debug 包、是不是系统应用、能不能被其他应用查询等等flags这块信息量其实很大但文档分散、不同版本位定义还会变我一般只挑几个稳定的看比如是否带DEBUGGABLE。判断一个应用是不是可调试还有个更直接的办法adb shell dumpsys package com.example.app | grep -i flags | tr \n | grep -i debug或者干脆用run-as试一把能进去就是可调试的这个下一节会细说。3. 查当前正在跑的包名进程、窗口、Activity 三条线装在哪查完了接下来是现在正在跑的是谁。这个需求在做自动化、做抓包、做界面分析的时候特别常见。方法不止一种各有各的适用边界。3.1 ps、pidof、top还在用但要看版本最传统的方式是列进程adb shell ps -A | grep com.example adb shell ps | grep com.example # Android 8 以前用这个 adb shell pidof com.example.app # 直接拿 PID最干净pidof是 Android 6 以后引入的输出就是纯数字 PID特别适合塞进脚本。拿到 PID 之后很多后续操作就打开了比如查这个进程打开了哪些文件、占用了哪些端口adb shell ls -l /proc/pid/fd adb shell cat /proc/pid/maps | head -40top也能用但要注意 Android 上的top和 Linux 上的参数不一样adb shell top -n 1 | grep com.example这里的坑在于不同 Android 版本、不同 ROM 上ps的参数支持差异很大。Android 8 是一个分水岭ps换成了 toybox 实现-A才表示全部进程不加的话只显示当前用户的。我在 Android 7 的机器上敲ps -A直接被提示参数非法。写脚本的时候最好先探一次版本或者干脆优先用pidof兼容性最好。3.2 dumpsys activity 与 dumpsys window 的分工如果你要的不是这个包有没有在跑而是当前前台是哪个页面那就得换工具。这两条命令我都常用adb shell dumpsys activity activities | grep -E mResumedActivity|mFocusedApp|topResumedActivity adb shell dumpsys window | grep -E mCurrentFocus|mFocusedApp它们的区别值得说清楚dumpsys activity activities关注的是Activity 栈能告诉你哪个 Activity 处于 resumed 状态输出里有完整包名 Activity 类名dumpsys window关注的是窗口焦点mCurrentFocus通常是当前拿到输入焦点的窗口弹窗、输入法弹出时会变实际用的时候我一般两个都跑互相印证。比如用户正在一个弹窗上打字mCurrentFocus可能显示的是输入法进程或者系统弹窗而mResumedActivity才是底下的宿主应用。只看一个很容易判断错。还有一个更早的写法dumpsys activity top输出里有个ACTIVITY行格式是ACTIVITY com.example.app/.MainActivity pid12345一行同时给了包名、类名和 PID做自动化的时候特别顺手。不过在新版本上它的输出结构有调整索引位置会变写脚本解析时要留个容错。3.3 高版本上 getRunningAppProcesses 返回空名单的原因从命令行切到代码里很多人的第一反应是ActivityManager am (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE); ListActivityManager.RunningAppProcessInfo list am.getRunningAppProcesses();然后发现list里只有自己或者干脆是空的。这不是你写错了而是系统从 Android 5.0 开始收紧了权限第三方应用调用这个接口只能拿到自己进程的信息要拿全局列表必须有系统级签名权限。那应用市场上那些进程管理功能是怎么做的一般两条路UsageStatsManager需要用户手动去设置里授予使用情况访问权限这是一项特殊权限不能通过运行时弹窗申请只能跳设置页。拿到之后可以按时间区间查询哪些包被放到过前台、前台停留了多久。无障碍服务AccessibilityService监听窗口变化事件通过TYPE_WINDOW_STATE_CHANGED事件里的包名判断前台应用。这条路权限门槛相对低但需要用户主动开启无障碍且功耗和隐私合规上都要谨慎处理。// UsageStatsManager 查询示例 UsageStatsManager usm (UsageStatsManager) getSystemService(Context.USAGE_STATS_SERVICE); long now System.currentTimeMillis(); ListUsageStats stats usm.queryUsageStats( UsageStatsManager.INTERVAL_DAILY, now - 60_000, now); for (UsageStats stats1 : stats) { if (stats1.getLastTimeUsed() now - 30_000) { Log.d(TAG, 最近活跃: stats1.getPackageName()); } }这两条路我都用过。UsageStatsManager的问题是时间粒度粗、有延迟不适合要求毫秒级响应的场景无障碍服务的实时性好但要处理大量事件过滤而且用户随时可能关掉开关。选哪个取决于你的场景对实时性和权限侵入性的取舍。4. 把查询逻辑写进代码PackageManager 侧的 API 组合命令行能解决的是一次性排查但如果要做成一个工具、一个检测模块就得落到代码上。这一节讲 PackageManager 这块几组 API 的差别这几组 API 名字长得像用错了结果差很远。4.1 sourceDir、publicSourceDir 和 dataDir 的区别ApplicationInfo里有这么几个长得像的字段我第一次用的时候也搞混过字段含义典型值sourceDir主 APK 的绝对路径/data/app/~~xxx/包名-yyy/base.apkpublicSourceDir对外可见的 APK 路径未做拆分时与 sourceDir 相同同上splitSourceDirsSplit APK 的路径数组可能为 null[split_config.arm64_v8a.apk, ...]splitPublicSourceDirs同上对外版本同上dataDir私有数据目录/data/user/0/包名nativeLibraryDirnative 库解压/加载目录/data/app/~~xxx/包名-yyy/lib/arm64关键点在于做 APK 备份或者拷贝的时候不能只拷sourceDir。如果目标应用是 Split APK 分发的只拿 base.apk 会导致装回去之后缺资源、缺 so 库一运行就崩。正确做法是把splitSourceDirs一起收集PackageInfo pi pm.getPackageInfo(pkgName, 0); ApplicationInfo ai pi.applicationInfo; ListString apkPaths new ArrayList(); apkPaths.add(ai.sourceDir); if (ai.splitSourceDirs ! null) { apkPaths.addAll(Arrays.asList(ai.splitSourceDirs)); }至于dataDir要有个心理预期第三方应用默认是无权读取它的内容的。你能拿到路径字符串但new File(ai.dataDir).listFiles()会返回 null。这是正常的沙箱隔离不是你代码写错了。要真正读到内容要么应用自己debuggabletrue配合run-as要么设备 root。4.2 包可见性过滤为什么你的列表里少了半屏应用Android 11 引入了一套叫包可见性的机制。简单说就是你的应用默认只能看到自己和少量系统包其他应用必须在清单里声明我想看到谁否则查不到。症状很典型一段在 Android 10 上跑得好好的代码到了 Android 11 的机器上getInstalledApplications返回的列表突然少了一大半。很多人第一反应是权限问题去加QUERY_ALL_PACKAGES结果发现有的渠道还上不了架。正确的做法是精细化声明。在当前工程的AndroidManifest.xml里加queries节点queries !-- 声明想查询的具体包名 -- package android:namecom.example.target / !-- 或者按 intent 特征声明 -- intent action android:nameandroid.intent.action.SEND / data android:mimeTypetext/plain / /intent !-- 按 provider authority 声明 -- provider android:authoritiescom.example.target.fileprovider / /queries如果你的工具确实需要枚举全部应用比如设备管理类、安全检测类那就得申请QUERY_ALL_PACKAGES权限同时在应用市场提交时说明用途审核会比较严格。我的经验是先按queries精细声明真的不够用了再考虑全量权限。因为全量权限除了审核问题在部分 ROM 上还会触发额外的权限提示用户体验会变差。4.3 外部存储路径不要在代码里拼字符串查外部数据目录我看到过这样的写法// 千万别这么写 String path /sdcard/Android/data/ getPackageName() /files;这段代码在大部分机型上看起来能跑但它依赖了三个不成立的假设外部存储一定挂在/sdcard、一定存在、一定可写。实际情况是——有些设备有内置存储加实体卡两套外部存储/sdcard可能是个软链或者根本不存在应用被装到 adopted storage合并后的存储卡上时路径完全不同Android 11 之后/sdcard/Android/data/包名对第三方应用和 adb shell 基本都关上了门。标准写法就一行File externalFiles getExternalFilesDir(null); // 典型结果: /storage/emulated/0/Android/data/包名/files File externalCache getExternalCacheDir(); // 典型结果: /storage/emulated/0/Android/data/包名/cache这两个方法的好处是系统帮你算好了路径返回的 File 对象一定落在应用有权读写的范围内。返回值可能为 null外部存储不可用时所以拿到之后要做判空别直接.mkdirs()。如果你就是要访问那个用户能看见的公共目录那得走MediaStore或者 SAF存储访问框架让用户主动选一个目录并授权。Android 10 之后引入了分区存储直接写公共目录的路子基本被堵死了硬闯只会拿到EACCES或者EROFS。5. content:// 开头的 URI 为什么会把人带进沟里做逆向的时候经常会截到一些字符串长这样content://com.tencent.mobileqq.sharefileprovider/external_files/Android/data/com.xxx/... content://com.baidu.searchbox.fileprovider/baiddpath/Android/data/com.baidu... content://com.ss.android.uri.key/external_root/Android/data/com.ss.andro...第一眼看上去路径信息全在里面好像把前缀去掉就能得到真实文件路径。但这是一个非常经典的误解必须单独拿出来讲。5.1 FileProvider 的 external_path 并不是磁盘路径content://是 Android 的内容 URI它的格式是content://authority/path-segment/相对路径第二段比如external_files、baiddpath、external_root只是这个应用在 FileProvider 配置里给自己起的名字是一个映射标签不是目录名。真正的映射关系写在应用内部的 XML 配置里形如paths external-files-path nameexternal_files path. / external-path nameexternal_root path. / /paths这几个标签对应的真实根目录是固定的files-path→Context.getFilesDir()即/data/user/0/包名/filescache-path→Context.getCacheDir()external-files-path→Context.getExternalFilesDir(null)即/storage/emulated/0/Android/data/包名/filesexternal-path→Environment.getExternalStorageDirectory()即/storage/emulated/0external-cache-path→Context.getExternalCacheDir()即使你知道了标签对应的根目录相对路径部分也未必是原样透传的。有些应用会在重写 URI 的时候做一层转义、拼接或者 base64 编码甚至干脆用一张内存映射表把长路径映射成一个短 key。这种情况下光靠字符串是还原不出来的。5.2 从 content URI 反推真实路径的可行与不可行那到底能不能还原分情况可以还原的情况应用用了默认的 FileProvider 实现配置里就是简单的path.透传没有额外编码。这时候你在反编译工具里找到AndroidManifest.xml里的provider android:authoritiesxxx节点顺藤摸瓜找到它引用的res/xml/*.xml文件把paths里的映射读出来再和 URI 逐段对照基本就能拼出真实路径。不能还原的情况应用自己继承了ContentProvider或者重写了getUriForFile中间做了编码。这种就得去看反编译出来的 smali 或者用动态调试跟一遍调用栈纯静态分析很难一步到位。还有一个更省事的角度——如果目标应用本身拥有这个文件的读写权限那么通过 URI 是可以直接读的adb shell content read --uri content://com.example.provider/external_files/test.txt或者用content query查数据库型的内容提供者。这条路绕开了路径还原的问题前提是你知道 URI 并且没有被权限校验拦住很多 provider 带android:exportedfalse或者grantUriPermissions校验。我个人的判断标准是如果只是为了读一份文件走 URI 比还原路径省事得多如果是为了理解应用的数据组织方式那还原路径这件事本身才是有价值的。前者是手段后者是目的别搞反。6. 实操中最容易翻车的几类路径误判前面讲的都是怎么查这一节讲查出来的东西为什么可能是错的。这些坑基本都是我自己踩过或者看别人踩过之后记下来的比正着讲命令更有用。6.1 查到的是 /data/app/... 却不是唯一一份同一个包名在设备上存在两份安装体这在今天非常常见。最典型的就是系统预装应用被应用商店更新过adb shell pm path com.example.prebundled # package:/data/app/~~Abc/com.example.prebundled-Xyz/base.apk adb shell pm list packages -s | grep prebundled # package:com.example.prebundled ← 它同时还被标记为系统应用系统分区里的那份旧版本还在/system/app/...躺着只是被/data里的新版本覆盖了。所以你查到的路径是新的但安装包来源标记仍然是系统的。这个现象带来的直接后果是卸载更新Uninstall updates之后应用会回退到系统分区里的老版本路径变了、版本号变了、行为也可能变了。做逆向分析的时候如果目标设备被卸载过更新你分析的可能是个几年前的版本跟线上跑的根本不是一套代码。验证方法很简单dumpsys package里的codePath、resourcePath和firstInstallTime、lastUpdateTime一起看。如果firstInstallTime很早但lastUpdateTime很近基本可以确定是被更新过的。6.2 多用户、工作资料与克隆应用带来的路径分叉再来看另一个高频误判。很多国产 ROM 支持应用分身、平行空间企业设备还会配工作资料Work Profile。这些功能背后的实现基本都基于 Android 的多用户机制——每个分身其实是一个独立的 user id拥有独立的数据目录。adb shell pm list users # Users: # UserInfo{0:机主:13} running # UserInfo{10:工作:1030} running # UserInfo{999:分身空间:1030} running主用户是 0分身一般是 10、11、999 这类。对应的数据路径就分叉了/data/user/0/包名 /data/user/10/包名 /data/user/999/包名如果你在主页面上操作应用、却在/data/user/0/包名里翻数据很可能翻到的是主用户那份没怎么用的数据真正活跃的是分身那一份。还有一个并行的机制叫isolated process隔离进程和app clone部分系统对每个用户的数据目录还会再加一层混淆路径。这种情况下/data/user/uid/包名不再是简单拼接得用ApplicationInfo实际查询。排查建议先用pm list users确认有几个用户然后在每个用户上下文里分别查询。跨用户查包可以用adb shell pm list packages --user 10 adb shell pm path --user 10 com.example.app--user这个参数很多人不知道但处理多用户场景时是刚需。6.3 run-as 只对 debuggable 应用有效这件事run-as是个很好用的命令它让你以目标应用的 uid 身份执行命令从而绕过沙箱读到/data/user/0/包名里的内容adb shell run-as com.example.app ls -l adb shell run-as com.example.app cat files/config.xml但它有个硬性门槛目标应用必须在 AndroidManifest 里声明了android:debuggabletrue否则会直接报package not debuggable。发布版的正式包几乎都是debuggablefalse所以run-as对它们是无效的。怎么快速判断一个包能不能run-as直接试一次最快adb shell run-as com.example.app id # 成功: uid10123(u0_a123) gid10123(u0_a123) ... # 失败: run-as: package not debuggable: com.example.app如果真的需要读正式包的数据路径只有两条设备 root 后以 root 身份访问或者应用自己做了数据导出/备份功能。还有一条历史遗留的路子——adb backup但从 Android 12 开始系统默认对allowBackup做了收紧很多应用也主动关闭了备份这条路能走通的场景越来越少不建议作为主要方案。顺带提醒一句如果你在做的分析涉及真实用户的设备数据务必先确认授权范围。技术手段是中性的用在哪、谁来用性质完全不同。只分析自己开发的应用、或者有明确书面授权的目标这是底线。6.4 路径里带 的随机串别拿去做字符串匹配最后说个小但烦人的点。前面提到 Android 8.0 之后/data/app下的目录名带随机串形如~~kR3n7QxLm9vA2bCdEfGh。这个串是 Base64 风格的可能包含、-、_这些字符而且每次重新安装都会变。这意味着两件事第一别把它写进任何持久化的配置或白名单里。你今天记下来的路径明天卸载重装就失效了。第二如果你要判断两个路径是不是同一个应用不要比完整路径要比包名。从路径里反推包名的正则大概是echo /data/app/~~kR3/com.example.app-xY8/base.apk | sed -E s#^.*/([^/])-[^/]/.*#\1# # com.example.app不过这个正则在包名本身带横线的情况下会出问题Android 包名里横线其实是非法字符但有些工具会产生奇怪的中间态。所以更稳的做法还是走pm path拿路径、走pm list packages拿包名把两者的对应关系交给系统去维护别自己写解析。我在实际工具里是这么处理的维护一个路径 → 包名的映射表但每次工具启动时重新扫描一次不做持久化缓存。多花几十毫秒省掉一堆为什么昨天还好好的今天就不行了的排查时间。这个习惯是从一次线上事故里养成——当时缓存了一份路径映射用户升级应用之后路径变了工具一直指着一个已经不存在的目录报的错还特别隐晦。再补一个我常用的兜底命令。当你实在搞不清一个包里到底有哪些路径信息时把dumpsys package的完整输出存成文件慢慢翻比在终端里 grep 高效得多adb shell dumpsys package com.example.app /tmp/pkg_dump.txt adb shell pm list packages -f -u /tmp/all_pkgs.txt adb shell pm list users /tmp/users.txt这三份文件基本覆盖了包名、路径、多用户三个维度的全部信息后面不管是写分析报告还是做交叉比对都不用再反复连设备了。这也是我在做长期分析项目时的固定开场动作——先把快照拉全再去慢慢啃。
返回列表