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 个属性: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> 间接影响

附录:延伸阅读

相关推荐
千里马学框架3 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台3 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone3 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc3 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo3 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077003 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼3 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
duyacode3 天前
FastAPI为什么成为默认答案
程序员
AFinalStone3 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen3 天前
属性动画原理与高级动画实现
android·kotlin·canvas