ARTICLE DETAIL

资讯详情

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

Unity双端不重新发版换图标:Android与iOS桥接方案实战

Unity双端不重新发版换图标:Android与iOS桥接方案实战 手游上线后想换个图标做活动、做节日限定、做渠道包差异化这个需求几乎每个运营团队都会提。但真到技术落地这一步很多人第一反应是重新打包发版结果就是审核周期、用户更新率、渠道排期全卡在一起一个图标换下来少说三五天。实际上 Android 和 iOS 都提供了不重新发版就能换图标的原生能力Unity 这边只要做好桥接层双端统一调用并不复杂。这篇就把我在几个上线项目里实际用过的方案完整拆一遍包括 Android 的activity-alias多入口方案、iOS 的setAlternateIconName方案、Unity 侧的 C# 桥接写法以及那些文档里不会写、但线上一定会遇到的坑。1. 先搞清楚双端换图标的底层机制差异在动手写代码之前必须先把两端的实现原理吃透否则后面桥接层设计会走很多弯路。Android 和 iOS 对这个功能的支持思路完全不同一个是多入口切换一个是运行时替换理解这个差异是后面所有设计决策的基础。1.1 Android 的 activity-alias 多入口机制Android 换图标的核心不是替换图标而是启用/禁用不同的启动入口。系统桌面上的每个应用图标本质上对应一个Activity或者activity-alias的入口声明。你可以在AndroidManifest.xml里为同一个主 Activity 声明多个activity-alias每个 alias 配置不同的android:icon和android:label然后在运行时通过PackageManager.setComponentEnabledSetting()动态启用其中一个、禁用其余。这个机制的关键点在于所有 alias 必须在打包时就写进 Manifest运行时只能切换启用状态不能凭空创建新图标。也就是说你能换的图标数量在发版那一刻就定死了。这一点直接决定了产品侧的图标方案设计——不能做成运营随时上传新图只能做成预置 N 套图标运行时选一套。activity-alias android:name.icon_default android:enabledtrue android:exportedtrue android:iconmipmap/ic_launcher_default 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.icon_festival android:enabledfalse android:exportedtrue android:iconmipmap/ic_launcher_festival 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注意每个 alias 都要带完整的intent-filter否则桌面不会把它识别成可启动入口。另外主 Activity 本身的intent-filter要移除 LAUNCHER category避免出现两个图标。1.2 iOS 的 setAlternateIconName 运行时替换iOS 走的是完全不同的路子。从 iOS 10.3 开始系统提供了UIApplication.setAlternateIconName(_:completionHandler:)接口允许 App 在运行时切换图标。图标资源需要在 Xcode 工程的Info.plist里通过CFBundleIcons的CFBundleAlternateIcons字典预先声明每个图标对应一组文件通常是 60x602x、60x603x 等尺寸。和 Android 一样备选图标也必须在打包时预置运行时只能在这些预置图标之间切换。传nil表示恢复主图标。切换时系统会弹一个短暂的提示框您已更改XX的图标这个提示无法通过公开 API 去掉只能接受。keyCFBundleIcons/key dict keyCFBundleAlternateIcons/key dict keyfestival/key dict keyCFBundleIconFiles/key array stringic_festival_60/string /array keyUIPrerenderedIcon/key false/ /dict /dict /dict1.3 两端机制对比与统一抽象把两端放在一起看差异其实很清晰维度AndroidiOS实现机制activity-alias 启用切换setAlternateIconName 替换图标预置位置AndroidManifest.xmlInfo.plist运行时限制只能切换已声明 alias只能切换已声明图标系统提示无有短暂提示框生效时机桌面图标立即变化桌面图标立即变化恢复默认启用默认 alias传 nil基于这个对比Unity 侧的桥接接口可以统一抽象成三个方法SetIcon(string iconKey)、GetCurrentIcon()、ResetIcon()。两端各自实现上层业务只认 iconKey不关心底层差异。这个抽象是后面所有代码的基础。2. Unity 侧桥接层的设计取舍桥接层看起来简单但实际写起来有几个关键决策点用 AndroidJavaObject 还是自己写 AAR、iOS 用 DllImport 还是直接写 .mm 文件、接口怎么设计才能让业务层无感。这几个选择直接影响后期的维护成本和出问题的概率。2.1 Android 侧直接调用还是封装 AAR最省事的做法是在 C# 里直接用AndroidJavaClass和AndroidJavaObject调 Java API不写任何 Java 代码。但实测下来setComponentEnabledSetting这套逻辑涉及字符串拼接、状态判断、异常处理纯 C# 调用会写得很啰嗦而且容易在 alias 名字上出错。我的建议是封装一个轻量 AAR把切换逻辑放在 Java 层C# 只负责传 iconKey。这样做的另一个好处是Java 层可以处理一些 C# 层拿不到的信息比如当前 alias 状态、包名拼接等。public class IconSwitcher { private static final String PKG com.yourcompany.yourgame; private static final String[] ALIASES { PKG .icon_default, PKG .icon_festival, PKG .icon_anniversary }; public static void switchTo(Activity activity, String iconKey) { int targetIndex mapKeyToIndex(iconKey); PackageManager pm activity.getPackageManager(); for (int i 0; i ALIASES.length; i) { int newState (i targetIndex) ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED : PackageManager.COMPONENT_ENABLED_STATE_DISABLED; pm.setComponentEnabledSetting( new ComponentName(activity, ALIASES[i]), newState, PackageManager.DONT_KILL_APP); } } }DONT_KILL_APP这个 flag 非常关键。如果不加切换图标时系统会杀掉当前进程用户会看到 App 突然闪退体验极差。加上之后进程保留但桌面图标刷新可能有延迟这个后面会细说。2.2 iOS 侧DllImport 桥接的写法iOS 侧没法像 Android 那样用 Java 桥接需要在 Unity 的Assets/Plugins/iOS目录下写一个.mm文件用 C 函数暴露接口C# 侧用DllImport调用。// IconSwitcher.mm #import UIKit/UIKit.h extern C void _SetAppIcon(const char* iconName) { NSString *name [NSString stringWithUTF8String:iconName]; if ([name isEqualToString:]) { name nil; } dispatch_async(dispatch_get_main_queue(), ^{ [[UIApplication sharedApplication] setAlternateIconName:name completionHandler:^(NSError *error) { if (error) { NSLog([IconSwitcher] switch failed: %, error); } }]; }); }这里有两个必须注意的点一是setAlternateIconName必须在主线程调用Unity 的 C# 调用默认不在主线程所以要dispatch_async到 main queue二是 iconName 传空字符串时要转成 nil否则会被当成一个不存在的图标名直接报错。2.3 C# 统一接口的实现两端桥接都准备好之后C# 层用一个静态类包起来用#if UNITY_ANDROID/#if UNITY_IOS做条件编译。public static class AppIcon { public static void SetIcon(string iconKey) { #if UNITY_ANDROID !UNITY_EDITOR using (var unityPlayer new AndroidJavaClass(com.unity3d.player.UnityPlayer)) using (var activity unityPlayer.GetStaticAndroidJavaObject(currentActivity)) using (var switcher new AndroidJavaClass(com.yourcompany.IconSwitcher)) { switcher.CallStatic(switchTo, activity, iconKey); } #elif UNITY_IOS !UNITY_EDITOR _SetAppIcon(iconKey); #else Debug.Log($[AppIcon] Editor mode, skip switch to {iconKey}); #endif } #if UNITY_IOS !UNITY_EDITOR [DllImport(__Internal)] private static extern void _SetAppIcon(string iconName); #endif }!UNITY_EDITOR这个条件不能省。编辑器下没有真实的 Activity 和 UIApplication直接调用会报错加个日志跳过就行方便在编辑器里调试业务逻辑。3. Android 端那些文档不写的坑Android 这套方案看起来简单但线上跑起来问题不少。我踩过的几个坑基本每个都会让功能看起来能用但实际有问题这里逐个说清楚。3.1 图标切换后桌面不刷新这是最常见的问题。调用setComponentEnabledSetting之后代码层面状态已经变了但桌面图标可能还是旧的甚至要等几秒到几十秒才刷新。原因是桌面 Launcher 有自己的图标缓存系统广播不一定能及时触发它刷新。实测下来加DONT_KILL_APP之后这个问题更明显因为进程没重启Launcher 拿不到进程变化信号。解决办法有两个一是切换后主动发一个Intent.ACTION_PACKAGE_CHANGED广播部分 Launcher 会响应二是接受这个延迟在 UI 上给用户一个图标将在几秒后更新的提示。我一般用第二种因为第一种在不同厂商 Launcher 上表现不一致反而容易出问题。注意不要为了强制刷新而故意不加DONT_KILL_APP进程被杀导致的闪退体验远比图标延迟严重。3.2 首次安装后默认 alias 的状态问题如果 Manifest 里默认 alias 是enabledtrue其他是false首次安装后桌面显示的是默认图标这没问题。但如果你在代码里第一次切换时把默认 alias 也 disable 了然后用户卸载重装某些机型上会出现没有可用入口的情况桌面图标直接消失。规避方法是切换逻辑里永远保证至少有一个 alias 是 enabled 状态并且在切换前先检查目标 alias 是否已经 enabled避免重复操作。另外默认 alias 建议保留enabledtrue作为兜底切换时只 disable 其他非目标 alias。3.3 多进程与多 Activity 场景如果 App 有多个进程比如推送进程、后台服务进程setComponentEnabledSetting的调用要注意在哪个进程执行。这个 API 是跨进程生效的但如果你在非主进程调用currentActivity可能拿不到导致调用失败。统一在主进程、主 Activity 里调用是最稳的。另外如果 App 有多个入口 Activity比如从通知、从分享进入要确保这些入口不会因为 alias 切换而失效。alias 只影响 LAUNCHER 入口其他 intent-filter 不受影响但保险起见还是测一遍。3.4 渠道包与 alias 数量的平衡每个 alias 都会在 Manifest 里增加一个入口声明alias 太多会让 Manifest 变大编译时间也会增加。更重要的是某些应用市场对 Manifest 里的 LAUNCHER 入口数量有审核要求alias 过多可能被质疑。我的经验是预置 3 到 5 套图标足够覆盖绝大多数运营场景默认、节日、周年庆、活动限定、渠道定制。再多的话建议改成图标包方案把图标资源做成可下载的但那样就超出 activity-alias 的能力范围了需要走动态加载 自定义 Launcher 的路子复杂度高很多一般项目没必要。4. iOS 端的限制与绕行思路iOS 这边限制比 Android 更多尤其是那个系统提示框很多产品经理看到就接受不了。但限制就是限制绕不过去只能想办法把体验做顺。4.1 系统提示框无法去除setAlternateIconName调用成功后系统会弹一个您已更改XX的图标的提示。这个提示是系统级的没有公开 API 可以屏蔽。网上有些黑科技方案比如用UIAlertController抢在系统提示前弹一个自定义框或者用 runtime hook 掉系统方法但这些方案要么不稳定要么有审核风险我都不建议用。实际项目里的做法是把切换图标的入口藏深一点让用户主动触发并且提前在 UI 上说明切换后系统会提示。这样用户有心理预期不会觉得突兀。如果是运营活动自动切换那更要谨慎最好做成用户手动确认。4.2 图标资源规格与命名iOS 的备选图标对尺寸和命名有要求。CFBundleIconFiles里填的是文件名前缀系统会自动找2x、3x的变体。常见规格是 60x60对应 120x120 和 180x180 的实际像素。如果图标有透明通道某些情况下会被系统加黑底所以备选图标建议做成不透明的。命名上建议用有意义的英文 key比如festival、anniversary不要用中文或特殊字符避免 plist 解析问题。每个 key 对应的文件放在 Xcode 工程的资源目录里确保打包时被正确包含。4.3 审核与合规注意事项iOS 对动态图标这个功能本身是允许的但有几个红线要注意一是不能用来伪装 App 身份比如切换成其他知名 App 的图标二是不能用来规避审核比如审核时用正常图标上线后换成违规内容三是图标内容本身要符合 App Store 审核指南。另外如果 App 有多个 target 或者用了动态框架要确保CFBundleIcons配置在正确的 Info.plist 里。我遇到过一次配置写在了 framework 的 plist 里结果主 App 读不到切换一直失败排查了半天。5. 双端联调与真机验证的完整流程代码写完只是开始真正麻烦的是双端联调和真机验证。这个环节如果没有清晰的流程很容易在到底哪端出问题上浪费时间。5.1 编辑器内的逻辑验证Unity 编辑器里没法真实切换图标但可以验证业务逻辑iconKey 的传递、状态存储、UI 反馈。建议在编辑器下把AppIcon.SetIcon做成只打日志然后在业务层加一个当前图标 key的显示方便确认逻辑正确。状态存储这块Android 可以用SharedPreferencesiOS 用NSUserDefaultsUnity 侧统一用PlayerPrefs也行但要注意PlayerPrefs在两端底层实现不同跨端同步时以各自原生存储为准。我一般用PlayerPrefs存一份用于业务判断原生侧存一份用于启动时恢复。5.2 Android 真机验证清单Android 真机验证要覆盖这些点首次安装后默认图标是否正确显示切换到每个备选图标桌面是否更新注意延迟切换后杀掉 App 重新打开图标是否保持切换后重启手机图标是否保持卸载重装后是否回到默认图标不同厂商机型小米、华为、OPPO、vivo的 Launcher 表现第 6 点特别重要国产 Launcher 对 alias 切换的响应差异很大有的立即刷新有的要等很久有的甚至要手动下拉刷新桌面。测试阶段一定要覆盖主流机型。5.3 iOS 真机验证清单iOS 这边验证点相对少但也要注意切换时系统提示是否正常弹出切换后桌面图标是否立即变化切换后从后台杀掉 App 重开图标是否保持恢复默认图标传 nil是否正常不同 iOS 版本14、15、16、17的表现iOS 的图标切换响应比 Android 快很多基本是立即生效这点体验好不少。但要注意如果 App 正在前台切换后桌面图标变化用户看不到需要退到桌面才能看到效果所以 UI 上最好提示用户请回到桌面查看。5.4 常见问题排查表现象可能原因排查方向Android 切换无反应alias 未在 Manifest 声明检查 Manifest 和包名拼接Android 切换后闪退未加 DONT_KILL_APP检查 flag 参数Android 图标延迟刷新Launcher 缓存属正常现象加 UI 提示iOS 切换报错图标名不在 plist 中检查 CFBundleAlternateIconsiOS 切换无提示可能未在主线程调用检查 dispatch_async两端状态不一致存储未同步统一用 PlayerPrefs 原生存储6. 上线后的运营配合与扩展思路功能上线只是第一步真正发挥价值的是运营侧的配合。这块分享几个实际项目里的做法。6.1 图标切换的触发时机设计图标切换的触发时机直接决定用户体验。常见的几种用户手动切换在设置页放一个更换图标入口用户自己选。体验最好但使用率低。活动自动切换进入活动期间自动切活动结束切回。使用率高但用户可能觉得突兀。首次启动询问新版本首次启动时弹窗询问是否切换。折中方案但要注意不要频繁弹。我一般推荐活动自动切换 设置页手动兜底的组合。自动切换负责曝光手动入口负责让用户有控制感。自动切换前最好在游戏内公告或弹窗里预告一下避免用户困惑。6.2 图标资源的管理与热更新前面说过图标必须在打包时预置不能运行时下载。但图标资源本身可以做成配置化的在服务端维护一个当前生效图标 key的配置客户端启动时拉取如果和本地不一致就切换。这样运营侧改配置就能控制图标不用发版。这个方案的关键是所有可能的图标 key 必须在客户端预置服务端只能在这些 key 里选。如果运营想用一个全新的图标那还是得发版。所以产品设计时要把图标方案想清楚预留足够的 key。6.3 数据埋点与效果评估图标切换的效果需要数据支撑。建议埋这几个点切换触发次数自动/手动分开切换成功率两端分开切换后次日留存对比用户主动切回默认的比例最后一项特别有意思如果切回比例很高说明用户不喜欢这个图标运营侧要调整。这些数据反过来能指导下一轮的图标设计。6.4 后续可扩展的方向这套方案跑通之后还能往几个方向扩展一是做成图标商店让用户用游戏内货币解锁特殊图标增加付费点二是结合节日做限时图标提升节日氛围三是渠道包差异化图标方便渠道识别。这些扩展都不需要改底层桥接只要在业务层加逻辑就行。不过要提醒一句图标切换功能本身不复杂复杂的是运营节奏和用户预期管理。技术方案只是工具用得好不好还是看运营怎么设计。我在实际项目里见过图标换得太频繁导致用户反感的也见过节日图标做得好、用户主动截图分享的差别就在运营的节奏把控上。最后分享一个实操小技巧Android 的 alias 名字建议用icon_前缀统一管理iOS 的 plist key 也用同样的命名规则这样两端配置能对上排查问题时一眼就能看出对应关系。另外切换逻辑建议加一个防抖避免短时间内频繁切换导致状态错乱尤其是在自动切换和手动切换同时存在的情况下。
返回列表