RK3568 Android12预置APK全解析:不可卸载、可卸载与永久删除

📅 发布时间:2026/10/2 1:02:44
RK3568 Android12预置APK全解析:不可卸载、可卸载与永久删除
预置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-appsystem 分区不能只能“禁用”仍在作为系统模块打进 system.img可卸载/data/appdata 分区可以正常卸载取决于实现方式可能重新安装预置于只读分区首次启动复制永久删除不存在无无无从所有编译入口和镜像中移除关键就一句话“不可卸载”的本质是把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 : falseAndroid 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_APPSLOCAL_MODULE_PATH : $(TARGET_OUT_DATA_APPS)两种方式效果类似。这个方案的好处是实现简单不需要写开机服务坏处是如果用户执行了恢复出厂设置/data分区被清空重做这个应用就彻底没有了。如果你需要“恢复出厂后还在”这个方案不适用。4.4 两种可卸载方案的出厂后行为对比这里我用一个表来区分对比项首次启动复制方案直接打进userdata方案用户能否卸载能能恢复出厂设置后会重新安装不会出现编译复杂度需要写开机服务或脚本只需一行拷贝配置是否吃data空间安装一次后占用data空间出厂前就占用data空间适用场景行业应用需要带回恢复一次性预装推广应用我在实际项目中行业终端类客户几乎都选了首次启动复制方案因为“恢复出厂设置后应用必须回来”是他们的硬性验收标准。5. 永久删除模式从固件里连根拔掉目标APK5.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预置不是把文件塞进去那么简单你要理解它背后这套包管理逻辑才能真正掌控自己的固件。