把应用丢进system/priv-app这件事,看起来只是把 APK 从system/app挪一个目录,实际做过的人都知道,真正花时间的从来不是挪目录,而是挪完之后应用起不来的那几十分钟。我最早接触这类需求是在给一台定制设备做预装的时候,需求方只丢过来一句“这个应用要拿系统级权限,装到 priv-app 下”,然后就没了。结果 APK 推进去、重启、应用直接闪退,日志里一行java.lang.UnsatisfiedLinkError,再往下翻是Permission Denial: requires android.permission.REBOOT。这两个报错分别对应两条完全不同的技术路线:一条是 so 库的安装路径与 ABI 匹配,另一条是特权权限白名单。本文围绕预置到system/priv-app的完整编译打包链路展开,把目录结构、编译脚本写法、so 库跟随、权限白名单、SELinux 上下文、增量验证这些环节一个个拆开讲,适合正在做系统预置、ROM 定制、设备出厂配置的同行参考,也适合刚接触 AOSP 编译、只会在 Android Studio 里点 Run 的朋友补齐系统层认知。
1. 先想清楚 priv-app 到底买到了什么
1.1 三个安装位置的权限等级差异
Android 里预装应用的落点大致有三个:/system/app、/system/priv-app、/data/app。很多人以为system/app就是“系统应用”,权限自然高,这是个流传很广的误解。真正的分水岭是ApplicationInfo.FLAG_SYSTEM与ApplicationInfo.FLAG_UPDATED_SYSTEM_APP这几个标志位,而priv-app额外多出一个FLAG_PRIVILEGED,这个标志位才是拿到signature|privileged保护级别权限的入场券。
具体说,/system/app下的应用能拿到signature级别的权限(前提是签名匹配),但拿不到signature|privileged。像android.permission.REBOOT、android.permission.WRITE_SECURE_SETTINGS、android.permission.INSTALL_PACKAGES、android.permission.MOUNT_UNMOUNT_FILESYSTEMS这一批,保护级别里带着privileged字样,只有 priv-app 才能授予。这个设计初衷是防止系统应用滥用高危能力,把“系统应用”和“特权应用”分成两档。
我在实际项目里见过最典型的误判,就是把一个需要写安全设置的应用放进system/app,签名用的还是 platform,一切看起来都对,但Settings.Secure.putString就是抛SecurityException。这类问题排查起来特别费劲,因为日志只告诉你权限被拒,不会告诉你“因为你放错了目录”。所以第一步永远是:确认目标权限的保护级别里有没有privileged这个词。
| 安装位置 | 系统标志 | 特权标志 | 可获取权限级别 |
|---|---|---|---|
/data/app | 无 | 无 | normal / dangerous |
/system/app | 有 | 无 | normal / dangerous / signature |
/system/priv-app | 有 | 有 | normal / dangerous / signature / signature|privileged |
这张表建议直接贴在工位上。每次有人问“我签名对了他为什么还是没权限”,先看这张表。
1.2 签名匹配只是必要条件,不是充分条件
签名这件事有个细节容易被忽略:signature级别的权限校验的是“申请方签名与声明方签名一致”,声明方是frameworks/base/core/res/AndroidManifest.xml里那个平台签名。所以你的 APK 必须用 platform key 签,或者用LOCAL_CERTIFICATE := platform让构建系统在编译期帮你签。这一步在 Android Studio 里做不了,因为它没有你的平台密钥。
但签名对了之后还有第二道关:权限白名单。从 Android 9(API 28)开始,系统引入了privapp-permissions机制。逻辑是:即使你的应用是特权应用、即使权限保护级别允许,只要这个权限没有在对应的白名单 XML 里显式列出,系统就会拒绝授予。这个机制的目的是防止特权应用悄悄申请一堆它根本不该有的权限。ro.control_privapp_permissions这个属性控制校验行为,值为enforce时直接拒绝并打日志,值为log时只记录不拦截,值为disable时彻底关闭。
注意:把
ro.control_privapp_permissions设成disable能让应用跑起来,但这是把问题藏起来而不是解决。出厂固件里这么干,后续任何一个预装应用申请越权都能顺利通过,等于把一道安全闸门拆了。
1.3 一个反直觉的现象:priv-app 里的应用反而不该随便更新
/system/priv-app是只读分区的一部分,普通安装方式装不进去,只能通过 OTA 或整包刷机更新。这意味着应用一旦预置在这里,它的升级路径就和普通应用完全不同。如果应用自身实现了自更新逻辑,下载新版本后调用PackageInstaller安装,系统会把它装到/data/app,形成一个“更新过的系统应用”。此时FLAG_PRIVILEGED依然保留,因为标志位是从原始安装位置继承的,这一点倒是没什么问题。
真正麻烦的是卸载。系统应用无法被普通用户卸载,ACTION_DELETE会直接失败。如果业务上有卸载需求,得走DevicePolicyManager或者干脆留一个“禁用”而非卸载的入口。我在一个项目里就吃过这个亏,需求方要求“用户能彻底移除”,最后只能改成把应用预置到/data/app加一个开机自启的安装服务,绕了一大圈。
2. 目录结构与编译描述文件:两种写法各有适用场景
2.1 Android.mk 写法与关键变量逐行解释
老一些的代码树还在用Android.mk,写法如下:
LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := MyPrivApp LOCAL_SRC_FILES := $(LOCAL_MODULE).apk LOCAL_MODULE_CLASS := APPS LOCAL_MODULE_TAGS := optional LOCAL_MODULE_SUFFIX := $(COMMON_ANDROID_PACKAGE_SUFFIX) LOCAL_CERTIFICATE := platform LOCAL_PRIVILEGED_MODULE := true LOCAL_PREBUILT_JNI_LIBS := \ lib/arm64-v8a/libfoo.so:lib/arm64-v8a/libbar.so \ lib/armeabi-v7a/libfoo.so:lib/armeabi-v7a/libbar.so include $(BUILD_PREBUILT)逐行说一下为什么要这么写。LOCAL_MODULE是模块名,它决定了PRODUCT_PACKAGES里要写的名字。LOCAL_SRC_FILES指向仓库里已有的 APK,注意这个路径是相对于LOCAL_PATH的。LOCAL_MODULE_CLASS := APPS告诉构建系统这是一个应用模块,走 APPS 的安装规则。LOCAL_MODULE_SUFFIX补上.apk后缀,很多人在这一步漏掉,结果编译出来的文件名少了后缀,安装时找不到。
LOCAL_CERTIFICATE := platform是让构建系统用平台密钥重新签一次。这里有个选择:如果你的 APK 出库时已经用 platform 签好了,可以改成LOCAL_CERTIFICATE := PRESIGNED,跳过重签。两者的区别在于,platform会重新签名并覆盖原有签名,PRESIGNED是原样保留。我一般倾向于PRESIGNED,原因是重签过程会丢掉 v3/v4 签名信息,某些依赖签名校验的第三方 SDK 会因此报错。
LOCAL_PRIVILEGED_MODULE := true就是那个把安装路径从system/app改成system/priv-app的开关。这个变量的实现逻辑在build/make/core/base_rules.mk里,它会检查这个标志并把LOCAL_MODULE_PATH指向$(TARGET_OUT)/priv-app。
LOCAL_PREBUILT_JNI_LIBS是最容易踩坑的一行。在 Android 8 之前,语法是每个 so 前面加@,比如@lib/arm64-v8a/libfoo.so;Android 9 之后必须写成源路径:目标路径的形式,冒号两边分别代表在源码树里的位置和最终安装到 APK 内部的相对路径。如果写成老的@语法,新版本编译会直接报错,或者更糟——静默忽略,so 根本不进包。
2.2 Android.bp 写法与 jni_libs 的引用约束
新代码树用Android.bp(Soong),写法是声明式的:
android_app_import { name: "MyPrivApp", apk: "MyPrivApp.apk", certificate: "platform", privileged: true, dex_preopt: { enabled: false, }, jni_libs: [ "libfoo", "libbar", ], required: [ "privapp-permissions-myprivapp.xml", ], presigned: false, }这里有个必须理解的点:jni_libs属性接受的是Soong 模块名,不是文件路径。也就是说你不能直接写lib/arm64-v8a/libfoo.so,得先为每个 so 定义一个cc_prebuilt_library_shared模块:
cc_prebuilt_library_shared { name: "libfoo", arch: { arm64: { srcs: ["lib/arm64-v8a/libfoo.so"], }, arm: { srcs: ["lib/armeabi-v7a/libfoo.so"], }, }, strip: { none: true, }, check_elf_files: false, }strip: { none: true }这一行很关键。默认情况下预编译库会被 strip 处理,某些加固过的 so 一旦被 strip 就会损坏,运行时报dlopen failed: invalid ELF header。check_elf_files: false是因为预编译库的 ELF 信息可能不完整(比如没有正确的 build-id),关闭校验能避免编译期误报。
dex_preopt: { enabled: false }是关闭预编译优化。默认情况下 priv-app 里的应用会在编译期生成 odex/vdex,好处是首次启动快,坏处是 APK 体积变化、调试麻烦、某些动态加载的 dex 会失效。如果应用用了插件化框架,这个必须关掉。
presigned和certificate是互斥的:presigned: true表示 APK 已经签好,不再重签;如果同时写了certificate,编译会报错。这一点和Android.mk里的逻辑一致,只是表现形式不同。
2.3 编译产物落到 out 目录后的自查方法
编译完之后别急着刷机,先在 out 目录里确认文件真的进去了:
ls -l out/target/product/<device>/system/priv-app/MyPrivApp/ unzip -l out/target/product/<device>/system/priv-app/MyPrivApp/MyPrivApp.apk | grep "lib/"第一条命令看的是 APK 有没有落到正确位置。第二条命令看的是 so 有没有被打进 APK 内部的lib/<abi>/目录。这是最直接的验证方式,比刷机快得多。
还要检查system/priv-app/MyPrivApp/下有没有额外的 oat 目录。如果dex_preopt开着,会生成oat/arm64/MyPrivApp.odex,这属于正常现象。但如果你的应用依赖DexClassLoader动态加载,这个 oat 目录反而会成为障碍,得关掉预编译。
有个小技巧:编译前先rm -rf out/target/product/<device>/system/priv-app/MyPrivApp,避免残留文件造成“明明改了却没生效”的假象。增量编译的缓存有时候确实不刷新这个目录,我至少被坑过三次。
3. 特权权限白名单:一个 XML 决定应用能不能拿到权限
3.1 XML 放哪里、怎么被系统扫到
白名单文件的命名没有强制规范,但社区惯例是privapp-permissions-<包名或模块名>.xml,内容结构固定:
<?xml version="1.0" encoding="utf-8"?> <permissions> <privapp-permissions package="com.example.myprivapp"> <permission name="android.permission.REBOOT"/> <permission name="android.permission.WRITE_SECURE_SETTINGS"/> <permission name="android.permission.INSTALL_PACKAGES"/> </privapp-permissions> </permissions>放的位置有几种常见选择。一是放在frameworks/base/data/etc/下,然后在产品 mk 里用PRODUCT_COPY_FILES拷到system/etc/permissions/。二是直接用prebuilt_etc模块:
prebuilt_etc { name: "privapp-permissions-myprivapp.xml", src: "privapp-permissions-myprivapp.xml", sub_dir: "permissions", filename_from_src: true, }不管哪种方式,最终目标路径必须是/system/etc/permissions/(Android 11 之后也可能是/system_ext/etc/permissions/或/product/etc/permissions/)。系统开机时PackageManagerService会扫描这些目录,把所有<privapp-permissions>节点读进来建索引,然后拿索引去比对特权应用申请的权限列表。
这里有个容易被忽略的点:<permission>里的权限名必须和AndroidManifest.xml里申请的完全一致,包括android.permission.前缀。少一个字符就匹配不上,日志里会明确说“权限未在白名单中”。
3.2 编译期与开机期的两道校验
从 Android 9 开始,这个校验分两个阶段。编译期阶段在build/make/tools/下有专门的脚本,会检查所有 priv-app 模块的 manifest 里申请的权限是否都有对应白名单覆盖,缺了直接编译失败,报错信息类似:
error: privileged permission android.permission.REBOOT is not in privapp-permissions whitelist这个报错很有迷惑性,因为它出现在编译阶段而不是运行阶段,很多人第一反应是“权限申请写错了”,实际上是白名单没配。
开 runtime 阶段就是前面提到的ro.control_privapp_permissions属性。默认值是enforce,日志里会打:
PackageManager: Signature|privileged permissions not in privapp-permissions whitelist: {com.example.myprivapp android.permission.REBOOT}看到这行日志,说明运行期校验拦截了。解决办法只有一个:老老实实把权限加进白名单,然后重新刷system分区。
注意:白名单里的权限必须是应用真的申请了的。如果 manifest 里没写
<uses-permission>,白名单里却列了,系统不会报错但也不会生效,等于白写。反过来,manifest 申请了但白名单没列,就是上面那个报错。两边必须严格对齐。
3.3 权限列表怎么确定:从 dumpsys 反推
不知道应用到底要哪些权限的时候,别猜,直接查。先在设备上手动把应用装上(临时装到 data 分区就行),然后跑:
adb shell dumpsys package com.example.myprivapp | grep -A 50 "requested permissions"这会列出 manifest 里声明的全部权限。但这里面混了大量normal和dangerous级别的权限,它们不需要进白名单。真正需要进白名单的只有保护级别带privileged的那部分。判断方法是逐条查frameworks/base/core/res/AndroidManifest.xml里对应的protectionLevel属性,含signature|privileged的才需要列。
另一个更快的办法是直接看 logcat。把ro.control_privapp_permissions临时设成log,开机后抓:
adb logcat -d | grep "privapp-permissions whitelist"系统会把所有缺失的权限一次性列出来,照着补就行。这个方法我用了很多次,比手动查表快得多,而且不会漏。
4. so 库的三条安装路线与选择依据
4.1 打进 APK 内部:最省心但受体积限制
jniLibs方式(也就是前面LOCAL_PREBUILT_JNI_LIBS或jni_libs干的事)是把 so 塞进 APK 的lib/<abi>/目录。优点是自包含,应用卸载时一起清掉,不存在版本错配;缺点是 APK 体积膨胀,而且同一个 so 如果多个应用都用,会重复存储多份。
这条路适合 so 比较小、只有单个应用使用的场景。我一般建议单个 so 超过 20MB 就考虑换方案,否则 APK 装进只读分区后,整个 system 镜像的体积会被撑大,OTA 包也跟着变大。系统分区空间紧张的项目尤其要注意这一点,system分区的可用空间往往只有几百 MB。
还有个隐藏限制:APK 内部的 so 在 Android 6 之后不能直接从文件系统加载,必须先解压到应用私有目录。这个解压发生在安装时(或者首次启动时),如果 so 特别大,首次启动会有明显延迟。extractNativeLibs属性控制这个行为,设成false时 so 保持未压缩状态直接 mmap,启动快但 APK 体积大;设成true时压缩存储,省空间但首次加载慢。这个取舍要看具体业务。
4.2 装到 system/lib 与 system/lib64:共享但要注意 ABI
第二条路是把 so 直接装到/system/lib/(32 位)和/system/lib64/(64 位)。这样多个应用可以共享同一份 so,省空间。Android.mk里的写法是覆盖安装路径:
include $(CLEAR_VARS) LOCAL_MODULE := libfoo LOCAL_SRC_FILES := lib/arm64-v8a/libfoo.so LOCAL_MODULE_CLASS := SHARED_LIBRARIES LOCAL_MODULE_PATH := $(TARGET_OUT_SHARED_LIBRARIES) LOCAL_MULTILIB := 64 include $(BUILD_PREBUILT)LOCAL_MODULE_PATH := $(TARGET_OUT_SHARED_LIBRARIES)这一行是核心,它把输出目录从默认位置改到了system/lib或system/lib64。LOCAL_MULTILIB := 64指定只编译 64 位版本;如果两个 ABI 都要,得写两个模块或者用LOCAL_MULTILIB := both配合 arch 相关的 srcs。
Soong下的等价写法是:
cc_prebuilt_library_shared { name: "libfoo", compile_multilib: "64", arch: { arm64: { srcs: ["lib/arm64-v8a/libfoo.so"], }, }, strip: { none: true }, check_elf_files: false, installable: true, }这条路的风险在于 ABI 匹配。把 64 位 so 放到system/lib/(32 位目录)下,应用加载时会报dlopen failed: is 64-bit instead of 32-bit,反之亦然。更隐蔽的情况是应用本身只有 32 位版本,却依赖了一个只提供 64 位 so 的库,这时无论怎么放都加载不了,只能让库提供方补 32 位产物。
4.3 独立目录加 ldconfig:适合大型第三方库
还有第三条路,把 so 放到/system/lib64/myprivapp/这样的独立目录,然后在/system/etc/ld.config.txt或者/vendor/etc/public.libraries.txt里注册。这条路适合大型第三方库(比如音视频处理、地图引擎),或者是多个版本共存需要隔离的场景。
代价是配置复杂,而且不同 Android 版本的 linker namespace 规则差异很大。Android 7 引入命名空间隔离之后,/system/lib64之外路径的 so 默认不可见,必须在ld.config.txt里显式声明搜索路径。我个人的经验是:除非有强需求,别走这条路,配置错一个字段就是几小时的排查。
4.4 依赖链自查:readelf 与 ldd 的用法
so 加载失败最常见的原因不是路径错,而是它自身依赖的库缺失。上设备之前先在本地查一遍:
readelf -d libfoo.so | grep NEEDED这会列出libfoo.so依赖的所有动态库。逐个确认这些库在目标系统的/system/lib64/下是否存在。常见的坑是依赖了libc++_shared.so,这个库在系统镜像里不一定有,得自己带。
还有个细节要注意:readelf -d看的是DT_NEEDED段,它只列直接依赖。间接依赖(依赖的依赖)得递归查。写个小脚本递归展开比较省事,或者干脆在目标设备上用ldd跑一遍——不过 Android 的toybox里ldd支持不完整,很多时候还是得靠readelf手动追。
5. 从编译到上机:增量验证的正确姿势
5.1 单模块编译与快速替换
整包编译一次动辄一两个小时,改个白名单就要重编显然不现实。正确做法是单模块编译:
source build/envsetup.sh lunch <product>-<build_type> m MyPrivApp privapp-permissions-myprivapp.xmlm命令会根据模块依赖自动编译相关产物,比mmm更省事(mmm需要指定目录)。编译完成后,产物在out/target/product/<device>/system/priv-app/MyPrivApp/下。
接下来是把它推到设备上。前提是设备已经adb root并且system分区可写:
adb root adb remount adb push out/target/product/<device>/system/priv-app/MyPrivApp/ /system/priv-app/ adb push out/target/product/<device>/system/etc/permissions/privapp-permissions-myprivapp.xml /system/etc/permissions/ adb shell chmod 644 /system/etc/permissions/privapp-permissions-myprivapp.xml adb rebootadb remount在 Android 10 之后经常失败,原因是dm-verity没关。得先adb disable-verity再adb reboot,开机后重新adb root && adb remount。这一步的报错信息很不友好,通常是remount failed: Permission denied,容易让人以为是 root 权限问题。
还有一个容易被忽略的细节:adb push过去的文件,SELinux 上下文可能不对。system/priv-app下的文件应该是u:object_r:system_file:s0,system/etc/permissions下也是。如果设备开着enforcing,上下文错了会导致文件不可读。修复方式是:
adb shell restorecon -R /system/priv-app/MyPrivApp adb shell restorecon -R /system/etc/permissions/restorecon会按file_contexts规则重新打标签,比手动chcon靠谱得多。
5.2 验证清单:五条命令确认真的生效
推完重启之后,按这个顺序验证:
| 步骤 | 命令 | 预期结果 |
|---|---|---|
| 1 | adb shell ls -l /system/priv-app/MyPrivApp/ | 看到 APK 文件,权限 644 |
| 2 | adb shell dumpsys package com.example.myprivapp | grep -i flags | 出现PRIVILEGED标志 |
| 3 | adb shell dumpsys package com.example.myprivapp | grep REBOOT | 权限状态为granted=true |
| 4 | adb logcat -d | grep privapp | 无not in privapp-permissions whitelist报错 |
| 5 | adb shell getprop ro.control_privapp_permissions | 返回enforce |
第 2 步最关键。dumpsys package输出里的flags字段会列出应用的所有标志位,看到PRIVILEGED说明安装位置对了,系统识别到了特权身份。如果只有SYSTEM没有PRIVILEGED,那就是目录放错了或者LOCAL_PRIVILEGED_MODULE没生效。
第 3 步看的是权限授予状态。granted=true表示权限真的拿到了,granted=false配合flags=[ ... ]里的拒绝原因,能直接定位是签名问题还是白名单问题。
5.3 整包编译时容易漏的一步
单模块验证通过之后,整包编译前记得确认device.mk或产品配置文件里把模块加进了PRODUCT_PACKAGES:
PRODUCT_PACKAGES += \ MyPrivApp \ privapp-permissions-myprivapp.xml漏了这一步,单模块能编出来,但整包编译时这个模块根本不会被打进镜像。我见过有人单模块测试一切正常,刷了整包之后应用消失了,排查了半天才发现是PRODUCT_PACKAGES没加。
另外要注意PRODUCT_PACKAGES和PRODUCT_PACKAGES_DEBUG的区别。后者只在eng和userdebug版本里生效,user版本不包含。如果预置的应用是正式功能,必须用PRODUCT_PACKAGES。
6. 几个真实踩过的坑与对应解法
6.1 签名用了 platform,权限还是被拒
这个现象我在两个项目里遇到过。第一次是因为白名单里的包名写错了——应用的实际包名是com.example.myapp,白名单里写成了com.example.myprivapp,差了三个字母,肉眼扫一眼根本看不出来。系统匹配是按包名精确比对的,错了就是全盘不生效。
第二次更隐蔽:APK 的 manifest 里<uses-permission>用的权限名带了版本后缀,比如android.permission.REBOOT写成了android.permission.REBOOT_TEMP(这是早年某些定制系统里的非标准权限名)。白名单里写的是标准名,两边对不上。解法是把 manifest 里的权限名改成标准名。
排查这类问题有个技巧:把ro.control_privapp_permissions设成log,然后adb logcat -b all -d | grep -i "privileged",系统会把“哪个包缺哪个权限”完整打出来,比看代码猜快得多。
6.2 预置后首次开机特别慢
priv-app 里的应用默认会被dex2oat预编译,这个过程在开机时执行,应用越多、dex 越大,开机越慢。如果预置了十几个应用,开机时间从 30 秒涨到两分钟很正常。
解决办法有两个:一是编译期就做好预编译(dex_preopt开启),这样开机时不用再编;二是对不敏感的应用直接关掉预编译(dex_preopt: { enabled: false }),让它运行时 JIT。前者省开机时间但占镜像空间,后者省空间但首次启动慢。
我一般的策略是:核心系统应用开预编译,第三方应用关掉。因为第三方应用更新频繁,预编译的产物经常失效,反而不如让它在首次启动时自己优化。
6.3 应用装了却启动不了:SELinux 的隐性拦截
有一类问题日志里完全看不到权限拒绝,应用就是起不来。这种情况八成是 SELinux 拦了。priv-app 下的应用默认运行在platform_app或system_app域,具体取决于签名和seapp_contexts配置。如果应用需要访问某些特殊路径(比如/dev/下的设备节点、/proc/下的某些文件),得单独写allow规则。
排查方法是临时把 SELinux 设成permissive:
adb shell setenforce 0如果应用能起来了,那就是 SELinux 问题。然后adb shell dmesg | grep avc能看到具体的拒绝记录,照着写规则就行。注意这只是排查手段,出厂固件里必须是enforcing,不能把permissive带出去。
6.4 增量替换后应用“看起来没更新”
这个坑特别容易让人怀疑人生。明明adb push了新版本,重启后应用行为还是老的。原因通常是dex2oat的产物还在——/system/priv-app/MyPrivApp/oat/目录里存着旧版本的编译产物,系统优先加载它。
解决方式是 push 新 APK 之前先删掉整个目录:
adb shell rm -rf /system/priv-app/MyPrivApp adb push out/target/product/<device>/system/priv-app/MyPrivApp /system/priv-app/注意这里删的是整个目录,不是只删 APK 文件。删完再推,就不会有残留。另外/data/dalvik-cache/下也可能有缓存,必要时adb shell rm -rf /data/dalvik-cache/*然后重启,代价是开机时间会变长,因为要重新编译所有应用。
我在实际操作中的体会是,priv-app 这条链路的技术点其实都不算深,难的是它横跨了编译系统、包管理、权限模型、SELinux 四个领域,每个领域出问题时的表现又都差不多——应用起不来或者权限拿不到。所以排查时不要一上来就乱改,按“位置对不对、签名对不对、白名单全不全、so 加载成不成功、SELinux 拦没拦”这个顺序走,基本上十分钟内能定位到具体环节。另外强烈建议在项目里维护一份预置应用的清单表,把包名、申请的 privileged 权限、so 依赖、是否需要预编译这些都列清楚,下一个接手的人能省掉大量重复劳动。