预置APK,这活儿说难不难,说简单也真不简单。我在RK3568 Android12项目里第一次接到这个需求时,客户只给了句话:“把我们这个APK预置进去,不能卸载。”我当时心想,这不就是把APK扔到system/app里嘛,有什么好纠结的?结果后来产品经理又提了新需求:“这款预置应用用户要能卸载,但恢复出厂设置后又得回来。”再后来,客户看到系统自带了一堆用不上的APK,又提出“能不能把这些从固件里永久删掉”。到了这一步我才意识到,同样是“预置APK”,背后其实藏着三种完全不同的机制和落地方式。
这篇文章就把我在RK3568 Android12上调通这三种模式的完整经验整理出来,包括具体的编译配置、模块写法、烧录验证、以及我在实操里踩过的各种坑。只要是做瑞芯微平台固件定制的人,无论你是刚接触Android编译的入门开发者,还是已经在做厂商方案的工程师,这篇都能直接“抄作业”。
1. 先理清楚三种模式要解决什么产品问题
1.1 从产品经理的“三句话需求”说起
我遇到过的最典型的需求描述,就跟标题里说的这样:
- “这个APK必须预置,不能卸载”——通常是指设备的基础功能、行业专项应用,比如收银机上的主程序、门禁终端上的对讲应用、医疗平板里的诊断软件。这种应用如果让用户误删了,设备可能直接变成砖。
- “这个APK预置上去,但用户可以卸载,恢复出厂后要回来”——通常是推广类、辅助类应用,厂商希望用户开箱即用,又不希望它占着“系统应用”的名分让用户不满。
- “把系统里某个APK彻底删掉”——一般是精简固件,去掉用不上的Google套件、运营商应用、方案商预置的演示APK,减少空间占用和后台耗电。
这三种需求,对应的技术方案完全不一样。如果一开始没弄明白,稀里糊涂把所有APK都往/system/app一塞,后面改起来非常痛苦。
1.2 三种模式在技术上的本质区别
我把它们的差别整理成了一张表:
| 模式 | 实际安装位置 | 所属分区 | 用户能否卸载 | 恢复出厂设置后 | 编译阶段的模块类型 |
|---|---|---|---|---|---|
| 不可卸载 | /system/app 或 /system/priv-app | system 分区 | 不能,只能“禁用” | 仍在 | 作为系统模块打进 system.img |
| 可卸载 | /data/app | data 分区 | 可以正常卸载 | 取决于实现方式,可能重新安装 | 预置于只读分区,首次启动复制 |
| 永久删除 | 不存在 | 无 | 无 | 无 | 从所有编译入口和镜像中移除 |
关键就一句话:“不可卸载”的本质是把APK放进system分区,因为system分区只读,系统在启动时会把里面的应用注册为系统应用,用户没有权限删除;“可卸载”的本质是把APK放进data分区,或者从data分区以用户身份安装,让PackageManager把它当成一个普通用户应用来管理;“永久删除”的本质则是让这个APK在编译产物里根本不存在。
1.3 怎么选:我的判断方法
拿到一个预置需求时,我一般先问三个问题:
- 这个应用是不是系统核心功能的一部分?如果是,走不可卸载,优先放
priv-app。 - 这个应用对用户来说是不是可有可无?如果是,走可卸载,用首次启动复制方案。
- 这个应用是不是压根不该出现在这台设备上?如果是,走永久删除,不光删APK,还要把相关配置清干净。
这里我特别想提一点:别因为“不可卸载”实现起来最简单,就把所有APK都做成不可卸载。用户对设备的掌控感,有时候比功能本身还重要。一个能卸载的预置应用,能减少很多售后投诉。
2. RK3568 Android12工程里预置APK的入口在哪
2.1 先摸清编译工程的位置
RK3568 Android12的SDK解压下来之后,工程结构跟AOSP基本一致。你要预置APK,天天打交道的主要是这几个目录:
device/rockchip/rk356x/:瑞芯微 RK3568 平台的设备配置目录,产品mk文件基本都在这里build/make/target/product/:AOSP通用产品配置,定义了很多公共预置模块vendor/rockchip/:瑞芯微方案商的公共配置和HAL层代码packages/apps/:AOSP自带应用源码目录frameworks/base/:系统框架层,预置APK相关的PackageManager逻辑在这里
在动手之前,我建议先把产品mk文件找出来。RK3568的SDK一般会有一个明确的板级配置,比如device/rockchip/rk356x/rk3568_evb7.mk或者你项目自己新建的rk3568_custom.mk。打开这个文件,你会看到很多PRODUCT_PACKAGES、PRODUCT_COPY_FILES之类的声明,这就是预置APK的“总入口”。
2.2 预置APK进固件的两大入口:PRODUCT_PACKAGES 与 PRODUCT_COPY_FILES
在Android编译系统里,往固件里加东西无非两条路:
PRODUCT_PACKAGES:注册一个编译模块,APK会被正确处理签名、DEX优化、ABI适配,然后安装到指定目录。这是最正规的做法。PRODUCT_COPY_FILES:直接拷贝文件到镜像的指定路径。简单粗暴,但APK不会做DEX优化,也不会被纳入系统包管理流程里,普通情况下不推荐用来预置APK。
我实测下来的感受是,PRODUCT_PACKAGES虽然写起来要多个模块文件,但它能保证APK以“一个合法应用”的身份进入系统。用PRODUCT_COPY_FILES直接拷贝,虽然省事,但后续你可能要面对应用运行时报找不到类、包解析失败、开机慢等问题,非常不划算。
举个最简单的查找例子,想确认一个应用是不是被编进固件了:
grep -rn "Launcher3" --include="*.mk" --include="*.bp" device/ build/ vendor/这样能快速定位这个模块是在哪个mk文件里被PRODUCT_PACKAGES引用的。
2.3 编译前的三件确认事项
我在RK3568 Android12上真正动手改编译配置之前,会反复确认这三件事:
- APK的ABI是否匹配。RK3568是64位ARM芯片,但很多行业APK只打了
armeabi-v7a的包,这也能跑,但如果你在64位进程里强制加载会出问题。最好用unzip -l app.apk | grep lib/看看so库架构。 - APK的目标SDK版本。Android12对
targetSdkVersion为30以上的应用有更强的包可见性限制,如果预置应用依赖查询其他应用,可能需要额外加权限。 - 平台签名是否与APK现有签名冲突。如果你要用platform签名,而APK之前已经被第三方签名过,那就必须重签,否则系统把它放错位置时会报签名不一致。
3. 不可卸载模式:把APK焊死在系统分区
3.1 放在priv-app还是app目录,差别很大
很多新手会问:/system/app和/system/priv-app不都是系统应用吗,随便放不行吗?事实上差别很大。
/system/priv-app里的应用被称为“特权应用”,可以持有signature|privileged级别的权限,可以直接调用一些系统隐藏API。比如一个需要读写/proc某个节点、需要下发AT命令、需要访问系统设置项的行业应用,就必须放在priv-app目录,并且使用平台签名。
/system/app里的应用虽然也是系统应用,但权限等级低一档,某些系统权限在Android 9之后会被拒绝,除非你额外把它们加入特权白名单。
Android 12上还要特别注意:放进priv-app的应用,如果声明了高特权权限,必须在etc/permissions/privapp-permissions-platform.xml里有对应声明,否则系统会拒绝授予权限。这个白名单是有强制校验的,漏了就直接报Permission Denial。
3.2 基于现有APK的模块定义写法
项目里拿到的通常是已经打包好的APK,不是源码工程,这种情况下用Android.mk写一个prebuilt模块是最稳的。我在RK3568 Android12下常用的模板是:
LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := MyIndustryApp LOCAL_SRC_FILES := MyIndustryApp.apk LOCAL_MODULE_CLASS := APPS LOCAL_MODULE_SUFFIX := $(COMMON_ANDROID_PACKAGE_SUFFIX) LOCAL_PRIVILEGED_MODULE := true LOCAL_CERTIFICATE := platform LOCAL_DEX_PREOPT := false LOCAL_ENFORCE_USES_LIBRARIES := false include $(BUILD_PREBUILT)把这个mk文件放到device/rockchip/rk356x/MyIndustryApp/目录下,APK和mk文件放一起。然后在产品mk文件里加:
PRODUCT_PACKAGES += MyIndustryApp这几个关键字段的作用分别是:
LOCAL_PRIVILEGED_MODULE := true:安装到/system/priv-app,如果没有这一行,默认进/system/app。LOCAL_CERTIFICATE := platform:使用平台密钥对APK重签,这样它才能拿到系统权限,也能避免与普通应用冲突。LOCAL_DEX_PREOPT := false:默认情况下编译器会做DEX预优化,但如果APK里含有压缩的so库或者特殊资源,优化失败会直接编不过。行业APK建议先设为false,稳妥。LOCAL_ENFORCE_USES_LIBRARIES := false:Android 12对uses-library的校验很严格,很多第三方APK编译时会报“requires unavailable shared library”的错,加这行能绕过。
3.3 为什么用户无法卸载系统分区里的APK
这里得说点原理。Android系统在开机时,PackageManager会扫描/system、/vendor、/product等只读分区,把这些分区里的APK统一注册为“系统应用”。系统应用能不能卸载,根本逻辑是看它所在的存储卷是否可写。
/system分区在编译阶段就固定死了,运行时不提供卸载入口。用户在设置里看到的不是“卸载”按钮,而是“停用”或“禁用”。即使你用ADB强行执行pm uninstall,系统也会告诉你Failure [DELETE_FAILED_INTERNAL_ERROR]。这是因为PackageManager不允许删除只读分区上的包,除非你改的是/data/app下的覆盖更新包。
正是这种“卸载不掉”的特性,让不可卸载模式非常适合承载那些设备核心功能型应用。
3.4 编译烧录后的验证步骤
编译烧录完成后,我习惯按这个顺序验证:
# 1. 确认APK已经进系统,路径在system分区的priv-app下 adb shell pm path com.myindustry.app # 期望输出:package:/system/priv-app/MyIndustryApp/MyIndustryApp.apk # 2. 查看系统应用列表里有没有它 adb shell pm list packages -s -f | grep MyIndustryApp # 3. 尝试卸载,应该报错 adb uninstall com.myindustry.app # 期望输出:Failure [DELETE_FAILED_INTERNAL_ERROR] # 4. 看设备设置里是否只有“停用”没有“卸载”如果第四步出现了“卸载”按钮,那就是有地方不对了。多半是APK被复制到了/data/app,或者你是用adb install把它装进去的,那跟预置是两回事。
4. 可卸载模式:让APK住进data分区
4.1 别再指望system分区里做出“可卸载”
我刚开始做这个需求时,想过一个偷懒的办法:把APK放到/system/app,然后通过pm uninstall -k --user 0把用户卸载记录标记上,这样用户看到的就是“已卸载”。但实测发现,这只是把应用从当前用户隐藏了,底部系统里它还占着空间,而且很多设备在重启后会“复活”。这根本不是真正的可卸载,产品经理一玩就能看出来,别走这条路。
真正的可卸载,一定是指这个APK的应用数据、安装记录位于/data分区。用户点“卸载”,PackageManager把它从整个设备上删掉,重启后也不会回来。
4.2 方案一:首次启动复制安装到data分区
这是国内方案商最常用的做法,也是我最终采用的稳妥方案。核心思路是:编译时把APK放在/system分区的一个隐藏目录里,开机的某个时机由系统服务自动执行一次安装,把它装进/data/app,这样用户看到的就是普通应用,能正常卸载。
具体步骤分四步:
第一步,把APK放到只读分区。我在产品mk文件里加一行:
PRODUCT_COPY_FILES += device/rockchip/rk356x/preinstall/MyApp.apk:system/preinstall/MyApp.apk这样编译后,APK会在/system/preinstall/MyApp.apk,不会出现在常规应用列表里。
第二步,写一个开机自启动的小服务执行安装。这里我用的不是init脚本,因为pm install在Android 12里需要比较完善的PackageManager环境,直接放在init.rc里执行容易遇到服务尚未就绪的问题。更稳的做法是写一个极简的Java应用或使用系统自带的SystemUpdateService思路,监听BOOT_COMPLETED广播,然后调用:
pm install -r /system/preinstall/MyApp.apk如果用ADB模拟,命令是:
adb shell pm install -r /system/preinstall/MyApp.apk-r表示覆盖安装,防止重复安装时报版本冲突。
第三步,加一个“已安装”标记。如果没有这个标记,每次开机都会重新执行一遍安装,用户刚卸载完,一重启又回来了。我用Settings.Global存标记:
adb shell settings put global preinstall_myapp_done 1服务启动时先查这个值,为1就直接跳过安装逻辑。
第四步,处理安装后的残留文件。APK一旦安装到/data/app,/system/preinstall/下的原始APK其实可以保留,因为它不参与应用解析。如果你介意APK被提取出来,也可以在安装成功后用rm删掉它。但我不建议删,保留还有一个好处:用户卸载掉应用之后,如果产品需求是“恢复出厂设置后还要回来”,那恢复出厂会重置/data,而/system下的备份还在,开机后又会重新安装。这正好满足需求。
4.3 方案二:把APK直接打进userdata镜像
还有一种更直接的做法:在编译阶段就把APK放到/data/app目录下,让它直接出现在userdata镜像里。用户的/data分区在首次开机时就有应用,而且因为它在/data/app下,PackageManager会把它当成用户应用处理,可以正常卸载。
产品mk文件里这样写:
PRODUCT_COPY_FILES += \ device/rockchip/rk356x/preinstall/MyApp.apk:data/app/MyApp/MyApp.apk或者用模块方式,把安装路径指向TARGET_OUT_DATA_APPS:
LOCAL_MODULE_PATH := $(TARGET_OUT_DATA_APPS)两种方式效果类似。这个方案的好处是实现简单,不需要写开机服务;坏处是,如果用户执行了恢复出厂设置,/data分区被清空重做,这个应用就彻底没有了。如果你需要“恢复出厂后还在”,这个方案不适用。
4.4 两种可卸载方案的出厂后行为对比
这里我用一个表来区分:
| 对比项 | 首次启动复制方案 | 直接打进userdata方案 |
|---|---|---|
| 用户能否卸载 | 能 | 能 |
| 恢复出厂设置后 | 会重新安装 | 不会出现 |
| 编译复杂度 | 需要写开机服务或脚本 | 只需一行拷贝配置 |
| 是否吃data空间 | 安装一次后占用data空间 | 出厂前就占用data空间 |
| 适用场景 | 行业应用需要带回恢复 | 一次性预装推广应用 |
我在实际项目中,行业终端类客户几乎都选了首次启动复制方案,因为“恢复出厂设置后应用必须回来”是他们的硬性验收标准。
5. 永久删除模式:从固件里连根拔掉目标APK
5.1 先查依赖,再动手删
永久删除一个预置APK,很多人上来就找PRODUCT_PACKAGES,把那一行删掉就完事。如果这个应用是独立的,那确实完事了。但很多APK之间有依赖,比如:
- 输入法APK被
Settings里某个设置项引用 - 某个应用与另一些系统应用共享UID或签名
- 系统服务在启动时会
bindService到这个应用,应用缺失会导致日志刷屏 - 预置APK删了,但它依赖的
lib、res、overlay还残留在系统里,占空间
所以我建议先做一次全工程搜索:
grep -rn "com.google.android.gms" --include="*.mk" --include="*.bp" --include="*.xml" --include="*.java" device/ build/ vendor/ packages/ 2>/dev/null | head -50通过搜索结果来判断这个包名是被谁引用、在哪些配置里出现。依赖关系查清楚了,再动手。
5.2 从编译清单中移除模块的正确姿势
对于通过PRODUCT_PACKAGES编译进去的APK,找到对应的一行,删掉或注释即可。比如:
PRODUCT_PACKAGES += \ Bluetooth \ Camera2 \ Gallery2 \去掉Gallery2,重新编译,Gallery就不会出现在固件里。
但要注意,有些预置APK不是产品mk文件直接指定的,而是通过inherit-product从别的mk里继承进来的。比如瑞芯微的通用配置device/rockchip/common/device.mk里可能加了一堆默认应用,你需要在你的产品mk文件覆盖或者直接修改继承的那个mk。如果你不想改动公共mk,可以在产品mk里用:
PRODUCT_PACKAGES -= Gallery2不过AOSP编译系统是否支持这种减法写法,不同版本有差异,RK3568 Android12上实测有些版本支持得不好。最保险的办法还是直接去源头mk里删行。
还有一些APK是直接拷贝进system的,比如某个默认音频文件、演示视频、离线地图包。这些要用PRODUCT_COPY_FILES的反向排除,或者直接删除对应源文件。注意,如果产品mk里有拷贝引用,你把源文件删了但引用没删,编译会直接报错,所以要先删引用,再删文件。
5.3 清理权限、白名单、overlay等“残留文件”
删掉APK之后,很多人会掉以轻心,结果系统里还残留着四面八方的东西。我在RK3568 Android12上做过一次彻底清理,发现残留主要有几类:
/etc/permissions/下的privapp-permissions-xxx.xml、xxx_whitelist.xml,这些是权限白名单,如果不清理,应用虽然删了,但白名单里的权限声明还在,不会直接出问题,但会给以后的维护制造混乱。overlay/目录下对应该包名的RRO叠加层,如果不删,系统在编译或运行时读不到目标包,日志会报Overlay ... target not found。/system/etc/init/下的rc文件,如果APK是作为服务运行的,删除APK后rc文件还在,开机时会一直尝试启动一个不存在的应用。data分区里的遗留数据,这个在编译固件时不存在,但如果用户之前跑过升级,旧数据可能还在,需要注意OTA升级脚本里的清理逻辑。
这些残留文件看着不起眼,但会把一个“永久删除”做得很不完美。全清干净,固件才算真正“瘦身”。
5.4 验证删除结果与系统稳定性
编译烧录完成后,我的验证清单是:
# 1. 应用不存在了 adb shell pm list packages | grep Gallery # 2. system分区里也没有对应文件 adb shell ls /system/app/ | grep -i gallery adb shell ls /system/priv-app/ | grep -i gallery # 3. 权限白名单里没有残留 adb shell ls /system/etc/permissions/ | grep -i gallery # 4. 系统日志里有没有因为缺应用而产生的报错 adb logcat -d | grep -i "not found\|Unable to resolve\|ServiceIntent"如果应用被系统服务在启动时引用,日志里会出现持续报错,这就要回去检查5.1里的依赖搜索。实测下来,大部分删除后的系统异常都是依赖问题没查清楚导致的。
6. 我在RK3568实测中踩过的坑和最后建议
6.1 签名校验是预置APK的第一道大坑
这个坑我踩了不止一次。预置APK时,APK原有的签名如果跟platform签名不一致,就会出现一种很诡异的现象:APK明明在/system/priv-app里,系统也能识别,但应用拿不到系统权限,某些功能启动时报SecurityException。
解决方法是,在Android.mk里必须明确指定:
LOCAL_CERTIFICATE := platform如果不写这一行,编译系统默认使用testkey签名,那预置出来的应用就不是带平台签名的特权应用了。另外,重签之后APK的内部UID与其他应用共享签名的情况会变,如果它和另一个应用使用sharedUserId,两边签名都得一致,否则装都装不上。
6.2 可卸载模式重启后应用又被装回来的坑
在首次启动复制方案里,我一开始没加安装标记,结果用户卸载了应用,重启之后又自动装回来了,客户立刻质疑“不是说了可以卸载吗”。这个问题就是4.2里说的没做Settings.Global标记导致的。
更隐蔽的另一种情况是,应用安装成功但Settings.Global写入失败,导致每次开机都重新装一遍。调试时我用下面的命令排查:
adb shell settings get global preinstall_myapp_done如果这个值一直为空,说明写入没成功,要做兜底。我在项目里的做法是不光检查这个标记,还会同时pm list packages | grep 包名,两者都满足才跳过安装。
6.3 几个对我帮助很大的调试命令
做预置APK调试,下面几个命令基本天天用:
# 查看APK在设备上的实际位置和安装来源 adb shell pm path com.myindustry.app # 查看一个包是不是系统应用,以及安装者的uid adb shell dumpsys package com.myindustry.app | grep -E "userId|packageName|codePath|flags" # 以当前用户模式卸载系统应用(可卸载的真正含义) adb shell pm uninstall --user 0 com.myindustry.app # 查看卸载后是否还有残留 adb shell pm list packages --user 0 | grep myindustry # 直接把本地APK推送到设备安装,用于验证可卸载模式 adb install -r MyIndustryApp.apk6.4 我的最终建议
如果你问我,要做RK3568 Android12的预置APK,重中之重是哪一步,我的答案不是写编译配置,而是“先把需求确认清楚”。三种模式对应的原理、目录、行为都不同,中途改需求非常耗时。
如果产品经理明确要“不可卸载”,优先走priv-app + platform签名,这是最稳的路线。如果要说“可卸载且恢复出厂后要回来”,老老实实写一次性复制安装服务。至于“永久删除”,一定要按5.1的依赖链排查流程做,不然你永远不知道自己删掉的东西还会从哪里冒出来。
这套流程我在这篇里用到的所有命令和配置,都是RK3568 Android12上实测过的。你拿着去改自己的工程,应该能少走不少弯路。最后还是那句话,APK预置不是把文件塞进去那么简单,你要理解它背后这套包管理逻辑,才能真正掌控自己的固件。