拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Unity手游双端动态换图标实战:Android activity-alias与iOS setAlternateIconName全解析

Unity手游双端动态换图标实战:Android activity-alias与iOS setAlternateIconName全解析

手游运营做久了,总会碰到一个绕不开的需求:节假日换图标、渠道包换图标、活动期间换图标。产品经理一句“能不能让用户自己选图标”,落到客户端这边就是Android和iOS两套完全不同的实现路径。Android靠activity-alias玩组件启用禁用,iOS靠setAlternateIconName走系统API,Unity层还要把两边封装成一套统一接口。这里面的坑不算深,但足够碎,碎到你不踩一遍根本不知道哪里有雷。下面把我实际落地这套方案的过程完整拆开讲一遍,包括选型理由、代码细节、真机验证结果,以及几个文档里不会写的注意事项。

1. 先搞清楚双端换图标的底层机制差异

动手写代码之前,必须先把两端的实现原理吃透。很多人一上来就搜“Unity换图标插件”,结果发现插件底层还是这两套东西,绕不开。理解机制差异,才能明白为什么接口设计要那样做,也才能在出问题时快速定位。

1.1 Android端:activity-alias是唯一正路

Android换桌面图标,本质上是切换AndroidManifest.xml里声明的一组activity-alias。每个别名指向同一个主Activity,但各自带不同的android:icon和android:label。系统桌面读取的是当前处于enabled状态的那个别名。

关键点在于:同一时刻只能有一个别名处于启用状态,主Activity本身要设为enabled="false",否则会出现两个图标。切换时通过PackageManager.setComponentEnabledSetting()动态启用目标别名、禁用其余别名。

<activity android:name=".MainActivity" android:exported="true" android:enabled="false"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity-alias android:name=".IconDefault" android:targetActivity=".MainActivity" android:enabled="true" android:exported="true" android:icon="@mipmap/ic_launcher_default" android:label="@string/app_name"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity-alias> <activity-alias android:name=".IconHalloween" android:targetActivity=".MainActivity" android:enabled="false" android:exported="true" android:icon="@mipmap/ic_launcher_halloween" android:label="@string/app_name"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity-alias>

这里有个容易忽略的细节:activity-alias的android:name建议用相对包名的形式(如.IconHalloween),不要写全限定名,否则在不同渠道包改包名时容易出错。另外每个别名都必须带完整的intent-filter,否则桌面识别不到。

1.2 iOS端:setAlternateIconName的硬性约束

iOS从10.3开始提供UIApplication.setAlternateIconName(_:completionHandler:),配合Info.plist里的CFBundleAlternateIcons字典声明备选图标。调用时传入图标名称即可切换,传nil则恢复主图标。

<key>CFBundleIcons</key> <dict> <key>CFBundlePrimaryIcon</key> <dict> <key>CFBundleIconFiles</key> <array> <string>AppIcon</string> </array> </dict> <key>CFBundleAlternateIcons</key> <dict> <key>Halloween</key> <dict> <key>CFBundleIconFiles</key> <array> <string>icon_halloween</string> </array> <key>UIPrerenderedIcon</key> <false/> </dict> </dict> </dict>

iOS这边的约束比Android多得多,我踩过的几个点:

  • 备选图标必须是完整的、已经打包进bundle的图片资源,不能运行时下载替换。想动态下发图标?没门。
  • 图标尺寸必须齐全(20/29/40/60/76/83.5pt的@2x@3x),缺一个在某些机型上就可能不显示或显示模糊。
  • 切换时系统会弹一个“您已更改App图标”的提示框,这个提示无法通过公开API去掉,只能接受。
  • 调用必须在主线程,且建议在applicationDidBecomeActive之后再调用,否则可能静默失败。

1.3 两端机制对比与接口设计启示

维度Android (activity-alias)iOS (setAlternateIconName)
图标来源打包进APK的资源打包进bundle的资源
切换方式启用/禁用组件调用系统API
是否重启App否,但桌面图标刷新有延迟否,但会弹系统提示
图标数量限制理论上无硬限制建议不超过10个
动态下发不支持不支持
用户可见提示无有系统弹窗

