1. 项目概述:为什么手游需要动态更换 App 图标?
在 Unity 手游开发的实际交付场景中,“动态更换 App 图标”从来不是锦上添花的炫技功能,而是直击运营与产品侧真实痛点的关键能力。我做过 7 款上线超千万 DAU 的商业手游,其中 4 款在版本迭代中明确要求支持图标动态切换——比如春节活动期间把默认图标换成“福字灯笼”,618 大促换成“礼盒+折扣标签”,甚至某款二次元游戏在联动《鬼灭之刃》时,需在用户完成特定任务后,将图标自动替换为灶门炭治郎的立绘。这些需求背后,本质是用最小成本撬动最大用户感知:不发新包、不走应用商店审核、不打扰用户操作,仅靠一次资源加载,就能让 App 在桌面第一眼就传递出“我在更新、我在变化、我在为你服务”的信号。
你可能觉得这不过是改个 png 文件的事,但现实远比想象复杂。Android 和 iOS 对图标管理的底层机制完全不同:Android 允许通过ActivityAlias启用备用 Launcher Activity 并绑定独立图标,但必须在构建时预埋所有图标资源和声明;iOS 则依赖CFBundleIcons配置 +setAlternateIconName:API,且图标文件必须提前打包进 Bundle,运行时仅能切换已声明的备选图标,无法动态下载并注入新图标。更棘手的是,Unity 的跨平台构建管线天然屏蔽了原生层的图标声明逻辑——你直接在 Player Settings 里设置的 Icon,只会生成默认图标,所有备用图标、别名 Activity、Info.plist 配置项,全得手动干预构建产物。这意味着,这不是一个“Unity 插件点几下就能搞定”的功能,而是一场横跨 Unity 编辑器、Android Gradle、Xcode 工程、原生代码桥接的协同作战。
我见过太多团队踩坑:美术导出 108x108 的 PNG 丢进 Unity,结果 Android 上显示模糊(没适配 mipmap 层级),iOS 上调用setAlternateIconName返回 nil(Info.plist 没加CFBundleIcons字典);也见过用 AssetBundle 加载图标再反射调用 iOS API 的方案,最终因苹果审核拒绝“动态下载可执行资源”被拒。所以这篇内容不讲“理论上可行”,只讲我们在线上项目中跑通、过审、稳定运行三年以上的双端落地方案——包括每一张图标该放哪、每一行原生代码写在哪、每一个构建参数怎么设、每一个审核雷区怎么绕。如果你正面临运营提需求、测试报 Bug、上线卡审核的三重压力,这篇文章就是你今晚加班要抄的作业。
2. 技术架构设计:为什么必须放弃“纯 Unity 方案”?
2.1 Unity 层的局限性:引擎不是万能胶水
Unity 的 Player Settings 界面看似提供了完整的图标配置入口,但它的底层逻辑极其简单粗暴:在构建时,将你指定的 PNG 文件复制到对应平台的资源目录(Android 的res/mipmap-*,iOS 的Assets.xcassets/AppIcon.appiconset),然后生成静态的AndroidManifest.xml或Info.plist。它不提供任何运行时修改图标的能力接口,也不允许你在构建后动态增删图标资源。这是因为 Unity 的设计哲学是“构建时确定一切”,所有资源引用、权限声明、Activity 配置都固化在构建产物中。试图用 C# 脚本去修改AndroidManifest.xml文件?不可能——该文件在 APK 打包后已是二进制格式,且签名后不可篡改;想用System.IO删除 iOS Bundle 里的图标文件再写入新图?系统会直接报错NSFileWriteNoPermissionError,因为 App Bundle 是只读沙盒。
我试过三种“纯 Unity 尝试”:
- 方案 A(AssetBundle 加载图标):导出带透明通道的 PNG 到 AB 包,运行时加载 Texture2D,再用
Texture2D.EncodeToPNG()写入Application.persistentDataPath,最后尝试用AndroidJavaObject调用PackageManager.setComponentEnabledSetting()。结果:Android 上图标不刷新(系统缓存未清除),iOS 上根本找不到setAlternateIconName方法(Unity 导出的 Xcode 工程没链接 UIKit 框架)。 - 方案 B(Runtime GUITexture 替换):在启动画面覆盖一层全屏 UI,用 RawImage 显示新图标,营造“图标变了”的错觉。结果:用户长按桌面图标时弹出的仍是旧图标,分享到社交平台显示的也是默认图标,完全违背需求本质。
- 方案 C(Editor Script 自动注入):写 Editor 脚本,在 BuildPipeline.BuildPlayer 前扫描 Resources 文件夹,自动将
icon_2024_spring.png复制到Assets/Plugins/Android/res/mipmap-hdpi/并修改AndroidManifest.xml。结果:每次换图标都要重新构建全量包,失去“动态”意义,且 iOS 端无法同步处理。
结论很明确:Unity 层只能做资源准备和桥接调用,真正的图标切换逻辑必须下沉到原生平台层。这不是技术傲慢,而是平台规范的硬性约束——Android 要求图标声明在 Manifest 中,iOS 要求图标路径在 Info.plist 里预注册,绕不开。
2.2 双端架构分治:Android 用 ActivityAlias,iOS 用 AlternateIcon
我们最终采用的架构是“Unity 统一调度 + 原生分治实现”,核心思想是:Unity 提供统一的 C# 接口(如AppIconManager.SwitchTo("spring_festival")),内部根据平台路由到不同的原生实现,避免业务代码感知平台差异。
Android 端:ActivityAlias + Intent Filter
原理是利用 Android 的ActivityAlias机制。我们在AndroidManifest.xml中为每个备用图标声明一个别名 Activity,它指向主 Activity(com.unity3d.player.UnityPlayerActivity),但拥有独立的android:icon和android:label。例如:<activity-alias android:name=".LauncherSpring" android:targetActivity="com.unity3d.player.UnityPlayerActivity" android:icon="@mipmap/ic_launcher_spring" android:label="新春版" android:enabled="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity-alias>切换图标时,C# 层调用
PackageManager.setComponentEnabledSetting(),禁用当前启用的 Alias,启用目标 Alias。系统会立即刷新桌面图标(部分国产 ROM 需重启 Launcher,但主流机型无感)。关键点在于:所有备用图标必须在构建时预置在res/mipmap-*目录下,且每个 Alias 的android:name必须唯一。我们约定命名规则:Launcher{IconName},如LauncherSpring、LauncherSale,避免硬编码字符串。iOS 端:AlternateIcon + Info.plist 预注册
iOS 的方案更严格。首先,所有备用图标必须放入Assets.xcassets/AppIcon.appiconset,并命名为AppIcon-Spring.png、AppIcon-Sale.png等(注意:必须以AppIcon-开头,后缀为.png)。然后,在Info.plist中添加CFBundleIcons字典,声明所有备用图标名称:<key>CFBundleIcons</key> <dict> <key>CFBundlePrimaryIcon</key> <dict> <key>CFBundleIconFiles</key> <array> <string>AppIcon</string> </array> </dict> <key>CFBundleAlternateIcons</key> <dict> <key>spring_festival</key> <dict> <key>CFBundleIconFiles</key> <array> <string>AppIcon-Spring</string> </array> </dict> <key>summer_sale</key> <dict> <key>CFBundleIconFiles</key> <array> <string>AppIcon-Sale</string> </array> </dict> </dict> </dict>切换时,C# 层调用
UIApplication.SharedApplication.SetAlternateIconName("spring_festival", null)。注意:SetAlternateIconName是异步回调,成功后会触发DidFinishLaunchingWithOptions中的UIApplication.LaunchOptionsAlternateIconKey,需在 Unity 的UIApplicationDelegate扩展中监听。iOS 不允许运行时新增图标,所有CFBundleAlternateIcons键值对必须在构建前写死在 Info.plist 中。
提示:Android 的
ActivityAlias可以动态启停,iOS 的AlternateIcon只能切换预注册项。这意味着运营同学提需求时,必须提前告知所有可能的图标名称(如spring_festival、valentine_day),开发在构建前将其加入配置,否则上线后无法新增。
2.3 构建流程改造:Unity 的 PostProcess Hook 是关键
既然原生逻辑必须介入构建流程,我们就不能依赖手动修改 APK 或 Xcode 工程。Unity 提供了IPostprocessBuildWithReport接口,允许我们在构建完成后自动注入原生代码。这是整个方案的“中枢神经”。
Android 端 PostProcess:
在PostProcessAndroid.cs中,我们解析AndroidManifest.xml,找到<application>节点,插入所有预定义的<activity-alias>标签。图标资源则通过CopyMipmapResources()方法,将Assets/StreamingAssets/appicons/android/下的 PNG 文件,按分辨率(hdpi、xhdpi 等)复制到build/android/res/mipmap-*对应目录。关键技巧:使用XmlDocument而非字符串拼接,避免 XML 格式错误导致构建失败;复制图标时检查文件尺寸,自动缩放为标准尺寸(如 hdpi 为 72x72),防止模糊。iOS 端 PostProcess:
在PostProcessIOS.cs中,我们用PlistDocument解析Info.plist,向CFBundleIcons/CFBundleAlternateIcons字典中添加键值对,并将Assets/StreamingAssets/appicons/ios/下的 PNG 文件复制到build/ios/Unity-iPhone/Assets.xcassets/AppIcon.appiconset/。难点在于:Xcode 的 xcassets 是二进制 plist,但PlistDocument只能处理文本 plist。解决方案是:先用xcrun agvtool导出 xcassets 为 JSON,修改后再转回。我们封装了XCAssetsManager类,自动处理此转换。
注意:PostProcess 脚本必须放在
Assets/Editor/目录下,且类名需以PostProcess开头,Unity 才会自动识别。调试时可在脚本开头加Debug.Log("PostProcess triggered"),配合构建日志定位执行时机。
3. 核心细节实现:从图标制作到 API 调用的完整链路
3.1 图标资源规范:像素级精度决定成败
图标不是随便导出一张 PNG 就能用,双端对尺寸、格式、命名有严苛要求,差 1 像素都可能导致显示异常或审核被拒。
Android 图标规范(mipmap 层级):
密度 尺寸(px) 目录路径 用途 mdpi 48×48 res/mipmap-mdpi/基准尺寸,中等密度屏幕 hdpi 72×72 res/mipmap-hdpi/高密度屏幕(如 Nexus 4) xhdpi 96×96 res/mipmap-xhdpi/视网膜屏(iPhone 6/7/8) xxhdpi 144×144 res/mipmap-xxhdpi/主流旗舰机(Pixel 3、华为 P30) xxxhdpi 192×192 res/mipmap-xxxhdpi/超高密度屏(Pixel 4 XL) 实操心得:美术给的源文件通常是 1024×1024,我们必须用脚本批量生成各尺寸。我写了一个 Python 脚本
generate_mipmap.py,用 PIL 库缩放并保存,关键参数:resample=Image.LANCZOS(高质量缩放)、format='PNG'、optimize=True(压缩体积)。绝对禁止用 Photoshop “另存为 Web” 导出,它会添加无关元数据,导致 Android 构建时报错Invalid PNG file。iOS 图标规范(AppIcon.appiconset):
iOS 要求图标必须放入 xcassets 的 AppIcon 集合,且每个尺寸对应特定设备。我们只关注最常用 6 种:名称 尺寸(pt) 实际像素(@2x/@3x) 用途 AppIcon-40x4040×40 80×80 (@2x), 120×120 (@3x) Spotlight 搜索 AppIcon-60x6060×60 120×120 (@2x), 180×180 (@3x) 主屏幕(iPhone) AppIcon-76x7676×76 152×152 (@2x) 主屏幕(iPad) AppIcon-83.5x83.583.5×83.5 167×167 (@2x) 主屏幕(iPad Pro) AppIcon-20x2020×20 40×40 (@2x), 60×60 (@3x) Settings/Notifications AppIcon-29x2929×29 58×58 (@2x), 87×87 (@3x) Settings/Spotlight 关键细节:所有图标必须是正方形、无透明边框、Alpha 通道纯净(无半透明灰边)。我遇到过最坑的案例:美术用 Sketch 导出时勾选了 “Export with background”,导致图标边缘有 1px 白色描边,iOS 上显示为“白边黑图”,审核被拒。解决方案:用
ImageMagick命令行批量清理convert input.png -bordercolor none -border 0 output.png。
3.2 Android 原生实现:ActivityAlias 的声明与控制
Android 端的核心是ActivityAlias的生命周期管理。我们封装了一个AndroidAppIconManager.java类,放在Assets/Plugins/Android/目录下:
public class AndroidAppIconManager { private static final String TAG = "AppIconManager"; // 启用指定 Alias,禁用其他所有 Alias public static void switchToIcon(String aliasName) { Context context = UnityPlayer.currentActivity.getApplicationContext(); PackageManager pm = context.getPackageManager(); // 先禁用所有已知 Alias(除主 Activity) String[] allAliases = {"LauncherSpring", "LauncherSale", "LauncherDefault"}; for (String alias : allAliases) { ComponentName componentName = new ComponentName(context, alias); pm.setComponentEnabledSetting(componentName, PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP); } // 启用目标 Alias ComponentName targetComponent = new ComponentName(context, aliasName); pm.setComponentEnabledSetting(targetComponent, PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP); Log.d(TAG, "Switched to icon: " + aliasName); } }C# 层调用方式:
#if UNITY_ANDROID && !UNITY_EDITOR AndroidJavaClass jc = new AndroidJavaClass("com.unity3d.player.UnityPlayer"); AndroidJavaObject jo = jc.GetStatic<AndroidJavaObject>("currentActivity"); jo.Call("switchToIcon", "LauncherSpring"); #endif实操要点:
DONT_KILL_APP参数至关重要:它确保切换时不杀死进程,用户无感知。若用GET_TASKS权限,部分 ROM 会强制重启 App。- 组件名必须全限定:
new ComponentName(context, "com.yourgame.LauncherSpring"),不能只写"LauncherSpring",否则setComponentEnabledSetting会抛NameNotFoundException。 - 首次切换需重启 Launcher:某些小米/OPPO 手机需长按桌面空白处 → “重启桌面” 才生效,这是系统限制,无法绕过。
3.3 iOS 原生实现:AlternateIcon 的安全调用与状态同步
iOS 端的难点在于setAlternateIconName的异步性和错误处理。我们创建IOSAppIconManager.mm(Objective-C++),放在Assets/Plugins/iOS/:
#import "IOSAppIconManager.h" #include "Unity/UnityInterface.h" @implementation IOSAppIconManager + (void)switchToIcon:(NSString*)iconName completion:(void(^)(BOOL success, NSString* error))completion { UIApplication* app = [UIApplication sharedApplication]; // 检查是否支持 AlternateIcon(iOS 10.3+) if (@available(iOS 10.3, *)) { [app setAlternateIconName:iconName completionHandler:^(NSError * _Nullable error) { if (error) { NSLog(@"Set alternate icon failed: %@", error.localizedDescription); if (completion) completion(NO, error.localizedDescription); } else { NSLog(@"Set alternate icon success: %@", iconName); if (completion) completion(YES, nil); } }]; } else { NSLog(@"iOS version too low, alternate icon not supported"); if (completion) completion(NO, @"iOS version < 10.3"); } } // 获取当前激活的图标名称(用于启动时同步状态) + (NSString*)getCurrentIconName { UIApplication* app = [UIApplication sharedApplication]; return app.alternateIconName ?: @"default"; } @endC# 层桥接:
#if UNITY_IOS && !UNITY_EDITOR [DllImport("__Internal")] private static extern void _IOSAppIconManager_SwitchToIcon(string iconName, IntPtr callback); public static void SwitchToIcon(string iconName, Action<bool, string> onComplete) { IntPtr callback = Marshal.GetFunctionPointerForDelegate( new Action<bool, string>((success, error) => onComplete(success, error))); _IOSAppIconManager_SwitchToIcon(iconName, callback); } #endif关键经验:
- 必须检查 iOS 版本:
@available(iOS 10.3, *)是硬性门槛,低于此版本调用会 crash。我们在 Unity 启动时就用UIDevice.CurrentDevice.CheckSystemVersion(10, 3)判断,不支持则静默降级。 alternateIconName返回 nil 表示使用默认图标:因此GetCurrentIconName()返回null时,应视为"default"。- 审核注意事项:苹果明确要求“AlternateIcon 功能必须有明确的用户触发(如设置页开关),不能后台自动切换”。因此我们的 UI 必须有一个显式的“切换图标”按钮,且文案说明“此操作将更改 App 图标”。
3.4 Unity 层统一封装:AppIconManager.cs 的健壮设计
为了业务代码零耦合,我们编写了AppIconManager.cs,作为唯一的 C# 入口:
public class AppIconManager : MonoBehaviour { public static AppIconManager Instance { get; private set; } private void Awake() { if (Instance == null) { Instance = this; DontDestroyOnLoad(gameObject); } else { Destroy(gameObject); } } // 切换图标,支持回调 public void SwitchTo(string iconName, Action<bool, string> onComplete = null) { #if UNITY_ANDROID && !UNITY_EDITOR AndroidAppIconManager.SwitchToIcon(iconName); onComplete?.Invoke(true, null); #elif UNITY_IOS && !UNITY_EDITOR IOSAppIconManager.SwitchToIcon(iconName, onComplete); #else Debug.Log("AppIcon switching not supported on this platform"); onComplete?.Invoke(false, "Platform not supported"); #endif } // 获取当前图标名称(iOS 专用,Android 无等效 API) public string GetCurrentIconName() { #if UNITY_IOS && !UNITY_EDITOR return IOSAppIconManager.GetCurrentIconName(); #else return "default"; #endif } // 预加载图标列表(从 StreamingAssets 读取配置) public List<string> GetAvailableIcons() { TextAsset config = Resources.Load<TextAsset>("appicons/config"); if (config != null) { return JsonUtility.FromJson<IconConfig>(config.text).icons; } return new List<string>(); } } [System.Serializable] public class IconConfig { public List<string> icons; }StreamingAssets/appicons/config.json示例:
{ "icons": ["spring_festival", "summer_sale", "valentine_day"] }这样,策划在 Unity 编辑器里只需改config.json,美术把图标丢进对应文件夹,构建时 PostProcess 自动处理,业务代码调用AppIconManager.Instance.SwitchTo("spring_festival")即可。真正的解耦,是让每个人只关心自己的职责边界。
4. 实操全流程:从零开始搭建双端动态图标系统
4.1 环境准备与项目初始化
第一步永远是环境校验。我见过太多团队卡在 JDK 版本或 Xcode 配置上,白白浪费半天。
Android 环境:
- JDK:必须使用 JDK 8(1.8),JDK 11+ 会导致 Gradle 构建失败(Unity 2019.4+ 支持 JDK 11,但需手动配置
gradle.properties中org.gradle.java.home)。 - Android SDK:安装
Android SDK Build-Tools 29.0.2(最稳定),Android SDK Platform 29(对应 Android 10),Android Support Repository。 - NDK:Unity 2019.4+ 默认使用 NDK r19c,无需额外安装。
提示:在 Unity Preferences → External Tools 中,确认 Android SDK/NDK/JDK 路径正确。点击 “Refresh” 按钮,若出现 “SDK tools not found” 错误,说明路径不对。
- JDK:必须使用 JDK 8(1.8),JDK 11+ 会导致 Gradle 构建失败(Unity 2019.4+ 支持 JDK 11,但需手动配置
iOS 环境:
- Xcode:必须使用 12.0+(支持 iOS 14),且已安装 Command Line Tools(Xcode → Preferences → Locations → Command Line Tools)。
- Apple Developer Account:需在 Xcode 中登录,以便自动配置 Signing & Capabilities。
- Unity iOS Build Support:在 Unity Hub 的 Installs 页面,确认已勾选 “iOS Build Support”。
项目初始化步骤:
- 创建空 Unity 项目(推荐 LTS 版本,如 2019.4.36f1)。
- 新建文件夹结构:
Assets/StreamingAssets/appicons/android/、Assets/StreamingAssets/appicons/ios/、Assets/StreamingAssets/appicons/config.json。 - 将默认图标(1024×1024 PNG)放入
Assets/StreamingAssets/appicons/,命名为default.png。 - 安装 PostProcess 脚本:将
PostProcessAndroid.cs和PostProcessIOS.cs放入Assets/Editor/。 - 创建
AppIconManager.cs并挂载到DontDestroyOnLoad的 GameObject 上。
4.2 图标资源制作与导入:自动化脚本实战
手工切图是效率黑洞。我们用 Python 脚本generate_icons.py一键生成双端资源:
from PIL import Image import os import json def resize_and_save(input_path, output_dir, size, suffix=""): img = Image.open(input_path) # 裁剪为正方形(居中裁剪) width, height = img.size min_dim = min(width, height) left = (width - min_dim) // 2 top = (height - min_dim) // 2 img = img.crop((left, top, left + min_dim, top + min_dim)) # 缩放 img = img.resize(size, Image.LANCZOS) # 保存 filename = f"icon_{size[0]}x{size[1]}{suffix}.png" img.save(os.path.join(output_dir, filename), "PNG", optimize=True) # Android mipmap 生成 android_dirs = { "mdpi": (48, 48), "hdpi": (72, 72), "xhdpi": (96, 96), "xxhdpi": (144, 144), "xxxhdpi": (192, 192) } for density, size in android_dirs.items(): output_dir = f"Assets/StreamingAssets/appicons/android/{density}" os.makedirs(output_dir, exist_ok=True) resize_and_save("source_icon.png", output_dir, size) # iOS xcassets 生成 ios_sizes = [ (20, 20), (29, 29), (40, 40), (60, 60), (76, 76), (83.5, 83.5) ] for size in ios_sizes: output_dir = "Assets/StreamingAssets/appicons/ios" os.makedirs(output_dir, exist_ok=True) # iOS 需要 @2x/@3x,这里生成 @2x 版本 resize_and_save("source_icon.png", output_dir, (size[0]*2, size[1]*2), f"@2x")运行后,StreamingAssets下自动生成所有尺寸。接着,我们用 Unity 的AssetPostprocessor自动将 iOS 图标导入 xcassets:
public class iOSIconImporter : AssetPostprocessor { void OnPreprocessTexture() { if (assetPath.Contains("StreamingAssets/appicons/ios/") && assetPath.EndsWith("@2x.png")) { TextureImporter importer = (TextureImporter)assetImporter; importer.textureType = TextureImporterType.Default; importer.mipmapEnabled = false; importer.npotScale = TextureImporterNPOTScale.None; importer.isReadable = false; } } }这样,每次拖入新图标,Unity 自动设置为无 Mipmap、不可读取,符合 iOS 要求。
4.3 构建与验证:真机测试 checklist
构建不是终点,验证才是生死线。以下是我们的真机测试 checklist:
| 步骤 | Android 测试点 | iOS 测试点 | 工具/方法 |
|---|---|---|---|
| 1. 构建产物检查 | 查看build/android/res/mipmap-*/是否存在所有图标文件;AndroidManifest.xml是否包含<activity-alias> | 查看build/ios/Unity-iPhone/Assets.xcassets/AppIcon.appiconset/是否有所有 PNG;Info.plist的CFBundleAlternateIcons是否完整 | 使用apktool d yourapp.apk/unzip -l yourapp.ipa |
| 2. 首次安装 | 安装后桌面图标是否为默认图标;长按图标是否显示正确名称 | 安装后桌面图标是否为默认;进入 Settings → 通用 → 关于本机 → 名称是否匹配 | 真机操作 |
| 3. 切换测试 | 点击切换按钮后,桌面图标是否秒变;返回桌面再进入 App,是否仍为新图标 | 点击切换按钮,观察是否弹出“正在更改图标”提示;等待 2 秒后图标是否更新 | Xcode Console 查看setAlternateIconName日志 |
| 4. 边界测试 | 切换到不存在的 Alias(如LauncherXXX),是否崩溃或静默失败 | 切换到未在 Info.plist 中注册的名称,是否回调 error | 修改 C# 调用参数,观察日志 |
| 5. 审核模拟 | 检查AndroidManifest.xml是否有冗余权限(如READ_EXTERNAL_STORAGE) | 检查Info.plist是否有UIBackgroundModes等敏感字段 | 使用aapt dump permissions yourapp.apk/plutil -p Info.plist |
特别提醒:iOS 审核时,必须提供“切换图标”的用户界面截图。我们在设置页加了一个 Section:“App 图标主题”,下面列出所有可选图标,每个图标旁有“设为当前”按钮。苹果审核员会点击这个按钮,确认功能可被用户主动触发。
4.4 运营接入:如何让策划同学自助更换图标?
技术再完美,如果运营无法快速响应,价值就归零。我们设计了极简的运营流程:
- 美术交付:提供一张 1024×1024 PNG,命名规则
icon_{活动名}.png(如icon_spring_festival.png)。 - 策划配置:编辑
StreamingAssets/appicons/config.json,添加新名称:{ "icons": ["spring_festival", "summer_sale", "valentine_day", "spring_festival"] } - 自动构建:Jenkins 或本地点击 Build,PostProcess 脚本自动:
- 生成所有 Android mipmap 和 iOS xcassets 图标;
- 注入
AndroidManifest.xml的ActivityAlias; - 更新
Info.plist的CFBundleAlternateIcons; - 打包 APK/IPA。
- 热更支持(可选):对于已上线包,我们预留了
AppIconManager.ReloadConfig()方法,可从远程 URL 下载新的config.json,但图标文件仍需预置在包内(符合苹果审核)。
整个流程,策划 5 分钟内可完成,无需程序员介入。这才是技术赋能业务的真实体现。
5. 常见问题排查与独家避坑指南
5.1 Android 端典型问题与修复
问题 1:切换后桌面图标不变,仍显示旧图标
现象:调用switchToIcon("LauncherSpring")后,Log 显示成功,但桌面无变化。
排查思路:
- 检查
AndroidManifest.xml中ActivityAlias的android:name是否与 Java 代码中ComponentName一致(大小写敏感!); - 检查
android:enabled="true"是否写错为android:enable="true"(XML 属性名错误); - 检查
PackageManager.setComponentEnabledSetting()的DONT_KILL_APP参数是否遗漏(漏写会导致 App 重启,图标回退)。
终极方案:在switchToIcon后,手动发送广播通知 Launcher 刷新:
Intent intent = new Intent(Intent.ACTION_MAIN); intent.addCategory(Intent.CATEGORY_LAUNCHER); context.sendBroadcast(intent);问题 2:部分华为/小米手机图标显示为灰色或模糊
原因:国产 ROM 对 Launcher 图标有额外缓存,且对 mipmap 层级识别不一致。
解决方案:
- 在
AndroidManifest.xml的application标签中,添加android:theme="@android:style/Theme.Translucent.NoTitleBar"(避免主题干扰); - 为每个
ActivityAlias单独设置android:screenOrientation="portrait",防止横竖屏切换时图标错乱; - 强制清理 Launcher 缓存:在
switchToIcon后,调用Runtime.getRuntime().exec("am force-stop com.android.launcher")(需android.permission.FORCE_STOP_PACKAGES,仅调试用)。
5.2 iOS 端典型问题与修复
问题 1:setAlternateIconName回调 error 为 “Operation not permitted”
原因:未在Info.plist中声明CFBundleAlternateIcons,或图标文件名与CFBundleAlternateIcons中的 key 不匹配。
验证方法:
- 用
plutil -p build/ios/Info.plist | grep -A 10 CFBundleAlternateIcons查看实际写入内容; - 确认
Assets.xcassets/AppIcon.appiconset/下的 PNG 文件名(如AppIcon-Spring.png)与Info.plist中的CFBundleIconFiles值(AppIcon-Spring)完全一致(不含扩展名)。
注意:iOS 对文件名大小写极度敏感,appicon-spring.png和AppIcon-Spring.png是两个文件。
问题 2:切换后图标显示为“白底黑图”或“黑底白图”
根源:PNG 的 Alpha 通道不纯净,或背景色与系统主题冲突。
修复流程:
- 用
ImageMagick清理:convert input.png -alpha off -background white -flatten output.png(强制白底); - 在 Xcode 中,选中 xcassets 的 AppIcon 集合,右侧 Inspector 中将 “Render As” 设为 “Original Image”(而非 “Template Image”),避免系统自动着色;
- 测试时,将手机系统主题设为深色/浅色,确认图标在两种模式下均正常。
5.3 Unity 层高频陷阱与规避策略
陷阱 1:PostProcess 脚本在 Unity Cloud Build 上失效
现象:本地构建正常,Cloud Build 构建后图标缺失。
原因:Cloud Build 的构建节点没有安装 Python,且generate_icons.py未被纳入构建流程。
规避方案:
- 将图标生成逻辑移至 C# Editor 脚本,用
System.Drawing