ARTICLE DETAIL

资讯详情

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

Unity手游动态切换桌面图标:Android/iOS双端实现与避坑指南

Unity手游动态切换桌面图标:Android/iOS双端实现与避坑指南 手游想换桌面图标最常见的场景就是运营节点春节、周年庆、版本大更新、某个IP联动上线。需求听起来特别简单——App不用重新装从下个版本开始桌面上的图标能自动变成活动皮肤活动结束后再恢复原样。但这句话落到Unity工程里牵涉的东西比预想的多得多。这篇文章把Android和iOS两条技术路径完整拆开讲。Android靠的是Activity Alias加PackageManager组件启停iOS用的是系统从10.3开始支持的Alternate Icon机制两种方案都不需要重新打包安装应用运行期间就能切换桌面入口图标。内容主要面向Unity手游客户端开发者也适合想了解双端差异的原生开发。我会把Manifest配置、Java桥接、Objective-C插件、C#封装以及我从真机测试里踩出来的坑全都写出来。项目代码已经稳定跑过线上版本不是概念验证级别的东西可以放心参考。1. 需求拆解与双端能力差异1.1 Android 与 iOS 的“换图标”根本不是一回事Android和iOS的“动态换图标”底层思路完全不同。Android这边桌面图标本质上是Launcher解析到的一个带MAIN/LAUNCHER过滤器的组件入口。同一个MainActivity可以通过多个activity-alias暴露给桌面每个alias都有自己的icon和label。运行时不改任何图片资源只需要通过PackageManager把当前alias禁用、把目标alias启用系统下次刷新桌面时就会展示新的图标。核心是组件启停不是换图。桌面Launcher显示的入口其实不是你直觉里的“那个Activity图标”而是当前处于enabled状态的那个alias组件。iOS这边系统直接提供了API。从iOS 10.3开始UIApplication支持supportsAlternateIcons和setAlternateIconName前提是备选图标在打包时就声明在Info.plist或Asset Catalog里。调用后系统会弹一个确认框用户同意后由系统完成图标替换还带一段过渡动画。开发者能做的很有限传一个字符串名字然后等回调。这个差异决定了实现顺序。Android要维护“当前启用了哪个入口”iOS则只需要给系统传一个名字。也决定了双端适合的使用频率Android可以悄悄切iOS每次切换都会被用户看到一次弹窗运营策略上必须克制。我见过有团队想学网约车搞“一天一个主题图标”结果iOS用户被弹窗烦到直接去应用商店打差评这个后续再细说。两种机制做一张对照表方便后面设计统一接口时查对比维度AndroidiOS底层机制activity-alias PackageManager 组件启停UIApplication.setAlternateIconName最低系统版本几乎无限制API 1 就有 activity-aliasiOS 10.3 及以上是否需要用户确认不需要后台直接生效需要系统弹确认框切换动画无取决于桌面Launcher系统自带过渡动画能否查询当前图标可通过PackageManager查组件状态无公开读取API需要业务层自维护能否从服务器下载新图标不可以图标必须打包进APK不可以图标必须打包进App这张表建议直接存档后面所有设计决策都能从这张表里找到依据。1.2 为什么这套方案在 Unity 里必须单独封装Unity项目里做这件事麻烦不在概念而在“中间层”。原因是Unity构建Android包时虽然支持自定义AndroidManifest.xml但默认模板里MainActivity是自带MAIN和LAUNCHER过滤器的。如果直接把Activity Alias方案硬塞进去不把intent-filter从MainActivity上摘掉桌面上会出现两个甚至多个图标。这种问题在测试机上非常迷惑因为看起来“就多了一个一模一样的图标”但本质上是一个主Activity入口加上一个alias入口同时存在。iOS那边更直接。Unity导出的是一个完整Xcode工程Info.plist里的CFBundleAlternateIcons需要自己往里加图标PNG要加进Xcode资源而这些在Unity编辑器里没有现成的可视化配置入口。你没法在Player Settings里点几下就声明一个备选图标必须改工程文件。所以一套完整的Unity方案至少包含四层Unity C#业务层、Android Java桥接层、iOS Objective-C插件层、构建后处理脚本。C#层负责接收运营指令Java和ObjC层负责调用系统能力构建脚本负责把Manifest、plist和资源文件自动塞进包体。后面几个章节就是按这四层展开的。2. Android 端实现Activity Alias 与 PackageManager 组合拳2.1 AndroidManifest.xml 配置一主三别名Android的Manifest配置是整套方案的地基。以一主三别名为例目标是一个默认图标、一个春节图标、一个版本图标。下面这个示例跟线上项目的基本结构一致只是包名做了替换manifest xmlns:androidhttp://schemas.android.com/apk/res/android packagecom.gamestudio.demo application android:labelstring/app_name android:iconmipmap/ic_launcher activity android:name.MainActivity android:exportedfalse / activity-alias android:name.MainAlias_Default android:exportedtrue android:enabledtrue 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.MainAlias_Festival android:exportedtrue android:enabledfalse 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 /application /manifest配置里有几个关键点每一个都是踩过坑才记住的。第一intent-filter只能放在alias上MainActivity本身不能有LAUNCHER入口否则系统会把MainActivity和alias同时识别为两个可启动入口桌面直接出现两个图标。Unity默认的Manifest是带入口的必须在自定义Manifest时把MAIN和LAUNCHER这两个action从MainActivity身上摘干净。这一点是整个Android方案里最容易被忽视的坑。第二每个alias都要显式写android:enabled。默认启用的那个置true其他置false。运行时就是靠PackageManager去改变这些组件的enabled状态。如果targetSdk是Android 12也就是API 31以上带intent-filter的组件都要显式声明exportedtrue我们这里的alias带LAUNCHER过滤器所以必须写成truetargetActivity本身不需要。图标资源方面如果只适配Android 7.0及以下随便放一套mipmap就行。但现在的真机基本是Android 8.0起建议给每个alias准备两套资源一套是普通mipmap-*dpi里的方形图标一套是mipmap-anydpi-v26里的adaptive-icon否则部分原生ROM会把图标显示成白色圆角块。简单说Android 8.0之后系统优先读取adaptive-icon它由前景层和背景层组成和传统的正方形位图不是一回事。资源目录结构大致长这样放在Unity工程里会被自动合并进APKAssets/Plugins/Android/res/ ├── mipmap-mdpi/ic_launcher_default.png ├── mipmap-hdpi/ic_launcher_default.png ├── mipmap-xhdpi/ic_launcher_default.png ├── mipmap-xxhdpi/ic_launcher_default.png ├── mipmap-xxxhdpi/ic_launcher_default.png ├── mipmap-anydpi-v26/ic_launcher_default.xml ├── mipmap-mdpi/ic_launcher_festival.png ├── mipmap-hdpi/ic_launcher_festival.png ├── mipmap-xhdpi/ic_launcher_festival.png ├── mipmap-xxhdpi/ic_launcher_festival.png ├── mipmap-xxxhdpi/ic_launcher_festival.png └── mipmap-anydpi-v26/ic_launcher_festival.xmladaptive-icon的XML和其他自适应图标一样foreground和background分开配置。这里不展开写法但值得专门提醒如果你换了alias却只更新了普通mipmap在Android 8.0以上的Pixel上Launcher的图标可能还是旧的因为anydpi-v26下系统读的是adaptive-icon配置普通位图被忽略了。这个问题在原生ROM上特别明显国内ROM反倒少见。2.2 Java 桥接层组件启停的核心逻辑Manifest配好后核心逻辑就是Java层的一段组件启停代码。我习惯用一个IconSwitcher类把所有alias收敛起来避免业务层拿着字符串到处拼包名package com.gamestudio.demo; import android.content.ComponentName; import android.content.Context; import android.content.pm.PackageManager; public class IconSwitcher { private static final String[] ALL_ALIASES { .MainAlias_Default, .MainAlias_Festival, .MainAlias_NewVersion }; public static void switchIcon(Context context, String targetAlias) { PackageManager pm context.getPackageManager(); String packageName context.getPackageName(); boolean found false; for (String alias : ALL_ALIASES) { ComponentName component new ComponentName(packageName, packageName alias); if (alias.equals(targetAlias)) { found true; pm.setComponentEnabledSetting(component, PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP); } else { pm.setComponentEnabledSetting(component, PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP); } } if (!found) { throw new IllegalArgumentException(unknown alias: targetAlias); } } }我把逻辑设计成每次全量设置目标alias启用其他全禁用。好处是不用维护“上一个alias是谁”也不依赖getComponentEnabledSetting的初始状态判断避免了Manifest默认enabled值带来的歧义。代价是每次会向PackageManager重复提交禁用请求这个开销在真机上基本可以忽略毕竟不是高频操作。这里必须解释一个关键参数为什么用DONT_KILL_APP。setComponentEnabledSetting的第三个参数是标志位如果传0系统在组件状态改变后会直接结束当前应用进程把玩家正在跑的游戏杀掉。用DONT_KILL_APP就只改状态不杀进程桌面图标下次刷新时就会更新。这个标志位在真机上你才能体会它的价值我第一次做的时候没注意一调用游戏直接闪退还以为是崩溃。另一个被很多人忽略的点组件名一定要拼成完整的“包名 .alias名”。直接用相对名。MainAlias_Default这种写法在某些API版本上会解析失败。上面代码里new ComponentName(packageName, packageName alias)就是完整的全限定名。我喜欢把alias短名作为外面传入的key在Java内部自行拼全名这样C#层传参简单也不会出错。2.3 Unity C# 侧调用AndroidJavaObject 的正确姿势Java层写好后放到Assets/Plugins/Android/下Unity构建时会一并编译并打进APK。接下来是C#侧调用。标准写法是这样using UnityEngine; public static class AndroidIconBridge { private const string SwitcherClass com.gamestudio.demo.IconSwitcher; public static void Switch(string aliasName) { using (var unityPlayer new AndroidJavaClass(com.unity3d.player.UnityPlayer)) { using (var activity unityPlayer.GetStaticAndroidJavaObject(currentActivity)) { using (var context activity.CallAndroidJavaObject(getApplicationContext)) { using (var switcher new AndroidJavaClass(SwitcherClass)) { switcher.CallStatic(switchIcon, context, aliasName); } } } } } }这套写法是Unity调用Android原生的标准姿势。拿到currentActivity再从Activity拿ApplicationContext最后调Java的静态方法。我建议传ApplicationContext而不是Activity因为切换图标是全局操作不依赖Activity生命周期也避免某些ROM上Activity上下文带来的泄漏隐患。用AndroidJavaClass和CallStatic看起来有点繁琐但这是Unity JNI层的安全做法。不要自己搞反射也不要把太多字符串判断放在C#层业务逻辑尽量收敛在Java层出问题好查日志。调用前有一个时机问题必须注意如果在游戏刚启动、引擎还没完全初始化时调用currentActivity可能为空。我实测下来最稳的做法是等一个启动帧至少要等一帧Update之后再去调。另外切换成功后马上给运营上报不要依赖“桌面图标是否真的变了”来判断成功因为有些桌面有缓存立即刷新看不出效果这个问题第5章会专门展开。3. iOS 端实现Alternate Icon 与原生插件封装3.1 Info.plist 声明备选图标iOS这边原理比Android简单配置反而麻烦因为Unity没有现成的UI入口。最直接的声明方式是改Info.plist在CFBundleIcons里加CFBundleAlternateIcons。下面是一份可用的配置keyCFBundleIcons/key dict keyCFBundleAlternateIcons/key dict keyFestival/key dict keyCFBundleIconFiles/key array stringIconFestival/string /array keyUIPreferredIcon/key true/ /dict keyNewVersion/key dict keyCFBundleIconFiles/key array stringIconNewVersion/string /array /dict /dict /dict字典里每个key对应一个备选图标标识C#和ObjC里传的就是这个key比如Festival。CFBundleIconFiles数组里填的是PNG资源的Base名系统会自动寻找Base名加各尺寸后缀的文件不需要每个尺寸都写进plist。图标PNG要真正打进bundle。Unity导出Xcode工程后需要在Assets/Plugins/IOS/下放PNG再通过构建后处理脚本把PNG加入Xcode的Build Phase资源编译中Info.plist也要在脚本里动态插入上面这段。如果项目不想写脚本手工在Xcode里加也可以但手游一般有自动化出包流程脚本化才是正道。如果你的项目已经接到Xcode 13以上的Asset Catalog流程也可以在Assets.xcassets里专门建一套Alternate Icon集合构建时系统会自动把这些图标写进Assets.car并生成对应的plist键。两种方式二选一。我的经验是Asset Catalog更省心但对Unity构建后处理来说直接改plist反而更可控因为不用关心Xcode工程里Asset Catalog的内部结构。图标尺寸上别只放一个60pt的图。系统在设置、通知、Spotlight等位置都可能用到备选图标建议至少准备20pt、29pt、40pt、60pt四个档位的2x和3x。实际手游项目里美术一般愿意给全套毕竟主图标本来就是整套出的。我这里还遇到过一个坑只改了Info.plist却没把图片资源加进Xcode工程运行时supportsAlternateIcons返回true但一调用setAlternateIconName就立刻回调error日志里会出现找不到图标资源的提示。配置完成后第一步应该先验证资源文件是否真的进了.app包再用代码触发切换。3.2 Objective-C 插件与 C# 的 P/Invoke 桥接iOS没有类似Android的Java桥Unity要调用原生功能标准做法是写一个Objective-C插件然后用C#的DllImport(__Internal)做P/Invoke。先看C#侧using System.Runtime.InteropServices; public static class IosIconBridge { [DllImport(__Internal)] private static extern void _gameSetAlternateIconName(string iconName); public static void Switch(string iconKey) { _gameSetAlternateIconName(iconKey); } }Unity在iOS平台会把所有链接进工程的原生符号暴露给C#的[DllImport(__Internal)]所以这个extern方法在真机上能直接定位到原生函数。注意只在UNITY_IOS !UNITY_EDITOR编译条件下调用否则Editor里会报DllNotFoundException这个下面会写。Objective-C插件写在.mm文件里放到Assets/Plugins/IOS/下比如AlternateIconBridge.mm#import UIKit/UIKit.h extern C { void _gameSetAlternateIconName(const char* name) { if (available(iOS 10.3, *)) { dispatch_async(dispatch_get_main_queue(), ^{ NSString* iconName nil; if (name ! NULL strlen(name) 0) { iconName [NSString stringWithUTF8String:name]; } if ([[UIApplication sharedApplication] supportsAlternateIcons]) { [[UIApplication sharedApplication] setAlternateIconName:iconName completionHandler:^(NSError * _Nullable error) { if (error) { UnitySendMessage(IconChangeListener, OnIconChangedFailed, error.localizedDescription.UTF8String); } else { UnitySendMessage(IconChangeListener, OnIconChanged, iconName ? iconName.UTF8String : default); } }]; } else { UnitySendMessage(IconChangeListener, OnIconChangedFailed, not_supported); } }); } } }有几个细节要特别讲清楚。第一为什么dispatch_async到主线程。setAlternateIconName需要在主线程调用否则系统可能直接拒绝。Unity的C#主线程在iOS上是引擎内部的线程不等于系统主线程。在C#侧直接调底层很容易跑在非main runloop线程上。我选择在ObjC里做dispatch业务层不用操心这个。第二iconName传nil时是恢复默认图标。C#层没法直接表达nil我在插件里做了转换空字符串转换成nil。上面的代码里先判断name是否为空这比在C#里搞特判要踏实。第一次写的时候我没做这个保护运营配置空key时直接请求一个不存在的备选图标回调永远报错。第三UnitySendMessage要求场景里必须存在一个名为IconChangeListener的GameObject并挂载接收方法否则回调会静默丢失。建议在游戏启动时创建一个常驻节点专门收原生回调这个节点不要被场景切换销毁。3.3 iOS 特有的交互细节和审核注意iOS端的坑主要集中在三个方面弹窗、状态管理、审核材料。先说弹窗。setAlternateIconName一旦调用系统马上弹出确认框文案类似“要将主屏幕图标更换为...吗”。用户点取消completionHandler收到的error是用户取消类型业务层不能把这个当作切换失败去上报事故要区分error code至少把“用户主动取消”和“系统能力不足”分开处理。再说状态管理。苹果没有提供公开API去读取当前用的是哪个备选图标。supportsAlternateIcons只是告诉你支不支持拿不到当前值。所以游戏内要自己维护一个当前图标标识切换成功后用PlayerPrefs或本地文件记下来。下次启动时根据本地状态决定要不要跳过这次切换避免重复弹窗。最后是审核。Alternate Icon是官方API本身不会因为这个被拒但有两个经验值得分享一是备选图标必须来自安装包内任何运营后台、热更包都不能在运行时往bundle里塞新图标文件这是硬限制被检查出来会被判违规二是如果备选图标包含需要版权的素材或者和当前版本差异太大让用户产生误导App Review可能会问你为什么App的图标会变化答复时把运营场景说明白就行。我自己上过一个带周年庆皮肤的备选图标审核里补充说明后正常过审。还有一个不太显眼的点iOS 18上我还没有发现对Alternate Icon机制的限制但每次系统大版本更新后建议安排一次真机回归重点看切换后的图标在设置界面和桌面上是否一致。系统版本迭代带来的变化不会写在更新日志里只能靠实测。4. 统一调度设计一个跨平台图标管理器4.1 设计带回调的统一接口到这一步Android和iOS的原生能力都打通了。业务层需要一个统一管理类避免每处业务都写平台判断。我最终沉淀下来的是一个很薄的管理器using UnityEngine; public static class DynamicIconManager { public static void ChangeIcon(string iconKey) { #if UNITY_ANDROID !UNITY_EDITOR AndroidIconBridge.Switch(iconKey); #elif UNITY_IOS !UNITY_EDITOR IosIconBridge.Switch(iconKey); #else Debug.LogWarning(Dynamic icon change is not supported in editor or on this platform.); #endif } public static void RestoreDefault() { ChangeIcon(Default); } }两个平台的iconKey保持同一套枚举比如Default、Festival、NewVersion。Android侧Switch参数是alias的短名iOS侧是plist字典里的key两边必须严格对齐。这是整个方案里最容易出管理问题的地方建议把这些key写成常量类不要散落得到处都是public static class IconKeys { public const string Default Default; public const string Festival Festival; public const string NewVersion NewVersion; }回调方面Android可以同步感知但iOS有系统弹窗必须等用户点击。所以ChangeIcon最好是异步语义通过OnIconChanged事件通知业务层。给管理器加一个静态事件iOS原生回调里UnitySendMessage到一个常驻GameObject再派发到这个事件。Android如果不做回调也尽量在调用后通过PlayerPrefs记录状态保持双端逻辑一致。这里有一个设计建议把切换动作抽象成“目标状态状态来源”。运营下发一个iconKey客户端启动时读取如果和本地记录不一致再执行切换到目标状态。这样即使用户离线、或运营活动结束后忘了下发恢复指令客户端也不会停留在过期图标上。我们在周年庆版本就是这么做的活动结束后后端把iconKey改成Default客户端第二天启动自动恢复不用等发版。4.2 资源准备与构建配置清单资源准备这里列一个清单照着做不容易漏。Android侧每套alias准备一套mipmap-mdpi到xxxhdpi的方形PNG文件名和Manifest里mipmap/引用保持一致。每套alias准备一份mipmap-anydpi-v26的adaptive-icon XML。如果targetSdk低于26可以不配adaptive-icon但新游戏基本不用考虑那么低。所有文件放到Assets/Plugins/Android/res/对应目录构建时自动合并。iOS侧每个备选图标准备一套PNG至少覆盖20pt、29pt、40pt、60pt的2x和3x。命名带前缀比如IconFestival2x.png避免和主图标资源重名。通过构建后处理脚本把PNG加入Xcode工程并向Info.plist写入CFBundleAlternateIcons。这里还要提一个Unity版本差异的坑。Unity 2019.2以后Player Settings里自定义Main Manifest的开关更直观了Unity 2021.3之后Android的res合并行为越来越接近Gradle。经验是如果发现自定义Manifest里的activity-alias没有生效大概率是因为Unity把默认Manifest和你的自定义Manifest做了合并而默认Manifest里的MainActivity还带着LAUNCHER过滤器。这时候用构建出的临时工程打开AndroidManifest.xml看一下最终合并结果一眼就知道问题在哪。4.3 从代码到上线的完整操作流程整条链路在项目里走一遍是这样的。第一步在Assets/Plugins/Android/AndroidManifest.xml配置好一主多别名同时把IconSwitcher.java放进Plugins/Android目录。第二步在Assets/Plugins/IOS/放好AlternateIconBridge.mm准备图标PNG和构建后处理脚本。构建脚本推荐继承IPostProcessBuild在OnPostProcessBuild里读取Info.plist注入CFBundleAlternateIcons然后通过PBXProject的AddFile把PNG加进Build Phase。第三步在Unity侧写DynamicIconManager启动时从运营配置拉取iconKey对比本地记录后决定是否切换。前面说的常驻节点IconChangeListener在启动时创建好。第四步打包安装用真机验证三种状态默认切节日、节日恢复默认、连续切换多次。这三个case都过了才敢往上提。第五步灰度发布。先放渠道包观察崩溃率再全量。这个功能如果代码写错轻则图标不变重则桌面出现两个图标甚至启动不了所以灰度这一步别省。一个我常用的验证技巧Android只要adb logcat过滤PackageManager相关日志能看到setComponentEnabledSetting执行时的ComponentName一眼定位是包名拼错还是Manifest配置缺失。iOS则在Xcode里看断点或打开Console系统日志里出现“Alternate icon name doesnt exist”之类的提示基本就是plist key和调用参数不一致。5. 实测踩坑与兼容性速查5.1 桌面不刷新与图标缓存问题Android端被问得最多的问题代码跑成功了但桌面图标怎么没变这个要先分清楚“系统层面已切换”和“桌面上看起来没变”。系统层面PackageManager的组件状态在调用返回后已经生效你通过系统设置里的应用信息页能看到入口图标已经变了。如果桌面没变九成是Launcher缓存。不同Launcher处理方式不一样。Pixel原生机型很快切换后回到桌面就能看到MIUI和部分ColorOS机型会有几秒甚至几分钟的缓存三星One UI有时候要重启最近任务或者锁屏再解锁才刷新。网上有一种说法是切换后发ACTION_PACKAGE_CHANGED广播强制刷新Launcher实测下来对国内ROM效果不稳定Android 8.0以后对隐式广播限制很严这种方案我建议只用来真机调试不放进正式代码。还有一个我踩过的坑主Activity的icon和alias icon都设置了桌面却始终显示主Activity的icon。原因是MainActivity自己带着intent-filter并作为LAUNCHER入口桌面实际显示的就是MainActivity自身图标alias根本没被当作入口。解决方式还是回到第2章说的确保Manifest合并后MainActivity真的没带MAIN/LAUNCHER过滤器。Android 12开始系统启动画面SplashScreen会从应用图标生成一张首屏图图标换了但系统为冷启动生成的那张图可能有缓存。实际表现是桌面图标已经是新的点进去启动时的Splash还是旧的。这个一般不用太担心系统会在一段时间后更新缓存。但如果重大活动当晚希望立即生效最好提前几个小时把切换动作下发出去留出缓存刷新时间。5.2 iOS 弹窗、主线程与状态管理问题iOS端的高频问题一个是找不到资源文件另一个是弹窗点击后没有任何反应。没有反应时优先检查三点是否在主线程调用、supportsAlternateIcons是否返回false、iconName参数对应的plist key是否存在。我遇到过一次很隐蔽的情况Unity 2020导出的Xcode工程插件编译没问题但setAlternateIconName回调一直不触发。最后发现是插件里的UnitySendMessage回调没被调用原因是场景里根本没有名为IconChangeListener的对象C#事件自然收不到。这属于Unity原生通信的老坑建议在管理器初始化时检查接收对象是否存在不存在就直接打警告日志。弹窗交互上用户第一次点击“更改”后图标会有一个系统过渡动画切回桌面后图标就是新的。如果用户点击“取消”下一次启动时运营配置还是这个iconKey客户端会再次触发弹窗体验很差。所以在C#层要记录“用户已拒绝过当前key”的状态比如用PlayerPrefs存一个refusedKeys遇到被拒绝的key就不要再弹。还有一点iOS切换后App自身的快捷方式、Widget小组件不会同步更新图标。Widget上显示的是静态图没有公开API能让Widget动态跟随图标变化只能靠Widget自己定时刷新。这个属于产品层面要接受的限制提前对齐需求别等功能上线了才被测试提bug。5.3 双端版本兼容速查表最后整理一份我在真机矩阵上测过的兼容速查表出包前对照一遍能省很多事。环境切换效果注意事项Android 7.0及以下alias机制可用普通mipmap无adaptive-icon概念桌面基本立即刷新Android 8.0 / 8.1alias机制可用需配置adaptive-icon部分原生ROM会把图标裁剪成异形Android 9 / 10正常建议以mipmap-anydpi-v26资源为主Android 11正常国内ROM桌面缓存现象明显Android 12正常但SplashScreen有缓存提前切换留缓存刷新时间iOS 10.3以下不支持代码里必须做低版本保护直接忽略调用iOS 10.3到11支持弹窗正常图标资源尺寸不全时会警告iOS 12到15支持稳定建议每次大版本真机回归iOS 16支持重点看切换后设置页是否一致iOS 18暂无差异继续观察这张表不是结论是提醒。动态换图标这个功能依赖系统和ROM的行为太多即使代码完全一致不同设备上的观感也可能不同。发布前至少准备一台Pixel、一台小米、一台华为、一台iPhone覆盖主要阵营。我在项目里把这套测试矩阵固定成了出包检查项每次带版本都要过一遍宁可多花半小时也不让玩家在桌面上看到一个半新半旧的图标。聊点实在的这个功能上线后收益和成本都要看清楚。收益是运营多了一个零门槛的曝光入口成本是双端代码、资源、缓存问题、用户弹窗体验都需要有人持续维护。我个人建议把“恢复默认”也做成运营可配置的指令而不是活动结束后靠客户端写死逻辑同时所有切换动作都加频率限制避免把玩家当成流氓行为来骚扰。真机上多跑几轮把桌面缓存这个预期跟运营对齐这个功能就能稳定落地。
返回列表