看完这张表就明白了:两端都不支持运行时下载图标,所以“动态更换”的真实含义是预置多套图标,运行时切换。接口设计上,Unity层只需要暴露一个SetIcon(string iconKey)方法,内部根据平台分发即可。图标资源在打包时就固定好,运营侧能选的只是“切哪个”,不是“传什么图”。

2. Unity层统一接口的封装思路

Unity本身没有跨平台的换图标API,必须写原生插件。我的做法是Android用AAR、iOS用静态库或直接C#调用Objective-C,Unity层只保留一个薄薄的封装。这样做的理由是:原生逻辑改动频繁,放在原生侧编译调试更快;Unity侧接口保持稳定,业务代码不用跟着改。

2.1 C#侧接口定义与平台分发

Unity侧的接口要足够简单,简单到业务同学看一眼就会用。我定义成这样:

using System.Runtime.InteropServices; using UnityEngine; public static class AppIconChanger { #if UNITY_IOS && !UNITY_EDITOR [DllImport("__Internal")] private static extern void _ChangeAppIcon(string iconName); #endif public static void SetIcon(string iconKey) { #if UNITY_EDITOR Debug.Log($"[Editor] 模拟切换图标: {iconKey}"); #elif UNITY_ANDROID using (var unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer")) using (var activity = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity")) using (var helper = new AndroidJavaClass("com.yourcompany.icon.IconHelper")) { helper.CallStatic("changeIcon", activity, iconKey); } #elif UNITY_IOS _ChangeAppIcon(iconKey); #endif } }

这里有个设计取舍值得说:为什么用静态类而不是MonoBehaviour单例?因为换图标是个无状态的一次性操作,不需要挂在场景里,静态类调用最省事,也不会因为场景切换丢失引用。另外iconKey用字符串而不是枚举,是为了让运营配置能直接映射,不用改代码。

2.2 Android原生侧的实现细节

Android侧的核心是遍历所有activity-alias,找到目标启用、其余禁用。注意setComponentEnabledSetting的第三个参数DONT_KILL_APP,不加这个App会被杀掉重启,用户体验很差。

package com.yourcompany.icon; import android.content.ComponentName; import android.content.Context; import android.content.pm.PackageManager; public class IconHelper { private static final String[] ALL_ALIASES = { ".IconDefault", ".IconHalloween", ".IconChristmas", ".IconSpring" }; public static void changeIcon(Context context, String iconKey) { String targetAlias = mapKeyToAlias(iconKey); PackageManager pm = context.getPackageManager(); String pkg = context.getPackageName(); for (String alias : ALL_ALIASES) { String fullName = pkg + alias; int newState = fullName.equals(pkg + targetAlias) ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED : PackageManager.COMPONENT_ENABLED_STATE_DISABLED; pm.setComponentEnabledSetting( new ComponentName(pkg, fullName), newState, PackageManager.DONT_KILL_APP ); } } private static String mapKeyToAlias(String key) { switch (key) { case "halloween": return ".IconHalloween"; case "christmas": return ".IconChristmas"; case "spring": return ".IconSpring"; default: return ".IconDefault"; } } }

实测下来有几个坑必须提醒:

  • 切换后桌面图标不会立即刷新,部分机型(尤其是国产ROM)需要几秒到几十秒,甚至要手动下拉通知栏触发刷新。这是系统行为,改不了。
  • 不要在主线程做耗时操作,虽然setComponentEnabledSetting本身很快,但连续切换多个别名时建议放到子线程,避免ANR。
  • 首次安装后如果默认别名没启用,会出现没有图标的尴尬情况。务必保证IconDefault初始enabled="true"。

2.3 iOS原生侧的实现与回调处理

iOS侧用Objective-C写一个C函数供Unity调用,内部转成setAlternateIconName。注意回调里要处理错误,否则失败了你都不知道为什么。

#import <UIKit/UIKit.h> extern "C" void _ChangeAppIcon(const char* iconName) { NSString *name = [NSString stringWithUTF8String:iconName]; NSString *alternateName = [name isEqualToString:@"default"] ? nil : name; dispatch_async(dispatch_get_main_queue(), ^{ UIApplication *app = [UIApplication sharedApplication]; if (![app supportsAlternateIcons]) { NSLog(@"[IconChanger] 当前设备不支持备选图标"); return; } [app setAlternateIconName:alternateName completionHandler:^(NSError * _Nullable error) { if (error) { NSLog(@"[IconChanger] 切换失败: %@", error.localizedDescription); } else { NSLog(@"[IconChanger] 切换成功: %@", alternateName ?: @"default"); } }]; }); }

iOS这边我踩过最深的坑是图标名称大小写敏感。Info.plist里写的是Halloween,代码里传halloween,直接静默失败,回调里error还是nil,查了半天才发现。另外supportsAlternateIcons在iOS 10.3以下返回false,虽然现在低版本占比极低,但保险起见还是判断一下。

3. 图标资源准备与打包配置的实操要点

代码写完了不代表能跑通,资源准备和打包配置才是真正耗时间的地方。我见过太多人代码没问题,卡在图标不显示上,最后发现是资源命名或plist配置错了。

3.1 Android图标资源的目录与命名规范

Android的图标资源放在res/mipmap-*目录下,每个密度一套。换图标场景下,我建议每套图标单独一个前缀,比如ic_launcher_default、ic_launcher_halloween,避免和默认图标混淆。

密度目录尺寸用途
mipmap-mdpi48x48低密度屏
mipmap-hdpi72x72中密度屏
mipmap-xhdpi96x96高密度屏
mipmap-xxhdpi144x144超高密度屏
mipmap-xxxhdpi192x192极高清屏

注意:如果只放一套mipmap-xxhdpi,低密度机型会缩放显示,可能模糊。建议至少提供hdpi、xhdpi、xxhdpi三套。

另外,Android 8.0以上支持自适应图标(Adaptive Icon),如果你的默认图标用了ic_launcher.xml(foreground+background),备选图标也建议用同样的方式,否则在部分启动器上显示效果不一致。自适应图标的XML长这样:

<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <background android:drawable="@color/icon_bg_halloween"/> <foreground android:drawable="@mipmap/ic_foreground_halloween"/> </adaptive-icon>

3.2 iOS备选图标的plist配置与尺寸清单

iOS的备选图标配置比Android更繁琐,因为要在Info.plist里逐个声明,而且每个图标都要提供完整尺寸。我整理了一份最小可用尺寸清单:

文件名尺寸(pt)倍率实际像素
icon_halloween@2x.png60x60@2x120x120
icon_halloween@3x.png60x60@3x180x180
icon_halloween_20@2x.png20x20@2x40x40
icon_halloween_20@3x.png20x20@3x60x60
icon_halloween_29@2x.png29x29@2x58x58
icon_halloween_29@3x.png29x29@3x87x87
icon_halloween_40@2x.png40x40@2x80x80
icon_halloween_40@3x.png40x40@3x120x120

Info.plist里对应的配置:

<key>CFBundleAlternateIcons</key> <dict> <key>Halloween</key> <dict> <key>CFBundleIconFiles</key> <array> <string>icon_halloween</string> <string>icon_halloween_20</string> <string>icon_halloween_29</string> <string>icon_halloween_40</string> </array> <key>UIPrerenderedIcon</key> <false/> </dict> </dict>

提示:CFBundleIconFiles里写的是文件名前缀,不带倍率和扩展名。系统会自动匹配@2x、@3x。如果只写icon_halloween,系统会找icon_halloween@2x.png和icon_halloween@3x.png。

3.3 Unity打包时的资源导入设置

Unity导入这些图标时,有个设置必须改:Texture Type要设为Sprite (2D and UI)或者Default都行,但关键是不要被Unity压缩。Android的mipmap目录Unity不直接管理,是放在Plugins/Android/res下;iOS的图标则要放在Assets/Plugins/iOS下,并在Xcode工程里正确引用。

我的做法是:Android图标直接放Assets/Plugins/Android/res/mipmap-*,Unity打包时会自动合并进APK。iOS图标放Assets/Plugins/iOS/Icons/,然后写一个PostProcessBuild脚本,在Xcode工程生成后自动把图标文件拷贝到正确位置并修改plist。

#if UNITY_IOS using UnityEditor; using UnityEditor.Callbacks; using UnityEditor.iOS.Xcode; using System.IO; public class iOSIconPostProcess { [PostProcessBuild(1000)] public static void OnPostProcessBuild(BuildTarget target, string path) { string projPath = PBXProject.GetPBXProjectPath(path); PBXProject proj = new PBXProject(); proj.ReadFromFile(projPath); string targetGuid = proj.GetUnityMainTargetGuid(); string plistPath = Path.Combine(path, "Info.plist"); PlistDocument plist = new PlistDocument(); plist.ReadFromFile(plistPath); // 这里动态写入CFBundleAlternateIcons // 具体代码略,核心是构造PlistElementDict plist.WriteToFile(plistPath); } } #endif

这个PostProcess脚本能省掉每次手动改plist的麻烦,尤其是图标数量多的时候。我一开始手动改,改到第三个图标就烦了,果断写脚本。

4. 真机验证中暴露的问题与排查链路

代码和资源都齐了,真机一跑,问题才真正开始。下面这几个是我实际遇到并解决的,排查过程完整记录,方便你对照。

4.1 Android切换后图标不刷新甚至消失

现象:调用切换后,桌面图标要么还是旧的,要么直接消失,重启手机才恢复。

排查链路:

  1. 先确认AndroidManifest.xml里主Activity的enabled是否为false。如果主Activity是true,会出现两个图标或冲突。
  2. 检查所有别名的intent-filter是否完整。少一个LAUNCHERcategory,桌面就认不出来。
  3. 用adb shell dumpsys package com.yourpackage | grep -A 5 "Activity Resolver"查看当前启用的组件状态。
  4. 确认setComponentEnabledSetting的flag是DONT_KILL_APP,不是0。

最终原因:我的问题是主Activity的enabled忘了设false,导致系统同时看到主Activity和别名,桌面渲染混乱。改成false后正常。

经验:国产ROM(某米、某为)对组件状态变更的响应比原生慢,切换后建议延迟1-2秒再提示用户“切换成功”,否则用户以为没生效又点一次。

4.2 iOS切换弹窗无法去除的应对

现象:每次切换图标,系统都弹“您已更改App图标”的提示,产品要求去掉。

结论:去不掉。这是iOS系统的强制提示,公开API无法屏蔽。网上有些“黑科技”用私有API或Method Swizzling绕过,但会导致审核被拒,绝对不能用。

应对方案:在产品层面接受这个提示,或者把切换入口做得更有仪式感,让用户觉得这个提示是“确认操作”的一部分。我在实际项目里是加了一个自定义的确认弹窗,用户点“确认更换”后才调用系统API,这样系统提示出现时用户不会觉得突兀。

4.3 编辑器下模拟与真机行为不一致

现象:Unity编辑器里测试正常,打包到真机后接口调用无效。

原因:编辑器下走的是#if UNITY_EDITOR分支,只打了Log,没真正调用原生。真机上如果原生插件没正确导入,或者包名对不上,就会静默失败。

排查方法:

  • Android用adb logcat | grep IconHelper看原生日志有没有输出。
  • iOS用Xcode连真机看Console,过滤IconChanger。
  • 确认AAR/JAR已放入Assets/Plugins/Android,iOS的.mm文件已放入Assets/Plugins/iOS。

我遇到过一次AAR放了但没生效,最后发现是AAR里的AndroidManifest.xml和主工程的合并冲突,把AAR里的manifest删掉只保留代码就好了。

4.4 多渠道包下别名冲突问题

现象:打多渠道包时,不同渠道的applicationId不同,但activity-alias的android:name用了全限定名,导致切换时找不到组件。

解决:别名统一用相对包名(.IconXxx),让构建系统自动补全当前包名。如果必须用全限定名,就要在代码里动态获取context.getPackageName()拼接,不要硬编码。

// 错误做法 new ComponentName("com.yourcompany.app", "com.yourcompany.app.IconHalloween"); // 正确做法 String pkg = context.getPackageName(); new ComponentName(pkg, pkg + ".IconHalloween");

这个坑在多渠道打包时特别隐蔽,因为单渠道测试时包名固定,不会暴露问题。

5. 上线前的兼容性检查与运营侧配合

功能跑通只是第一步,上线前还有一堆兼容性和运营配合的事要处理。这部分往往被技术同学忽略,但直接关系到功能能不能真正用起来。

5.1 低版本系统的降级策略

Android这边,activity-alias从API 1就支持,基本不用担心。但要注意Android 8.0的自适应图标、Android 12的启动画面(SplashScreen)对图标的影响。iOS这边,setAlternateIcons需要iOS 10.3+,supportsAlternateIcons返回false时要有降级提示。

我的降级策略是:不支持就隐藏切换入口,而不是让用户点了没反应。判断逻辑放在原生侧,Unity侧只拿一个bool结果。

public static bool IsIconSwitchSupported() { #if UNITY_ANDROID return true; // Android全版本支持 #elif UNITY_IOS && !UNITY_EDITOR return _IsIconSwitchSupported(); #else return false; #endif }

5.2 图标切换与热更新的边界

这里必须说清楚:换图标不能和热更新混在一起做。因为图标资源是打包进安装包的,热更新只能更新代码和部分资源,改不了已经安装的APK/IPA里的图标。所以“动态更换”的“动态”指的是运行时切换预置图标,不是运行时下载新图标。

如果运营真的需要“活动期间临时换图标”,正确做法是:活动开始前发一个版本,把活动图标预置进去,活动开始时通过配置下发指令切换。活动结束后再切回默认。整个过程不需要重新发版,但图标本身必须提前打包。

5.3 运营配置表的设计建议

运营侧需要一个配置表来管理“什么时间切什么图标”。我建议的字段:

字段类型说明
icon_keystring图标标识,与代码里的key对应
start_timedatetime生效开始时间
end_timedatetime生效结束时间
priorityint优先级,多个活动重叠时取高优先级
platformstringandroid/ios/all

客户端启动时拉取配置,根据当前时间判断该用哪个图标,和本地记录的上次图标对比,不一致就调用切换。这样运营改配置就能控制,不用发版。

注意:配置下发要考虑网络失败的情况,本地要缓存上一次的配置,避免断网时图标乱切。

5.4 用户手动切换的入口设计

如果功能是给用户自己选图标(比如会员特权),入口设计也有讲究。我见过把入口藏在设置页第五层的,用户根本找不到。建议放在“设置-个性化”或者“我的-装扮”这种显眼位置,并且切换后给一个toast提示“图标已更换,请查看桌面”。

另外,用户手动切换的图标要本地持久化(PlayerPrefs或原生SharedPreferences/NSUserDefaults),下次启动时检查当前图标是否和记录一致,不一致就重新切换。因为有些系统在App更新后会重置图标状态。

6. 几个文档里不会写的实操心得

最后这部分是我踩坑踩出来的经验,官方文档不会告诉你,但实际项目里能救命。

第一,Android切换图标后,桌面快捷方式会失效。如果用户之前把App图标拖到了桌面,切换别名后那个快捷方式指向的还是旧组件,点击可能无响应。这是系统机制,无法避免。缓解办法是切换后提示用户“如果桌面图标异常,请重新添加”。

第二,iOS的备选图标数量不要超过10个。虽然理论上没硬限制,但每个图标都要打包进bundle,数量多了会显著增大包体。而且Info.plist里配置太多,Xcode编译和审核都可能变慢。我一般控制在5个以内。

第三,测试时一定要用真机,模拟器不可靠。Android模拟器对activity-alias的支持不完整,iOS模拟器根本不支持setAlternateIconName。必须真机验证,而且最好覆盖至少一台国产ROM和一台原生Android。

第四,图标切换的时机要避开App启动瞬间。我试过在Awake里调用切换,结果iOS上偶发失败。后来改到启动后延迟1秒,或者等applicationDidBecomeActive之后再调用,成功率明显提升。Android这边倒是没这个问题,但统一延迟处理更稳妥。

第五,做好日志埋点。切换成功、失败、用户点击、系统不支持,这几个事件都要埋点。上线后你才能知道到底有多少用户在用这个功能,失败率高不高。我上线第一周就靠埋点发现某款机型切换失败率高达30%,及时加了降级处理。

这套方案我在两个项目里落地过,Android和iOS双端跑通,线上稳定运行了大半年。核心代码量不大,但细节多,尤其是资源准备和真机验证阶段,急不得。如果你正准备做这个功能,建议先把第3章的资源配置和第4章的排查链路看两遍,能帮你省下不少调试时间。

返回列表