Android 16 MTK平台内置应用可卸载方案:分区配置与OTA策略详解
1. 从“预装即永久”说起:为什么要在Android 16上折腾可卸载
做过系统定制的朋友都清楚一个老规矩:预装应用一旦进了system分区,用户就只能在设置里“停用”,想彻底删掉基本没门。这个设计在早期是为了保证系统完整性和运营商合规,但放到今天,用户对“我的设备我做主”的诉求越来越强,尤其是那些占着几百兆空间、平时根本用不上的内置应用,删不掉就成了体验上的硬伤。Android 16配合MTK平台在这一块给出了更成熟的方案,核心思路是把“预装”和“不可卸载”这两件事解耦——应用可以随系统出厂,但用户有权把它请出去。
这篇内容面向的是做ROM定制、系统集成、以及负责预装应用管理的工程师。我会把MTK平台上实现内置应用可卸载的完整链路拆开讲:从分区布局、配置文件的写法,到卸载后如何保证系统不崩、OTA升级怎么处理、以及实测中那些文档里不会写的坑。关键词围绕Android 16、MTK平台、内置应用、可卸载、预装配置、分区管理展开,读完之后你应该能独立完成一套可卸载预装方案的落地。
先说清楚一个前提:可卸载不等于随便删。系统级应用分三六九等,核心服务、框架组件、签名权限相关的应用绝对不能开放卸载,否则轻则功能异常,重则开不了机。所以这套方案的本质是“有选择地开放卸载能力”,而不是一刀切。下面从最基础的分区认知开始,一步步往上搭。
2. 分区布局决定了应用能不能被卸载
2.1 system、product、vendor三个分区的卸载权限差异
在MTK的Android 16方案里,预装应用通常分布在三个位置:/system、/product、/vendor。这三个分区对卸载的友好程度完全不同,理解这一点是后续所有配置的基础。
/system分区承载的是最核心的框架和系统应用,这里的应用默认是ro挂载、受签名保护,直接开放卸载风险极高。/product分区是OEM放定制应用的主力位置,Android 10之后Google大力推行product分区隔离,就是为了让厂商预装和系统核心解耦,所以可卸载应用优先放product分区。/vendor分区则是给芯片平台和硬件相关的应用,MTK的很多底层服务在这里,一般不建议动。
我实测下来的经验是:把需要开放卸载的应用从system挪到product,是整套方案里性价比最高的一步。原因很简单,product分区的应用在卸载时不会触碰system的完整性校验,系统启动时的dm-verity校验压力小很多。具体操作上,在device/mediatek/项目名/下的分区配置里调整PRODUCT_PACKAGES的归属,把目标应用从system的mk文件移到product的mk文件里。
注意:挪分区不是改个路径就完事,应用的签名、共享UID、权限声明都要重新核对。尤其是带
sharedUserId的应用,跨分区后UID分配可能变化,会导致数据访问失败。
2.2 为什么product分区是开放卸载的最佳落点
接着上面说,product分区之所以适合,还有一个容易被忽略的原因:它在OTA时的更新粒度更细。system分区的任何改动都会触发全量校验,而product分区支持独立更新。这意味着你给某个预装应用打了可卸载标记后,后续通过OTA调整这个标记,不需要动整个system镜像,升级包体积和风险都可控。
从用户视角看,product分区的应用卸载后,系统设置里的“已卸载应用”列表能正确记录,恢复出厂设置时也能按预期还原。这一点在MTK的实测中表现稳定,而system分区的应用卸载后,恢复逻辑经常出问题——要么恢复不回来,要么恢复后权限丢失。所以我的建议很明确:凡是计划开放卸载的预装应用,一律迁到product分区,这是后面所有配置能顺利生效的前提。
2.3 分区迁移的实操步骤与校验方法
具体操作分三步。第一步,在应用的Android.bp或Android.mk里确认模块归属,把LOCAL_MODULE_PATH指向product对应目录,MTK平台通常是$(TARGET_OUT_PRODUCT)/app。第二步,在产品的分区配置文件里把模块名加到PRODUCT_PACKAGES,同时从system的列表里移除,避免重复打包。第三步,编译后通过adb shell pm path 包名确认应用的实际安装路径,输出里带/product/才算迁移成功。
校验环节我习惯多做一步:用adb shell dumpsys package 包名看codePath和resourcePath是否都指向product,再看flags里有没有异常标记。曾经遇到过一次迁移后应用能装但无法启动,最后查出来是resourcePath还残留system路径,原因是mk文件里有个旧的覆盖规则没删干净。这种问题编译阶段不报错,只能靠运行时校验发现,所以这一步别省。
3. 可卸载标记的配置入口与写法
3.1 从Android 14到16的标记机制演进
早些年开放卸载靠的是在应用清单里加android:enabled配合系统白名单,做法零散且容易失效。Android 14开始,AOSP引入了更规范的预装应用管理机制,到Android 16这套机制在MTK平台上已经比较完善。核心是一个系统级的配置文件,用来声明哪些预装应用允许被用户卸载,以及卸载后的行为策略。
这个配置的载体在不同版本里位置有调整,Android 16上MTK的方案主要落在/product/etc/下的预装应用配置文件中。它的好处是把“应用清单”和“卸载策略”集中管理,不用在每个应用的清单文件里散落标记,维护成本低很多。理解这个演进很重要,因为网上很多老教程还在教改清单文件,那套方法在新版本上要么不生效,要么和系统策略冲突。
3.2 配置文件的关键字段逐个拆解
配置文件的核心字段不多,但每个都有讲究。我按实际用到的顺序说:
| 字段 | 作用 | 常见取值与注意点 |
|---|---|---|
| 包名标识 | 指定目标应用 | 必须和实际包名完全一致,大小写敏感 |
| 卸载策略 | 控制是否允许卸载 | 通常有“允许”“禁止”“条件允许”几档 |
| 恢复策略 | 卸载后如何还原 | 决定恢复出厂时是否回来 |
| 数据保留 | 卸载时是否清数据 | 影响用户重装后的体验 |
这里重点说“恢复策略”。它有两个典型取值:一是卸载后彻底移除,恢复出厂也不回来;二是卸载后保留恢复能力,恢复出厂时重新出现。前者适合那些纯占位、用户基本不需要的应用,后者适合可能后续还要用的工具类应用。选错这个字段,用户会投诉“删了又回来”或者“想恢复恢复不了”,所以要根据应用定位仔细定。
3.3 一个可直接套用的配置示例
下面是我在MTK Android 16方案上验证过的一段配置结构,包名用占位符代替:
<preinstalled-apps> <app package="com.example.demo.tool"> <uninstall allowed="true" /> <restore on-factory-reset="true" /> <data keep-on-uninstall="false" /> </app> <app package="com.example.demo.service"> <uninstall allowed="false" /> </app> </preinstalled-apps>这段配置的意图很直白:第一个应用允许卸载、恢复出厂会回来、卸载时清数据;第二个应用禁止卸载。实际写的时候要注意,配置文件的根节点名称和属性名以你手上MTK release的文档为准,不同季度版本可能有微调,但整体结构稳定。写完编译进product镜像后,用adb shell进到对应目录cat一下确认文件真的进去了,再进设置里验证卸载按钮是否出现。
提示:配置文件语法错误不会导致编译失败,但会让整个策略静默失效。建议改完后第一时间用
logcat过滤预装应用管理相关的tag,看解析日志有没有报错。
4. 卸载之后系统为什么还能稳住
4.1 卸载动作在系统层到底发生了什么
很多人以为卸载就是把apk删了,实际远不止。用户点卸载时,系统会走一遍PackageManager的卸载流程:先校验这个包是否在允许卸载的白名单里,再检查有没有其他应用依赖它的共享库或ContentProvider,然后清理它的数据目录、权限授予记录、以及注册的组件。任何一环出问题,卸载就会失败或者留下残留。
MTK平台在这套流程上做了自己的加固,尤其是对预装应用的卸载会额外触发一次系统完整性检查。这就是为什么前面强调要把应用放product分区——product分区的卸载不会触发system的dm-verity重校验,流程顺畅得多。如果应用还在system分区,卸载时系统会尝试用overlay机制“遮盖”而不是真删,用户看到的是卸载了,实际apk还在,空间也没释放,体验很割裂。
4.2 依赖关系排查:哪些应用绝对不能开放卸载
开放卸载前必须做依赖排查,这一步偷懒后面必出事。排查方法不复杂:用adb shell dumpsys package看目标应用的sharedUserId、uses-library、以及有没有被其他应用通过<queries>或Provider引用。重点盯三类应用:提供系统服务的、被其他应用共享UID的、以及声明了核心权限的。
我踩过的一个坑是某个预装输入法,看着人畜无害,结果它给系统设置提供了一个Provider。开放卸载后用户删掉它,设置里的语言选项直接崩溃。后来用dumpsys activity providers才定位到依赖关系。所以我的做法是:任何要开放卸载的应用,先在测试机上手动卸载,然后跑一遍核心功能回归,包括设置、拨号、相机、文件管理这些高频入口,确认没有连锁反应再放行。
4.3 卸载失败与残留的典型场景处理
实测中卸载失败最常见的原因是应用正在运行且持有前台服务。系统会拒绝卸载有活跃前台服务的应用,这时候要么引导用户先停止,要么在配置里允许强制卸载。残留问题则多出现在数据目录清理不彻底,表现为重装后读到旧数据。解决办法是在配置里明确keep-on-uninstall="false",同时在应用侧确保android:allowBackup和卸载清理逻辑不冲突。
还有一种隐蔽情况:应用卸载后,它的桌面快捷方式、通知渠道、账户记录没有同步清理。这些残留不影响功能,但用户会觉得“没删干净”。处理方式是在系统侧监听卸载广播做二次清理,或者在应用清单里正确声明相关组件,让系统卸载流程自动带走。这块细节多,建议列个清单逐项验证,别凭感觉。
5. OTA升级与恢复出厂时的策略衔接
5.1 用户卸载过的应用在OTA后会不会复活
这是产品经理最爱问的问题,也是方案设计里必须提前定死的点。答案取决于配置里的恢复策略和OTA的类型。如果是全量OTA,系统会按新镜像重建product分区,此时配置里restore on-factory-reset="true"的应用会重新出现,用户之前的卸载记录会被覆盖。如果是增量OTA,系统会尽量保留用户数据区的卸载状态,但前提是OTA包没有强制覆盖该应用。
我的建议是把策略写进产品需求文档:明确哪些应用OTA后允许复活、哪些必须尊重用户选择。技术上可以通过在数据区记录卸载状态、OTA时读取该状态来决定是否重新预装。MTK的方案里对这块有支持,但需要额外配置,不是默认行为。不做这层处理,用户升级完发现删掉的应用又回来了,投诉量会很难看。
5.2 恢复出厂设置时的还原逻辑验证
恢复出厂是另一条关键路径。配置里声明可恢复的应用,在恢复出厂后应该重新出现在系统里,且功能正常。验证方法是:卸载目标应用,执行恢复出厂,进系统后确认应用存在、能启动、权限正常。这里容易出问题的是权限授予——恢复后的应用如果拿不到运行时权限,首次启动会弹一堆授权框,体验很差。
处理办法是在预装配置里同时声明默认权限授予,让系统在恢复时自动补齐关键权限。另外要注意,恢复出厂会清数据区,如果应用依赖数据区的某些初始化文件,恢复后可能缺文件。这类应用要么把初始化逻辑做进首次启动,要么在恢复流程里加一步数据重建。实测下来,纯工具类应用问题不大,带账户体系的应用要格外小心。
5.3 增量升级包对卸载状态的兼容处理
增量OTA的兼容性最考验方案成熟度。核心原则是:OTA脚本在更新product分区时,要能识别并跳过用户已卸载的应用,而不是无脑覆盖。这需要在OTA的升级脚本里加入状态判断逻辑,读取数据区的卸载记录,对已卸载应用做条件安装。
具体实现上,可以在升级脚本里调用一个查询接口,返回目标应用是否处于“用户主动卸载”状态,是则跳过安装步骤。这个接口的实现依赖系统侧的卸载记录存储,MTK平台有对应的数据表,但需要确认你的release版本是否默认开启记录。没开启的话,卸载状态在OTA后就丢了,等于白做。所以方案落地前,务必在测试机上跑一遍“卸载—增量OTA—检查状态”的完整链路。
6. 实测踩坑记录与几条硬经验
6.1 配置文件生效但卸载按钮不出现的排查链路
遇到过最典型的问题:配置文件写对了、编译进去了、日志也没报错,但设置里的卸载按钮就是不出现。排查链路我整理成了一条线:先确认应用实际安装路径是不是product(pm path),再确认配置文件所在目录的权限和SELinux标签是否正确,然后看PackageManager启动时有没有加载到这份配置(过滤相关log),最后检查应用本身有没有声明不可卸载的flag。
我那次卡在SELinux标签上,配置文件放对了目录但标签是默认的,系统读的时候被拒了,日志里只有一行很不起眼的avc denied。加上正确标签后立刻生效。这个坑的教训是:Android 16上SELinux策略收紧,任何新增的配置文件都要同步配标签,别指望默认策略放行。
6.2 多用户场景下卸载状态的隔离问题
设备支持多用户时,卸载状态是按用户隔离的。A用户卸载了某应用,B用户登录后应该还能看到它。这个行为本身合理,但如果配置或存储逻辑没处理好,会出现A卸载导致B也看不到的情况。实测中这问题出现在卸载记录存储到了全局而非按用户分区的位置。
处理方式是确认卸载状态记录在用户数据分区下,按userId隔离。验证方法很简单:建两个用户,在用户A下卸载,切到用户B看应用是否还在。不在就说明隔离没做好。这个场景在平板和商用设备上很常见,做方案时别只测单用户。
6.3 给后来者的三条实操建议
第一条,先小范围灰度。挑一两个低风险的工具类应用先开放卸载,跑一轮完整回归再扩大范围,别一上来就把十几个应用全放开。第二条,把依赖排查做成脚本。手动查依赖容易漏,写个脚本自动扫sharedUserId和Provider引用,每次加新应用跑一遍,省时省心。第三条,OTA和恢复出厂必须纳入测试用例。很多方案在单机卸载上没问题,一到升级和恢复就露馅,这两条路径的测试成本不能省。
这套方案在MTK Android 16上跑通之后,维护成本其实不高,真正花时间的是前期的依赖梳理和后期多路径验证。把分区、配置、依赖、OTA这四块吃透,剩下的就是按部就班地填应用清单。我在实际项目里最大的体会是:可卸载这件事,技术实现只占三成,七成在于想清楚“哪些能删、删了怎么办”,把策略定在前面,后面写配置就是水到渠成的事。