在上一篇文章中, 我们介绍了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内置资源
有了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("Common_Logo");
Assert.IsNotNull(m_Texture);
}
void LoadPrefab() {
m_Prefab = m_Ab.LoadAsset("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各个阶段, 各种情况下的内存情况.
因为整个分析过程是一个动态的过程, 很难使用文章清晰的表达, 导致很多内容没办法介绍清楚.
动态分析类的文章比较干, 能写清楚的就更少了, 视频介绍更方便一些, 如果有机会的话再给大家录制.
感兴趣的同学可以基于今天的内容, 自行构建测试用例来分析, 想必一定会有自己的收获.
下一阶段将进入本专栏最后一部分的内容: 一套商业化资源管理方案.
年底了, 公司业务繁忙, 希望能在过年之前更新完毕, 实在没时间的话只能等明年了.
好了, 今天的内容就这么多, 为了这篇文章, 作者的头发都少了几根, 希望对大家有所帮助哈!



