- 文中若有疏漏,欢迎评论区指正交流
- 转载请注明出处,谢谢支持
目录
- 一、概念与定位
- 二、属性速查
- 三、典型场景
- [3.1 动态切换桌面图标](#3.1 动态切换桌面图标 "#31-%E5%8A%A8%E6%80%81%E5%88%87%E6%8D%A2%E6%A1%8C%E9%9D%A2%E5%9B%BE%E6%A0%87%E5%B8%B8%E7%94%A8")
- [3.2 重命名 Activity 不破坏快捷方式](#3.2 重命名 Activity 不破坏快捷方式 "#32-%E9%87%8D%E5%91%BD%E5%90%8D-activity-%E4%B8%8D%E7%A0%B4%E5%9D%8F%E5%BF%AB%E6%8D%B7%E6%96%B9%E5%BC%8F%E5%85%B3%E9%94%AE")
- [3.3 节日 / 主题临时图标](#3.3 节日 / 主题临时图标 "#33-%E8%8A%82%E6%97%A5--%E4%B8%BB%E9%A2%98%E4%B8%B4%E6%97%B6%E5%9B%BE%E6%A0%87")
- [3.4 同一 Activity 不同启动方式](#3.4 同一 Activity 不同启动方式 "#34-%E5%90%8C%E4%B8%80-activity-%E4%B8%8D%E5%90%8C%E5%90%AF%E5%8A%A8%E6%96%B9%E5%BC%8F")
- [3.5 隐藏 / 显示应用入口](#3.5 隐藏 / 显示应用入口 "#35-%E9%9A%90%E8%97%8F--%E6%98%BE%E7%A4%BA%E5%BA%94%E7%94%A8%E5%85%A5%E5%8F%A3")
- 四、运行时切换代码
- 五、与相关方案对比
- 六、版本兼容性
一、概念与定位
1.1 是什么
<activity-alias> 给真实的 <activity> 额外挂一个或多个入口标识 。它本身不对应任何 Activity 类,但拥有独立的 ComponentName,可独立配置图标、标签、intent-filter 等"入口侧"属性;启动后实际运行的仍是 targetActivity 指向的真实 Activity。
css
┌──────────────────────────┐
桌面图标 A ───► │ │
│ targetActivity = │
桌面图标 B ───► │ .MainActivity │
│ (唯一真实 Activity) │
快捷方式 C ───► │ │
└──────────────────────────┘
每个 alias:
- 拥有独立的
ComponentName,系统据此区分入口 - 可独立声明 7 个属性:
enabled、exported、icon、label、name、permission、targetActivity - 可独立包含
<intent-filter>与<meta-data>两个子元素 - 共用 target Activity 的类与业务逻辑
1.2 用来解决什么
| 场景 | 不用 alias | 用 alias |
|---|---|---|
| 动态换桌面图标 | 做不到(图标在 APK 里固定) | 切换 alias 的 enabled 状态 |
| 多个桌面入口 | 多写 Activity,逻辑重复 | 一个 Activity + 多个 alias |
| 改 Activity 类名 | 用户快捷方式全失效 | 留老类名 alias 兜底 |
| 节日 / 活动入口 | 重新发版换主题 | 预埋 alias,远程开关启用 |
| 隐藏入口(家长模式 / MDM) | 直接禁带 LAUNCHER 的 Activity,与业务耦合 | 用 alias 承载 LAUNCHER,禁的是 alias,与业务 Activity 解耦 |
1.3 前置约束
xml
<activity android:name=".MainActivity" android:exported="false" />
<activity-alias
android:name=".MainActivityAlias"
android:targetActivity=".MainActivity" ... />
targetActivity必须指向同一 application 内已声明的<activity>- alias 节点必须出现在 target
<activity>节点之后,否则编译失败
二、属性速查
2.1 官方支持的 7 个属性
| 属性 | 必填 | 默认 | 说明 |
|---|---|---|---|
android:name |
✓ | --- | 别名标识,类似类全限定名,但不必对应真实类 |
android:targetActivity |
✓ | --- | 真实 Activity 的类名,必须是同一 application 内已声明的 <activity> |
android:enabled |
--- | true |
是否启用,运行时可通过 setComponentEnabledSetting 修改 |
android:exported |
--- | 见下 | 是否可被外部应用启动;强制声明规则见下 |
android:label |
--- | 回退到 application | 别名独立标签,不继承 target 的 label |
android:icon |
--- | 回退到 application | 别名独立图标,不继承 target 的 icon |
android:permission |
--- | 无 | 调用方需持有的权限;设置后替代 target 的 permission,未设置则通过 alias 启动不需要权限(不是回退到 target 的 permission) |
上述 7 项以外的属性(theme、launchMode、taskAffinity、directBootAware、process、configChanges、screenOrientation 等)不能在 alias 上声明,运行时直接沿用 target 的值。
android:roundIcon不在官方<activity-alias>语法里。部分 AGP / 设备允许在 alias 上声明并生效,但不是官方契约。推荐做法:在<application>统一声明roundIcon,alias 只覆盖icon;必须在 alias 上区分时请实测。
2.2 exported 的两条规则
默认推断 (与 <activity> 一致):
- 含
<intent-filter>→ 默认true - 不含
<intent-filter>→ 默认false
强制显式声明 :targetSdkVersion ≥ 31(Android 12)且 alias 含至少一个 <intent-filter> 时,必须显式写 android:exported,否则构建失败。这条规则同样适用于 <activity>、<service>、<receiver>,不是 alias 专属。
2.3 启用链
<application android:enabled> 与 <activity-alias android:enabled> 必须同时为 true ,alias 入口才会生效;任一为 false,alias 失效。这是官方明确声明的条件。
target <activity> 的 enabled 不在官方的"alias 启用链"中,但 target 被禁用后仍会启动失败(找不到可实例化的目标)。生产环境一般保持 target enabled=true。
2.4 alias vs <activity> 一眼对比
| 维度 | <activity> |
<activity-alias> |
|---|---|---|
| 对应真实类 | 必须 | 不必(name 只是标识) |
theme / launchMode / taskAffinity / directBootAware |
可声明 | 不能声明,沿用 target |
configChanges / screenOrientation / process |
可声明 | 不能声明,沿用 target |
label / icon / enabled / exported / permission |
可声明 | 可独立声明,不沿用 target |
<intent-filter> / <meta-data> |
可声明 | 可独立声明 |
一句话:alias 只能改"入口侧",改不了 Activity 的运行时行为。
三、典型场景
3.1 动态切换桌面图标【常用】
最常见的用法:让用户在 App 内选择图标(默认 / 暗黑 / Pro / 多语言 / 节日...),切换后桌面图标随之更新。无论承载哪种业务维度,套路相同:清单预埋多个 alias,运行时启用其一、禁用其余。
清单:预埋多套入口
xml
<application
android:name=".App"
android:icon="@mipmap/ic_launcher_default"
android:label="@string/app_name">
<!-- 真实 Activity:本身不作为启动入口 -->
<activity android:name=".MainActivity" android:exported="false" />
<!-- 默认图标:默认启用 -->
<activity-alias
android:name=".alias.Default"
android:targetActivity=".MainActivity"
android:icon="@mipmap/ic_launcher_default"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>
<!-- 暗黑图标:默认禁用;其他业务维度(Pro / 多语言 / 节日)照搬这块即可 -->
<activity-alias
android:name=".alias.Dark"
android:targetActivity=".MainActivity"
android:icon="@mipmap/ic_launcher_dark"
android:enabled="false"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>
</application>
运行时切换
切换逻辑见 §四,直接调用 IconSwitcher.switchTo(context, "Dark") 即可。
要点
- 同一时刻只启用一个 LAUNCHER alias:全部禁用会让桌面图标消失(隐藏入口场景另说,见 §3.5),多个启用则桌面出现多个图标。
- 真实 Activity 不要带 LAUNCHER filter,否则它会变成一个关不掉的图标。
- 切换为异步操作,桌面刷新通常需几秒,部分国产 ROM 需重启 Launcher(详见 §4.3)。
- 旧 alias 被禁用后,绑在它身上的 pinned shortcut 会失效,需提示用户重新固定。
- 不要用多个 LAUNCHER alias 做"分流" (语言、版本等)。alias 启动后
intent.component多数返回 target Activity 类名,跨 ROM 不一致,无法稳定区分"从哪个图标进入"。需要切图标 + 不同启动行为时,应在 App 内主动切(如选语言时同步调用setApplicationLocales+IconSwitcher.switchTo);如果真的需要"不同入口跑不同 Activity",应使用多个不同的 target Activity,每个各挂一个 alias。
3.2 重命名 Activity 不破坏快捷方式【关键】
把 com.example.MainActivity 重构成 com.example.ui.HomeActivity 时,用户桌面上的快捷方式、第三方挂件、ADB / Intent 写死的入口都绑在旧的 ComponentName 上,直接改类名会让这些入口集体失效。
做法:新 Activity 用新类名,旧类名改成 alias 长期保留。
xml
<activity
android:name=".ui.HomeActivity"
android:exported="false" />
<!-- 旧类名作为 alias 兜底外部入口 -->
<activity-alias
android:name=".MainActivity"
android:targetActivity=".ui.HomeActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>
已发布的 ComponentName 永远不要删 。删除后桌面快捷方式会变灰,外部 startActivity 会抛 ActivityNotFoundException。需要废弃只能改 enabled=false,留着名字。
3.3 节日 / 主题临时图标
清单结构跟 §3.1 完全一样,每个节日预埋一个 enabled="false" 的 alias。运行时由本地日历或远程配置决定切到哪个:
kotlin
fun applySeasonalIcon(context: Context, todayKey: String) {
// todayKey 由调用方根据本地日期或远程配置算出,如 "SpringFestival" / "Halloween" / "Default"
IconSwitcher.switchTo(context, todayKey)
}
3.4 同一 Activity 不同启动方式
给同一个 Activity 挂多个 alias,每个挂不同的 <intent-filter>:
- 主入口:
MAIN + LAUNCHER - 分享目标:
SEND + mimeType="image/*" - 深链接:
VIEW + BROWSABLE + scheme
桌面 LAUNCHER 入口写法同 §3.1,下面只列"分享目标"这一非桌面入口示例:
xml
<activity android:name=".ShareActivity" android:exported="false" />
<activity-alias
android:name=".alias.ShareReceiver"
android:targetActivity=".ShareActivity"
android:label="@string/share_target_name"
android:icon="@mipmap/ic_share_target"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
</intent-filter>
</activity-alias>
收益:系统分享面板可单独配置图标和标签;也可单独禁用"分享目标"能力,不影响桌面入口。
3.5 隐藏 / 显示应用入口
家长模式、企业 MDM、测试机控制台等场景:让应用从桌面消失,但仍能通过 deep link / 推送 / 其他 App 从外部调起。
只声明一个 LAUNCHER alias、再禁用它,应用就彻底失联 ------用户找不到、外部也调不进。可靠做法是至少保留一个不被禁用的 exported 入口(不带 LAUNCHER,只作 deep link / 推送的目标)。
清单:一个桌面入口 + 一个长期保留的外部入口
xml
<activity android:name=".MainActivity" android:exported="false" />
<!-- 桌面可见入口:可被 hideAppIcon 禁用 -->
<activity-alias
android:name=".alias.Visible"
android:targetActivity=".MainActivity"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>
<!-- 长期保留的外部入口:推送 / deep link / 其他 App 调起 -->
<activity-alias
android:name=".alias.ExternalEntry"
android:targetActivity=".MainActivity"
android:enabled="true"
android:exported="true">
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https"
android:host="example.com"
android:pathPrefix="/open" />
</intent-filter>
</activity-alias>
切换桌面入口(外部入口不动,仍可被调起)
kotlin
fun setDesktopIconVisible(context: Context, visible: Boolean) {
val cn = ComponentName(context, "com.example.alias.Visible")
val state = if (visible) PackageManager.COMPONENT_ENABLED_STATE_ENABLED
else PackageManager.COMPONENT_ENABLED_STATE_DISABLED
context.packageManager.setComponentEnabledSetting(cn, state, PackageManager.DONT_KILL_APP)
}
如果不需要外部调起(纯客户端家长锁),可只留 Visible 一个 alias,但 App 内必须提供可靠的解锁路径(密码 / 系统设置 / 召回入口),否则用户找不到入口极可能直接卸载。
合规边界:用 alias 隐藏入口伪装卸载、绕过家长控制 / MDM、骚扰用户等均属 Play 明确违规。合法用途仅限用户主动配置的家长模式、企业 MDM 受控设备、测试 / 调试机。
四、运行时切换代码
4.1 核心 API
切换 alias 状态的常用入口:PackageManager.setComponentEnabledSetting()(单组件)。Android 13 / API 33 起,还提供 setComponentEnabledSettings(List<ComponentEnabledSetting>),可在一次调用里原子地批量更新多个组件的启用状态------多 alias 一起切换时更推荐使用,能避免桌面图标"先消失再出现"的中间态。
kotlin
pm.setComponentEnabledSetting(componentName, state, flags)
| 参数 | 取值 | 含义 |
|---|---|---|
state |
COMPONENT_ENABLED_STATE_ENABLED |
强制启用,覆盖 manifest 的 enabled=false |
COMPONENT_ENABLED_STATE_DISABLED |
强制禁用 | |
COMPONENT_ENABLED_STATE_DEFAULT |
恢复 manifest 声明的 enabled |
|
flags |
DONT_KILL_APP |
建议必加:提示系统不要因状态变更杀进程。仅为提示而非强保证;官方明确指出修改组件状态可能导致应用行为不可预测,关键路径需测试覆盖 |
4.2 完整工具类(Kotlin)
kotlin
object IconSwitcher {
// 业务维度(图标 / 节日 / Pro 等)统一在这里注册:key -> alias 全限定名
private val ALIAS_MAP = mapOf(
"Default" to "com.example.alias.Default",
"Dark" to "com.example.alias.Dark",
"Pro" to "com.example.alias.Pro",
)
/** 切到指定 key,其余全部禁用;未知 key 回落到 "Default" 并持久化归一化后的值。 */
fun switchTo(context: Context, target: String) {
val key = if (target in ALIAS_MAP) target else "Default"
val pm = context.packageManager
ALIAS_MAP.forEach { (k, alias) ->
val cn = ComponentName(context, alias)
val state = if (k == key) PackageManager.COMPONENT_ENABLED_STATE_ENABLED
else PackageManager.COMPONENT_ENABLED_STATE_DISABLED
if (pm.getComponentEnabledSetting(cn) != state) {
pm.setComponentEnabledSetting(cn, state, PackageManager.DONT_KILL_APP)
}
}
context.getSharedPreferences("launcher", Context.MODE_PRIVATE)
.edit { putString("active_alias", key) }
}
}
若
minSdk ≥ 33,可改用pm.setComponentEnabledSettings(list)一次性原子提交所有 alias 的状态变更,避免逐个调用导致的中间态闪烁。
4.3 触发桌面刷新
切换后,系统会通过 PACKAGE_CHANGED 广播通知 Launcher,多数情况下无需手动干预。少数国产 ROM 会延迟几秒,甚至需要重启 Launcher 才能刷新。
如需兜底,只有一种相对温和的办法:跳一次桌面,部分 ROM 会顺带刷新(不保证):
kotlin
private fun nudgeLauncher(context: Context) {
val intent = Intent(Intent.ACTION_MAIN).apply {
addCategory(Intent.CATEGORY_HOME)
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
context.startActivity(intent)
}
两种"看起来管用但别做"的操作:
- 主动发
ACTION_PACKAGE_REPLACED------ protected broadcast,普通应用无权发送,调用无效 - 切完立刻
Process.killProcess(myPid())------ 等同于应用闪退,体验灾难
4.4 切换前置检查
kotlin
fun safeSwitch(context: Context, target: String) {
if (isAppInForeground(context)) {
// 前台切换易导致任务栈错乱,延后到下次冷启动
context.getSharedPreferences("launcher", Context.MODE_PRIVATE)
.edit { putString("pending_icon", target) }
} else {
IconSwitcher.switchTo(context, target)
}
}
五、与相关方案对比
| 方案 | 适用场景 | 优势 | 局限 |
|---|---|---|---|
<activity-alias> 切换 |
桌面图标、多入口、隐藏入口 | 兼容性最好,全 API 可用;intent-filter 独立 | 切换有延迟;图标必须打包内置 |
| App Shortcuts(API 25+) | 长按图标弹二级菜单 | 动态创建,可携带 Intent extras | 不能换主图标;static + dynamic 数量以 getMaxShortcutCountPerActivity() 为准(多数 launcher 一次显示 4 个),pinned 不受此限制 |
| Themed Icons(API 33+) | 跟随系统主题色 | 系统级支持,无需代码 | 仅单色化,不能换图样 |
| Launcher Layer 动态绘制 | 角标 / 红点 | 视觉灵活 | 各 OEM API 不统一,不可靠 |
| 多 APK / 多渠道 | 完全独立的应用 | 包体最优 | 用户要装两份,不能动态切 |
选型速判 :App 内切图标 → <activity-alias>;长按弹二级菜单 → App Shortcuts;跟随壁纸换色 → Themed Icons。
六、版本兼容性
<activity-alias> 自 API 1 起即存在,至今没有破坏性变更。开发上的版本差异基本来自 <activity> 的属性约束,通过 targetActivity 间接影响到 alias。
| Android | API | 与 alias 相关的关键点 |
|---|---|---|
| 1.0 | 1 | <activity-alias> 元素引入(官方:introduced in API level 1) |
| 5.0 | 21 | android:banner 在 <application>/<activity> 上引入用于 TV 启动器;不在 alias 官方语法中 ,需在 <application> 或 target Activity 上声明 |
| 7.1 | 25 | android:roundIcon 在 <application>/<activity> 上引入,但不在 alias 官方语法里 ,建议放 <application> |
| 8.0 | 26 | 引入 Adaptive Launcher Icons 支持;应用需自行提供 adaptive icon 资源,最终效果依赖 launcher / 设备实现 |
| 11 | 30 | Package Visibility:查询别人需要 <queries>,别人启动你的 alias 仍只看 exported 和 intent-filter |
| 12 | 31 | targetSdkVersion ≥ 31 且 alias 含 intent-filter 时,exported 必须显式声明(构建期强制);Splash 主题绑在 target Activity 上,alias 不能单独设 theme |
| 13 | 33 | Themed Icons:alias 用含 monochrome 层的 adaptive icon 可独立生效;LocaleConfig / per-app language 与 alias 无冲突 |
| 14 / 15 | 34 / 35 | 没有针对 alias 自身的破坏性变更;组件安全收紧、边到边等都是通过 <activity> / <application> 间接影响 |