做手游的朋友应该都遇到过这种需求:版本更新、节日活动、周年庆的时候,产品拿着设计好的新图标跑过来问——“咱们能不能在活动当天自动把商店和桌面的图标换掉?”如果只是换商店图标,那很简单,发版前上传就行;但“桌面上的图标也跟着变”,这就不再是运营问题,而是双端原生能力的问题。这篇文章把我自己在 Unity 手游项目里实现动态更换 App 图标的全过程整理出来,覆盖 Android 的 Activity Alias 切换方案和 iOS 的 setAlternateIconName 方案,包括两端的配置、代码、资源要求,以及在真机实测中遇到的桌面不刷新、厂商 ROM 缓存、系统版本差异等一堆坑。无论你是客户端主程、运营工具开发者,还是想给项目加个“变图标”玩法的独立开发者,这篇都值得参考。
1. 运营活动、主题版本、AB测试:动态图标的三类驱动场景与平台能力边界
先聊清楚一件事:动态换图标这个需求不是“炫技”,它背后有非常明确的业务价值。我把它见过的需求归成三类。
第一类是节点运营。春节、中秋、版本周年庆这类时间节点,运营希望当天零点开始图标自动切换成活动版本,活动结束后再自动换回默认图标。中间如果涉及商店审核和包体更新,时效性完全不可控,所以必须客户端本地实现定时或接收远端指令后切换。
第二类是主题版本或联动版本。比如某游戏跟知名动漫联动,联动期间把图标换成联动角色;或者游戏推出了“暗黑模式”“周年限定”主题,在某个时间段内让图标配合主题变化。这类需求往往有明确起止时间,但具体哪天生效,可能要等版权方确认,所以开关最好由服务端动态下发。
第三类是 AB 测试。图标对用户点击率的影响很大,团队想测“默认图标 A”和“活动图标 B”哪个转化更好。这种场景下,需要能做到用户维度精确控制:一部分用户看 A,一部分用户看 B,下一次拉起时根据实验分组决定要不要换。动态换图标在技术上的成本,比发两套包低太多。
但是,Android 和 iOS 对“动态换图标”的支持程度完全不同。很多人搜资料时会看到网上一堆“Android 可以,iOS 不行”的说法,这个说法一半对,一半过时。实际上 iOS 从 10.3 开始提供了官方 API,允许 App 在运行时切换到预设的备用图标,不过有前提:图标资源必须打包进安装包,不能运行时从网络下载。而 Android 则通过 Activity Alias(活动别名)机制实现,原理上更接近“让桌面上出现一个伪装成主入口的新组件”。
下面是双端核心差异对比:
| 维度 | Android | iOS |
|---|---|---|
| 核心技术 | Activity Alias + PackageManager 组件开关 | setAlternateIconName 备用图标 API |
| 是否需要预置资源 | 是,图标需随包发布 | 是,图标需随包发布 |
| 切换后应用是否重启 | 视系统而定,通常会短暂退出 | 不重启,图标稍后自动刷新 |
| 系统限制程度 | 较开放,但厂商 ROM 有差异 | 较严格,无法动态拉取外部图片 |
| 支持的最低版本 | Android 5.0 基本都能用 | iOS 10.3+ |
| 合规性 | 正常使用,无审核风险 | 官方公开 API,无审核风险 |
这页表格基本就是整个方案的骨架。接下来我会分别把两端从原理到代码完整过一遍,最后给出一个可落地的双端统一架构。
2. Android 端:用 Activity Alias 让桌面图标“分身”
Android 端的实现思路,简单说就是:在 AndroidManifest 里给主 Activity 声明好几个“别名”,每个别名可以有自己的图标和名称。默认情况下只启用其中一个,需要切换时通过 PackageManager 把当前启用的别名禁用,再启用另一个。桌面 Launcher 感知到组件状态变化后,会重新读取图标并刷新显示。
2.1 Activity Alias 的底层原理:为什么它能骗过桌面
要理解这个方案,得先明白桌面图标到底是什么。Android 桌面上每个图标对应的是某个应用组件的一个入口,通常是带MAIN和LAUNCHER过滤器条件的 Activity 或 Activity Alias。Launcher 通过 PackageManager 查询这些入口,读取组件信息里的 icon 和 label 来绘制图标。
正常情况下,应用的主 Activity 本身带有LAUNCHER条件,Launcher 找到这个 Activity,把它的 icon 当作 App 图标。而 Activity Alias 相当于一个“组件替身”:它指向同一个目标 Activity,但可以拥有完全独立的组件名、图标、标签和启用状态。当 alias 被 Launcher 识别为入口后,Launcher 显示的就是 alias 上的图标,而不是目标 Activity 的图标。
所以实现思路就清晰了:
- 主 Activity 不要直接带
LAUNCHER条件,把启动入口全部转移到 alias 上。 - 声明多个 alias,各自指向同一个主 Activity,分别配置不同的图标。
- 默认只启用其中一个 alias,其余保持禁用状态。
- 切换时先禁用当前 alias,再启用目标 alias。
这套机制的妙处在于,Launcher 看到的始终是一个可启动的入口,切换前后行为完全一致,用户点图标进入的都是同一个 Activity。
2.2 Manifest 配置:多入口声明与主 Activity 的去图标化处理
下面是核心的 Manifest 配置写法,我以默认图标和节日图标两个入口为例:
<application android:allowBackup="true" android:icon="@mipmap/ic_launcher_default" android:roundIcon="@mipmap/ic_launcher_default_round" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/AppTheme"> <!-- 主 Activity 不带 LAUNCHER 条件,仅作为目标 --> <activity android:name=".MainActivity" android:exported="false"> </activity> <!-- 默认图标入口 --> <activity-alias android:name=".MainActivity_Alias_Default" android:enabled="true" android:exported="true" android:icon="@mipmap/ic_launcher_default" android:roundIcon="@mipmap/ic_launcher_default_round" android:label="@string/app_name" android:targetActivity=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity-alias> <!-- 节日图标入口 --> <activity-alias android:name=".MainActivity_Alias_Festival" android:enabled="false" android:exported="true" android:icon="@mipmap/ic_launcher_festival" android:roundIcon="@mipmap/ic_launcher_festival_round" android:label="@string/app_name_festival" android:targetActivity=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity-alias> </application>这里有两个非常关键的细节,直接决定方案能不能跑通。
第一个细节:主 Activity 绝对不能有自己的LAUNCHER条件,否则桌面上会出现两个图标。很多第一次做这个功能的人把 alias 加上了,但忘了去掉主 Activity 原来带的 intent-filter,结果切换完发现桌面出现重复图标。标准做法是让主 Activity 的exported保持false,只作为 alias 的targetActivity存在。
第二个细节:每个 alias 的exported必须设置为true。因为 Launcher 需要能通过隐式 Intent 启动这个入口,如果exported是false,系统会因为权限问题拒绝 Launcher 的启动请求,表现就是点了图标没反应或直接报“无法启动”。
另外提醒一下android:enabled的初始值。默认图标那个 alias 保持true,其他备用 alias 全部false。这样首次安装后桌面上只显示默认图标,不会因为复用MAIN内容出现多个入口。
2.3 切换代码的写法与 PackageManager 的调用细节
配置写好之后,核心就是运行时代码。Android 通过PackageManager.setComponentEnabledSetting来改变组件的启用状态,这个方法会直接影响 Launcher 的图标读取结果。完整切换代码如下:
public class IconSwitcher { private static final String DEFAULT_ALIAS = ".MainActivity_Alias_Default"; private static final String FESTIVAL_ALIAS = ".MainActivity_Alias_Festival"; private static final String MAIN_ACTIVITY = ".MainActivity"; public static void switchToFestivalIcon(Context context) { try { PackageManager pm = context.getPackageManager(); String packageName = context.getPackageName(); // Step 1: 先禁用当前的默认别名 pm.setComponentEnabledSetting( new ComponentName(packageName, packageName + DEFAULT_ALIAS), PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP); // Step 2: 再启用目标别名 pm.setComponentEnabledSetting( new ComponentName(packageName, packageName + FESTIVAL_ALIAS), PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP); // Step 3: 发送组件状态变化通知,提示桌面刷新 sendPackageChangedBroadcast(context); } catch (Exception e) { Log.e("IconSwitcher", "switchToFestivalIcon failed", e); } } private static void sendPackageChangedBroadcast(Context context) { try { Intent intent = new Intent(Intent.ACTION_PACKAGE_CHANGED); intent.setData(Uri.parse("package:" + context.getPackageName())); intent.setPackage(context.getPackageName()); context.sendBroadcast(intent); } catch (Exception ignored) { // 部分系统版本对隐式广播限制较严,发不出去就依赖桌面自行刷新 } } }这里我强烈建议按“先禁用当前别名,再启用目标别名”的顺序来做。如果反过来,先启用新别名,再禁用旧别名,在部分 ROM 上会出现一个极短暂的“双图标”窗口期,截图或自动化测试时偶尔能抓到。
关于DONT_KILL_APP标志,它的作用是让组件状态变更不杀掉当前进程。但坦白说,在真机上这个标志的效果并不总是符合预期。很多系统在禁用当前正在运行的组件时,仍然会把 Activity 销毁掉。所以我在下面的踩坑章节会专门讲这个现象,这里先记住一个结论:切换完成后不要立刻依赖当前进程继续做繁重任务,最好让用户回到桌面查看图标变化。
2.4 资源文件与 Adaptive Icon 的配套处理
如果只给 alias 配置android:icon,在 Android 8.0 以上的设备上会遇到一个问题:没有roundIcon或者 Adaptive Icon 图层配置不正确时,桌面图标可能出现白底、变形、被裁切的情况。现在的国产 ROM 基本都是 Android 8.0 起步,所以资源部分必须做全套。
我的建议是,每个图标版本都提供两套资源:一套传统 PNG(放在mipmap-mdpi/hdpi/xhdpi/xxhdpi/xxxhdpi),一套 Adaptive Icon(放在mipmap-anydpi-v26下面,用 XML 定义前景和背景层)。Adaptive Icon 的配置类似这样:
<!-- res/mipmap-anydpi-v26/ic_launcher_festival.xml --> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <background android:drawable="@drawable/ic_festival_background" /> <foreground android:drawable="@drawable/ic_festival_foreground" /> </adaptive-icon>这里有个小经验:背景层建议用纯色或简单的图层,不要直接放完整的方形图片;前景层要保证图标主体区域在安全范围内,否则在部分桌面上会被裁掉一圈。对于没有适配 Adaptive Icon 的旧版本系统,继续使用mipmap下的 PNG 即可,系统会自动选择合适的一套。
如果你用的是 Unity 项目,资源处理要额外注意:Unity 打包 Android 时,res目录可能会被合并或重构,建议直接通过 Android Library 工程管理图标资源和 Manifest,Unity 主工程通过依赖方式接入,避免打包工具的优先级把自定义 Manifest 节点覆盖掉。
3. Android 端避坑:桌面刷新、厂商 ROM 与进程状态的那些坑
这一节是我个人认为整篇最有价值的部分。动态换图标的核心原理不复杂,真正折磨人的是边界情况。我做这个功能时,踩过的坑比写代码花的时间多得多。
3.1 桌面图标不刷新:三种典型场景与解决思路
图标切换完成后,桌面上的图标没变,这是最常见的问题。我把它拆成三种场景。
场景一:切换后需要等待几秒。部分桌面(特别是国产 ROM 自带桌面)有图标缓存,收到PACKAGE_CHANGED广播后不会立即重新读取,而是等一个周期性同步。这种情况下,让用户回桌面盯着看三到五秒,一般会自己刷新。测试时不要刚切换完就去截图,很容易误判为失败。
场景二:第三方桌面不响应。如果你装了 Nova Launcher、微软桌面这类第三方桌面,它们对组件状态变化的响应逻辑各不相同。有的会监听PACKAGE_CHANGED并自动刷新,有的则完全不监听。这时候最粗暴有效的办法是让用户长按桌面空白处进入“桌面设置”,触发一次图标重建;或者在代码里引导用户“重启桌面”或“返回桌面”。我实测下来,无论第三方桌面还是系统桌面,只要重新加载一次应用列表,图标都会变成新状态。
场景三:部分 ROM 广播被限制。我前面写的sendPackageChangedBroadcast,在 Android 8.0 之后隐式广播限制收紧,这条广播在部分系统上可能发不出去。其实不用担心,因为大多数桌面是自己通过LauncherApps或PackageMonitor监听组件变化的,依赖的是系统服务层的回调,而不是这个显式广播。所以发送广播是锦上添花,不是雪中送炭。
3.2 华为、小米、OPPO 等厂商 ROM 的差异化表现
国产 ROM 是这个功能最大的不确定性来源。我无法给你一份“所有机型全部适配”的保证,但可以把实测中的典型表现列出来。
小米 MIUI 大部分情况表现优秀,切换后桌面图标很快会更新,但在少数版本上图标缓存比较顽固。如果遇到,可以让用户在下拉通知栏里做一次“重新加载桌面”操作,或者重启一次手机。另外 MIUI 的“图标重绘”机制会影响主题图标,如果你的游戏接入了主题商店逻辑,切换后图标可能被主题风格覆盖,需要额外判断。
华为 EMUI / HarmonyOS 的表现比较“慢热”,图标刷新往往有延迟,有时需要等十几秒甚至更久。我怀疑是系统图标加载服务有更强的缓存策略。实测中有一次等了接近三十秒才刷新,差点以为失败了。华为设备上,切换完成后建议不要立刻挂后台,多保持应用在前台一两秒,给系统广播足够时间下发。
OPPO / vivo 的 ColorOS 和 OriginOS 表现中规中矩,大多数情况下能刷新,但偶尔也有失败案例。我没有找到完全通用的规律,只能说“等待 + 触发重启桌面”是最后的兜底方案。
三星 One UI 的适配度相对较好,图标刷新速度和稳定性居中。我测试过的机型上基本都能在五秒内刷新成功。
3.3 切换时应用被“杀掉”的机制说明与规避
前面提到DONT_KILL_APP并不总是奏效。实际情况是,当系统检测到你禁用的组件正是当前正在运行的组件,或者组件状态变化影响到了包的整体可见性时,系统会销毁你的进程。这在部分系统上会表现为:切换图标后应用自动退出,回到桌面,然后新图标出现。
从用户视角看,这个行为其实“意外地合理”——用户看到图标变了,应用回到桌面,以为功能正常生效了。但从开发视角,如果你在切换后还有未完成的任务(比如上报埋点、弹窗提示),进程被杀会导致这些逻辑丢失。
规避方案有三个层次:
第一个层次,切换动作尽量放在一个独立的、非关键的流程里。比如用户点击“应用节日图标”后,先弹 Toast/对话框提示“图标已切换,应用将返回桌面”,然后执行切换,最后调用moveTaskToBack(true)把应用退到后台而不是直接等待被杀。
第二个层次,把切换逻辑抽到一个轻量的前台 Service 或 WorkManager 任务中,确保即使 Activity 被销毁,切换流程也能走完。
第三个层次,如果功能要支持“定时自动切换”,不要依赖应用进程存活。建议用 AlarmManager 或 WorkManager 到点触发切换,而不是让玩家一直开着游戏。这样可以避免进程被杀导致整个切换任务中断。
3.4 Android 12 之后的启动画面与图标状态不一致问题
Android 12 引入全新的 SplashScreen 机制后,又出现了一个新问题:应用冷启动时,系统启动画面显示的是启动入口的图标,但如果你刚完成图标切换,系统启动画面的缓存可能还挂着旧图标,导致用户看到“启动画面图标和桌面图标不一样”的尴尬情况。
这个问题目前没有完美解法,因为 SplashScreen 的图标来自系统侧的启动画面缓存,应用层无法强行刷新。实测下来的应对策略是:切换后的第一次冷启动,用户有一定概率看到旧的启动画面图标,但游戏内容正常;从第二次冷启动开始,图标就会保持一致。所以不必太过担心,只要不在切换后立刻引导用户杀进程再启动,影响基本可控。
4. iOS 端:setAlternateIconName 是唯一公开路径,但有限制
iOS 端没有 Android 那么“绕”,Apple 从 iOS 10.3 开始提供了官方备用图标 API:setAlternateIconName(_:completionHandler:)。这套 API 允许 App 在运行时把主屏幕图标切换成预设的备选图标之一,不需要用户手动设置,也不用经过设置页。但能力上有清晰边界,下面我把边界和工程实现讲透。
4.1 为什么 iOS 不能真正“动态”换图标
很多网上资料说“iOS 不能动态换图标”,严格来说不准确。准确表述是:iOS 不允许 App 在运行时动态生成或者从网络下载图标并设置为主屏图标,Apple 只允许你在安装包内预置多套图标,运行时在预置列表里切换。
这意味着两件事:
- 所有备选图标必须在发版前准备好,并且随 App 包一起分发。
- 切换动作是“本地预设值之间的跳转”,无法做到“设计图上传后全网立即生效”。
对游戏项目来说,这个限制其实可以接受。因为一个版本周期内涉及到的图标版本是有限的,通常在几个到十几个之间。运营只要提前一个版本规划好节点,把图标资源全部打进包,服务端在对应时间点下发切换指令即可。
4.2 Info.plist 配置:备用图标声明与资源命名规则
iOS 的备用图标信息全部写在 Info.plist 里。核心字段是CFBundleIcons下的CFBundleAlternateIcons。一个简单的配置片段如下:
<key>CFBundleIcons</key> <dict> <key>CFBundleAlternateIcons</key> <dict> <key>FestivalIcon</key> <dict> <key>CFBundleIconFiles</key> <array> <string>FestivalIcon60</string> <string>FestivalIcon120</string> <string>FestivalIcon180</string> </array> <key>UIPrerenderedIcon</key> <false/> </dict> </dict> </dict>这里的关键点是CFBundleIconFiles数组里的名字,对应工程 Asset Catalog 里的图片资源名称,不需要带文件扩展名。iOS 会自动根据设备类型选取合适分辨率(60pt、120pt、180pt 分别对应普通屏、2x、3x)。
在 Unity 工程里操作时,有个很方便的做法:直接修改 Xcode 工程导出后的 Info.plist,或者使用 Unity 的 iOS 构建后处理接口,自动把备用图标配置注入到生成的 Info.plist 中。我在项目里就是这么做的,在IPostprocessBuildWithReport里读取构建配置,把当前版本要启用哪些备用图标写进 plist,避免手工维护。
4.3 切换代码:调用、回调和恢复默认图标的正确姿势
iOS 端的代码比 Android 简单直接,核心就是UIApplication的备用图标 API。Swift 代码示例:
import UIKit final class AppIconManager { static let shared = AppIconManager() func switchToFestivalIcon() { guard UIApplication.shared.supportsAlternateIcons else { // 当前设备或系统版本不支持备用图标 return } UIApplication.shared.setAlternateIconName("FestivalIcon") { [weak self] error in if let error = error { print("图标切换失败:", error.localizedDescription) } else { print("已切换到节日图标") } } } func switchToDefaultIcon() { guard UIApplication.shared.supportsAlternateIcons else { return } UIApplication.shared.setAlternateIconName(nil) { error in if let error = error { print("恢复默认图标失败:", error.localizedDescription) } else { print("已恢复默认图标") } } } }这里要注意几个细节。
第一,传入的 name 必须和 Info.plist 里CFBundleAlternateIcons的 key 完全一致,大小写和标点都不能错。不一致时系统会直接报错。
第二,传nil是恢复默认图标的唯一方式,不要试图传默认图标的 name,那是无效的。
第三,回调里的 error 一定要处理。在我测试过的 iPhone 机型上,有一种情况是在系统刚升级完、图标缓存还没有完全建立时调用 API,会收到一个“图标名称无效”的错误,等几分钟后再调用就正常了。如果不做错误提示,玩家会以为功能坏了。
4.4 iOS 端的实际表现与系统限制
因为 iOS 生态高度统一,真机表现比 Android 好预测得多。但也不是完全没有坑。
第一个坑是图标刷新时机。调用setAlternateIconName成功后,桌面图标并不是立刻切换,通常在 1 到 3 秒内更新,如果你退出 App 过快,偶尔会在桌面看到短暂延迟。这是正常的,不用焦虑。
第二个坑是 Honor 和部分旧机型上偶尔会出现“图标切换了,但通知中心下拉界面里的图标没变”的现象。这个我没找到解法,属于弹簧系统自身的问题,过一段时间会自动恢复。
第三个坑是它是一款“一次性用户可见”的切换,频繁切换可能引起用户反感。如果你做 AB 测试时频繁变图标,有些用户会在系统设置里给 App 开启“不自动更换图标”的权限吗?答案是不会,因为 iOS 目前没有这个开关。但基于用户体验,我建议一个自然日内不要超过一次,否则会显得很“弹窗广告风”。
5. 双端统一:把图标切换封装成运营可配置的能力
两个平台的实现方式完全不同,但如果我们的目标是让运营像配置活动一样配置图标,就需要在上层做一层统一抽象。我建议把图标切换能力封装成一个独立模块,对外暴露统一接口。
5.1 客户端接口设计:游戏侧不感知平台差异
在 Unity 里做双端封装是最自然的,因为 Unity 本身就是统一开发层,通过原生桥接调用底层 API。我设计的接口大致是这样:
public enum GameAppIconType { Default = 0, Festival = 1, Anniversary = 2, Collab = 3 } public static class GameAppIconManager { private static IAppIconBridge _bridge; static GameAppIconManager() { #if UNITY_ANDROID && !UNITY_EDITOR _bridge = new AndroidAppIconBridge(); #elif UNITY_IOS && !UNITY_EDITOR _bridge = new IOSAppIconBridge(); #else _bridge = new EditorAppIconBridge(); // 编辑器下模拟,用于测试 #endif } public static void SwitchIcon(GameAppIconType iconType, System.Action<bool> callback) { _bridge.SwitchIcon(iconType, callback); } }Android 桥接类通过 Unity 的AndroidJavaObject调用前面写的IconSwitcher,iOS 桥接类通过UnitySendMessage或 Swift 原生插件调用AppIconManager。编辑器下用一个模拟桥接,直接打印日志,方便在没有真机的情况下联调逻辑。
5.2 服务端下发与控制策略:状态机与幂等性
有了统一接口,接下来就是服务端怎么控制。我的建议是引入一个“图标状态机”,而不是简单地下发一个“切到节日图标”的指令。
为什么?因为运营配置图标通常会带有时间属性:某天 0 点切节日图标,某天 0 点切回默认。如果只下发“目标状态”,那么客户端在离线期间可能漏掉切换;如果下发“目标状态+生效时间点”,客户端逻辑就要复杂很多。更稳妥的做法是:服务端维护一个“预期的图标状态”,客户端每次启动或回到前台时拉取一次;本地检测到“预期状态 != 当前状态”时,执行切换。
这个模型本质上是幂等的:不管客户端当前处于什么状态,只要最终能收敛到服务端预期状态即可。意外的网络异常、进程被杀、系统切换失败,都不会导致长期状态错误。
下发格式可以非常简单,例如:
{ "icon": "festival", "startTime": "2025-01-01 00:00:00", "endTime": "2025-01-15 23:59:59" }客户端拿到后,只要当前时间落在起止时间内,就确保图标是 festival;超出区间则切回 default。我把这段逻辑放在 Unity 的Update外层,每隔一段时间检查一次,同时监听 App 从后台返回的事件,保证玩家切回来时图标已经正确。
5.3 定时切换的边界与离线兜底
有一种场景会让“定时切换”失效:玩家在图标切换期间恰好没打开过 App,也没有通过任何机制触发检查。那么服务端下发的“切换”指令永远不会执行。
这种离线兜底是否要做,取决于业务容忍度。我的建议是:如果切换是强运营需求,就必须做服务端兜底,即在商店图标的展示层也同步替换,并且设置足够的活动周期,给“未及时切换”的用户留出窗口;如果只是弱装饰需求,例如主题变色类切换,就接受部分用户延迟生效,不做复杂兜底。
另外,即使客户端本地没有执行切换,下次启动时也会拉取服务端状态并纠正,所以最坏情况是“晚生效一个用户启动周期的时长”,不会出现永不生效。
6. 上线前必须过的检查清单与我的经验总结
做到这里,方案已经基本完整。最后整理一份我每次发版前都会过的检查清单,以及几条值得写进团队文档的经验。
6.1 双端功能检查清单
| 检查项 | 说明 |
|---|---|
| Android 图标不重复 | 确认主 Activity 无 LAUNCHER 条件 |
| Android 别名 exported 状态 | 确认所有 alias 的 exported 为 true |
| Android 资源完整性 | 确认每个 alias 都有默认圆角图标和 Adaptive Icon |
| Android 真机切换验证 | 至少覆盖小米、华为、OPPO、三星各一台真机 |
| Android 切换后桌面刷新 | 等待 10 秒以上再判断,必要时重启桌面 |
| iOS 备用图标资源入库 | 确认 CFBundleAliasedIcons 数组中的资源名与实际文件一致 |
| iOS 真机切换验证 | 确认 supportsAlternateIcons 返回 true |
| iOS 恢复默认图标 | 验证 setAlternateIconName(nil) 的正常回切 |
| 服务端下发连通性 | 弱网、断网、App 结束后拉起,三种情况各测一轮 |
| 幂等性验证 | 重复执行同一目标切换,确认最终状态不变 |
此外还有一项容易忽略的检查:游戏内如果做了“自定义桌面图标”的增值功能,需要在 App Store 审核备注里说明使用的 API 和用途。虽然setAlternateIconName是公开 API,审核基本不会卡,但主动说明可以减少“被误判为异常功能”的风险。
6.2 Unity 项目里的工程化注意事项
Unity 项目做这个功能,和纯原生项目有个重要区别:Unity 打包出来的 Android 工程会自动生成并合并 Manifest。如果你直接在 Unity 的 Plugins/Android 下放自定义 Manifest 或 Android Library,一定要确认打包后的最终 Manifest 内容符合预期。最好用 Android Studio 打开 Unity 导出的工程,检查一次最终合并结果,而不是只改 Unity 侧的 Manifest 就发版。
iOS 侧则要注意构建后处理。Unity 每次构建 Xcode 工程时会重新生成 Info.plist,如果手动改 Xcode 工程,下一次构建可能被覆盖。我已经把备用图标的 plist 注入逻辑放进了构建后处理脚本,每次构建自动执行,省了很多重复工作。
6.3 一些值得沉淀的个人经验
最后分享几条我做这个功能之后的直接体会。
第一,动态换图标不是“一次写代码,永久能用”的功能。Android 厂商 ROM 迭代很快,每次 Android 大版本升级或新桌面发布后,都值得重新做一轮真机回归。至少要保证核心机型——你游戏用户量最大的那些机型——切换功能不出大问题。
第二,图标的切换频率是真实的用户体验风险。我见过有运营把图标切换做成“一天换一个颜色”,结果几天后玩家在社交媒体骂“App 是不是中毒了”。建议把动态图标当成一个需要克制的运营手段,重点场景才使用。
第三,如果图标切换失败,千万不要让用户卡在异常状态。我在 Android 端做过一个保护逻辑:切换超过三次仍然失败时,自动恢复默认图标并上报埋点,避免出现桌面图标状态和业务配置不一致的“半失败”状态。这个兜底在实际运营中救了不止一次。
动态更换 App 图标这个需求,听起来像是平台层面的“魔法”,拆开看其实就是一个组件状态切换加一张图标资源的事。但它涉及 Manifest、资源、厂商兼容、系统限制、服务端控制多个层面,每一步都有细节。希望这篇整理能帮你在做同类功能时少踩几个坑。