ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

Unity手游动态图标双端工程化方案:Android activity-alias与iOS setAlternateIconName实战

Unity手游动态图标双端工程化方案:Android activity-alias与iOS setAlternateIconName实战 1. 动态图标这件事到底在解决什么问题做过手游运营的人都有一个共识App图标是成本最低、触达最广的运营位。它不需要用户打开游戏不需要推送权限甚至不需要用户在线——只要图标躺在手机桌面上每一次解锁屏幕都是一次曝光。春节换红底、周年庆换金色、联动活动换角色立绘这套玩法在国内头部手游里早就跑通了。但问题在于Unity作为跨平台引擎本身并没有提供一套统一的动态图标API。Android和iOS两端的实现路径完全不同Android靠activity-alias做组件切换iOS靠setAlternateIconName做系统级替换。更麻烦的是Unity的构建流程会覆盖原生工程配置你手动改的东西下一次出包就没了。所以真正要解决的不是怎么换图标这个单点问题而是怎么在Unity的构建管线里把双端的动态图标能力做成可配置、可自动化、不丢配置的工程化方案。这篇文章适合三类人看一是正在做手游运营功能开发的同学二是需要给项目加动态图标但不知道从哪下手的Unity客户端三是想了解Androidactivity-alias和iOS alternate icon底层机制的移动端开发者。我会把两端的原理、Unity侧的工程改造、构建脚本、踩过的坑全部摊开讲代码和配置都可以直接抄。先说结论Android端用activity-alias方案切换时会有一次桌面图标的闪动系统会短暂移除再添加组件但兼容性最好从Android 5.0到14都没问题iOS端用setAlternateIconName切换时系统会弹一个您已更改图标的提示框这个提示无法绕过除非用私有API不建议。两端都需要在Unity构建后处理原生工程文件这部分我用Editor脚本自动化掉了。2. 双端方案选型为什么Android用activity-aliasiOS用setAlternateIconName2.1 Android端activity-alias是唯一靠谱的路Android换图标这件事网上能搜到好几种说法我一个个说清楚为什么最后选了activity-alias。第一种是直接改AndroidManifest.xml里application的android:icon属性然后重新安装。这显然不行用户不可能为了换个图标重新下载APK。第二种是用PackageManager.setComponentEnabledSetting动态启用/禁用组件。这个API本身没问题关键在于你要启用/禁用什么组件。Android的桌面图标本质上是一个activity组件带LAUNCHERcategory的那个你没法直接替换一个activity的图标但你可以通过activity-alias创建多个别名每个别名指向同一个主Activity但各自带不同的android:icon。然后通过setComponentEnabledSetting控制哪个别名处于启用状态。具体来说主Activity保持android:enabledfalse且不带LAUNCHER category然后为每个图标创建一个activity-alias只有当前选中的那个alias是enabledtrue且带LAUNCHER category。切换图标 禁用旧alias 启用新alias。activity android:namecom.unity3d.player.UnityPlayerActivity android:exportedtrue android:enabledfalse intent-filter action android:nameandroid.intent.action.MAIN / /intent-filter /activity activity-alias android:name.icon_default android:targetActivitycom.unity3d.player.UnityPlayerActivity android:enabledtrue android:exportedtrue android:iconmipmap/app_icon_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.icon_festival android:targetActivitycom.unity3d.player.UnityPlayerActivity android:enabledfalse android:exportedtrue android:iconmipmap/app_icon_festival android:labelstring/app_name intent-filter action android:nameandroid.intent.action.MAIN / category android:nameandroid.intent.category.LAUNCHER / /intent-filter /activity-alias这里有几个必须注意的细节我踩过坑主Activity的intent-filter里不能有LAUNCHER category否则桌面上会出现两个图标。只保留MAIN action即可。每个alias的android:name建议用相对路径如.icon_default这样会自动拼上包名避免硬编码包名出错。android:enabled的初始状态默认图标那个alias设为true其他全部false。这个状态会被系统持久化用户重启手机也不会丢。切换时用PackageManager.setComponentEnabledSetting必须传DONT_KILL_APP标志否则切换的瞬间你的进程会被杀掉用户体验极差。// Android侧切换代码 public void switchIcon(String aliasName) { PackageManager pm getPackageManager(); String pkg getPackageName(); // 先禁用所有alias String[] allAliases {icon_default, icon_festival, icon_anniversary}; for (String alias : allAliases) { pm.setComponentEnabledSetting( new ComponentName(pkg, pkg . alias), PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP ); } // 启用目标alias pm.setComponentEnabledSetting( new ComponentName(pkg, pkg . aliasName), PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP ); }实测下来这个方案在小米、华为、OPPO、vivo、三星的主流机型上都能正常工作。唯一的问题是切换瞬间桌面图标会有一个短暂的消失再出现的过程这是系统launcher重新加载组件导致的无法避免。我的做法是在切换前给用户一个loading提示切换完成后弹一个Toast让用户知道发生了什么。2.2 iOS端setAlternateIconName的能与不能iOS从10.3开始提供了setAlternateIconName:completionHandler:这个API允许App在运行时切换图标。但苹果对这个功能加了很多限制你必须提前知道第一所有备选图标必须在Info.plist里预先声明。你不能在运行时动态生成图标只能从预先打包进Bundle的图标里选。声明方式是在Info.plist里加一个CFBundleIcons字典keyCFBundleIcons/key dict keyCFBundlePrimaryIcon/key dict keyCFBundleIconFiles/key array stringAppIcon60x60/string /array /dict keyCFBundleAlternateIcons/key dict keyfestival/key dict keyCFBundleIconFiles/key array stringAppIcon_Festival60x60/string /array keyUIPrerenderedIcon/key false/ /dict keyanniversary/key dict keyCFBundleIconFiles/key array stringAppIcon_Anniversary60x60/string /array keyUIPrerenderedIcon/key false/ /dict /dict /dict第二图标文件必须放在Bundle根目录下不能放在Assets.xcassets里。这是最容易踩的坑。Xcode的Asset Catalog虽然方便但setAlternateIconName只认Bundle根目录下的png文件。你需要把备选图标以AppIcon_Festival60x60.png、AppIcon_Festival60x602x.png、AppIcon_Festival60x603x.png的命名方式直接拖进Xcode工程选择Create folder references而不是Create groups。第三切换时会弹系统提示框。这是iOS的硬性行为提示内容大概是您已更改XXX的图标。这个提示无法通过公开API绕过。有些团队用UIApplication.shared.isStatusBarHidden之类的hack去遮盖但在新版本iOS上已经失效了不建议折腾。第四切换必须在主线程调用且completionHandler里要处理错误。如果图标名不存在或者文件缺失会返回error。// iOS侧切换代码 - (void)switchIcon:(NSString *)iconName { if (![UIApplication sharedApplication].supportsAlternateIcons) { NSLog(当前设备不支持切换图标); return; } NSString *targetName [iconName isEqualToString:default] ? nil : iconName; [[UIApplication sharedApplication] setAlternateIconName:targetName completionHandler:^(NSError * _Nullable error) { if (error) { NSLog(切换图标失败: %, error.localizedDescription); } else { NSLog(切换图标成功); } }]; }注意传nil表示恢复默认图标。另外supportsAlternateIcons这个属性在iOS 10.3才可用低版本要加可用性判断。2.3 两端方案对比对比项Android (activity-alias)iOS (setAlternateIconName)最低支持版本Android 5.0iOS 10.3图标来源打包进APK的mipmap资源打包进Bundle的png文件切换时系统提示无但桌面图标会闪动有弹窗无法绕过是否需要重启App否否图标数量限制理论上无限制建议不超过10个审核风险无需在审核时说明用途3. Unity工程侧改造让构建管线自动处理原生配置3.1 为什么不能手动改原生工程Unity的构建流程是这样的每次Build它都会重新生成Android的AndroidManifest.xml和iOS的Info.plist或者至少覆盖你手动改的部分。你这次手动加了activity-alias下次出包就没了。所以必须把配置注入到Unity的构建管线里。Unity提供了两个关键接口IPreprocessBuildWithReport和IPostprocessBuildWithReport。前者在构建前执行后者在构建后执行。对于Android我们需要在构建后修改AndroidManifest.xml因为Unity生成的manifest在构建后才最终确定对于iOS我们需要在构建后修改Info.plist并拷贝图标文件。3.2 Android侧PostprocessBuild注入activity-aliasAndroid的manifest处理有个坑Unity构建出来的工程AndroidManifest.xml在Temp/StagingArea目录下构建完成后会被拷贝到最终的Gradle工程里。我们需要在OnPostprocessBuild里找到这个文件并修改。using UnityEditor; using UnityEditor.Callbacks; using System.IO; using System.Xml; using UnityEngine; public class AndroidIconPostprocessor { [PostProcessBuild(1000)] public static void OnPostprocessBuild(BuildTarget target, string pathToBuiltProject) { if (target ! BuildTarget.Android) return; string manifestPath Path.Combine(pathToBuiltProject, AndroidManifest.xml); // 如果是Gradle工程路径不同 if (EditorUserBuildSettings.exportAsGoogleAndroidProject) { manifestPath Path.Combine(pathToBuiltProject, unityLibrary, src, main, AndroidManifest.xml); } if (!File.Exists(manifestPath)) { Debug.LogError(找不到AndroidManifest.xml: manifestPath); return; } XmlDocument doc new XmlDocument(); doc.Load(manifestPath); XmlNode applicationNode doc.SelectSingleNode(/manifest/application); if (applicationNode null) return; // 找到主Activity移除LAUNCHER category设置enabledfalse XmlNode mainActivity FindMainActivity(applicationNode); if (mainActivity null) return; SetMainActivityDisabled(mainActivity); // 注入activity-alias string packageName doc.DocumentElement.GetAttribute(package); string[] iconNames { default, festival, anniversary }; foreach (string iconName in iconNames) { XmlElement alias CreateActivityAlias(doc, packageName, iconName); applicationNode.AppendChild(alias); } doc.Save(manifestPath); Debug.Log(Android动态图标配置注入完成); } private static XmlNode FindMainActivity(XmlNode applicationNode) { foreach (XmlNode child in applicationNode.ChildNodes) { if (child.Name ! activity) continue; XmlNode intentFilter child.SelectSingleNode(intent-filter); if (intentFilter null) continue; XmlNode launcher intentFilter.SelectSingleNode(category[android:nameandroid.intent.category.LAUNCHER]); if (launcher ! null) return child; } return null; } private static void SetMainActivityDisabled(XmlNode activity) { XmlElement elem (XmlElement)activity; elem.SetAttribute(enabled, http://schemas.android.com/apk/res/android, false); // 移除LAUNCHER category XmlNode intentFilter activity.SelectSingleNode(intent-filter); if (intentFilter ! null) { XmlNode launcher intentFilter.SelectSingleNode(category[android:nameandroid.intent.category.LAUNCHER]); if (launcher ! null) intentFilter.RemoveChild(launcher); } } private static XmlElement CreateActivityAlias(XmlDocument doc, string packageName, string iconName) { XmlElement alias doc.CreateElement(activity-alias); alias.SetAttribute(name, http://schemas.android.com/apk/res/android, .icon_ iconName); alias.SetAttribute(targetActivity, http://schemas.android.com/apk/res/android, com.unity3d.player.UnityPlayerActivity); alias.SetAttribute(enabled, http://schemas.android.com/apk/res/android, iconName default ? true : false); alias.SetAttribute(exported, http://schemas.android.com/apk/res/android, true); alias.SetAttribute(icon, http://schemas.android.com/apk/res/android, mipmap/app_icon_ iconName); alias.SetAttribute(label, http://schemas.android.com/apk/res/android, string/app_name); XmlElement intentFilter doc.CreateElement(intent-filter); XmlElement action doc.CreateElement(action); action.SetAttribute(name, http://schemas.android.com/apk/res/android, android.intent.action.MAIN); XmlElement category doc.CreateElement(category); category.SetAttribute(name, http://schemas.android.com/apk/res/android, android.intent.category.LAUNCHER); intentFilter.AppendChild(action); intentFilter.AppendChild(category); alias.AppendChild(intentFilter); return alias; } }这段代码有几个关键点PostProcessBuild的优先级设为1000确保在其他后处理脚本之后执行处理了Gradle工程和直接APK两种构建模式用XmlDocument操作而不是字符串替换避免格式错误。3.3 iOS侧PostprocessBuild修改Info.plist并拷贝图标iOS这边更麻烦一点因为除了改Info.plist还要把图标文件拷贝到正确的位置。Unity构建出来的Xcode工程Info.plist在Unity-iPhone/Info.plist图标需要拷贝到Unity-iPhone/根目录下。using UnityEditor; using UnityEditor.Callbacks; using System.IO; using UnityEditor.iOS.Xcode; using UnityEngine; public class IOSIconPostprocessor { [PostProcessBuild(1000)] public static void OnPostprocessBuild(BuildTarget target, string pathToBuiltProject) { if (target ! BuildTarget.iOS) return; string plistPath Path.Combine(pathToBuiltProject, Info.plist); PlistDocument plist new PlistDocument(); plist.ReadFromFile(plistPath); PlistElementDict rootDict plist.root; PlistElementDict bundleIcons rootDict.CreateDict(CFBundleIcons); // 主图标 PlistElementDict primaryIcon bundleIcons.CreateDict(CFBundlePrimaryIcon); PlistElementArray primaryFiles primaryIcon.CreateArray(CFBundleIconFiles); primaryFiles.AddString(AppIcon60x60); // 备选图标 PlistElementDict alternateIcons bundleIcons.CreateDict(CFBundleAlternateIcons); string[] iconNames { festival, anniversary }; foreach (string iconName in iconNames) { PlistElementDict iconDict alternateIcons.CreateDict(iconName); PlistElementArray iconFiles iconDict.CreateArray(CFBundleIconFiles); iconFiles.AddString(AppIcon_ Capitalize(iconName) 60x60); iconDict.SetBoolean(UIPrerenderedIcon, false); } plist.WriteToFile(plistPath); // 拷贝图标文件到Bundle根目录 string sourceDir Assets/Editor/DynamicIcons/iOS; string destDir pathToBuiltProject; foreach (string iconName in iconNames) { string baseName AppIcon_ Capitalize(iconName) 60x60; CopyIconFile(sourceDir, destDir, baseName .png); CopyIconFile(sourceDir, destDir, baseName 2x.png); CopyIconFile(sourceDir, destDir, baseName 3x.png); } Debug.Log(iOS动态图标配置注入完成); } private static void CopyIconFile(string sourceDir, string destDir, string fileName) { string src Path.Combine(sourceDir, fileName); string dst Path.Combine(destDir, fileName); if (File.Exists(src)) { File.Copy(src, dst, true); } else { Debug.LogWarning(图标文件不存在: src); } } private static string Capitalize(string s) { if (string.IsNullOrEmpty(s)) return s; return char.ToUpper(s[0]) s.Substring(1); } }这里用了Unity的PlistDocument类来操作plist比直接改XML安全得多。图标文件我放在Assets/Editor/DynamicIcons/iOS/目录下构建时自动拷贝。3.4 Unity与原生层的桥接Unity侧要调用原生的切换方法需要做平台判断和桥接。Android用AndroidJavaObjectiOS用DllImport。using UnityEngine; public class DynamicIconManager : MonoBehaviour { public static void SwitchIcon(string iconName) { #if UNITY_ANDROID !UNITY_EDITOR using (AndroidJavaClass unityPlayer new AndroidJavaClass(com.unity3d.player.UnityPlayer)) using (AndroidJavaObject activity unityPlayer.GetStaticAndroidJavaObject(currentActivity)) { activity.Call(runOnUiThread, new AndroidJavaRunnable(() { activity.Call(switchIcon, icon_ iconName); })); } #elif UNITY_IOS !UNITY_EDITOR _SwitchIcon(iconName); #else Debug.Log(编辑器模式下不执行图标切换: iconName); #endif } #if UNITY_IOS !UNITY_EDITOR [System.Runtime.InteropServices.DllImport(__Internal)] private static extern void _SwitchIcon(string iconName); #endif }iOS侧需要一个.mm文件来暴露C接口给Unity// DynamicIcon.mm #import UIKit/UIKit.h extern C { void _SwitchIcon(const char* iconName) { NSString *name [NSString stringWithUTF8String:iconName]; dispatch_async(dispatch_get_main_queue(), ^{ if (![UIApplication sharedApplication].supportsAlternateIcons) return; NSString *target [name isEqualToString:default] ? nil : name; [[UIApplication sharedApplication] setAlternateIconName:target completionHandler:^(NSError *error) { if (error) NSLog(切换图标失败: %, error); }]; }); } }这个.mm文件需要放在Assets/Plugins/iOS/目录下Unity会自动把它编译进Xcode工程。4. 实操全流程从资源准备到出包验证4.1 图标资源规范与准备Android和iOS对图标尺寸的要求不一样我整理了一个对照表平台用途尺寸命名规范存放位置Androidmipmap-mdpi48x48app_icon_default.pngAssets/Plugins/Android/res/mipmap-mdpi/Androidmipmap-hdpi72x72app_icon_default.pngAssets/Plugins/Android/res/mipmap-hdpi/Androidmipmap-xhdpi96x96app_icon_default.pngAssets/Plugins/Android/res/mipmap-xhdpi/Androidmipmap-xxhdpi144x144app_icon_default.pngAssets/Plugins/Android/res/mipmap-xxhdpi/Androidmipmap-xxxhdpi192x192app_icon_default.pngAssets/Plugins/Android/res/mipmap-xxxhdpi/iOS60x601x60x60AppIcon_Festival60x60.pngAssets/Editor/DynamicIcons/iOS/iOS60x602x120x120AppIcon_Festival60x602x.pngAssets/Editor/DynamicIcons/iOS/iOS60x603x180x180AppIcon_Festival60x603x.pngAssets/Editor/DynamicIcons/iOS/Android的图标放在Assets/Plugins/Android/res/下Unity构建时会自动合并到APK的res目录。注意不要放在Assets/Resources/下那样不会被打进原生资源。iOS的图标命名有个坑CFBundleIconFiles里写的是AppIcon_Festival60x60系统会自动去找AppIcon_Festival60x60.png、AppIcon_Festival60x602x.png、AppIcon_Festival60x603x.png。所以命名必须严格一致大小写敏感。4.2 构建脚本的完整配置把上面两个Postprocessor脚本放到Assets/Editor/目录下Unity会自动识别。但有几个配置项需要确认Player Settings → Publishing Settings → Build如果勾选了Custom Main ManifestUnity会用你提供的manifest而不是自动生成。这种情况下你需要手动在自定义manifest里加aliasPostprocessor脚本要相应调整。Player Settings → Other Settings → Package Name确保包名正确alias的name会基于包名生成。iOS Player Settings → Icon这里配置的是主图标备选图标不走这里走我们的Postprocessor。我建议的做法是不勾选Custom Main Manifest让Unity自动生成然后Postprocessor在构建后注入。这样最省心。4.3 切换逻辑的时机与状态管理图标切换不是随便什么时候都能调的。我的经验是不要在App启动时立即切换。启动阶段系统资源紧张切换容易失败。建议在进入主界面后延迟1-2秒再执行。切换状态要持久化。用PlayerPrefs记录当前图标名下次启动时对比服务端下发的配置如果一致就不重复切换避免不必要的闪动和弹窗。切换前检查网络和资源。如果图标资源是热更下载的Android可以iOS不行因为图标必须打包进Bundle要确保下载完成再切换。public class IconSwitchController : MonoBehaviour { private const string ICON_KEY current_icon_name; void Start() { Invoke(CheckAndSwitchIcon, 2f); } private void CheckAndSwitchIcon() { string serverIcon GetServerIconConfig(); // 从服务端或本地配置读取 string localIcon PlayerPrefs.GetString(ICON_KEY, default); if (serverIcon ! localIcon) { DynamicIconManager.SwitchIcon(serverIcon); PlayerPrefs.SetString(ICON_KEY, serverIcon); PlayerPrefs.Save(); } } }4.4 出包验证清单出包后别急着发按这个清单过一遍Android安装后桌面图标是否正常显示默认图标用adb shell dumpsys package 包名查看alias的enabled状态是否正确。Android调用切换后桌面图标是否变成目标图标杀掉App重开图标是否保持Android切换时App是否被杀掉如果被杀检查DONT_KILL_APP标志是否传了。iOS安装后图标是否正常切换时是否弹出系统提示切换后图标是否变化iOS杀掉App重开图标是否保持恢复默认图标是否正常双端连续切换多次是否有异常图标资源是否都正确打包5. 常见问题与排查技巧实录5.1 Android端典型问题问题一桌面上出现两个图标。这是最常见的问题。原因通常是主Activity的intent-filter里还保留着LAUNCHER category。检查AndroidManifest.xml确保主Activity的intent-filter里只有MAIN action没有LAUNCHER category。另外检查是否有多个alias同时处于enabled状态。问题二切换后图标没变但alias状态变了。这种情况通常是launcher缓存导致的。不同厂商的launcher刷新机制不一样小米和华为一般会立即刷新OPPO和vivo可能有延迟。解决办法是切换后发一个广播通知launcher刷新但这不是标准API各厂商支持情况不一。我的做法是切换后弹一个Toast提示用户图标将在几秒后更新给用户预期。问题三切换时App闪退。检查是否在主线程调用setComponentEnabledSetting。这个API必须在主线程执行。另外检查alias的name是否拼写正确ComponentName的第二个参数必须是完整类名包名alias名。问题四某些机型上alias不生效。部分定制ROM尤其是早期的EMUI和MIUI对activity-alias的支持有问题。实测下来Android 7.0以上的主流机型基本没问题Android 5.x和6.x的部分机型可能有兼容性问题。如果目标用户里老机型占比高建议做降级处理检测切换是否成功失败则提示用户手动更换。5.2 iOS端典型问题问题一切换时报错icon name not found。检查三件事Info.plist里CFBundleAlternateIcons的key是否和传入的name一致图标文件是否在Bundle根目录下不是Assets.xcassets文件名是否和CFBundleIconFiles里声明的一致包括2x、3x后缀。问题二切换时弹窗提示您已更改图标能否去掉不能。这是iOS的系统行为公开API无法绕过。有些团队尝试用UIAlertController的私有方法去拦截但在iOS 13以后已经失效。接受它或者在产品层面引导用户比如切换前先弹一个自定义提示说明会有一个系统弹窗。问题三审核时被拒说动态图标功能不明确。App Store审核指南对动态图标没有明确禁止但如果你的App切换图标后功能没有变化审核员可能认为这是无意义的功能。建议在审核时提供说明动态图标用于节日活动或用户个性化并在App内提供明确的切换入口。问题四iPad上不生效。iPad的图标尺寸和iPhone不一样CFBundleIconFiles需要额外声明iPad的尺寸如AppIcon_Festival76x76。如果只声明了60x60iPad上可能不生效。建议同时声明iPhone和iPad的图标尺寸。5.3 双端通用问题速查表问题现象可能原因排查方向解决方案切换后图标不变资源未打包检查APK/Bundle里是否有图标文件确认资源路径正确切换后App重启未传DONT_KILL_APP检查Android代码加上标志iOS切换报错plist配置错误检查CFBundleAlternateIcons修正key和文件名桌面出现双图标LAUNCHER category重复检查manifest移除主Activity的LAUNCHER切换状态丢失未持久化检查PlayerPrefs切换后立即Save部分机型不生效ROM兼容性查看系统版本做降级提示5.4 我踩过的几个坑第一个坑是Android的alias name用了绝对路径。一开始我写的是com.example.game.icon_default结果在某些机型上不生效。后来改成相对路径.icon_default让系统自动拼包名问题解决。原因是绝对路径在某些ROM上会被当成不同的组件处理。第二个坑是iOS图标文件放进了Assets.xcassets。Xcode的Asset Catalog会把图标编译成Assets.carsetAlternateIconName根本找不到。必须用folder reference的方式直接放Bundle根目录。第三个坑是Unity构建时覆盖了Info.plist。我一开始在Xcode里手动改了plist结果Unity重新构建后全没了。后来用Postprocessor脚本自动注入才彻底解决。第四个坑是切换时机太早。在Awake里调用切换Android上经常失败因为Activity还没完全初始化。改成延迟2秒后调用稳定了。6. 工程化扩展与进阶玩法6.1 图标配置表驱动硬编码图标名不是好做法。我建议用ScriptableObject做配置表把图标名、资源路径、生效时间、平台限制都配置化。[CreateAssetMenu(fileName IconConfig, menuName DynamicIcon/IconConfig)] public class IconConfig : ScriptableObject { [System.Serializable] public class IconEntry { public string iconName; public string displayName; public long startTime; public long endTime; public bool enableAndroid; public bool enableIOS; } public ListIconEntry icons new ListIconEntry(); public string GetActiveIcon(long currentTime) { foreach (var icon in icons) { if (currentTime icon.startTime currentTime icon.endTime) return icon.iconName; } return default; } }这样运营配置活动时只需要改配置表不需要改代码。6.2 服务端下发与灰度图标切换的配置最好走服务端下发这样可以随时调整活动时间不需要发版。服务端返回一个JSON客户端解析后对比本地状态决定是否切换。灰度方面可以按用户ID哈希或者渠道包来控制哪些用户参与。6.3 与热更系统的配合Android的图标资源可以走热更因为alias的icon属性可以指向下载到本地的资源实际上不行alias的icon必须是打包进APK的资源。所以Android的备选图标也必须预先打包进APK不能热更。这一点和iOS一样。所以动态图标的动态指的是切换时机动态而不是图标资源动态。这个认知很重要很多团队一开始以为可以热更图标结果发现不行。如果确实需要大量图标比如用户自定义图标Android可以通过PackageManager的setComponentEnabledSetting配合动态生成的alias来实现但需要反射调用隐藏API风险较高不建议。6.4 用户体验优化建议切换图标这件事用户感知很强但也很容易做成骚扰。我的建议是给用户选择权。不要强制切换在设置里提供图标选择入口让用户自己选。切换前告知。尤其是iOS系统弹窗会让用户困惑提前用自定义弹窗说明。提供恢复默认的入口。用户换了图标后想换回来要能方便地找到。不要频繁切换。一天切好几次会让用户觉得App不稳定。我在实际项目里的做法是默认图标保持不变只在重大活动时通过服务端配置切换活动结束后自动恢复。同时在设置里提供图标样式选项让喜欢个性化的用户自己选。这样既保证了运营效果又不打扰普通用户。最后分享一个小技巧Android端可以在切换后发一个自定义广播Intent.ACTION_PACKAGE_CHANGED部分launcher会响应这个广播刷新图标能减少图标闪动的时间。但这个广播不是所有launcher都认只能算锦上添花。iOS端则可以在切换完成后用UIApplication.shared.applicationIconBadgeNumber触发一次刷新有时候能让图标更快更新但效果不稳定看系统版本。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表