☰
RK3568 Android12预置APK全解析:不可卸载、可卸载与永久删除
2026/10/2 1:02:40 网站建设 项目流程

预置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 怎么选:我的判断方法

拿到一个预置需求时,我一般先问三个问题:

  1. 这个应用是不是系统核心功能的一部分?如果是,走不可卸载,优先放priv-app。
  2. 这个应用对用户来说是不是可有可无?如果是,走可卸载,用首次启动复制方案。
  3. 这个应用是不是压根不该出现在这台设备上?如果是,走永久删除,不光删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上真正动手改编译配置之前,会反复确认这三件事:

  1. APK的ABI是否匹配。RK3568是64位ARM芯片,但很多行业APK只打了armeabi-v7a的包,这也能跑,但如果你在64位进程里强制加载会出问题。最好用unzip -l app.apk | grep lib/看看so库架构。
  2. APK的目标SDK版本。Android12对targetSdkVersion为30以上的应用有更强的包可见性限制,如果预置应用依赖查询其他应用,可能需要额外加权限。
  3. 平台签名是否与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.apk

6.4 我的最终建议

如果你问我,要做RK3568 Android12的预置APK,重中之重是哪一步,我的答案不是写编译配置,而是“先把需求确认清楚”。三种模式对应的原理、目录、行为都不同,中途改需求非常耗时。

如果产品经理明确要“不可卸载”,优先走priv-app + platform签名,这是最稳的路线。如果要说“可卸载且恢复出厂后要回来”,老老实实写一次性复制安装服务。至于“永久删除”,一定要按5.1的依赖链排查流程做,不然你永远不知道自己删掉的东西还会从哪里冒出来。

这套流程我在这篇里用到的所有命令和配置,都是RK3568 Android12上实测过的。你拿着去改自己的工程,应该能少走不少弯路。最后还是那句话,APK预置不是把文件塞进去那么简单,你要理解它背后这套包管理逻辑,才能真正掌控自己的固件。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询