ARTICLE DETAIL

资讯详情

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

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

Unity手游双端动态换图标实战:Android activity-alias与iOS setAlternateIconName全解析 手游运营做久了总会碰到一个绕不开的需求节假日换图标、渠道包换图标、活动期间换图标。产品经理一句“能不能让用户自己选图标”落到客户端这边就是Android和iOS两套完全不同的实现路径。Android靠activity-alias玩组件启用禁用iOS靠setAlternateIconName走系统APIUnity层还要把两边封装成一套统一接口。这里面的坑不算深但足够碎碎到你不踩一遍根本不知道哪里有雷。下面把我实际落地这套方案的过程完整拆开讲一遍包括选型理由、代码细节、真机验证结果以及几个文档里不会写的注意事项。1. 先搞清楚双端换图标的底层机制差异动手写代码之前必须先把两端的实现原理吃透。很多人一上来就搜“Unity换图标插件”结果发现插件底层还是这两套东西绕不开。理解机制差异才能明白为什么接口设计要那样做也才能在出问题时快速定位。1.1 Android端activity-alias是唯一正路Android换桌面图标本质上是切换AndroidManifest.xml里声明的一组activity-alias。每个别名指向同一个主Activity但各自带不同的android:icon和android:label。系统桌面读取的是当前处于enabled状态的那个别名。关键点在于同一时刻只能有一个别名处于启用状态主Activity本身要设为enabledfalse否则会出现两个图标。切换时通过PackageManager.setComponentEnabledSetting()动态启用目标别名、禁用其余别名。activity android:name.MainActivity android:exportedtrue android:enabledfalse intent-filter action android:nameandroid.intent.action.MAIN / category android:nameandroid.intent.category.LAUNCHER / /intent-filter /activity activity-alias android:name.IconDefault android:targetActivity.MainActivity android:enabledtrue android:exportedtrue android:iconmipmap/ic_launcher_default android:labelstring/app_name intent-filter action android:nameandroid.intent.action.MAIN / category android:nameandroid.intent.category.LAUNCHER / /intent-filter /activity-alias activity-alias android:name.IconHalloween android:targetActivity.MainActivity android:enabledfalse android:exportedtrue android:iconmipmap/ic_launcher_halloween android:labelstring/app_name intent-filter action android:nameandroid.intent.action.MAIN / category android:nameandroid.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则恢复主图标。keyCFBundleIcons/key dict keyCFBundlePrimaryIcon/key dict keyCFBundleIconFiles/key array stringAppIcon/string /array /dict keyCFBundleAlternateIcons/key dict keyHalloween/key dict keyCFBundleIconFiles/key array stringicon_halloween/string /array keyUIPrerenderedIcon/key false/ /dict /dict /dictiOS这边的约束比Android多得多我踩过的几个点备选图标必须是完整的、已经打包进bundle的图片资源不能运行时下载替换。想动态下发图标没门。图标尺寸必须齐全20/29/40/60/76/83.5pt的2x3x缺一个在某些机型上就可能不显示或显示模糊。切换时系统会弹一个“您已更改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-CUnity层只保留一个薄薄的封装。这样做的理由是原生逻辑改动频繁放在原生侧编译调试更快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.GetStaticAndroidJavaObject(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初始enabledtrue。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.xmlforegroundbackground备选图标也建议用同样的方式否则在部分启动器上显示效果不一致。自适应图标的XML长这样adaptive-icon xmlns:androidhttp://schemas.android.com/apk/res/android background android:drawablecolor/icon_bg_halloween/ foreground android:drawablemipmap/ic_foreground_halloween/ /adaptive-icon3.2 iOS备选图标的plist配置与尺寸清单iOS的备选图标配置比Android更繁琐因为要在Info.plist里逐个声明而且每个图标都要提供完整尺寸。我整理了一份最小可用尺寸清单文件名尺寸(pt)倍率实际像素icon_halloween2x.png60x602x120x120icon_halloween3x.png60x603x180x180icon_halloween_202x.png20x202x40x40icon_halloween_203x.png20x203x60x60icon_halloween_292x.png29x292x58x58icon_halloween_293x.png29x293x87x87icon_halloween_402x.png40x402x80x80icon_halloween_403x.png40x403x120x120Info.plist里对应的配置keyCFBundleAlternateIcons/key dict keyHalloween/key dict keyCFBundleIconFiles/key array stringicon_halloween/string stringicon_halloween_20/string stringicon_halloween_29/string stringicon_halloween_40/string /array keyUIPrerenderedIcon/key false/ /dict /dict提示CFBundleIconFiles里写的是文件名前缀不带倍率和扩展名。系统会自动匹配2x、3x。如果只写icon_halloween系统会找icon_halloween2x.png和icon_halloween3x.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切换后图标不刷新甚至消失现象调用切换后桌面图标要么还是旧的要么直接消失重启手机才恢复。排查链路先确认AndroidManifest.xml里主Activity的enabled是否为false。如果主Activity是true会出现两个图标或冲突。检查所有别名的intent-filter是否完整。少一个LAUNCHERcategory桌面就认不出来。用adb shell dumpsys package com.yourpackage | grep -A 5 Activity Resolver查看当前启用的组件状态。确认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/AndroidiOS的.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.3supportsAlternateIcons返回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章的排查链路看两遍能帮你省下不少调试时间。
返回列表