ARTICLE DETAIL

资讯详情

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

Unity手游动态换图标:Android与iOS双端完整实现方案

Unity手游动态换图标:Android与iOS双端完整实现方案 先说个背景运营周五下班前丢来一句“下个版本我们把App图标换成春节活动的”我第一反应是改一下Unity工程里的Player Settings重新打一版包丢商店。但转念一想老玩家手里的包根本不会因为我发新包就自动更新指望用户主动升级不现实于是这个需求就变成了一个绕不开的技术问题在已安装的包上让桌面图标跟着运营动作变。这个需求在手游圈其实很常见春节、周年庆、新赛季、IP联名运营都想在用户桌面上刷一波存在感。但真正动手的时候你会发现Android和iOS完全是两个世界Android可以做到准实时切换代价是桌面刷新在各家ROM上表现不一样iOS则是“能切但只能从预置图标里切”而且每切一次系统都要弹窗让用户确认。这篇我就把双端都做过的完整方案、踩过的坑、以及Unity侧怎么封装出一套统一接口一次说清楚。1. 需求拆解你以为在“换App图标”其实是在换“快捷方式图标”1.1 玩家看到的图标和系统认识的图标是两回事做这个功能之前得先搞清楚一件事你在手机桌面上看到的那个图标并不是App包里的那张图片本身而是系统根据配置生成的“入口”。Android这边Launcher也就是桌面展示的图标本质上是一个指向某个Component的快捷方式。这个Component在manifest里声明带上了android:icon属性。系统默认会让你主Activity做入口它的icon就成了桌面图标。当你把一个Activity禁用、把另一个Activity启用桌面图标的指向就会发生变化——这是Android能动态换图标的根基。iOS这边完全不是这个逻辑。SpringBoard直接把App沙盒里已签名的图标资源展示在主屏幕上App运行时根本没有权限去改这个展示关系。Apple给的正路只有一条把备用图标预置进Info.plist然后调用系统API让它切换而且这个切换动作系统会弹窗让用户确认。一句话总结Android是切换入口iOS是替换资源。理解了这一点后面看实现就顺了。1.2 哪些场景真的需要动态换图标开发侧经常觉得“换图标”就是运营拍脑袋其实拆开看用途很清晰节日运营春节、中秋、周年庆图标换成节日主题增加用户点击冲动。版本节点王者荣耀这种游戏新赛季开启那天应用商店里的图不换但老玩家桌面上的图标得换成新视觉。IP联动跟某部电影、某个动漫联动图标换成联名款制造稀缺感。定向用户给某个测试用户群体显示特殊图标方便客服识别版本。这个需求少但也不是没遇到过。在Unity里改Player Settings的icon只影响新装用户和商店展示页对老用户是无效的。从产品角度来说如果这个运营动作很重要就必须做运行时切换。1.3 结论先放前面双端完整做完之后我给你的结论是Android端可用、成熟能做到秒切但桌面刷新有兼容性坑iOS端能切但必须预置图标包、用户会看到系统确认弹窗且不能太频繁。先做AndroidiOS做基础版这是性价比最高的落地顺序。2. Android端用Activity-alias让桌面图标“秒变”2.1 原理Launcher入口的偷梁换柱Android的动态换图标标准做法是Activity-alias。所谓alias就是“替身”它不承载真实代码只是指向你的主Activity但它可以有自己的icon、label和intent-filter。你可以把它理解成一个公司前台主Activity是真正的办公区alias是前台工位。你让哪个工位挂着公司的牌子访客用户看到的就是哪个形象。系统桌面上那一堆图标本质上就是一堆“挂着LAUNCHER牌子的入口”你切换的只是牌子不是办公楼本身。控制牌子是否挂出去靠PackageManager.setComponentEnabledSetting()。这个方法可以动态启用/禁用某个组件Activity、Service、Receiver都行。把旧的alias禁用、新的alias启用桌面快捷方式指向的ComponentName就变了图标也就跟着变了。这里有个关键原则同一时刻只能有一个LAUNCHER入口处于enabled状态。如果两个alias同时启用桌面会出现两个一模一样的游戏图标用户会以为装了两个App。2.2 图标资源准备一张图引发的血案Android的Launcher图标不是随便丢一张图就行的各密度要出全套。基础尺寸表如下密度像素尺寸mdpi48x48hdpi72x72xhdpi96x96xxhdpi144x144xxxhdpi192x192这是老规矩。Android 8.0API 26之后加入了自适应图标Adaptive Icon系统会把前景层和背景层分开处理然后根据不同的形状圆形、圆角矩形、泪珠形进行裁切。图标设计尺寸是108dp但真正安全区域只有中间的66dp四周会被裁掉。如果你设计给的图是满幅铺开的直接塞进mipmap在Android 9的桌面上很可能会被裁掉边上的重要元素。我的做法是让设计出一张1080x1080的方形图中心720x720为核心元素四周留安全边距然后用脚本切成五套密度。跑过几个项目这个方案最稳不会出现某个ROM上图标被裁成“大头贴”的问题。2.3 Manifest的正确姿势主入口不直接挂Launcher很多教程会让主Activity继续保留LAUNCHER同时加一个alias。这种写法问题在于切换时你有时候要禁主Activity、有时候要禁alias很容易出现“两个图标”或者“一个图标都点不开”的边界情况。我实践下来更稳的写法是主Activity只声明MAIN不挂LAUNCHER所有LAUNCHER入口都放alias。application android:labelstring/app_name android:iconmipmap/ic_launcher_default !-- 主Activity真正干活的页面不直接作为桌面入口 -- activity android:name.MainActivity android:exportedfalse / !-- 默认图标入口 -- activity-alias android:name.IconAliasDefault android:targetActivity.MainActivity android:labelstring/app_name android:iconmipmap/ic_launcher_default android:exportedtrue android:enabledtrue intent-filter action android:nameandroid.intent.action.MAIN / category android:nameandroid.intent.category.LAUNCHER / /intent-filter /activity-alias !-- 春节活动图标入口 -- activity-alias android:name.IconAliasFestival android:targetActivity.MainActivity android:labelstring/app_name android:iconmipmap/ic_launcher_festival android:exportedtrue android:enabledfalse intent-filter action android:nameandroid.intent.action.MAIN / category android:nameandroid.intent.category.LAUNCHER / /intent-filter /activity-alias /application注意三个细节每个alias都必须有android:exportedtrue否则点击图标没反应。每个alias的label建议和主图标保持一致避免切换后桌面App名出现“跳动”。android:enabled只是初始状态运行时由代码控制别把这个属性当成正式开关。2.4 C#侧切换三行代码解决战斗Unity里调用Android原生API不需要写Java插件也能做直接用AndroidJavaClass调PackageManager就行。using UnityEngine; public static class AndroidAppIconSwitcher { public static void SwitchIcon(string aliasName, bool enabled) { using var unityPlayer new AndroidJavaClass(com.unity3d.player.UnityPlayer); using var activity unityPlayer.GetStaticAndroidJavaObject(currentActivity); using var pm activity.CallAndroidJavaObject(getPackageManager); string packageName activity.Callstring(getPackageName); using var component new AndroidJavaObject( android.content.ComponentName, packageName, aliasName ); // packageManager.COMPONENT_ENABLED_STATE_ENABLED 1 // packageManager.COMPONENT_ENABLED_STATE_DISABLED 0 // packageManager.DONT_KILL_APP 1 int newState enabled ? 1 : 0; pm.Call(setComponentEnabledSetting, component, newState, 1); } }这里enabled参数别理解反了切到春节图标要做两件事——先SwitchIcon(.IconAliasFestival, true)再SwitchIcon(.IconAliasDefault, false)。顺序一定不能错否则中间一瞬间所有入口都被禁用用户从桌面点图标会提示“应用未安装”。DONT_KILL_APP这个参数很关键值为1意思是切换组件时不要杀掉当前运行的App进程。如果漏了这个用户切完图标游戏就没了。2.5 桌面没刷新这口锅该谁背代码写完之后事情远没有结束。最常遇到的反馈是“我切了图标桌面没变。”实测结果是这样的Google Pixel的原生桌面会立即刷新基本是秒变。但MIUI、EMUI、ColorOS这些定制ROM有的要锁屏再亮屏才刷新有的要等Launcher重启甚至要重启手机。网上有各种“发送桌面刷新广播”的偏方我试过几个兼容性都很差不值得在这个方向上较劲。我的土办法是切换后弹一个Toast“桌面图标已切换如果未更新重启桌面或手机即可看到”。丑是丑了点但能解决90%的客服咨询量。另外有个现实要注意最近任务列表后台卡片里显示的图标经常是老图标。这个截图反差会让运营紧张但实际上这是系统级别的缓存App完全控制不了。提前跟运营说清楚别到时候被当成bug追着打。2.6 Android 13主题图标新问题Android 13开始支持THEMED_ICONS系统会把桌面图标变成单色主题风格跟随系统深色模式、壁纸颜色走。这个模式下你动态换的节日图标会被系统重新“画”成单色轮廓不是设计稿里的彩色效果。这个没有完美的解决方式。目前Android的公开API里没有针对主题图标渲染的控制能力。只能说如果你的用户大量开启主题图标模式这个功能对他们的视觉影响力会打折扣但功能本身不会出错。3. iOS端不是“不能换”而是“只能从预置图标里换”3.1 为什么iOS不能像Android那样“随便换”iOS从10.3开始提供了Alternate Icons能力核心API就是UIApplication.setAlternateIconName(_:completionHandler:)。表面上看起来这个API很直接但限制藏得很深所有备用图标必须在Info.plist的CFBundleAlternateIcons里声明随安装包一起签名分发。不能从网络下载新图标再运行时设置签名机制不允许审核也不允许。每次切换系统会弹窗让用户确认App无法跳过这个确认环节。主图标可以通过传nil切回来但同样要确认。所以iOS的方案重点不是“怎么切换”而是“怎么在上架之前把备用图标预置好、怎么让运营接受弹窗体验”。3.2 Info.plist与Assets.xcassets的配置在Xcode工程里备用图标的添加方式很简单把图片资源放进Assets.xcassets的AppIcon里然后在Info.plist里声明。keyCFBundleIcons/key dict keyCFBundlePrimaryIcon/key dict keyCFBundleIconFiles/key array stringAppIcon60x60/string /array keyUIPrerenderedIcon/key false/ /dict keyCFBundleAlternateIcons/key dict keyFestivalIcon/key dict keyCFBundleIconFiles/key array stringFestivalIcon60x60/string /array keyUIPrerenderedIcon/key false/ /dict /dict /dict实际在Unity工程里Xcode工程是打包后生成的所以这些配置要在iOS原生插件或PostProcessBuild脚本里完成不是手改就行的。备用图标的图片名也是约定好之后在构建阶段塞进xcassets。尺寸方面iPhone用2x和3x也就是120x120和180x180。iPad虽然现在手游很少专门适配了但如果你的游戏还支持iPad最好也把167x167的2x加上。图片不能带透明度之外的奇怪格式最好是PNG。一个容易踩的坑备用图标文件名不能带空格否则打包的时候Asset Catalog会报错而且这个错误隐藏得很深有的版本要到上传App Store时才会暴露。3.3 Unity调用iOS原生的桥接代码iOS侧要写的东西不多把OC方法导出成C函数Unity C#侧用[DllImport(__Internal)]声明即可。我建议直接用.mm文件Objective-C省去C/C互操作的一堆麻烦。#import UIKit/UIKit.h #import UnityAppController.h extern C void iOS_SetAlternateIconName(const char *iconName) { if (![[UIApplication sharedApplication] supportsAlternateIcons]) { return; } NSString *name nil; if (iconName ! NULL strlen(iconName) 0) { name [NSString stringWithUTF8String:iconName]; } [[UIApplication sharedApplication] setAlternateIconName:name completionHandler:^(NSError * _Nullable error) { if (error) { NSLog(ChangeIconError: %, error.localizedDescription); const char *msg [[NSString stringWithFormat:error:%, error.localizedDescription] UTF8String]; UnitySendMessage(AppIconManager, OnSwitchResult, msg); } else { UnitySendMessage(AppIconManager, OnSwitchResult, success); } }]; }C#侧#if UNITY_IOS !UNITY_EDITOR using System.Runtime.InteropServices; #endif public static class IosAppIconSwitcher { #if UNITY_IOS !UNITY_EDITOR [DllImport(__Internal)] private static extern void iOS_SetAlternateIconName(string iconName); #endif public static void SwitchIcon(string iconId) { #if UNITY_IOS !UNITY_EDITOR iOS_SetAlternateIconName(iconId); #endif } }注意iconId传空字符串表示切回主图标在OC里会转成nil。这个约定要跟运营对齐他们眼里是“换回默认”代码里就是“传空”。3.4 用户实际看到的切换体验调用之后系统会弹一个确认框大致意思是“是否将主屏幕图标替换为xxx”。用户点允许后桌面图标马上更新。这个弹窗由系统UI接管文案和样式App都无法定制体验上是打断性的。所以iOS端必须对运营做一次预期管理iOS切图标不是无感的用户会看到系统弹窗。如果活动图标切得太频繁用户确认的次数多了会觉得很烦。我见过有的产品直接在首次切换后不再允许主动切回就是怕反复弹窗引发反感。另一个要提前说的事App Store商店页面显示的永远是提交审核时填的主图标不是用户手机桌面上的那个。你在商店里看到的还是那个经典Logo但老用户手机上是节日图。这不是bug是Apple商店架构决定的。3.5 那些“绕开API”的野路子为什么都不行iOS社区一直有人在找“无感换图标”的方案我帮大家把试过的路数排一下网页添加到主屏幕本质是Web Clip快捷方式走Safari渲染没有角标、没有推送、没有启动动画游戏App不可能接受。快捷指令自动化iOS 14之前能通过自动化触发换图标现在每次执行都要弹“快捷指令想要更改主屏幕图标”的确认比系统弹窗还烦。描述文件/企业证书只适合企业内部分发正常上架App Store的开发者别碰。结论很明确官方API就是唯一正路把备用图标预置进包里接受弹窗完事。4. Unity侧的统一接口设计双端差异收进一个Manager4.1 对外API长什么样业务层不应该出现#if UNITY_ANDROID满天飞的代码。我习惯封装一个静态Manager对外暴露最小接口public static class AppIconManager { public static void SwitchIcon(AppIconId id, System.Actionbool callback); public static AppIconId GetCurrentIcon(); }AppIconId是一个带字符串值的枚举Android对应alias后缀iOS对应备用图标名public enum AppIconId { Default -1, Festival 0, Anniversary 1, NewSeason 2 }底层把枚举映射成两端的不同标识。Android映射成.IconAliasFestivaliOS映射成FestivalIcon。这样运营说“切春节图”程序里就一行AppIconManager.SwitchIcon(AppIconId.Festival, (success) { Debug.Log(切换结果 success); });4.2 Android和iOS插件的工程目录组织Unity工程里插件目录这样放Assets/ ├── Plugins/ │ ├── Android/ │ │ ├── AndroidManifest.xml # 带alias声明的manifest │ │ └── res/mipmap-*/ic_launcher_*.png │ └── iOS/ │ ├── AppIconBridge.mm │ └── AppIconBridge.hAndroid的manifest是Unity打包时要用的如果你用自动集成SDK那一套记得在Plugins/Android里放完整清单别让它和主工程模板冲突。iOS的桥接代码可以放在Plugins/iOSUnity构建Xcode工程时会自动拷贝过去不需要手动引用。iOS还涉及到Info.plist生成配置和xcassets的填充建议在IPostprocessBuildWithReport里用脚本处理别人工去每次改Xcode工程。写一次脚本后面所有版本都省事。4.3 启动恢复与并发保护切完图标之后App进程本身和桌面的快捷方式状态是分开的。千万别忘了记录当前状态否则用户杀进程重启手机之后你都不知道现在是哪套图标。我的做法是PlayerPrefs.SetString(AppIconCurrent, iconId)每次切换都写。启动时读取如果发现记录和系统实际状态不一致做一个静默恢复。启动恢复要注意一个问题iOS端恢复默认图标也会弹窗所以不要每次冷启动都“恢复”只在新装App后的首次启动或者版本更新后图标配置变化时做一次。Android端虽然不会弹窗但也要防止重复调用。比如你在OnApplicationPause(false)里做切换逻辑用户按Home键再回来切换动作触发两次第二次如果判断不严会把原来切好的又切回去。并发保护最通用的做法是Manager内部维护一个mutableState切换过程中把状态标记为“切换中”回调返回之前丢弃新的切换请求。实际操作中运营不会频繁点但代码必须扛得住。4.4 为什么不建议把图标资源放AssetBundle有的同学会想既然要动态那我把图标放AssetBundle里运行时下载后给原生层用不就能做到“今天出的图明天就换上”吗这个思路在Android和iOS都走不通。Android的activity-alias里引用的icon是APK内mipmap资源系统Launcher只认这个静态资源根本没有给你代码注入图片的机会。iOS的备用图标必须在安装包内、被签名锁定网络下载的图片不可能变成受信任的图标资源。所以“动态换图标”里的“动态”准确说是入口的切换是动态的资源包必须预置。这也决定了你要在发版前确定所有候选图标。我一般建议预置2到4套覆盖当年的大运营节点避免下一版更新之前素材不够用。4.5 运营侧的配合节奏这个功能不是纯客户端的事运营必须参与。我做过一张配合表供参考时间节点运营动作客户端动作活动前2周提交图标素材PNG/AI含安全区标注资源检查、尺寸切片、提交测试包活动前3天确认最终命名和切换时间点预置进版本走测试活动前1天下发切图指令服务器开关开启灰度切换活动结束次日下发切回指令切回默认图标切图动作不应该写死在客户端而是由服务器下发开关控制。客户端进入前台时请求一次“当前应显示的图标ID”和本地一致就不动不一致才触发切换。这样活动时间变了运营自己改后台就行不用等发版。5. 上线前后的验证清单与那些绕不开的坑5.1 双端验证清单功能做完不是结束上线前要有一张验证表。我每次发版前都会过一遍检查项AndroidiOS图标切换成功后桌面即时刷新Pixel原生机必测MIUI/EMUI/ColorOS各测一轮系统弹窗出现确认后图标变化切回默认图标同切换路径反向执行传nil确认后恢复切换后App冷启动正常杀进程后从新图标进入杀进程后从新图标进入图标资源在各机型上无裁切重点看自适应图标和圆形图标无圆角裁切问题但注意别设计太满通知栏/横幅图标不受影响无误无误最近任务列表显示老图标确认可接受确认可接受网络切图开关稳定弱网、断网、开关抖动同左有一个细节Android切换后如果用户正在使用App切换动作本身会触发一次Activity pause/resume吗实测不会但部分ROM上桌面图标更新时游戏会短暂失焦一下这是正常现象不是闪退。5.2 高发坑位与根因这一节的标题我写得很直接因为下面每一条都是真金白银换来的经验。第一个坑华为、荣耀桌面不刷新。这是EMUI的老毛病切完图标桌面纹丝不动要锁屏或重启后才变。原因在于华为Launcher缓存快捷方式的机制和原生Android不同它不监听组件启用状态的广播。破解方案没有只能提示用户或者切换后主动发一个ACTION_LOCALE_CHANGED广播偶尔能触发刷新但不保证。我们最终选择接受这个问题客服话术备好。第二个坑Android 8.0后图标被裁切。前面说过安全区的问题。实际操作中很多设计为了视觉冲击力会把Logo铺满到了Android 13的一加、小米上边角直接被切掉。这个坑在上线后很难补必须发版前检查。第三个坑iOS切换弹窗文案不能定制。系统弹窗会展示备用图标的显示名称这个名称默认取自CFBundleIconName。如果你给备用图标起的名字是内部代号用户看到的弹窗就很莫名其妙。所以图标资源命名要么不显示、要么起一个用户能看懂的活动名。第四个坑切换后ClassLoader报错。这个发生在极端场景切换组件的同时系统正在杀掉并重建进程。我遇到过一次ClassNotFoundException根因是manifest里的activity在低内存环境被回收和切换动作撞在一起。后来在切换逻辑里加了重试机制切换失败就回滚没有再出现过。这种偶发问题不常遇到但建议你做切换回调的容错。第五个坑三星部分机型图标变成默认Android机器人形状。这是系统缓存的老bug出现概率很低重启可恢复。客服遇到时让用户重启一次桌面就行。第六个坑上线当天运营说活动延期但图标已经切了。这个不是技术问题是流程问题。解决办法就是前面说的切图由服务端开关控制运营改后台配置即可。千万别在客户端里写死在某个日期切图。5.3 灰度与监控切图标这种“用户可见变化”的动作一定不要全员一刀切。我建议流程是服务端开关先开5%用户确认成功率、崩溃率正常后再放量到30%最后100%。需要监控的数据切换成功率客户端回调为success的概率低于98%就要查。崩溃率重点看Android端setComponentEnabledSetting有没有抛IllegalArgumentException。入参不对时崩溃率会上去catch层要做防护。iOS弹窗拒绝率用户点了“不允许”的比例。这个指标高说明弹窗太频繁或图标名称有歧义要考虑文案和频率。埋点设计不要复杂三条事件就够AppIcon_SwitchRequest # 发起切换 AppIcon_SwitchSuccess # 切换成功 AppIcon_SwitchFailed # 切换失败带错误码5.4 如果产品说“我要支持服务器下发新图标”这大概率是迟早会来的需求。我用大白话告诉你为什么做不到Android的桌面图标必须指向一个manifest里已声明的组件组件附带的icon资源必须在APK里。长按菜单的快捷方式ShortcutManager可以动态指定icon但那不是桌面主图标是长按App后的弹出菜单。用AppWidgetProvider做伪图标可以做到桌面显示自定义图片但系统会云控识别并清理不适合游戏这类要常驻桌面的场景。iOS更不可能签名机制锁死了所有图标资源。所以如果产品抛出这个需求你的回答应该是要么预置未来几期的图标要么发新包。不存在第三条路。这个边界清晰了运营的排期反而会保守很多。我自己做完这个功能最大的感受是Android端花了两天搞定核心iOS端花了一天搞定代码剩下80%的时间都花在跟ROM厂商的兼容性、跟运营对齐预期、还有给测试列验证清单上。如果你也要做我给你一个最实际的建议先把Android的切图逻辑做成服务端可控开关。因为一旦某个活动延期你能做的最快动作就是不下发切图指令而不是临时跑去找ROM厂商要补丁。iOS端记得把备用图标命名起得通俗一点弹窗显示的是那个名字不是你的代码变量名。最后图标设计的安全区问题一定要跟设计同学反复强调这是整个功能里最容易翻车的环节。
返回列表