Android `<activity-alias>` 指南:动态图标 · 多入口 · 重命名兼容

  • 文中若有疏漏,欢迎评论区指正交流
  • 转载请注明出处,谢谢支持

目录

  • 一、概念与定位
  • 二、属性速查
  • 三、典型场景
    • [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 个属性:enabledexportediconlabelnamepermissiontargetActivity
  • 可独立包含 <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 项以外的属性(themelaunchModetaskAffinitydirectBootAwareprocessconfigChangesscreenOrientation 等)不能在 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> 间接影响

附录:延伸阅读

相关推荐
new_dev8 小时前
Python实现Android自动化打包工具:加固、签名、多渠道一键完成
android·python·自动化
彩票管理中心秘书长8 小时前
智能体状态指示:何时思考、何时调用工具、何时出错
前端·后端·程序员
彩票管理中心秘书长8 小时前
React + TypeScript拆解一整套“AI 变现代码流程”
前端·后端·程序员
AskHarries8 小时前
OpenClaw 是什么?为什么它不是普通 AI Agent
人工智能·后端·程序员
QING6188 小时前
Kotlin inline 实战详解 —— 新手须知
android·kotlin·android jetpack
AskHarries8 小时前
如何判断一个需求是真需求
人工智能·程序员·产品
ElevenS_it1888 小时前
MySQL慢查询监控与告警实战:从slow_log采集到分钟级定位慢SQL的完整链路配置
android·sql·mysql
沐言人生8 小时前
ReactNative 源码分析12——Native View创建流程onBatchComplete
android·react native
caicai_xiaobai8 小时前
QT搭建安卓开发环境
android