2026/10/2 14:04:38

Unity游戏动态更换App图标:Android与iOS双端实现全解析

Unity游戏动态更换App图标:Android与iOS双端实现全解析 1. 为什么要做动态图标需求场景与方案选型做游戏运营的朋友一定深有体会版本更新、节日活动、联动 IP 上线都是拉新和召回的关键节点。App 在桌面上的图标其实是用户每天打开手机第一眼就看到的核心广告位成本为零但触达率是百分之百。很多手游团队都会想能不能让这个图标跟着运营节奏走比如春节换成喜庆的红色主题新版本换成新角色立绘活动结束再一键换回原版。这就是“动态更换 App 图标”最典型的落地场景。Unity 开发者接到这个需求时第一反应往往是头疼。因为 Unity 引擎本身并没有提供“运行时切换图标”的接口这件事绕不开原生层。Android 和 iOS 两套系统对图标切换的开放程度完全不同Android 靠的是 Activity 别名activity-alias加组件开关理论上可以任意切换、随意切换iOS 则从 iOS 10.3 开始提供了系统级的setAlternateIconNameAPI但限制不少比如图标不能带透明通道每次切换还会弹系统确认框。所以接到这个需求第一步不是急着写代码而是弄清楚你到底要哪种“动态”。如果只是包体打多个图标、安装后用户手动换那 Android 用 Launcher 图标本身就支持iOS 也能靠描述文件做到但这些都算不上真正的“动态”。本文讨论的“动态”是指游戏运行过程中客户端向服务端请求配置拿到目标图标名之后调用原生接口直接把桌面图标换掉玩家不需要重新安装 App。这套方案的技术栈涉及 Unity 与原生交互、双端平台差异、系统资源文件打包三块内容。适合已经具备一定 Unity 工程经验的开发者参考你要看得懂 AndroidManifest.xml能写简单的 Objective-C 或 Swift知道怎么打 Android 的 aar 和 iOS 的 framework。如果这些还不熟也没关系下面每个步骤我都会把原理讲清楚照着做也能跑通。方案选型上我最终采用的是“原生能力封装 Unity 统一调用层”的结构。Android 端使用PackageManager.setComponentEnabledSetting切换两个 activity-alias 的启用状态iOS 端使用UIApplication.shared.setAlternateIconNameUnity 侧写一个 C# 管理器通过 AndroidJavaObject 和 Objective-C 插件分别桥接。这样设计的原因是动态图标的逻辑本质上属于“平台强相关”功能如果试图在 C# 层做统一抽象反而会因为系统差异过大致使抽象层漏洞百出不如各端各做各的只暴露两个同名接口给上层。2. Android 端实现基于 activity-alias 的动态切换2.1 核心原理一个图标就是一个可开关的组件Android 桌面上每个 App 图标本质上都对应一个入口组件通常是带MAIN和LAUNCHER意图过滤器的 Activity或者指向某个 Activity 的activity-alias。“桌面图标”和“Activity 组件”的关系可以理解成快捷方式与目标程序的关系桌面上那个图标只是指向 Activity 的一个壳。activity-alias的作用就是给同一个 Activity 再起一个“马甲”。系统允许你在 Manifest 里给同一个 Activity 配置多个别名每个别名都可以拥有自己的图标和标签。默认情况下哪个别名是启用状态桌面上就显示哪个图标。关键来了通过PackageManager.setComponentEnabledSetting你可以在运行时把某个别名禁用、把另一个别名启用。组件状态一变系统会发出广播Launcher 收到后刷新桌面图标。这就是 Android 动态换图标的底层逻辑不需要替换任何图片资源只是切换组件的启用/禁用状态本质是“换入口”。2.2 Manifest 配置两个 alias一个 Activity先看一段我实际在项目中使用的 Manifest 配置application android:allowBackuptrue android:iconmipmap/ic_launcher android:labelstring/app_name activity android:name.MainActivity android:exportedtrue intent-filter action android:nameandroid.intent.action.MAIN / category android:nameandroid.intent.category.LAUNCHER / /intent-filter /activity activity-alias android:name.MainActivity_Alias_Default android:enabledtrue android:exportedtrue android:iconmipmap/ic_launcher android:labelstring/app_name android:targetActivity.MainActivity intent-filter action android:nameandroid.intent.action.MAIN / category android:nameandroid.intent.category.LAUNCHER / /intent-filter /activity-alias activity-alias android:name.MainActivity_Alias_NewYear android:enabledfalse android:exportedtrue android:iconmipmap/ic_launcher_newyear android:labelstring/app_name_newyear android:targetActivity.MainActivity intent-filter action android:nameandroid.intent.action.MAIN / category android:nameandroid.intent.category.LAUNCHER / /intent-filter /activity-alias /application这里有个非常容易踩的坑主 Activity 本身不能同时带 LAUNCHER 的 intent-filter否则系统会认为 App 有两个入口桌面会出现两个图标或者切换失效。正确做法是主 Activity 只保留默认的 MAIN 意图真正的桌面入口全部由 alias 承担。我在前期调研时看到不少团队的代码图省事让 Activity 自己和 alias 都带 LAUNCHER结果就是每次切换后桌面残留两个图标玩家体验极其糟糕。每个 alias 的android:icon指向不同的 mipmap 资源android:label可以顺便连应用名一起换。注意资源名要提前在 res 目录下准备好不能运行时动态指定不存在的资源。icon 资源本身建议用自适应图标Adaptive Icon即mipmap-anydpi-v26目录下同时配置前景和背景这样切到不同图标时在不同厂商桌面上都能保持统一形状。2.3 运行时切换代码Java 层写什么切换逻辑的核心代码并不长我把它封装在一个名为IconSwitcher.java的文件里public class IconSwitcher { private static final String ALIAS_DEFAULT .MainActivity_Alias_Default; private static final String ALIAS_NEW_YEAR .MainActivity_Alias_NewYear; public static void switchIcon(Context context, boolean useNewYear) { String targetAlias useNewYear ? ALIAS_NEW_YEAR : ALIAS_DEFAULT; String activeAlias useNewYear ? ALIAS_DEFAULT : ALIAS_NEW_YEAR; PackageManager pm context.getPackageManager(); ComponentName targetName new ComponentName(context.getPackageName(), context.getPackageName() targetAlias); ComponentName activeName new ComponentName(context.getPackageName(), context.getPackageName() activeAlias); pm.setComponentEnabledSetting( targetName, PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP ); pm.setComponentEnabledSetting( activeName, PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP ); } }注意ComponentName的构造如果在 Manifest 里写的是.MainActivity_Alias_Default那么 Java 侧拼接时前面带点new ComponentName(context, context.getPackageName() aliasName)。这里用context.getPackageName()获取应用包名不要硬编码避免不同渠道包改写包名后失效。DONT_KILL_APP标志的意思是切换组件状态时不要杀掉应用进程。因为动态换图标往往发生在游戏运行中比如玩家在活动页面点了“换肤”按钮你当然不希望切换完游戏被系统杀掉。但要注意setComponentEnabledSetting本身是异步的调用后系统需要一点时间刷新桌面图标正常情况下 12 秒内生效。如果你发现调用后桌面迟迟不刷新可以在切换后加一句Intent intent new Intent(Intent.ACTION_MAIN); intent.addCategory(Intent.CATEGORY_HOME); context.startActivity(intent);这一招是让桌面重新布局、触发图标刷新的常见土办法。不过实测中大多数主流 Launcher 不调用也能自动刷新倒是部分深度定制的 ROM 需要这个额外触发。2.4 从 Unity 调用AndroidJavaObject 与原生桥接Unity 侧的调用特别简单因为只涉及静态方法。我在 C# 里写了一个AndroidIconManagerpublic class AndroidIconManager { private static readonly string ClassName com.yourgame.nativebridge.IconSwitcher; public static void SwitchIcon(bool useNewYear) { #if UNITY_ANDROID !UNITY_EDITOR using (var jc new AndroidJavaClass(ClassName)) { jc.CallStatic(switchIcon, GetUnityActivity(), useNewYear); } #endif } private static AndroidJavaObject GetUnityActivity() { using (var unityPlayer new AndroidJavaClass(com.unity3d.player.UnityPlayer)) { return unityPlayer.GetStaticAndroidJavaObject(currentActivity); } } }这里有一个容易忽略的细节方法参数Context context在 Unity 侧传的是UnityPlayer.currentActivity。我们把 Activity 实例传给原生层是因为setComponentEnabledSetting需要 Context而 Activity 本身就是 Context。如果你忘了传直接在非 Activity 的 Context 下切换部分 ROM 上可能不会刷新图标。原生代码封装成 aar 后放到 Unity 工程的Plugins/Android目录下即可。要提醒一点IconSwitcher类所在的包名和 Unity 工程的com.company.product包名可以不一致但这时context.getPackageName()返回的一定是 Unity 应用的包名而不是静态类所在包的包名。也就是说无论原生桥接代码放在哪个包最终操作的对象都是 Unity 入口 Activity 和 Manifest 里声明的组件这一点不会错。3. iOS 端实现系统 API 与 Info.plist 配合3.1 iOS 的限制为什么 Apple 的“动态”这么保守如果说 Android 的动态图标是一把自由开关那 iOS 就是一把带安全锁的钥匙。Apple 提供的是UIApplication.shared.setAlternateIconName(_:completionHandler:)方法只允许你在 App 内置的“备用图标集合”里做替换且每次切换都会弹窗提示用户这张图标即将被替换。用户点确认后才生效。这意味着你不能在运行时通过网络下载一张图片当图标必须在安装包里预置所有可选图标。你的图标必须预声明在 Info.plist 的CFBundleAlternateIcons字典里。替换图标时不能有透明通道Alpha 通道这是审核红线很多人被拒都是栽在这里。同一个图标文件名一旦被系统缓存短时间内频繁切换可能不生效。从产品视角看iOS 的限制决定了你只能做“有限的动态”图标集合是固定的、由研发预置的服务端只能决定用哪一张不能决定上传哪一张。这个差异一定要和运营同学提前对齐否则对方拿着 Android 版本的需求过来说“我要灰度测试 100 套图标”在 iOS 上这是不可能实现的。3.2 Info.plist 配置把备用图标登记在册以 Xcode 工程为例打开 Info.plist 的 Source Code 视图添加如下内容keyCFBundleIcons/key dict keyCFBundlePrimaryIcon/key dict keyCFBundleIconFiles/key array stringAppIcon/string /array /dict keyCFBundleAlternateIcons/key dict keyNewYear/key dict keyCFBundleIconFiles/key array stringAppIconNewYear/string /array /dict keySummer/key dict keyCFBundleIconFiles/key array stringAppIconSummer/string /array /dict /dict /dictCFBundleAlternateIcons的 key 就是你自己定义的图标名NewYear、Summer这些名字后面会作为参数传给系统 API。CFBundleIconFiles数组里填的是 Assets.xcassets 里图片集的名称注意这个名称不是物理文件名而是图片集的 resource 名称。这里有一个很隐蔽的坑如果 Assets.xcassets 中同时存在多个尺寸的同一图标比如 60pt、76pt、83.5pt系统会自行挑选合适尺寸你不需要手动指定。但如果你把图标直接作为独立图片文件丢进 Bundle而不是通过 Assets 管理就很容易出现“真机图标加载失败退回原始图标”的情况。我在项目里统一改用 Assets 管理后这个问题就再没出现过。3.3 切换代码Objective-C 还是 Swift 都行iOS 端的切换逻辑更简单因为系统 API 封装得很干净。Objective-C 版本如下- (void)switchToIcon:(NSString *)iconName completion:(void (^)(BOOL success, NSError *error))completion { if (!iconName || iconName.length 0) { [UIApplication.sharedApplication setAlternateIconName:nil completionHandler:completion]; return; } [UIApplication.sharedApplication setAlternateIconName:iconName completionHandler:completion]; }传nil表示切回主图标即CFBundlePrimaryIcon这是很多人会忽略的一个用法——你不需要定义一个“默认图标”的备用名。Swift 版本逻辑完全相同UIApplication.shared.setAlternateIconName(iconName) { error in if let error error { print(切换失败: \(error.localizedDescription)) } }从 Unity 侧调用时通常把这段代码放进一个.mm文件里暴露给 C# 的 extern 方法。注意 Unity 的 iOS 插件机制.mm文件放到Plugins/iOS目录C# 侧用[DllImport(__Internal)]声明即可。这个方法名称要避免和系统 API 同名防止符号冲突。3.4 审核注意事项Alpha 通道是最大雷区iOS 动态图标踩坑最多的地方就是图标素材的 Alpha 通道问题。Apple 明确要求备用图标不能包含透明像素否则上架审核会被拒被拒理由通常是“App icon cannot contain transparency”。这个问题在本地测试时还不一定能发现因为模拟器上看起来一切正常真机上也可能正常显示但审核人员那边一看到带透明区域的图标直接打回。我的处理办法是在做图阶段就要求美术导出 PNG 时去掉透明通道或统一填充纯色背景后再交付。如果你拿到素材后发现确实带透明区域可以用脚本来一次批量处理比如 Python 的 Pillow 库把PIL.Image.open读取后convert(RGB)强制去除 Alpha 通道。这一步建议纳入 CI 或资产导出流水线人工手动检查太容易漏。另外还有一条潜规则setAlternateIconName调用后系统弹窗要求用户确认而且每次调用之间建议间隔几秒。如果游戏内某个页面频繁触发切换比如玩家疯狂点击换肤按钮就会出现连续弹窗用户体验非常差。我在封装层加了一个节流策略同一目标图标 5 秒内重复请求直接忽略避免连续弹窗。4. Unity 工程侧的插件封装与调用4.1 统一接口设计一套 C# API双端分发双端原生逻辑都做好了Unity 侧要做的事是“屏蔽平台差异”。我在项目里设计了一个静态管理类DynamicIconManager对外只暴露两个方法public static class DynamicIconManager { // 切到指定图标key 为预置图标名null 表示恢复默认 public static void SwitchIcon(string iconKey, System.Actionbool, string callback null) { if (string.IsNullOrEmpty(iconKey)) { RestoreDefaultIcon(callback); return; } #if UNITY_EDITOR Debug.Log($[DynamicIcon] Editor mode skip switch, target: {iconKey}); #elif UNITY_ANDROID AndroidIconManager.SwitchIcon(iconKey, callback); #elif UNITY_IOS IOSIconManager.SwitchIcon(iconKey, callback); #else Debug.LogWarning([DynamicIcon] Not supported platform); #endif } public static void RestoreDefaultIcon(System.Actionbool, string callback null) { SwitchIcon(null, callback); } }这样设计的好处是业务层完全感知不到平台差异。比如活动弹窗里有一个“换新装扮图标”的按钮业务脚本只需要这样调public class NewYearActivity : MonoBehaviour { public void OnClickChangeIcon() { DynamicIconManager.SwitchIcon(NewYear, (success, msg) { if (success) { // 可以在这里弹个 Toast 或刷新 UI 状态 } else { Debug.LogWarning($换图标失败: {msg}); } }); } }Android 和 iOS 的差异被完全隔离在这层抽象后面。但请注意这个抽象只能统一调用形式不能抹平能力边界。比如 iOS 的弹窗确认机制、Android 的桌面刷新延迟这些平台特性还是要靠运营侧和文档去管控预期。4.2 Android 桥接增强支持多图标动态适配前面 Java 代码演示的是固定两个图标的情况但实际项目里往往有 46 个图标版本图标、活动图标、节日图标、默认图标。这时再做useNewYear ? A : B这种判断就太死板了。我重构了一版用目标图标名去 Manifest 里找对应 aliaspublic class IconSwitcher { public static void switchIcon(Context context, String iconKey) { String packageName context.getPackageName(); String targetAlias packageName .MainActivity_Alias_ iconKey; // 如果主图标对应的 key 传 Default则对应的别名是 MainActivity_Alias_Default // 这一步需要读取 Manifest 中已声明的 alias避免把未注册的组件名传入 // 如果 target 不是当前启用的组件则启用 target、禁用旧组件 // 具体实现可以通过 PackageManager.queryIntentActivities 动态查找也可以维护映射表 } }在具体实现中我倾向于维护一张映射表private static final MapString, String ALIAS_MAP new HashMap(); static { ALIAS_MAP.put(Default, .MainActivity_Alias_Default); ALIAS_MAP.put(NewYear, .MainActivity_Alias_NewYear); ALIAS_MAP.put(Summer, .MainActivity_Alias_Summer); // 每新增一个图标在这里登记 alias }这样每次新增图标只需要三步美术出图、Manifest 加一个 alias 节点、映射表加一行。要注意映射表中的 alias 名称必须和 Manifest 里声明的android:name完全一致多一个点少一个点都会导致ComponentName找不到组件切换直接失败。还有一个经验之谈动态切换前最好查询当前启用的 alias避免重复设置同一个组件导致额外开销。可以用PackageManager.getComponentEnabledSetting(componentName)拿到当前状态如果已经是 ENABLED 就直接跳过。虽然设置同一个组件状态不会报错但多一次 Binder 调用在频繁切换时会造成不必要的延迟。4.3 iOS 桥接增强正确传参和回调iOS 侧桥接时最容易出错的是 C# 和 Objective-C 的字符串参数传递。C# 侧声明如下#if UNITY_IOS !UNITY_EDITOR [DllImport(__Internal)] private static extern void _switchAppIcon(string iconName, System.Actionint, string callback); #endifObjective-C 侧实现void _switchAppIcon(const char *iconName, UnityCallback callback) { NSString *name iconName ? [NSString stringWithUTF8String:iconName] : nil; if (name.length 0) name nil; [[DynamicIconHelper sharedInstance] switchToIcon:name completion:^(BOOL success, NSError *error) { if (success) { callback(1, ok); } else { callback(0, error.localizedDescription.UTF8String ?: unknown); } }]; }这里必须小心 C# 回调函数的生命周期。System.Actionint, string传给原生层时本质上是一个函数指针如果 C# 侧那个委托对象被 GC 回收了原生层再回调就会导致崩溃。稳妥做法是在 C# 侧用一个字典保存所有待回调的委托键可以是一个自增 ID原生层切换完成后再调用 C# 侧的静态方法取出委托并执行。这种方法虽然繁琐但抗风险能力强不至于因为一个回调崩溃整个项目。4.4 资源与构建图标素材进包的正确姿势Android 端所有图标资源必须放在modules/unityLibrary/src/main/res/mipmap-*目录下Unity 打包时会合并进 APK/AAB。如果你用的是 Unity 2020 及以上版本还可以通过res目录自定义资源。我把不同图标的 mipmap 资源做成一个独立的 Android Library 模块然后在 Unity 工程的build.gradle里依赖它。这样做的优势是资源模块可以独立维护图标更新时不需要动 Unity 工程只需要重新构建 aar。iOS 端所有备用图标要放进主工程的Assets.xcassetsUnity 打包生成的 Xcode 工程中Assets.xcassets默认位于Unity-iPhonetarget 名下。因为 Unity 打包时不会主动读取你自定义的 Xcode 工程所以我的做法是iOS 桥接代码和图标资源都放进同一个 Unity 插件目录Assets/Plugins/iOS其中.xcassets目录会直接被 Unity 复制到生成的 Xcode 工程根目录。只要你确认 Xcode 工程里Build Phase Copy Bundle Resources包含这组资源打包后就能正常访问。这里有一个容易迷惑的点Assets.xcassets 是目录不是文件。在 Unity 里往Plugins/iOS放的时候需要保留完整的.xcassets目录结构而不是只放里面的 PNG。Unity 对文件夹的复制是递归的所以没有问题但版本控制时要注意.xcassets内部的文件名尽量不要包含空格Xcode 能处理但容易出幺蛾子。5. 双端实测经验与常见坑5.1 Android 桌面图标不刷新怎么办这是 Android 端被问最多的问题我自己的实测结果是这样的华为、小米、OPPO、vivo 等主流厂商的系统 Launcher在组件启用状态变更后基本都能在 13 秒内自动刷新图标但三星等海外机型上偶尔会出现调用完迟迟不更新的情况。这时候可以尝试确认调用成功检查pm.getComponentEnabledSetting返回的新状态是否已经是 ENABLED。如果状态没变说明我们的包名或 alias 拼接有误。强制刷新桌面通过startActivity触发CATEGORY_HOME目的意的变化或者直接锁屏再点亮屏幕多数 Launcher 会重新读取应用列表。确认没有多入口残留如果原有 Activity 和 alias 同时带 LAUNCHER桌面会显示两个入口导致新图标的 alias 虽然切换成功了但用户看到的是旧入口的图标。这个前面已经强调过再重点重复一遍主 Activity 不能带 LAUNCHER只让 alias 带。5.2 iOS 切图标后没有变化、系统弹窗消失iOS 上最常见的现象是调用setAlternateIconName后弹窗出现又立刻消失桌面图标没变。排查方向有两个一是图标名传错了。setAlternateIconName的参数是CFBundleAlternateIcons字典的 key不是图片集名字。我见过有同事把AppIconNewYear图片集名当参数传进去结果系统找不到对应的备用图标名只能弹窗然后失败。传参应该是NewYear。二是图标资源格式问题。如果有透明通道系统在处理时可能直接忽略请求。把图片重新导出去掉 Alpha 通道后重试。还有一个 iOS 独有的怪问题当你连续快速调用切换 API 时系统可能只保留最后一次请求中间的弹窗全部被跳过。如果运营配置的活动页有多个换肤入口务必做好节流和幂等目标图标相同就不要再调用。5.3 图标切换与游戏内状态的同步功能上线前记得处理一个问题图标切换成功后游戏内的“当前图标状态”UI 怎么同步比如玩家在设置页看到“当前图标春节版”切换成功后按钮要变为置灰或显示“已启用”。这个状态不要只靠本地记录因为用户可能杀进程重进或者系统恢复组件状态极少见但存在。我的做法是切换成功回调里更新本地 PlayerPrefs每次进入游戏时再调用原生查询接口Android 用getComponentEnabledSetting反向推断当前启用的是哪个 aliasiOS 用UIApplication.shared.alternateIconName获取当前备用图标名。这样即使本地缓存被清掉UI 状态依然能和系统真实状态保持一致。封装一个查询接口到 C# 层其实很简单public static string GetCurrentIconKey() { #if UNITY_ANDROID return AndroidIconManager.GetCurrentIconKey(); #elif UNITY_IOS return IOSIconManager.GetCurrentIconKey(); #else return Default; #endif }5.4 版本更新后的图标残留问题有一种边界情况要特别留意用户安装了旧版本图标被切到“春节版”然后游戏更新到新版本Manifest 里 alias 结构调整或者把“春节版”图标删掉了。这时桌面图标可能加载失败显示系统默认图标。虽然概率不高但一旦出现用户会认为 App 坏了。避免这个问题的思路是版本升级时如果服务端下发布置要让所有用户回退到默认图标客户端可以在启动阶段检测到新版本号后主动调用一次恢复默认图标。但这里有个矛盾用户还没打开游戏你怎么让他执行恢复答案是无法预先恢复只能确保新版本中Defaultalias 是启用的、旧 alias 不存在导致系统回退到主图标。我的建议是尽量不要移除旧 alias即使图标资源废弃了也保留一个不使用的 alias 声明防止升级时桌面图标崩溃。这与代码重构的直觉相反但在动态图标的场景下是实用经验。6. 集成流程与上线检查清单最后整理一下从需求到上线的完整流程方便你直接照着排期。第一步需求评审阶段。和运营确认图标集合是否固定Android 端是否允许服务端下发任意 key 切换可以做动态拉取配置iOS 端是否接受系统弹窗确认的限制这里必须让运营在文档上签字确认不然后面天天扯皮。第二步资源准备阶段。美术输出所有图标必须包含不同尺寸Android 通用 mipmap 建议至少做 mdpi、hdpi、xhdpi、xxhdpi、xxxhdpi 五档iOS 让 Xcode 自动适配 AppIcon 尺寸。美术输出时就要确认 iOS 图标无透明通道。这一步我会写一个小脚本做校验扫描所有 PNG 的 Alpha 信息发现透明直接报错。第三步原生封装与联调阶段。Android 写IconSwitcher.javaiOS 写DynamicIconHelper.mmUnity 侧写DynamicIconManager.cs。先用 Unity Editor 模式跑通“假切换”逻辑再用真机验证 Android 和 iOS。真机联调阶段至少覆盖切默认到活动、活动切回默认、活动之间互切、切换后杀进程重进、弱网下连续切换。第四步服务端接入阶段。如果要做服务端动态下发格式建议这样{ iconKey: NewYear, startTime: 1704038400, endTime: 1706716800 }客户端启动时拉取配置如果当前时间在时间段内且 iconKey 存在则调用切换超过 endTime 则恢复默认图标。建议客户端本地缓存这份配置避免每次启动都强依赖网络同时要处理“切换失败”的降级逻辑不要影响正常游戏流程切换失败只在日志里打 Warning。第五步上线检查清单。整理了一份简要表格检查项AndroidiOS图标素材已入包检查 mipmap 资源是否齐全确认 Assets.xcassets 已打包Manifest/Info.plist 预声明alias 全部声明且只有一个启用的CFBundleAlternateIcons 字典完整主入口无 LAUNCHER主 Activity 不带 LAUNCHER不涉及切换逻辑真机验证主流 ROM 各测一轮真机验证弹窗与图标刷新恢复默认路径确认 Default alias 可恢复传 nil 可恢复打包后资源检查解包 APK/AAB 看 res看 Xcode 工程内 Copy Bundle Resources这套流程走下来双端动态换图标的核心功能就已经完成了。整个方案的复杂度主要集中在原生层和平台差异处理上Unity 侧的代码量其实不大。只要把各端的坑提前埋好雷排掉后面运营每次做活动要换图标时你只需要让美术出一套图、服务端加一条配置客户端一行代码都不用改这件事就能非常优雅地滚动起来。这也是我为什么推荐提前做好映射表和通用调用层的原因功能的价值不在首版跑通而在后续每次活动都能低成本复用。