2026/10/2 1:23:43

RK3568 Android 12预置APK三种模式全解析:不可卸载、可卸载与永久删除

RK3568 Android 12预置APK三种模式全解析:不可卸载、可卸载与永久删除 做RK3568平台Android固件开发的同学应该都接到过“帮我把某某APK预置进系统”这样的需求。如果你以为预置APK就是把APK丢进system/app里完事那后面大概率要翻车。实际上“预置”这个动作在Android 12的RK3568工程里至少能拆成三种模式用户怎么都卸载不掉的系统应用、用户可以正常卸载的预置应用、以及从系统里彻底删掉的“预置”任务。这三种模式不只是一个目录路径的差异背后的分区逻辑、签名方式、权限模型、升级影响完全不一样。本篇就基于RK3568 Android 12把三种模式的原理、实现步骤和坑全部盘清楚。1. 先搞懂预置APK背后的分区与Android 12的约束1.1 预置APK的本质你的APK最终落在哪个分区预置APK这件事说白了就是让APK文件进入系统镜像里的某个分区等PackageManagerServicePMS在开机扫描的时候注册成可用应用。Android系统的分区大致有这么几块system是只读系统分区vendor是厂商只读分区product是产品定制分区data是用户可写的数据分区cache和odm分别管缓存和厂商私有定制。这些分区里面system、vendor、product、odm都是只读的设计上不允许用户直接改所以放在这些分区里的APK天然具备“不可卸载”的物理基础。data分区是用户数据和第三方应用安装的地方应用装到这里之后用户可以随时卸载。这么一理就清晰了不可卸载的预置就是把APK送到只读分区可卸载的预置本质上是把APK在这个“出厂镜像”里预先放好等系统启动后安装到data分区去。永久删除则刚好反过来是从所有编译入口和镜像产物中把APK的痕迹彻底拔掉甚至还要处理残留数据。RK3568的Android 12 SDK在Rockchip官方方案里system、vendor、product这些只读分区已经默认采用动态分区Dynamic Partition方案挂载路径在super分区之上。这对APK预置的影响是我们编译时放进这些分区的文件最终会被打包进super镜像而不再是独立的system.img那么简单。所以排查问题的时候不要只盯着单个分区的镜像文件要习惯用lpdump、simg2img或者SDK自带的打包脚本去解包super镜像验证。1.2 Android 12对预置应用的新约束Android 12相比老版本在预置应用这件事上多了一些必须注意的点。第一个是非SDK接口限制预置应用如果targetSdkVersion指向31访问隐藏API时会受到系统强约束之前很多开发机在Android 9上用的反射黑科技到12上可能直接抛异常。第二个是包可见性Package VisibilityAndroid 11开始应用不能随意查询其他应用预置App如果要和系统里另一个预置App做交互必须在AndroidManifest里声明queries否则代码里queryIntentActivities返回空列表。第三个是priv-app权限白名单机制从Android 9开始对priv-app有了硬性要求凡是放在/system/priv-app下的应用如果想使用signature|privileged级别的权限必须在/system/etc/permissions/privapp-permissions-*.xml中显式列出来否则系统会直接拒绝授予对应权限而且logcat里会刷出一堆红色警告。这三个约束在做RK3568 Android 12预置的时候非常容易踩中下面会反复提到。1.3 找到RK3568的编译入口和PRODUCT_PACKAGESRK3568的Android 12 SDK通常基于AOSP 12定制device端的核心配置在device/rockchip/rk3568/目录。你需要关注的几个文件是RK3568.mk、device.mk和BoardConfig.mk。Rockchip公共代码在vendor/rockchip/common/很多默认预置的APK源码或预编译APK都在vendor/rockchip/common/apps/下面。最终决定“哪些应用会被编进系统”的关键变量是PRODUCT_PACKAGES这是一个make变量在mk文件里用PRODUCT_PACKAGES 模块名把模块引入到编译。Android 12的构建系统会把PRODUCT_PACKAGES里的模块作为产品必选包编译后安装到对应分区。模块名并不是AndroidManifest里的package属性而是Android.bp里的name字段或Android.mk里的LOCAL_MODULE/LOCAL_PACKAGE_NAME这个区别后面排查问题时会经常用到。2. 模式一不可卸载APK做成真正的系统应用2.1 什么场景必须用不可卸载模式机型是收银机、自助终端、工控设备或者App本身就是输入法、桌面Launcher、系统设置类核心应用时一般要求用户不能卸载。这类设备通常是单一用途App如果被卸载设备就变砖或者业务直接中断。另外如果App需要申请signature级别的系统权限比如读取IMEI、修改系统设置、静默安装、访问其他受保护接口那不仅要做成不可卸载一般还需要用platform签名并且把APK放进priv-app目录。2.2 源码型App预置Android.bp和Android.mk两种写法如果你的App有完整源码可以直接放到packages/apps/MyApp/或者放到device/rockchip/rk3568/apps/MyApp/下然后写构建文件。Android.bp的写法// packages/apps/MyApp/Android.bp android_app { name: MyApp, srcs: [src/**/*.java], resource_dirs: [res], manifest: AndroidManifest.xml, sdk_version: system_current, certificate: platform, dex_preopt: true, privileged: true, // 如果希望编译到product分区可以加上 // product_specific: true, }Android.mk的写法也是一样的效果LOCAL_PATH : $(call my-dir) include $(CLEAR_VARS) LOCAL_PACKAGE_NAME : MyApp LOCAL_SDK_VERSION : system_current LOCAL_CERTIFICATE : platform LOCAL_PROGUARD_ENABLED : disabled include $(BUILD_PACKAGE)写完构建文件之后再到产品配置mk里引用。RK3568一般是在device/rockchip/rk3568/RK3568.mk里加PRODUCT_PACKAGES MyAppcertificate: platform和LOCAL_CERTIFICATE : platform的意思是用平台密钥给APK签名这样App才能拿到signature级别的系统权限。privileged: true决定APK最终是放到/system/priv-app还是/system/app前者可以使用privileged级别的权限但要求配白名单后者只能使用普通权限和signature级别的权限不需要白名单。这里有个经验之谈只要业务上不是非要privileged权限就不要轻易用priv-app因为白名单配置和维护很麻烦一不小心漏了一项App启动就会静默报权限拒绝。2.3 预编译第三方APK没有源码怎么玩市面上更多的预置需求是“我有一坨APK你帮我做进系统”。这种情况不需要源码直接用预编译模块方式处理。推荐在vendor/rockchip/common/apps/MyApp/目录下建一个Android.mkLOCAL_PATH : $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE : MyApp LOCAL_MODULE_CLASS : APPS LOCAL_MODULE_SUFFIX : .apk LOCAL_SRC_FILES : MyApp.apk LOCAL_CERTIFICATE : platform LOCAL_MODULE_PATH : $(TARGET_OUT_PRIV_APPS) include $(BUILD_PREBUILT)把MyApp.apk放在和Android.mk同一个目录然后同样在PRODUCT_PACKAGES里加MyApp。编译时系统会把这个APK拷贝到/system/priv-app/MyApp/MyApp.apk然后执行dex优化、签名校验等流程。如果你的APK内部使用了第三方so库而且so库被打包在APK的lib/目录下那么编译期需要确认平台的USE_32_BIT或CPU架构变量匹配。如果目标是RK3568的64位系统但APK里只有lib/armeabi-v7a的so系统可能会在运行时找不到native库。这时候要么找供应商要64位版的APK要么在mk里添加兼容性配置。预编译APK还有一个坑是原始签名如果APK是用市场签名或第三方release签名打包的而你改成LOCAL_CERTIFICATE : platformAPK里原有的签名会被重新签名覆盖但应用内部如果对签名做了二次校验很多加固应用会校验自己是否被重打包那开机后就会崩溃或直接退出。2.4 不可卸载模式最容易翻车的三个点第一点priv-app权限白名单。放到/system/priv-app后如果App有READ_PRIVILEGED_PHONE_STATE之类的privileged权限必须排查白名单文件比如/system/etc/permissions/privapp-permissions-platform.xml。缺了的话logcat会打出一行类似Privileged permission android.permission.READ_PRIVILEGED_PHONE_STATE for package com.example.myapp not in privapp-permissions whitelist的警告然后App拿不到权限表现为功能异常但不一定崩溃特别迷惑。第二点签名一致性。用platform签名预置后用户后续通过应用商店升级如果商店的包是debug签名或第三方签名就会出现INSTALL_FAILED_UPDATE_INCOMPATIBLE导致无法升级。第三点odex/vdex预优化。Android 12在编译期默认会生成odex文件如果预置后你在adb shell里去看会看到/system/priv-app/MyApp/oat/arm64/下有MyApp.odex和MyApp.vdex。这个优化能加快开机和启动速度但如果后续通过adb安装新版本而新版本代码没变PMS有时候会复用旧的odex导致明明装了新版代码却没生效。遇到这种诡异问题先把旧的预置包和oat目录清干净再验证。3. 模式二可卸载APK出厂预置但不绑死用户3.1 什么时候用可卸载模式商用设备里可卸载预置最常见的需求是“出厂带一个XX应用用户可以装也可以卸”。比如视频App、天气预报、浏览器、推广类应用甚至一些给客户试用的APK。这种模式下你不希望APK被用户直接删文件但你需要它在设置-应用里有一个红色的“卸载”按钮。这三种模式里可卸载预置是最容易被误解的很多人以为让用户能卸载就是把LOCAL_MODULE_PATH换成$(TARGET_OUT_DATA_APPS)然后加PRODUCT_PACKAGES就结束了实际上这里面的逻辑还涉及首次开机安装机制。3.2 方案一把预置模块输出到data/app在Android.mk里把LOCAL_MODULE_PATH指向$(TARGET_OUT_DATA_APPS)include $(CLEAR_VARS) LOCAL_MODULE : MyApp LOCAL_MODULE_CLASS : APPS LOCAL_MODULE_SUFFIX : .apk LOCAL_SRC_FILES : MyApp.apk LOCAL_CERTIFICATE : releasekey LOCAL_MODULE_PATH : $(TARGET_OUT_DATA_APPS) include $(BUILD_PREBUILT)编译后APK会出现在out/target/product/rk3568/data/app/MyApp/MyApp.apk。如果你的平台打包逻辑会把这个data目录打进了userdata镜像或super镜像那么首次开机时PMS扫描/data/app就会把它当作一个普通应用安装好用户可以卸载。卸载后文件从/data/app删除恢复出厂设置时data分区被重置APK又从基础镜像的预置数据里重新出现。这里要特别提醒TARGET_OUT_DATA_APPS这个路径在通用AOSP的构建里并不保证一定会被打进最终固件。RK SDK通常会把data目录纳入固定打包但具体看方案商怎么改的。如果你的编译产物里能找到APK但烧录后在/data/app却看不到先检查打包脚本是否过滤了data目录或者直接改用下面的方案二。3.3 方案二只读分区预置目录 首次开机安装服务这个方案更通用也更贴合很多方案公司的做法。把APK放在/system/preinstall/或者/vendor/preinstall/目录下然后在系统里内置一个预置安装服务PreinstallAppInstaller首次开机的服务阶段扫描这个目录调用PackageInstaller或者PackageManager的静默安装接口把APK装到data分区。之后/data/app里就有了这个应用用户可以卸载。实现一个简单的预置安装服务核心逻辑大致是检查/data/system/preinstall_done标记是否存在遍历/system/preinstall目录下的APK文件解析包名和版本号如果该包未安装或版本不同调用PackageInstaller.Session静默安装全部安装完毕后写入标记文件。这段逻辑要注意避免开机时一次性安装太多大APK导致启动时间暴涨一般做法是延迟到开机完成广播后再执行并且只做差量安装。卸载后不会重复安装因为标记文件已经存在如果你希望恢复出厂后重新安装清空data分区自然就清掉了标记预置目录里的APK会在下一次开机重新被服务安装。这套逻辑好理解也好排查适合定制化项目。3.4 可卸载预置的细节坑可卸载预置的第一个坑是签名。如果你用platform签名给“可卸载”预置APK签名用户卸载后通过应用商店装了一个普通签名版本会因为签名不一致而无法覆盖安装更麻烦的是即便用户卸载干净了签名不一致的包还可能触发INSTALL_FAILED_PERMISSION_MODEL_UPGRADE之类的错误。所以可卸载预置应尽量使用releasekey或者普通签名不要为了省事用platform。第二个坑是卸载后系统功能缺失。如果这个App是设备的默认浏览器卸载后点击浏览器链接系统会找不到handler这需要产品侧评估。第三个坑是apk文件的目录权限和SELinux标签预置目录和APK的标签如果不对安装服务去读文件会SELinux拒绝logcat里能看到avc: denied { read }这不是小问题因为服务是后台静默执行的出错了也不会有弹窗提醒。4. 模式三永久删除APK把痕迹从固件里拔干净4.1 永久删除的场景比你想的复杂永久删除的常见需求有客户不要系统自带的浏览器/计算器、要去掉厂商Demo应用、要去掉测试阶段塞进去的APK。这个需求看起来简单实际做起来坑不少。最典型的场景是明明我把PRODUCT_PACKAGES里的模块注释掉了重新编译烧录App还是出现在应用列表里。这种情况说明你只删了一个入口但预置APK还有其他来源没有清理干净。4.2 清理源码型预置模块如果App是源码型预置类似packages/apps/Calculator需要做两件事一是从所有产品mk里搜Calculator模块名把PRODUCT_PACKAGES对应的行删掉二是确认没有其他地方通过PRODUCT_PACKAGES ...动态拼接引用。可以用一条命令在SDK根目录下搜索grep -rn Calculator device/rockchip/ vendor/rockchip/ packages/apps/ build/target/ --include*.mk --include*.bp如果模块名没有其他地方引用重新编译后system镜像里就不会再有Calculator了。源码目录不一定要删除保留代码但不在产品引用不会编进系统这样以后还可以快速恢复。4.3 清理预编译APK的完整步骤预编译APK大多放在vendor/rockchip/common/apps/XXX/下。删除时先看这个目录里Android.mk或Android.bp中的模块名然后在device/rockchip/rk3568/RK3568.mk、device.mk以及vendor/rockchip/common/下的相关mk里移除该模块名。特别留意Rockchip的公共mk有的SDK会把一批预置应用打包成一个变量通过循环添加你只看RK3568.mk是发现不了的。# vendor/rockchip/common/apps/Android.mk 示例 PRODUCT_PACKAGES \ RockchipVideoPlayer \ RockchipFileManager \ RockchipUpdateService删除时要把对应行摘掉或者把整个变量里的模块移除。还要检查是不是用PRODUCT_COPY_FILES直接拷贝的。这种预置方式比较原始但依然存在PRODUCT_COPY_FILES \ vendor/rockchip/common/apps/MyApp.apk:system/app/MyApp/MyApp.apk如果是这样直接把这一行注释掉即可但要注意通过PRODUCT_COPY_FILES拷贝的APK不会做签名处理直接就是原文件放进去这种预置方式要单独处理签名问题。4.4 永久删除后“删不干净”的隐藏来源删除操作里最容易忽略的是system分区里APK之外的关联文件。权限白名单文件就是典型即使模块已经从PRODUCT_PACKAGES移除如果/system/etc/permissions/privapp-permissions-platform.xml里还留着该包的权限声明logcat会一直出现unknown package之类的警告虽然不影响运行但很脏。要一并删除。其次是一些App依赖的jar包或so库例如某App把公共库放在/system/framework/删App不删lib会导致库文件躺在系统里占空间但这个要谨慎不能凭感觉删要搜一下是否还有其他App依赖。还有一个非常容易踩的坑是data分区残留。用户手里的设备如果之前装过旧固件待删除的App可能已经作为用户应用存在/data/app里。全新烧录固件后data分区被格式化这个问题在开发机上基本不会看到但如果是OTA升级固件data分区不会清空旧App还留在data分区用户会看到一个“返祖”现象新固件明明删了预置App升级后App还在而且可以正常打开。解决方式是在OTA升级脚本中针对这个包名做pm uninstall或者删除/data/app下对应文件并在packages.xml里清理记录。4.5 永久删除的Checklist具体操作时我习惯按下面这个清单逐项排查全仓搜索模块名和apk文件名确认所有引用点清理PRODUCT_PACKAGES和PRODUCT_COPY_FILES中的条目移除privapp-permissions-*.xml里的权限声明检查/system/framework、/system/lib、/system/lib64下的关联文件搜索sepolicy相关规则删除单独的SELinux策略文件如果有OTA升级场景准备升级脚本中的清理逻辑清理后再编译解包镜像并确认目标APK确实不存在。5. 三种模式对比与选型建议5.1 一张表看清三种模式的本质对比项不可卸载可卸载永久删除放置分区system/vendor/product只读分区data分区或首次开机安装不放置用户能否卸载不能能不适用恢复出厂后仍然存在会重新预置不存在权限模型可申请signature/privileged权限普通第三方应用权限不适用签名要求建议platform签名releasekey或普通签名不适用应用商店升级困难签名冲突和分区限制正常升级不适用主要风险升级维护成本高用户卸载后业务中断误删关键系统组件从这张表能看出来不可卸载和可卸载的差异本质不在“系统里有没有这个APK”而是APK的物理载体在哪里。只读分区的APK系统压根不会有卸载选项data分区里的APK哪怕出厂镜像里放了一份也只是一个“预置安装源”安装完成后它就是普通应用用户可以卸载。5.2 选型建议问自己三个问题我现在接到预置需求第一反应不是去写mk而是先问产品经理三个问题这个App需不需要系统权限用户有没有正当理由卸载掉它后续迭代频率高不高如果App为了读取设备序列号、修改网络配置等必须用系统权限那几乎只能选不可卸载而且要提前规划好签名和白名单。如果只是一个内容类App用户很可能会卸载那就选可卸载不要为了“固件里必须有”去牺牲用户体验。如果是“客户不想要某个系统App”那就是永久删除删除前务必评估这个App是不是系统里某个功能的唯一提供者比如把系统设置删了机器直接没法用了。在影响范围层面不可卸载模式影响最大因为它涉及到OTA升级、权限白名单、SELinux策略、应用签名等多项系统机制改一个App的bug可能要整体发版。可卸载模式影响小一些用户可以从应用商店拉新版本不用等整包OTA。永久删除的影响是隐性且长期的如果删得不够干净后续维护者很容易被残留文件误导浪费大量排查时间。6. 实操踩坑与问题排查实录6.1 预置后开机找不到APK这个问题的排查路径是先看编译产物out/target/product/rk3568/system/app/或priv-app/有没有对应目录没有的话看模块名有没有被PRODUCT_PACKAGES正确引用有产物但烧录进设备找不到再看logcat里PMS扫描结果搜索PackageManager scan相关日志。常见原因是模块名拼写错误或者APK的AndroidManifest里没有launcher入口宣告为普通应用时PMS扫描后也不会显示在应用列表但ADB里pm list packages能看到。6.2 APK一直崩溃或权限被拒绝如果是启动崩溃logcat里常见Permission Denial或SecurityException。优先怀疑两点签名不匹配和priv-app白名单缺失。如果你用了platform签名但App运行时的调用者是第三方签名系统校验签名权限就会失败。如果你把App放进了priv-app立刻检查白名单。很多人容易忽略的是App的targetSdkVersion为31以上时Android 12对REQUEST_INSTALL_PACKAGES这类权限的授权方式也有变化需要在manifest里声明。6.3 可卸载预置恢复出厂后没有重新出现这个问题多半出在“方案一”中TARGET_OUT_DATA_APPS没有被正确打包到用户数据镜像。检查你的编译脚本确认out目录下的data目录有没有打进固件。如果平台不支持就改用/system/preinstall 首次开机安装服务。另外还要确认预置安装服务是否被安全软件或系统去掉了有些精简固件会把安装服务模块裁剪掉这会导致预置功能名存实亡。6.4 可卸载APK卸载后无法重装如果可卸载APK用的是platform签名卸载后用户从网上下载的是普通签名包安装时会报签名冲突或INSTALL_FAILED_UPDATE_INCOMPATIBLE。这个问题的根源是签名策略选错了可卸载预置应该用releasekey或普通签名而不是platform。如果已经发布出去没法改签名只能让用户先清除残留记录再安装或者引导用户恢复出厂设置。6.5 “永久删除”后应用依然出现先别急着怪自己没删干净。在全新烧录的机器上如果新固件里没有App但pm list packages还能看到十有八九是data分区有残留。OTA升级场景尤其常见。另外有一种情况是App被另一个系统组件作为“依赖包”拉起比如某个Android.mk里通过LOCAL_REQUIRED_MODULES把它带了回来。这时候全仓搜索模块名就会找到真正的入口。6.6 常见问题速查表问题现象排查方向解决方案预置后应用列表找不到编译产物、PMS日志、manifest入口检查PRODUCT_PACKAGES和manifest launcher开机崩溃或权限拒绝logcat搜索Permission Denial换platform签名或补priv-app白名单logcat大量priv-app白名单警告搜索not in privapp-permissions把缺失权限加进白名单xmlSELinux avc denieddmesg查看avc日志在sepolicy中补充allow规则可卸载预置开机后没安装data/app路径是否打包进镜像改用只读目录首次安装服务可卸载预置卸载后装不回签名不一致换releasekey/普通签名删除后OTA升级又出现data分区残留升级脚本清理包和数据删了模块还有依赖LOCAL_REQUIRED_MODULES或PRODUCT_COPY_FILES全仓搜索并按清单逐项清理以上这些坑基本都是我在实际RK3568项目里一个个踩过来的。特别是第一种“不可卸载”的落地从编译能通过到开机不崩再到功能和性能都正常这条路需要走好几轮真机验证。我的个人体会是不要迷信“某个方案一定是对的”每种模式都是一个取舍选型之前把分区、签名、权限、升级路径全部想清楚比盲目写代码节省的时间多得多。如果项目里可以接受用户卸载我通常优先选用可卸载方案因为后期不需要为预置应用的更新去挤OTA窗口如果必须做系统级功能那不可卸载模式一定要在立项时就把签名和权限白名单的维护成本一并算进去。