ARTICLE DETAIL

资讯详情

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

Unity手游动态更换App图标:Android与iOS双端技术方案详解

Unity手游动态更换App图标:Android与iOS双端技术方案详解 1. 动态图标这件事到底在解决什么问题做手游运营的同行大概都遇到过这种场景春节要换红色喜庆图标情人节要换粉色爱心图标版本大更新要换成新角色立绘甚至某些渠道包还需要根据活动周期每天轮换图标来刺激点击。如果每次换图标都走一次发版流程那基本等于运营节奏被审核周期彻底绑死——安卓渠道快则几小时慢则一天iOS 审核动辄一两天活动热度早就过去了。动态更换 App 图标的核心价值就在这儿让图标变成一份可以远程下发的资源而不是一个必须重新打包签名的静态资产。用户装完游戏之后客户端在合适的时机把图标切过去桌面上的样子立刻变了不需要重新下载安装包也不需要走任何商店审核。这件事在 Android 和 iOS 上的实现路径完全不同。Android 靠的是activity-alias这套组件别名机制本质上是提前在 Manifest 里注册好几个入口每个入口挂一个不同的图标运行时通过PackageManager把某个入口启用、其余禁用系统桌面就会跟着刷新。iOS 则依赖系统提供的setAlternateIconName接口从 iOS 10.3 开始支持但要求所有备选图标必须提前打进 App 包里运行时只能在这些预置图标之间切换没法真正动态下载一个新图标。所以标题里说的双端技术方案实际要解决的是两套逻辑的封装问题Android 端要处理别名切换、图标刷新延迟、进程被杀等一堆坑iOS 端要处理预置图标数量限制、切换时的系统弹窗、以及 Unity 层如何优雅地调用原生接口。下面我按实际项目里踩过的顺序把整套方案拆开讲。2. 双端方案的整体设计思路2.1 为什么不能只做一套逻辑很多团队一开始的想法是能不能写一套 C# 代码两端通用。实测下来这个思路走不通因为两端的底层机制差异太大。Android 的图标切换是组件级别的启停切换后桌面上的图标是系统重新读取组件信息渲染出来的跟 App 进程本身没关系iOS 的图标切换是App 级别的资源替换调用后系统会弹一个已更改图标的提示框而且必须在 App 处于前台时调用才生效。更关键的是Android 的别名方案允许你运行时决定用哪个图标理论上可以配合服务端下发配置今天用 A 图标明天用 B 图标iOS 只能在编译期打进包里的那几个图标之间选服务端下发的只是一个选哪个的索引。这个差异决定了架构上必须做一层抽象Unity 层只负责我要切到第 N 个图标这个意图具体怎么切交给各平台的原生实现。2.2 抽象层的接口设计我在项目里用的接口大概长这样Unity 侧通过AndroidJavaObject和DllImport分别调用两端public static class AppIconChanger { public static void SetIcon(int iconIndex) { #if UNITY_ANDROID !UNITY_EDITOR using (var jc new AndroidJavaClass(com.yourgame.icon.IconHelper)) { jc.CallStatic(switchIcon, iconIndex); } #elif UNITY_IOS !UNITY_EDITOR _SetIOSIcon(iconIndex); #endif } #if UNITY_IOS !UNITY_EDITOR [DllImport(__Internal)] private static extern void _SetIOSIcon(int index); #endif }这个接口设计的关键点是用整数索引而不是字符串名字。原因是 iOS 的setAlternateIconName接受的是图标名字符串Android 的别名也是字符串但两端命名规则不一样如果 Unity 层直接传字符串很容易出现一端能识别另一端识别不了的情况。统一用索引两端各自维护一份索引到名字的映射表出问题时排查范围就小很多。2.3 图标资源的组织方式Android 端每个别名对应一套 mipmap 资源命名建议带上前缀区分比如ic_launcher_default、ic_launcher_spring、ic_launcher_summer。iOS 端则是在 Xcode 工程的Info.plist里配置CFBundleAlternateIcons字典每个 key 是图标名value 里指定对应的 png 文件。这里有个容易忽略的点iOS 的备选图标必须是 60x602x 和 60x603x 两种尺寸也就是 120x120 和 180x180 像素格式必须是 PNG 且不能带 alpha 通道带 alpha 的图标在部分系统版本上会显示异常。Android 的图标则建议提供 mipmap-mdpi 到 mipmap-xxxhdpi 全套虽然现在大部分设备只读 xxxhdpi但部分老机型还是会按密度去取。3. Android 端 activity-alias 方案详解3.1 activity-alias 的工作原理activity-alias是 Android Manifest 里的一个组件声明它本身不是一个真正的 Activity而是给某个已存在的 Activity 起的一个别名入口。系统在解析安装包时会把每个 alias 当成一个独立的组件看待如果这个 alias 声明了android:icon那它就会在桌面上生成一个独立的图标入口。关键属性有三个android:name别名的完整类名比如.icon.SpringAliasandroid:targetActivity指向真正的启动 Activity比如.MainActivityandroid:enabled控制这个别名是否启用初始状态一般只启用默认图标那个一个典型的 Manifest 配置大概是这样activity-alias android:name.icon.DefaultAlias android:targetActivity.MainActivity android:enabledtrue android:exportedtrue android:iconmipmap/ic_launcher_default intent-filter action android:nameandroid.intent.action.MAIN / category android:nameandroid.intent.category.LAUNCHER / /intent-filter /activity-alias activity-alias android:name.icon.SpringAlias android:targetActivity.MainActivity android:enabledfalse android:exportedtrue android:iconmipmap/ic_launcher_spring intent-filter action android:nameandroid.intent.action.MAIN / category android:nameandroid.intent.category.LAUNCHER / /intent-filter /activity-alias注意每个 alias 都必须带完整的intent-filter否则系统不会把它当成启动入口。这一点新手特别容易漏漏了之后切换代码执行成功但桌面没反应排查半天找不到原因。3.2 切换逻辑的实现切换的核心就是调用PackageManager.setComponentEnabledSetting把目标 alias 设为COMPONENT_ENABLED_STATE_ENABLED其余设为COMPONENT_ENABLED_STATE_DISABLED。这里有个顺序问题必须先启用目标别名再禁用当前别名如果反过来中间会有一个瞬间两个别名都禁用桌面上的图标会短暂消失部分启动器甚至会因为找不到入口而把 App 从桌面移除。public static void switchIcon(Context ctx, int index) { String pkg ctx.getPackageName(); String[] aliases { pkg .icon.DefaultAlias, pkg .icon.SpringAlias, pkg .icon.SummerAlias }; PackageManager pm ctx.getPackageManager(); for (int i 0; i aliases.length; i) { int state (i index) ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED : PackageManager.COMPONENT_ENABLED_STATE_DISABLED; pm.setComponentEnabledSetting( new ComponentName(pkg, aliases[i]), state, PackageManager.DONT_KILL_APP); } }DONT_KILL_APP这个 flag 很重要不加的话系统会在切换组件状态时把 App 进程杀掉用户体验就是点一下换图标App 直接闪退重开。加上之后进程保留但代价是桌面图标的刷新可能有延迟。3.3 图标刷新延迟的处理实测下来setComponentEnabledSetting调用后桌面图标的刷新时间在不同启动器上差异很大。原生 Pixel Launcher 基本是秒刷小米的启动器可能要等几秒部分第三方启动器甚至要等用户下次解锁屏幕才刷新。我试过几种加速方案比较有效的是在切换后主动发一个广播触发启动器刷新但这个方法不是所有启动器都认。更稳妥的做法是在 UI 上给用户一个提示比如图标将在几秒后更新若未变化请稍等或重启桌面把预期管理好避免用户以为功能坏了。还有一个坑部分国产 ROM 会缓存桌面图标即使组件状态变了桌面还是显示旧图标。这种情况只能靠用户手动清理启动器缓存或者等系统自己刷新。我在项目里加了一个强制刷新按钮点击后重新执行一次切换逻辑实测能解决大部分缓存问题。4. iOS 端 setAlternateIconName 方案详解4.1 备选图标的预置配置iOS 的动态图标必须在 Xcode 工程的Info.plist里预先声明。具体是在CFBundleIcons字典下加一个CFBundleAlternateIcons子字典每个 key 是图标名value 是一个字典里面指定CFBundleIconFiles数组和UIPrerenderedIcon布尔值。keyCFBundleIcons/key dict keyCFBundleAlternateIcons/key dict keySpringIcon/key dict keyCFBundleIconFiles/key array stringic_spring_120/string stringic_spring_180/string /array keyUIPrerenderedIcon/key false/ /dict /dict /dict图标文件本身要放在 Xcode 工程的资源目录里命名跟CFBundleIconFiles里的一致。这里有个细节文件名不要带 2x 3x 后缀Xcode 会根据设备自动匹配如果你手动带了后缀反而可能匹配不上导致图标显示为默认的白色方块。4.2 切换接口的调用iOS 的切换接口是UIApplication.shared.setAlternateIconName传入图标名字符串传nil表示恢复默认图标。这个接口有个特性调用后系统会自动弹一个提示框内容是您已更改App名的图标这个弹窗无法屏蔽是系统行为。- (void)setIcon:(int)index { NSArray *names [SpringIcon, SummerIcon]; NSString *iconName (index 0 index names.count) ? names[index] : nil; if (![UIApplication sharedApplication].supportsAlternateIcons) { return; } [[UIApplication sharedApplication] setAlternateIconName:iconName completionHandler:^(NSError *error) { if (error) { NSLog(set icon failed: %, error); } }]; }supportsAlternateIcons这个检查不能省虽然 iOS 10.3 以上都支持但部分企业签名或者特殊分发方式的包可能不支持不检查直接调用会崩。4.3 系统弹窗的规避思路那个已更改图标的弹窗确实影响体验尤其是游戏里做活动切换图标时突然弹一个系统框很出戏。社区里流传过一些规避方法比如在completionHandler里遍历 window 找 UIAlertController 然后 dismiss但这种方法在新版本 iOS 上已经失效而且有被审核拒绝的风险。我的建议是接受这个弹窗但把它变成体验的一部分。比如在切换图标前先弹一个自己的确认框用户点确认后再调用系统接口这样系统弹窗出现时用户已经有心理预期不会觉得突兀。另外切换时机最好选在用户主动触发的场景比如设置页里的更换图标按钮而不是 App 启动时自动切换后者会让用户莫名其妙。5. Unity 层的桥接与工程配置5.1 Android 侧的 AAR 封装Unity 调用 Android 原生代码最干净的方式是把 Java 逻辑打成一个 AAR 插件放在Assets/Plugins/Android目录下。AAR 里包含IconHelper类和必要的资源引用Unity 打包时会自动合并到最终的 APK 里。打 AAR 的时候要注意minSdkVersion 和 targetSdkVersion 必须跟 Unity 工程一致否则打包时会报冲突。另外 AAR 里不要包含AndroidManifest.xml里的activity-alias声明那些应该写在 Unity 工程的主 Manifest 里通过Assets/Plugins/Android/AndroidManifest.xml覆盖。5.2 iOS 侧的 .mm 文件桥接iOS 侧需要写一个.mm文件Objective-C暴露 C 接口给 Unity 调用。文件放在Assets/Plugins/iOS目录下Unity 打包时会自动编译进 Xcode 工程。extern C { void _SetIOSIcon(int index) { [[IconManager sharedInstance] setIcon:index]; } }注意函数名前面的下划线这是 Unity 的DllImport约定C# 侧声明_SetIOSIcon时不需要写下划线Unity 会自动处理。5.3 打包后的验证清单两端桥接做完之后打包验证有几个必查项检查项AndroidiOS图标资源是否打进包检查 APK 里 mipmap 目录检查 IPA 里 Assets.car别名是否注册成功adb shell dumpsys package查组件检查 Info.plist 的 CFBundleAlternateIcons切换接口是否可调用日志打印 setComponentEnabledSetting 返回值检查 supportsAlternateIcons桌面是否刷新手动触发启动器刷新系统自动刷新这个表格我在每个项目上线前都会过一遍能挡掉八成以上的低级问题。6. 实操中踩过的坑与排查技巧6.1 Android 别名切换后图标不刷新这是最高频的问题。排查顺序建议是先确认setComponentEnabledSetting的返回值是否正常没有抛异常再用adb shell dumpsys package your.package看组件状态是否真的变了。如果状态变了但桌面没刷新基本就是启动器缓存问题换个启动器测试就能确认。有个特殊情况如果 App 是通过 Android Studio 直接安装的 debug 包部分机型上别名切换会失效必须打 release 包签名后才正常。这个坑我踩过一次排查了大半天最后发现是 debug 包的组件状态被系统特殊处理了。6.2 iOS 切换后图标显示为白色方块这个问题九成是图标资源的问题。检查三点图标是否是 PNG 格式、是否带了 alpha 通道、尺寸是否是 120x120 和 180x180。带 alpha 的图标在 iOS 上会被系统当成无效资源显示成白色方块。用 Preview 或者 Photoshop 把 alpha 通道去掉再重新导出即可。另一个可能的原因是CFBundleIconFiles里的文件名跟实际资源文件名不匹配。Xcode 对文件名大小写敏感SpringIcon和springicon是两个不同的文件配置时一定要仔细核对。6.3 切换图标导致 App 重启Android 侧如果忘了加DONT_KILL_APPflag切换组件状态时系统会杀掉进程。加上之后正常但要注意部分国产 ROM 会忽略这个 flag仍然杀进程。这种情况没有完美解法只能尽量在用户不活跃的时机切换比如 App 退到后台时执行。iOS 侧一般不会重启但如果切换时 App 不在前台setAlternateIconName会失败。所以调用前要确认UIApplication.shared.applicationState .active不在前台就先记下待切换的图标等回到前台再执行。6.4 常见问题速查表现象可能原因解决方向Android 切换无反应别名未注册或 intent-filter 缺失检查 ManifestAndroid 图标短暂消失禁用顺序错误先启用后禁用Android 切换后闪退缺少 DONT_KILL_APP加 flagiOS 图标白色方块资源带 alpha 或尺寸错误重新导出iOS 切换失败App 不在前台回前台再调用iOS 弹窗无法关闭系统行为提前用自定义弹窗引导7. 服务端下发与运营侧配合动态图标真正的价值在于运营灵活性所以服务端配置这块也得设计好。我的做法是在服务端维护一份图标配置表字段包括图标索引、生效时间段、适用渠道、优先级。客户端启动时拉取配置根据当前时间和渠道匹配出应该用哪个图标然后调用切换接口。配置表大概长这样{ icons: [ {index: 0, name: default, start: 2024-01-01, end: 2024-12-31, priority: 0}, {index: 1, name: spring, start: 2024-02-01, end: 2024-02-15, priority: 10}, {index: 2, name: summer, start: 2024-06-01, end: 2024-08-31, priority: 10} ] }客户端匹配逻辑是筛选出当前时间在生效区间内的配置按优先级排序取最高优先级的那条。如果都没有匹配就用默认图标。这样运营只需要在后台改配置不需要发版就能控制图标切换。有个细节要注意iOS 的备选图标数量是有限制的虽然官方没明确说上限但实测超过 10 个之后部分设备会出现加载问题。所以配置表里的图标索引要跟客户端预置的图标一一对应不能服务端随便加一个新图标客户端就能用。Android 虽然没这个限制但每多一个别名就多一份图标资源包体积会涨一般控制在 5 个以内比较合理。8. 性能与包体积的权衡每增加一个图标别名Android 侧要增加一套 mipmap 资源按 5 个密度算一套大概 50KB 到 200KB 不等取决于图标复杂度。iOS 侧每个备选图标要 120x120 和 180x180 两张加起来大概 30KB 到 80KB。如果做 5 个备选图标两端加起来包体积会增加 1MB 左右。对于手游来说 1MB 不算什么但如果你的包体积已经卡在渠道限制边缘就得考虑压缩。我的做法是图标资源用 WebP 格式Android 支持iOS 不支持iOS 侧则用 PNG 压缩工具过一遍能省 30% 左右。另外非当前使用的图标资源可以考虑放到 AssetBundle 里动态加载但这样会增加切换时的加载时间需要权衡。还有一个性能点是切换操作的耗时。Android 的setComponentEnabledSetting是同步调用实测在低端机上可能耗时 100ms 到 300ms建议放到子线程执行避免卡主线程。iOS 的setAlternateIconName是异步的有 completionHandler不会阻塞但系统弹窗的出现会有延迟UI 上要做好过渡。9. 一些实战经验和小技巧最后分享几个实际项目里总结出来的经验。第一个是切换图标后主动刷新桌面快捷方式Android 上可以通过发送ACTION_PACKAGE_CHANGED广播触发部分启动器刷新虽然不是所有启动器都认但加上没坏处。第二个是在设置页里做一个图标预览列表让用户自己选喜欢的图标这样既满足了动态切换的需求又给了用户参与感比运营单方面切换体验好很多。第三个技巧是关于 iOS 弹窗的可以在调用setAlternateIconName之前先把 App 的 keyWindow 做一个短暂的遮罩等系统弹窗出现后再移除遮罩这样视觉上弹窗是从遮罩上浮出来的比直接弹出来自然一些。这个做法不涉及任何私有 API审核安全。第四个是做好降级处理。如果切换接口调用失败要能回退到默认图标并且记录失败日志上报。我遇到过部分定制 ROM 上别名机制完全失效的情况这时候降级到默认图标至少保证 App 能正常用不会因为图标问题导致用户找不到入口。这套方案我在三个项目里落地过Android 覆盖了主流国产 ROM 和原生系统iOS 从 12 到 17 都测过整体稳定性没问题。真正麻烦的从来不是技术实现而是各端各机型的兼容性细节这些只能靠实际测试一点点磨出来。
返回列表