Unity 的图片最终通常会被打进图集,但业务代码仍然需要处理图集加载、Sprite 查找、引用持有和资源释放。
MyFramework 使用 AtlasManager 同时封装 SpriteAtlas v2 和传统 Multi Sprite 图集,并通过 AtlasRef 管理图集引用,避免图片仍在使用时图集被提前卸载。
项目地址:
https://github.com/ZHOURUIH/MyFramework
一、AtlasManager 解决什么问题
直接使用 Unity 图集时,业务层需要考虑:
图集资源如何加载
如何通过名称查找 Sprite
多个界面共享图集时何时卸载
异步加载完成时界面是否还存在
SpriteAtlas 在真机上如何响应 atlasRequested
TexturePacker 图集和 SpriteAtlas 如何使用统一接口
AtlasManager 将这些操作统一为:
加载图集
↓
获得 AtlasRef
↓
通过名称获取 Sprite
↓
界面或对象持有 AtlasRef
↓
使用结束后释放引用
↓
引用数量为 0 时自动卸载
业务层不需要区分当前图集来自 SpriteAtlas,还是一张设置为 Multiple 的纹理。
二、同步加载图集
图集路径相对于:
Assets/GameResources
并且必须带后缀。
例如图集位于:
Assets/GameResources/UI/Common.spriteatlasv2
加载方式为:
using static FrameBaseHotFix;
protected AtlasRef mCommonAtlas;
protected void loadAtlas()
{
mCommonAtlas = mAtlasManager.getAtlas(
"UI/Common.spriteatlasv2");
if (mCommonAtlas == null ||
!mCommonAtlas.isValid())
{
return;
}
Sprite buttonSprite =
mCommonAtlas.getSprite("ButtonNormal");
}
getAtlas() 首先检查图集是否已经加载。
已经存在时,只创建一份新的 AtlasRef;不存在时,才通过 ResourceManager 加载图集资源并解析其中的 Sprite。
三、AtlasRef 表示一次独立引用
AtlasRef 并不只是包装 AtlasBase,它还会为每个持有者生成独立凭证:
public void setAtlas(AtlasBase atlas)
{
mAtlas = atlas;
mToken = generateToken();
mAtlas.addReference(mToken);
}
图集内部保存全部引用凭证:
protected HashSet<long> mReferenceTokenList = new();
同一个图集被三个界面使用时,结构类似:
Common.spriteatlasv2
├── AtlasRef:主界面
├── AtlasRef:背包界面
└── AtlasRef:商店界面
关闭背包界面,只会释放背包自己的引用,不会影响主界面和商店界面。
因此,不要把一份 AtlasRef 同时交给多个生命周期不同的对象。
每一个独立持有者,都应该通过 getAtlas() 获得自己的引用。
四、释放图集引用
不再使用图集时调用:
mAtlasManager.unloadAtlas(
ref mCommonAtlas);
unloadAtlas() 会:
校验 AtlasRef 是否有效
移除当前引用凭证
回收 AtlasRef
把变量设置为 null
不要直接卸载底层纹理或 SpriteAtlas:
Resources.UnloadAsset(texture);
Object.Destroy(sprite);
这些操作会绕过 AtlasManager 的引用记录,可能让其他仍在显示图片的界面失去资源。
五、为 UI 动态切换图集
需要运行时切换图片的节点,应使用:
myUGUIImage
例如一个专门显示物品图标的节点:
public class UIItemDetail : LayoutScript
{
protected myUGUIImage mItemIcon;
protected AtlasRef mItemAtlas;
public override void assignWindow()
{
newObject(
out mItemIcon,
"Background/ItemIcon");
}
public override void onGameState()
{
base.onGameState();
mItemAtlas = mAtlasManager.getAtlas(
"UI/Item.spriteatlasv2");
mItemIcon.setAtlas(mItemAtlas, true);
mItemIcon.setSpriteName("Item_Sword");
}
public override void onHide()
{
mItemIcon.setAtlas(null, true);
mAtlasManager.unloadAtlas(
ref mItemAtlas);
base.onHide();
}
}
myUGUIImage.setAtlas() 只负责切换当前使用的图集,并不会接管外部传入图集的释放。
因此,动态加载的 AtlasRef 仍然需要由业务对象保存,并在生命周期结束时主动卸载。
六、为什么需要 ImageAtlasPath
运行时只通过一个 Sprite,无法可靠得知它来自哪个资源路径或哪个图集。
所以 MyFramework 为需要切换图片的节点提供了:
ImageAtlasPath
它会在编辑模式下记录当前图片所在图集:
public string mAtlasPath;
myUGUIImage.init() 中会读取这个路径,并获取初始图集引用:
Image
↓
ImageAtlasPath 记录图集路径
↓
myUGUIImage 初始化
↓
AtlasManager 加载初始图集
因此,需要使用 myUGUIImage 动态切换 Sprite 的节点,应确保:
存在 Image 组件
存在 ImageAtlasPath 组件
ImageAtlasPath 中记录的路径正确
普通静态图片不需要在代码中切换时,可以继续使用 myUGUIImageSimple。
七、异步安全加载图集
图集较大,或者界面需要异步打开时,可以使用:
getAtlasAsyncSafe
示例:
protected AtlasRef mItemAtlas;
protected void loadAtlasAsync()
{
mAtlasManager.getAtlasAsyncSafe(
this,
"UI/Item.spriteatlasv2",
(atlas) =>
{
mItemAtlas = atlas;
mItemIcon.setAtlas(
mItemAtlas,
true);
mItemIcon.setSpriteName(
"Item_Sword");
});
}
第一个参数需要实现 IRecyclable。
开始加载时,框架会记录对象当前的 AssignID。完成后再次检查:
AssignID 没有变化
执行回调
对象已销毁或被对象池重新分配
不执行旧回调
这样可以避免界面已经关闭后,旧的图集加载结果又设置到被复用的界面对象上。
八、相同图集的异步请求会被合并
AtlasManager 内部维护:
protected Dictionary<
string,
List<AtlasLoadParam>>
mLoadRequestList = new();
protected Dictionary<
string,
List<AtlasLoadParam>>
mLoadingList = new();
当多个对象同时请求同一个图集时,不会重复发起多次资源加载。
流程为:
界面A请求 Item.spriteatlasv2
↓
开始加载图集
界面B再次请求同一路径
↓
只把回调加入等待列表
图集加载完成
↓
创建一个 AtlasBase
↓
依次为所有请求创建 AtlasRef
↓
分别执行回调
同时加载的不同图集数量最多为:
protected const int MAX_LOADING_COUNT = 20;
超过上限的请求会暂时保留,等待其他图集完成后再开始。
九、统一支持两种图集格式
图集加载完成后,AtlasManager 会根据后缀决定解析方式。
SpriteAtlas v2
后缀为:
.spriteatlasv2
创建:
AtlasUGUI atlas = new(mainAsset);
然后通过:
spriteAtlas.GetSprites(spriteList);
取出图集中的全部 Sprite,并以名称缓存。
Multi Sprite 图集
其他包含多个 Sprite 的纹理会创建:
AtlasTP atlas = new(mainAsset);
加载结果中:
Texture2D
用于保存实际图集纹理
Sprite
逐个加入名称缓存
最终两者都继承:
AtlasBase
业务层始终使用:
atlasRef.getSprite(spriteName);
不需要为不同图集格式分别编写代码。
十、图集如何自动卸载
AtlasManager 每隔两秒检查一次已加载图集:
protected const float CHECK_INTERVAL = 2.0f;
满足以下条件时自动销毁:
AtlasRef 引用数量为 0
并且
图集不在常驻列表中
核心判断为:
if (atlas.getReferenceCount() == 0 &&
allowUnloadAtlas(atlasPath))
{
atlas.destroy();
mAtlasList.remove(atlasPath);
}
销毁时会:
清空 Sprite 名称缓存
释放主资源 ResourceRef
从 AtlasManager 中移除图集
高频使用、不希望反复卸载的图集可以设置为常驻:
mAtlasManager.addDontUnloadAtlas(
"UI/Common.spriteatlasv2");
常驻图集仍然需要正确释放 AtlasRef,只是引用归零后不会被自动卸载。
十一、真机上的 SpriteAtlas 加载
Unity 在运行时可能通过:
SpriteAtlasManager.atlasRequested
请求某个 SpriteAtlas。
AtlasManager.init() 中会注册:
SpriteAtlasManager.atlasRequested +=
onAtlasRequested;
收到请求后,框架通过:
AtlasPathConfig.txt
把图集名称转换为资源路径,再使用 ResourceManager 加载对应的 SpriteAtlas。
这一步主要用于保证真机上 Prefab 或场景中的 Sprite 能正确绑定到动态加载的图集。
AtlasPathConfig.txt 应由框架的资源流程生成和维护,业务代码不需要手动响应 atlasRequested。
十二、总结
MyFramework 的图集管理流程是:
加载 SpriteAtlas 或 Multi Sprite
↓
统一解析为 AtlasBase
↓
缓存全部 Sprite
↓
为每个持有者创建 AtlasRef
↓
通过名称获取 Sprite
↓
使用结束后释放 AtlasRef
↓
引用归零后自动卸载图集
常用接口包括:
mAtlasManager.getAtlas(...);
mAtlasManager.getAtlasAsyncSafe(...);
atlasRef.getSprite(...);
mAtlasManager.unloadAtlas(ref atlasRef);
mAtlasManager.addDontUnloadAtlas(...);
AtlasManager 的核心并不只是通过名字查找图片,而是把图集加载、格式兼容、异步请求合并、引用计数和自动卸载统一起来。
业务层只需要明确使用哪个图集、获取哪张图片,以及什么时候结束持有。