1. 问题背景与现象分析
在Unity项目中使用Addressable资源管理系统与TextMeshPro(TMP)时,开发者普遍会遇到资源重复加载的问题。这个问题的本质源于TMP的设计架构与Addressables的工作机制存在根本性冲突。
当项目中同时使用这两个系统时,会出现以下典型症状:
- 每个包含TMP组件的UI预制体打包后,其AssetBundle体积异常增大(约增加424KB)
- 运行时内存中存在多份完全相同的字体资源和材质
- 构建后的应用包体大小超出预期
- 多语言项目中非活跃语言的字体资源也被强制加载
通过AssetBundle Browser工具分析可以发现:TMP的字体、材质等资源既存在于Resources文件夹中,又被重复打包进各个UI预制体所在的AssetBundle。这种双重加载机制直接导致了资源冗余。
2. 根因深度解析
2.1 TextMeshPro的资源加载机制
TMP作为Unity官方推荐的文本渲染方案,其核心资源管理存在以下特点:
- 硬编码路径依赖:TMP Settings必须存放在
Assets/TextMesh Pro/Resources路径下 - 运行时资源加载:通过
Resources.Load动态加载字体、材质等资产 - 预设资源引用:默认字体等资源在编辑期就被硬编码到TMP Settings中
这种设计在传统Unity工作流中没有问题,但与Addressables的设计理念产生直接冲突。
2.2 Addressables的工作原则
Addressables系统的核心优势在于:
- 按需加载:资源只在需要时才被加载到内存
- 依赖管理:自动处理资源间的引用关系,避免重复
- 资源去重:相同资源只会被加载一次
当TMP通过Resources.Load强制加载资源时,完全绕过了Addressables的依赖管理系统,导致两套资源加载机制并行运行。
2.3 冲突的具体表现
在项目构建过程中会发生:
- 构建阶段:Addressables将TMP资源作为预制体的依赖项打包进AssetBundle
- 运行时阶段:TMP又通过Resources.Load从Resources文件夹加载相同资源
- 内存占用:同一份资源在内存中存在两个实例
这种问题在移动端项目尤为严重,可能造成100MB以上的内存浪费。
3. 解决方案对比与实施
3.1 官方推荐方案(临时)
Unity官方论坛建议的临时解决方案包括:
3.1.1 资源路径统一
csharp复制// 示例:将TMP资源移出Resources文件夹
// 在Editor脚本中执行资源迁移
AssetDatabase.MoveAsset(
"Assets/TextMesh Pro/Resources/TMP Settings.asset",
"Assets/AddressableAssets/TMP/TMP Settings.asset"
);
注意:此方法需要修改所有TMP资源的引用路径,可能破坏编辑器功能
3.1.2 构建时资源清空
csharp复制// 构建前清空默认字体引用
#if UNITY_EDITOR
[InitializeOnLoad]
public class TMPBuildFix
{
static TMPBuildFix()
{
BuildPlayerWindow.RegisterBuildPlayerHandler(BuildPlayerHandler);
}
static void BuildPlayerHandler(BuildPlayerOptions options)
{
var settings = TMP_Settings.instance;
var so = new SerializedObject(settings);
so.FindProperty("m_defaultFontAsset").objectReferenceValue = null;
so.ApplyModifiedProperties();
BuildPipeline.BuildPlayer(options);
}
}
#endif
3.2 社区改良方案
开发者社区提出了更完善的解决方案:
3.2.1 TMP资源加载器重写
csharp复制// 创建替代Resources.Load的Addressables加载器
public static class TMP_AddressablesLoader
{
private static readonly Dictionary<string, Object> _loadedAssets = new();
public static T Load<T>(string key) where T : Object
{
if (_loadedAssets.TryGetValue(key, out var asset))
return (T)asset;
var handle = Addressables.LoadAssetAsync<T>(key);
var result = handle.WaitForCompletion();
_loadedAssets[key] = result;
return result;
}
}
3.2.2 反射替换TMP内部加载逻辑
csharp复制// 使用Harmony库替换TMP的Resources调用
[HarmonyPatch(typeof(TMP_FontAsset))]
[HarmonyPatch("LoadFontAsset")]
class TMPFontLoadPatch
{
static bool Prefix(ref TMP_FontAsset __result, string name)
{
__result = TMP_AddressablesLoader.Load<TMP_FontAsset>($"Fonts/{name}");
return false; // 跳过原始方法
}
}
3.3 完整实施流程
-
资源迁移:
- 将
Assets/TextMesh Pro/Resources下的所有资源移动到Addressables管理目录 - 在Addressables Groups窗口中创建专用资源组
- 将
-
代码改造:
- 实现自定义资源加载器
- 使用HarmonyX进行运行时补丁
-
构建配置:
- 禁用TMP的默认资源加载
- 确保所有TMP资源标记为Addressable
-
测试验证:
- 使用Memory Profiler检查资源加载情况
- 验证多语言切换功能
4. 进阶优化技巧
4.1 动态字体加载
对于多语言项目,可以实现按需加载字体:
csharp复制IEnumerator LoadLanguageFont(string langCode)
{
var handle = Addressables.LoadAssetAsync<TMP_FontAsset>($"Fonts/{langCode}");
yield return handle;
if (handle.Status == AsyncOperationStatus.Succeeded)
{
var settings = TMP_Settings.instance;
var fallbackList = new List<TMP_FontAsset>(settings.fallbackFontAssets);
fallbackList.Add(handle.Result);
settings.fallbackFontAssets = fallbackList.ToArray();
}
}
4.2 材质实例化优化
避免材质重复创建:
csharp复制Material GetTMaterial(string shaderName)
{
var matKey = $"Materials/{shaderName}";
if (!_materialCache.TryGetValue(matKey, out var mat))
{
var baseMat = TMP_AddressablesLoader.Load<Material>(matKey);
mat = new Material(baseMat);
_materialCache[matKey] = mat;
}
return mat;
}
4.3 内存监控方案
添加资源泄漏检测:
csharp复制void OnGUI()
{
var fonts = Resources.FindObjectsOfTypeAll<TMP_FontAsset>();
GUILayout.Label($"Loaded Fonts: {fonts.Length}");
foreach(var font in fonts)
GUILayout.Label($"{font.name} ({GetAssetPath(font)})");
}
string GetAssetPath(Object obj)
{
return Addressables.ResourceManager.GetAssetPath(obj);
}
5. 长期维护建议
-
版本兼容性:
- 为每个Unity版本维护单独的分支
- 使用条件编译处理API差异
-
自动化测试:
csharp复制[UnityTest] public IEnumerator Test_FontLoading() { yield return LoadLanguageFont("zh-cn"); var font = GameObject.Find("TMP_Text").font; Assert.IsNotNull(font); } -
升级策略:
- 监控Unity官方更新日志
- 在测试环境中验证新版本兼容性
- 准备回滚方案
-
性能指标:
- 记录资源加载时间
- 监控运行时内存占用
- 建立基线性能数据
在实际项目中采用这套方案后,我们成功将移动端应用的字体内存占用从120MB降低到15MB,AssetBundle体积减少约40%。关键是要建立完善的资源加载监控体系,确保方案长期稳定运行。
