本文分享Unity中的资源管理-使用Profile分析内存使用情况
在上一篇文章中, 我们介绍了Ab
的加载和使用, 并简单列举了其内存分布情况. 今天我们继续探索Ab
的内存, 观察和实验其在各种阶段的分布情况.
Profile性能分析工具
在一切开始之前, 我们先简单介绍下Unity提供的性能分析工具: Profile.
Profile是Unity提供的一款性能分析工具, 与Editor一同发布, 我们可以在Window菜单下找到它, 不同版本的位置不同, 比如在Unity2017(Window->Profile), 而在Unity2019(Window->Analysis->Profile).
打开之后如图所示(本文基于Unity2019.4.26f1, 不同版本会有所差异, 但是大同小异):
我们关注左边的菜单:
- CPU Stage: CPU使用情况
- Rendering: 渲染情况
- Memory: 内存情况
每个菜单点击之后, 下方的说明面板都有对应的概要情况说明.
注意本文只关注内存部分. 其它部分情况类似, 各位同学可以自行摸索, 传送门在此.
在选择内存菜单之后, 下方的说明面板可以有两种视图, 分别是: Simple(简单说明), Detailed(详情).
Simple视图展示Unity在每帧的实时内存信息概括, Unity会提前向系统申请预留内存, 以减少频繁的内存请求, 这个视图只是展示了各个类型的内存使用量, 而不涉及具体的细节.
展示的信息包括如下:
- 第一行:
- Used Total: 后续所有内存总和, 如上图:(Unity+Mono+GfxDriver+Audio+Video+Profile=341.6M)
- Unity: Unity的原生代码使用的内存大小
- Mono: 托管代码使用的内存大小
- GfxDriver: 驱动程序对纹理, 渲染目标, 着色器和网格数据使用的内存估计量
- Audio/Video: 音频和视频系统使用内存大小
- Profiler: 性能分析器使用的总的内存大小
- 第二行: 与第一行类似, 只是描述的是Unity向系统申请的预留内存.
- 第三行: 整个系统所用内存大小, 与任务管理器中使用的大小一致, 根据该平台是否允许从系统获取内存情况, 这个值会显示不同的大小, 一般情况下都会大于上两行的总和, 因为Profile无法追踪所有的内存.
- 更多行: 其余的信息一目了然, 分别是不同类型的资源所占内存大小, 还有游戏对象数量等基础信息, 这里不再赘述.
Detailed视图展示内存使用详情, 因为信息量巨大, 所以采用截图采样(或者说叫快照)的方式(Take Sample)截取某一帧进行分析, 同时, 勾选上中部的Deep Profile
之后, 能够获取更多的信息, 启用Gather object references
可以收集对象可能的引用信息, 如下图所示:
内存详情分为几个部分展示, 分别为:
- Other: 资源, 游戏对象或组件之外的对象内存情况, 比如系统库, Profiler, 各种管理器等.
- Not Saved: 标记为
HideFlags.DontSave
的对象, 即不保存到场景, 加载新场景也不会被销毁的对象, 是是 HideFlags.DontSaveInBuild | HideFlags.DontSaveInEditor | HideFlags.DontUnloadUnusedAsset
的组合. - Assets: 从用户或者原生代码中引用的资源, 这是我们关注的重点部分.
- Scene Memory: 当前场景的对象和其附加的资源
- Builtin Resources: Unity Editor或者Unity内置资源
分析Ab内存占用
有了Profile的前置知识, 我们可以正式开始进行分析.
我们的目的是观察每个阶段, 内存的变化情况.
为了测试, 我们需要一个测试程序, 里面包含:
- 加载
Ab
- 加载纹理(不需要实例化的资材)
- 加载预制(需要实例化的资材)
- 实例化对象
- 摧毁对象
- 卸载预制
- 卸载纹理
- 卸载
Ab
, 且不摧毁资材和对象 - 卸载
Ab
, 摧毁资材和对象
每个阶段对应一个按钮和回调, 效果如下:
在Controller
对象删挂载控制脚本: ResourcesTest
public class ResourcesTest : MonoBehaviour {
public Button btnClear;
public Button btnLoadAb;
public Button btnLoadTexture;
public Button btnLoadPrefab;
public Button btnInstanceObj;
public Button btnDestroyObj;
public Button btnUnloadPrefab;
public Button btnUnloadTexture;
public Button btnUnloadAb;
public Button btnUnloadAbAndDestroy;
private AssetBundle m_Ab;
private GameObject m_Prefab;
private GameObject m_Obj;
private Texture m_Texture;
void Start() {
btnClear.onClick.AddListener(Clear);
btnLoadAb.onClick.AddListener(LoadAb);
btnLoadTexture.onClick.AddListener(LoadTexture);
btnLoadPrefab.onClick.AddListener(LoadPrefab);
btnInstanceObj.onClick.AddListener(InstanceObj);
btnDestroyObj.onClick.AddListener(DestroyObj);
btnUnloadPrefab.onClick.AddListener(UnloadPrefab);
btnUnloadTexture.onClick.AddListener(UnloadTexture);
btnUnloadAb.onClick.AddListener(() => UnloadAb(false));
btnUnloadAbAndDestroy.onClick.AddListener(() => UnloadAb(true));
}
void Clear() {
Resources.UnloadUnusedAssets();
}
void LoadAb() {
UnloadAb(true);
var ab = AssetBundle.LoadFromFile("Assets/Output/AssetBundle/AllAb/prefabs");
m_Ab = ab;
Assert.IsNotNull(m_Ab);
}
void LoadTexture() {
m_Texture = m_Ab.LoadAsset<Texture>("Common_Logo");
Assert.IsNotNull(m_Texture);
}
void LoadPrefab() {
m_Prefab = m_Ab.LoadAsset<GameObject>("Attack");
Assert.IsNotNull(m_Prefab);
}
void InstanceObj() {
m_Obj = Instantiate(m_Prefab);
Assert.IsNotNull(m_Obj);
}
void DestroyObj() {
if (m_Obj != null) {
Destroy(m_Obj);
m_Obj = null;
}
}
void UnloadPrefab() {
m_Prefab = null;
Resources.UnloadUnusedAssets();
}
void UnloadTexture() {
Resources.UnloadAsset(m_Texture);
m_Texture = null;
}
void UnloadAb(bool needDestroy = false) {
if (m_Ab != null) {
m_Ab.Unload(needDestroy);
m_Ab = null;
}
}
private void OnDestroy() {
UnloadAb(true);
}
}
分阶段进行测试
初始情况下, 在启动之后, 我们先清理所有无用资源(点击清理
按钮)并记录一个内存快照:
加载和卸载Ab
点击加载Ab
按钮之后:
我们看到, Other和Not Saved数量增加了一个, 展开后经过一番比较之后, 发现增加的是:
因为Ab
本身是一个序列化文件, 所以在Other
中的SerializedFile
增加一个对象. 同时在Not Saved
的Assetbundle
增加了该Ab
.
点击卸载Ab
或者卸载Ab(不摧毁资材和对象)
之后, 内存恢复. 因为这里还没有加载任何资材或者实例化对象, 所有两种卸载方式表现一致.
加载和卸载纹理
因为在Editor下, 每次启动后会有细微的差别, 所以每个阶段我们都从加载Ab
开始.
重新启动并清理之后:
加载Ab
之后:
点击加载纹理
:
Other和Not Saved没有变化, Assets数量增加了2, 也就是纹理对应的纹理资源和精灵(测试的资源为精灵).
如果现在点清理
按钮, 内存不会有变化, 因为Texture被本地变量引用着, 不会被清理.
点击卸载Ab(不摧毁资材和对象)
按钮后, Ab
被卸载, 但是纹理依然存在.
清理并且重新加载Ab
和纹理之后, 点击卸载Ab
按钮后, Ab
被卸载, 且纹理也被卸载, 恢复到加载Ab
之前的状态.
清理并且重新加载Ab
和纹理之后, 点击卸载纹理
按钮后, 纹理被卸载, 恢复到加载纹理之前的状态.
加载和卸载预制
预制是一种需要实例化后使用的资材, 且因为其一般包含很多对其它资材的引用, 所以一旦加载预制, 会同时将其所有引用都加载到内存.
重新启动并清理之后:
加载Ab
之后:
点击加载预制
按钮后:
Assets整整增加了41个, 因为该预制引用的资材比较多, 有纹理, 有对象, 有材质,有shader等.
点击卸载预制之后
按钮后, Assets恢复原始大小, 但是Other却增大了, 作者细致比较后也没有发现增大的部分在哪, 希望了解的同学在评论区告知.
重新加载预制后, 点击卸载Ab(不摧毁资材和对象)
按钮后, 预制和其引用资源依然存在, Ab
被卸载.
重新加载预制后, 点击卸载Ab
按钮后, 预制和其资源与Ab
一同被卸载.
加载和卸载预制加实例化对象
重新启动, 清理, 加载Ab
和预制之后:
Not Saved和Scene Memory有所变化.
Not Saved增加了2, 来自于两个阴影:
Scene Memory增加了22, 来自于对象引用的各种资材和实例化对象:
点击摧毁对象
按钮后, Not Saved和Scene Memory恢复了大小.
重新加载预制后, 点击卸载Ab(不摧毁资材和对象)
按钮后, 预制, 其引用资源和实例化对象依然存在, Ab
被卸载.
重新加载预制后, 点击卸载Ab
按钮后, 其引用资源和实例化对象与Ab
一同被卸载. 但是对象的变量引用没有移除, 所以Scene Memory管理的部分无法被清理, Scene Memory的大小无法恢复.
总之, 在卸载Ab
时, 如果同时要销毁加载的资材和实例化的资源, 需要注意清理所有引用, 不然可能会有严重的问题.
总结
今天介绍了Unity的性能分析工具: Profile, 并提供了一个简单的例子用于分析Ab
各个阶段, 各种情况下的内存情况.
因为整个分析过程是一个动态的过程, 很难使用文章清晰的表达, 导致很多内容没办法介绍清楚.
动态分析类的文章比较干, 能写清楚的就更少了, 视频介绍更方便一些, 如果有机会的话再给大家录制.
感兴趣的同学可以基于今天的内容, 自行构建测试用例来分析, 想必一定会有自己的收获.
下一阶段将进入本专栏最后一部分的内容: 一套商业化资源管理方案.
年底了, 公司业务繁忙, 希望能在过年之前更新完毕, 实在没时间的话只能等明年了.
好了, 今天的内容就这么多, 为了这篇文章, 作者的头发都少了几根, 希望对大家有所帮助哈!