1. 为什么我盯上了"动态换图标"这个需求
先交代一下背景。项目是Unity开发的休闲手游,上线半年后运营开始频繁做节日活动,每次活动前运营和美术都要折腾一轮:更新商店图、更新开屏图、更新icon外的所有露出位。最后运营提了一个需求——能不能像很多头部产品那样,节假日直接把桌面图标换成活动皮肤?用户还没点开App,一眼就能在桌面看到品牌变化。
当时团队第一反应是:换图标不是得重新出包吗?就算可以动态换,双端限制一堆,搞不好就是个吃力不讨好的脏活。后来我把Android和iOS两端的原生方案摸了一遍,又做了Unity桥接层,发现在Unity手游里做动态换图标完全可行,但确实有不少坑。这篇文章就把完整的技术方案、踩坑过程、双端差异一次性说清楚,给同样在Unity里做动态图标需求的兄弟留个参考。
先说结论:Unity手游动态换App图标,Android靠Activity-alias多入口切换,iOS靠系统级图标替换接口(CFBundleAlternateIcons)。Unity层不需要太多代码,核心在原生桥接和很多容易被忽略的平台细节。如果你已经知道双端原理,可以直接跳到第3章看Unity侧的完整调用链路和踩坑记录。
2. 双端实现原理:系统能力和Unity之间的关系
2.1 Android端:Activity-alias和桌面图标的"真身切换"
Android的桌面图标本质上是Launcher解析到的一个入口。对同一个应用,我们可以声明多个入口,每一个入口由<activity-alias>完成映射。默认的入口一般指向启动时的主Activity(通常是UnityPlayerActivity),换图标时只需要把当前生效的入口切换到另一个携带不同icon资源的alias。
具体来说,AndroidManifest里可以这样组织:
<activity-alias android:name=".MainActivity_Skin_Default" android:enabled="true" android:exported="true" android:icon="@mipmap/ic_launcher_default" 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_Skin_Summer" android:enabled="false" android:exported="true" android:icon="@mipmap/ic_launcher_summer" android:targetActivity=".MainActivity"> ... </activity-alias>系统在桌面只认enabled="true"的那个alias。所以换图标的逻辑很简单:
- 先把当前alias的
enabled改为false。 - 再把目标alias的
enabled改为true。 - 调用
PackageManager的setComponentEnabledSetting应用这套改变。 - 等系统桌面刷新桌面入口图标。
这种方式有一个重要特点:图标的切换不需要重新杀掉App进程,但桌面上的图标刷新有一定的延迟,且某些ROM在短时间频繁切换时会漏刷。后面我会讲到实测过程中怎样规避这种可靠性问题。
2.2 iOS端:系统级替换,一次只能对应一个备用图标文件
iOS端在iOS 10.3之后开放了UIApplication的setAlternateIconName接口。你在工程中把一组备用图标放进Asset Catalog或直接放到Bundle指定目录,然后在代码里调用接口让系统把桌面图标换掉。
[[UIApplication sharedApplication] setAlternateIconName:@"AppIconSummer" completionHandler:^(NSError * _Nullable error) { if (error) { // 切换失败,可能是文件名没配对或plist配置有问题 } }];iOS的动态换图标原理其实比Android简单,但限制比Android严格:
- 备用图标数量不能太大,必须在
CFBundleIcons的CFBundleAlternateIcons字典里预先声明。 - 一份备用图标必须包含所有尺寸,通常建议把
Icon-Small(40pt)、Icon-Small-50(iPad)、Icon(60pt)、Icon-72等规格都带上,少尺寸会导致系统拒绝替换或图标显示模糊。 - 切换过程需要用户在前台触发,并且系统会弹一个确认提示,无法静默换。
- 从iOS 10.3到现在,这套机制没有太大变化,但它不会影响下一次提交审核时的截图逻辑,App Store审核界面里看到的图标始终是当前生效的那个。
2.3 Unity在其中扮演的角色:桥接层
Unity手游的代码主体是C#,但换图标需要调用原生系统能力。无论Android还是iOS,Unity都提供了C#到原生层的调用通道:
- Android:通过
AndroidJavaObject/AndroidJavaClass调用Java代码,或者用一个aar插件封装。 - iOS:通过
DllImport("__Internal")把C#方法映射到原生C/Objective-C函数,或者走Unity的UnitySendMessage反向通信。
把换图标理解成一个"系统级开关"动作的话,Unity侧做的事情其实很少:接收服务端下发的活动版本号,调原生接口记录当前图标状态,必要的场景切前台北竖屏提示,具体切换动作交给原生。所以整个项目的架构可以固定为:
C#事件层(收到活动指令) ↓ 原生桥接层(Android/iOS各自实现切换协议) ↓ 系统图标刷新(桌面可见变化) ↓ 把当前图标状态上报/SDK记录,用于下次冷启动时恢复这套结构的核心点在于:原生层只管能不能切、怎么切,C#层管什么时候切、切到哪一套。分工一旦清楚,后面很多问题都好排查。
3. Android侧实操:Activity-alias多图标切换的完整落地
3.1 工程准备:图标资源与Manifest规划
我先在Unity导出的Android工程里做验证。注意如果你是在Unity里直接出aar再打包到Android原生工程,等于是多套了一层,操作路径稍有不同,但Manifest调整思路是一样的。
我准备了5套图标:默认、春季、夏季、秋季、冬季,分别放在不同的mipmap目录。建议在Unity的Plugins/Android路径下维护一个AndroidManifest.xml,直接在清单里预先声明好alias。不要想着运行时动态改Manifest,这条路是不通的,而且如果Manifest里根本没有对应alias,setComponentEnabledSetting会直接抛IllegalArgumentException。
每一项alias都要注意exported属性。如果是给桌面入口用,exported=true是必须的,否则点击图标无法拉起Activity。另外,targetActivity指定的必须是实际存在的Activity类,建议统一指向UnityPlayerActivity,避免多个入口之间状态不一致。
<application android:label="@string/app_name" ...> <activity android:name="com.unity3d.player.UnityPlayerActivity" ... /> <activity-alias android:name="com.game.launcher.default" android:enabled="true" android:exported="true" android:icon="@mipmap/ic_launcher_default" android:targetActivity="com.unity3d.player.UnityPlayerActivity" /> <activity-alias android:name="com.game.launcher.spring" android:enabled="false" android:exported="true" android:icon="@mipmap/ic_launcher_spring" android:targetActivity="com.unity3d.player.UnityPlayerActivity" /> </application>注意:默认主Activity即
UnityPlayerActivity本身不要加intent-filter。如果MainActivity上带了MAIN和LAUNCHER,那么它本身就是一个桌面入口,与alias的图标会同时存在,即使你去disable它,系统在某些ROM上仍会残留缓存入口。正确做法是:主Activity不声明入口,所有入口全部由alias承担。
这里特别说一下为什么采用"默认alias + 多个皮肤alias"而不是"默认Activity不加alias、只在需要时enable某个alias"。因为一旦你disable掉当前唯一入口,在所有图标切换完成的瞬间,系统可能找不到任何LAUNCHER入口,App直接变成"无桌面入口"状态。某些国产ROM会把应用排序到"未安装完整应用"一类。所以必须保证至少有1个alias永远处于enabled状态。这也是我推荐始终保留一个默认alias的原因。
3.2 Java层切换逻辑
在Android端,我在Unity导出的工程里加了一个类,例如IconSwitchHelper.java,封装切换逻辑:
public class IconSwitchHelper { public static final String[] ALIAS_NAMES = { "com.game.launcher.default", "com.game.launcher.spring", "com.game.launcher.summer", "com.game.launcher.autumn", "com.game.launcher.winter" }; public static void switchIcon(Context context, int index) { if (index < 0 || index >= ALIAS_NAMES.length) return; PackageManager pm = context.getPackageManager(); String target = ALIAS_NAMES[index]; // 先把所有alias关掉 for (String alias : ALIAS_NAMES) { pm.setComponentEnabledSetting( new ComponentName(context, alias), PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP ); } // 打开目标alias pm.setComponentEnabledSetting( new ComponentName(context, target), PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP ); } }这段代码在整个流程里最需要留意的是DONT_KILL_APP这个flag。如果不加这个flag,PackageManager在修改组件状态时会顺带杀掉App进程。对Unity手游来说,进程被杀意味着用户当前会话直接中断,C#与Java的通信也会断掉。千万别漏。
3.3 冷启动恢复:桌面图标不是"切了就永久换"的保险箱
Activity-alias方案有个很要命的情况:你的代码里虽然切换了启用状态,但用户在桌面短时间看不到变化,这是正常的,因为桌面Launcher的图标缓存有自己的刷新机制。但如果用户等半天还不刷新,多半是以下原因之一:
- 桌面App没有收到系统广播,图标缓存还是旧的。
- 系统直接把App的"最近任务卡片"里的图标也缓存了。
- 部分ROM要求桌面内"应用信息"页手动刷新一次。
所以我强烈建议在切换后调用一次sendBroadcast来触发桌面刷新。常见做法是发系统级刷新,不过这个broadcast在不同版本有权限限制。更通用的做法是调用pm.setComponentEnabledSetting之后再通过ACTION_PACKAGE_CHANGED通知桌面,让Launcher重新读取入口:
Intent intent = new Intent(Intent.ACTION_PACKAGE_CHANGED); intent.setPackage(context.getPackageName()); context.sendBroadcast(intent);部分桌面收到该广播后能立刻重读icon,但另一部分(尤其非Google原生的桌面)依然会延迟。实测中发现,最后能稳定生效的,还是让用户把App从"最近任务"划掉,或者直接杀进程重启冷启动。因为冷启动时Launcher会强制做一次完整解析。
既然冷启动是保险手段,那在Unity侧也要做对应的"恢复逻辑"。我们在C#层记住当前生效的icon版本,例如存到本地PlayerPrefs或者通过SDK的远端配置读取。每次Unity启动时,首帧回调里把这个版本号传给Java层,Java层启动时再走一遍switchIcon到对应alias。这样即使某次切换后桌面缓存没刷新,用户杀进程再点开App,也能看到正确图标。
这个"启动恢复"逻辑看起来多此一举,实际项目里它是防呆用的。因为在开发自测阶段我发现,我连续切换图标后,即便Java层状态已经改了,桌面图标却还是旧的,杀进程重启一轮才正常。当时差点怀疑是代码没生效,后来确认就是Launcher缓存问题。
3.4 Unity C#侧与Android原生互相调用的写法
在Unity工程里加桥接类,方式很多。我用的是最简单的一种:打一个aar放入Assets/Plugins/Android,同时C#侧直接操作Java静态方法。C#示例:
using UnityEngine; public class AndroidIconSwitcher : MonoBehaviour { private static AndroidJavaObject _helper; private static void PrepareHelper() { if (_helper != null) return; using (var player = new AndroidJavaClass("com.unity3d.player.UnityPlayer")) { var activity = player.GetStatic<AndroidJavaObject>("currentActivity"); var helperClass = new AndroidJavaClass("com.xxx.IconSwitchHelper"); _helper = helperClass.CallStatic<AndroidJavaObject>("getInstance", activity); } } public static void SwitchIcon(int index) { if (Application.platform != RuntimePlatform.Android) return; PrepareHelper(); _helper.Call("switchIcon", index); } }要注意C#调Java时,千万不要在子线程直接操作Unity的Activity。尽量把切换动作放到Android主线程(或者Unity主线程回调)里。之前测试时我在一个HttpClient回调里直接调了Java层切换,结果部分机型上直接闪退,后来加了RunOnUiThread稳定解决。
public void switchIcon(final Context context, final int index) { if (Looper.myLooper() == Looper.getMainLooper()) { doSwitch(context, index); return; } activity.runOnUiThread(new Runnable() { @Override public void run() { doSwitch(context, index); } }); }4. iOS侧实操:备用图标声明和切换链路
4.1 Info.plist与图标文件规划
iOS端我按Xcode工程的方式处理,在Info.plist里配置CFBundleIcons。这里有一个通用配置项和一套备用配置项:
<key>CFBundleIcons</key> <dict> <key>CFBundleAlternateIcons</key> <dict> <key>Spring</key> <dict> <key>CFBundleIconFiles</key> <array> <string>AppIconSpring</string> </array> <key>UIPrerenderedIcon</key> <false/> </dict> <key>Summer</key> <dict> <key>CFBundleIconFiles</key> <array> <string>AppIconSummer</string> </array> <key>UIPrerenderedIcon</key> <false/> </dict> </dict> <key>CFBundlePrimaryIcon</key> <dict> <key>CFBundleIconFiles</key> <array> <string>AppIcon</string> </array> <key>UIPrerenderedIcon</key> <false/> </dict> </dict>需要注意,AppIconSpring和AppIconSummer这组图标资源必须全部以实际PNG或Assets目录里的图片存在。最好是按照iOS标准图标尺寸全部提供,不要只丢一个60pt的图让系统自动缩放。系统会自动降采样大图,但前提是同一个名字下必须有足够大的尺寸,例如至少提供Icon-App-60@3x.png(180x180)、Icon-App-60@2x.png(120x120)、Icon-Small-40@3x.png(120x120)、Icon-Small-40@2x.png(80x80)等。如果尺寸不全,setAlternateIconName的completionHandler里一定会报错,而且是"contains unprocessed icon files"那种很难排查的错误。
另一件重要的事:不要只在Assets里加图却忘了在Build Phase里把图片加进Copy Bundle Resources。经常有同事把图标贴到工程文件夹里,但没加入Target,运行时切图标就报"File not found in bundle"。排查半天发现工程配置问题,特别浪费时间。
4.2 切换代码与C#桥接
iOS端原生切换代码很简单:
- (void)switchToIcon:(NSString *)iconName { [[UIApplication sharedApplication] setAlternateIconName:iconName completionHandler:^(NSError *error) { if (error) { NSLog(@"[IconSwitch] failed: %@", error.localizedDescription); } }]; }把这段代码放到一个能被Unity C#调用的类里即可。我这里用的是extern方式,在.mm文件里直接实现:
extern "C" void _SwitchToIcon(const char *iconName) { if (iconName == NULL) return; NSString *name = [NSString stringWithUTF8String:iconName]; [[UIApplication sharedApplication] setAlternateIconName:name completionHandler:^(NSError *error) { if (error) { NSLog(@"[IconSwitch] failed: %@", error.localizedDescription); } }]; }C#侧:
using System.Runtime.InteropServices; public static class IOSIconSwitcher { [DllImport("__Internal")] private static extern void _SwitchToIcon(string iconName); public static void SwitchToIcon(string iconName) { if (Application.platform == RuntimePlatform.IPhonePlayer) { _SwitchToIcon(iconName); } } }这里需要注意,之所以用__Internal,是因为Unity在iOS平台会把所有C/Objective-C代码链接到主二进制里,调用时直接把符号映射过去。C#的string类型在传到原生侧时,Unity会自动处理好UTF-8到NSString的转换,不需要额外编码处理。
4.3 iOS切换时的限制与用户交互
iOS端切换有两个影响体验的因素:
- 系统会弹出确认框,询问是否更换图标。这没法静默绕过。
- 切换过程不是瞬间的,系统有一定动画替换过程,快速连续切换容易出现只生效最后一次的情况。
在游戏内体验上,推荐的做法是:弹出一个活动公告界面,用户点击"换个节日图标",确认后立刻调原生接口。因为确认框是系统弹的,游戏内不要再做一层二次确认,否则用户被问两次会很烦。
另一个细节:
setAlternateIconName传nil表示恢复主图标。所以"默认状态"可以用SwitchToIcon(null)实现。C#侧调用前要判断字符串为空的情况,原生侧也要做NULL防护。
5. 双端方案会碰到的常见坑和可靠性处理
5.1 测试机验证的结论和坑
我在Android侧测试了Pixel 3、华为Mate 40 Pro、小米11、一加9T;iOS侧测试了iPhone 8、iPhone 12、iPhone 13 Pro(iOS 15/16)。把实际碰到的状况列在这里:
| 现象 | 出现环境 | 原因 | 解决方案 |
|---|---|---|---|
| 图标切换后桌面无变化 | 多款ROM | Launcher图标缓存未刷新 | 加广播通知 + 冷启动兜底恢复 |
| 首次安装后图标为系统默认 | 所有机型 | 首次安装时Launcher尚未收到alias状态更新 | 启动时根据本地存档恢复一次 |
| 切换后App闪退 | Android 8以下部分机型 | setComponentEnabledSetting触发组件状态广播导致进程被杀 | 使用DONT_KILL_APP+主线程调用 |
| 图标更新后显示模糊 | iOS | 备用图标尺寸不满足所有档位 | 补齐所有尺寸图片 |
| 连续切换后图标错乱 | Android | 多次快速切换导致Launcher读取竞争 | 每次切换之间增加时间间隔,或合并成一次切换 |
| 更新应用后图标恢复默认 | Android/iOS双端 | 安装包更新会重置组件状态 | 版本启动时按保存状态重设 |
其中最坑的是"首次安装后图标可能不生效"这个点。最开始用aar方式集成测试时,我们等了几分钟桌面图标都是默认的,但代码确实切到了夏季皮肤。后来发现是Android 8之后部分桌面在图标缓存刷新之外还会读一次"应用信息"里的默认icon。也就是说,即使你改了alias状态,桌面入口的android:icon可能还是从manifest解析缓存的旧值。解决办法就是在首次启动时也走一遍恢复逻辑,把当前状态强制设一遍。
5.2 高版本系统的兼容性问题:Android 12/13
Android 12引入了主题图标(monochrome icon),Android 13也有相关优化。如果你的应用声明了<adaptive-icon>,动态换图标换成非adaptive格式,系统有可能直接用默认背景色兜底,看起来就是方形色块。这一点必须提前在mipmap-anydpi-v26里给每一套皮肤配好<adaptive-icon>版本的资源,否则在Pixel系列等原生Android 12+设备上会出现图标风格突兀的问题。
适配图标结构大概是:
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <background android:drawable="@mipmap/ic_launcher_background_summer" /> <foreground android:drawable="@mipmap/ic_launcher_foreground_summer" /> </adaptive-icon>动态切换时,setComponentEnabledSetting对adaptive-icon的alias一样有效,所以Android 12/13上直接能用,但前提是你资源里必须真的存在对应的adaptive-icon配置。
5.3 审核红线:iOS更换图标不能与"功能性核心无关的频繁更换"绑定
做iOS动态图标,容易踩到一个灰色地带:苹果审核要求App Store展示的图标与用户最终看到的内容一致,但这不代表不能有备用图标。很多天气类、日历类App都有动态换图标功能。不过要注意,如果你把"换图标"做成需要付费解锁、或者做得太频繁、太复杂(比如每天随机换),审核有概率被打回。我见过项目因为图标切换后没有恢复默认、导致用户桌面出现明显与App内容无关的图标,被审核方向询问的案例。
稳妥的做法是:
- 服务端下发活动图标时,同时下发一个"活动结束自动恢复主图标"的时间戳,客户端在做热更逻辑时一并处理。
- 不要做成"每次打开App自动随机换图标",这会让用户对桌面上图标变化感到困惑,也可能收到投诉。
- 活动期间换上的备用图标,必须与App本身内容或节日主题强相关,避免"搞怪图"被审核方认定为误导用户。
5.4 与热更新体系协同:图标切换的触发链路
Unity手游大多有热更新。图标要跟着运营活动变,最合理的触发链路不是发一个强更包,而是通过服务端配置下发:
- 服务端下发活动配置(活动ID、图标ID、生效时间、失效时间)。
- Unity客户端拉取到配置后,本地记录生效图标ID。
- 客户端在空闲时(比如主界面停留超过3秒)调原生桥接层切换图标。
- 切换完成后通过SDK埋点上报结果。
- 下次冷启动时读取本地记录,做一次恢复型设置。
这个链路里有一个容易踩的坑:很多Unity游戏在启动阶段就收到活动配置,直接尝试换图标,但此时原生层组件可能还没初始化完成,或者Android某个版本的PackageManager操作过慢导致卡顿。建议把图标切换动作延迟到"主界面加载完成回调"之后,并且包一层幂等操作:当前状态等于目标状态时直接跳过。
6. 双端统一切换接口的设计心得
6.1 状态机与幂等性
原生操作不像C#上操作变量那样瞬间可见,所以C#层最好抽象成一个状态机:
Idle → Requesting → Done → (本地永久化)不要在"Done"之前重复发起切换请求。应设计一个简单的防重入标记,避免连续点击活动页面换图按钮导致双端同时接到多条指令。我实现的是一个单例管理器,内部持有一个枚举状态字段,只有状态为Idle时才允许发起新的切换请求:
public enum IconState { Idle, Switching, Done } public static void Switch(string iconId) { if (_currentState != IconState.Idle) return; _currentState = IconState.Switching; #if UNITY_ANDROID AndroidIconSwitcher.SwitchIcon(ParseIndex(iconId)); #elif UNITY_IOS IOSIconSwitcher.SwitchToIcon(iconId); #endif _currentState = IconState.Done; SaveLocal(iconId); }6.2 图标ID命名统一
Android端用索引int方便遍历alias数组,但iOS端用的是字符串iconName。为避免双端逻辑混乱,我们统一在服务端下发一个字符串ID,例如skin_lv0_default、skin_lv2_summer。C#层在桥接时做映射:Android按ID找到alias数组索引,iOS直接作为备用图标名使用。
这看起来是小事,但项目里多个客户端共用一套配置时尤其容易乱。最好在C#层做一次iconId -> MatchResult的解析,避免Android和iOS用各自不同的索引规则,导致服务端配置管理混乱。
6.3 生命周期和内存水位
换图标本身不涉及大内存操作,原生层只是改几个组件开关或调一个系统API。但要注意Unity侧的配置解析、图标名映射、状态记录尽量不要频繁分配堆内存,避免在活动切换期间和场景加载的内存峰值打架。尤其Android侧如果还要更新mipmap资源,先解引用旧的Drawable再侦察新的资源,否则会触发不必要的GC。
7. 上线后的运营玩法扩展
做完这套动态换图标能力后,最简单的玩法是节日皮肤,但还可以继续扩展:
- 新用户7日签到第7天换成庆祝图标。
- 联动活动期间换成合作方风格的图标。
- 赛季结算当天换成"最强王者"样式。
- 电竞战队夺冠后紧急换图标做实时热点营销。
这些玩法本质上都是同一套能力,换的只是"何时触发"和"换成什么图标"。有了动态换图标作为基础设施,很多活动创意都展开了一个新的可实施维度。美术资源成本是最主要的部分,原生层代码基本不用再动。
举例来说,S12赛季结算那天我们临时上线了一版"冠军之夜"图标,服务端下发配置后30分钟内全量用户被替换成活动皮肤。因为不用发版,活动当天早上提交美术,中午审核,下午生效。这在以前完全不可想象。
有一说一,运营侧其实最看重这个"不发版还能改桌面图标"的能力,远比技术侧想的要多。节日皮肤只是一个吸引用户的外衣。
8. 个人踩坑记录与工程化建议
做这个项目前后一共踩了比较大的坑大约5个,小的细节问题不下10个。逐个说一下值得注意的:
Android切图标用
PackageManager时必须指定DONT_KILL_APP。这个前面已经反复强调,但测试时还是会有人漏写,因为很多示例源码里没有这个flag。漏写一次就会发现在调完接口后Unity的Logcat突然断开,进程被杀。iOS的
setAlternateIconName只能在真机调试时触发。模拟器里调用永远会报一个icon.png not found之类的错误,会让你以为配置有问题。实际只要真机正常,模拟器忽略即可。Android上连续切换多个alias,中间不要做UI展示等待。直接全部disable再启用目标alias,比逐个切换要可靠。我最初写的是"先切到默认,再切到目标",导致桌面有时会闪现默认图标。
iOS备用图标不要和主图标同名。同名时系统会拒绝切换。如果你代码里写
setAlternateIconName:@"AppIcon",大概率执行会失败。备用图必须用不同于主图的名字,如AppIconSummer、AppIconFestival。打包流程要注意图标资源有没有被压缩。Android的
mipmap如果被shrinkResources移除,会出现运行时资源找不到的问题。建议在build.gradle里对图标资源加tools:keep,例如:
resConfig "zh-rCN" // 暂时保留下划线命名资源 android { buildTypes { release { shrinkResources true resValue "string", "keep_icon_res", "true" } } }具体可以直接用res/raw/keep.xml:
<resources xmlns:tools="http://schemas.android.com/tools"> <resource name="ic_launcher_summer" type="mipmap" tools:keep="@mipmap/ic_launcher_summer"/> </resources>- 服务端配置最好带一个"回滚开关"。如果某一版活动图标出现被用户大面积投诉(例如颜色过于刺眼、视觉恐怖),运营要能一键让所有客户端恢复默认图标。这个开关不需要客户端发版,只要下次判断活动状态时读到"已下线"即自动恢复默认。
最后提一个工程化建议:把换图标能力封装成一个独立的Unity Package或插件模块,不要散落在主工程里。这样以后接新项目时可以直接复用。我把Android的aar、iOS的源文件、C#管理器、服务端协议文档放在同一个插件目录下,后续两个项目接入基本一周内完成。
动态换图标这功能,技术上确实不难,但细节多、涉及系统能力,稍不注意就做成"看起来能换,真机上各种不生效"。把状态管理和恢复逻辑做扎实,才是能上线运营的稳定方案。