【小松教你手游开发】【unity实用技能】Unity内存申请和释放(转自tnqiang)

这里先声明转自http://www.jianshu.com/p/b37ee8cea04c

1.资源类型

GameObject, Transform, Mesh, Texture, Material, Shader, Script和各种其他Assets。

2.资源创建方式

  • 静态引用,在脚本中加一个public GameObject变量,在Inspector面板中拖一个prefab到该变量上,然后在需要引用的地方Instantiate;
  • Resource.Load,资源需要放在Assets/Resources目录下;
  • AssetBundle.Load, Load之后Instantiate。

    3. 资源销毁方式

  • GameObject.Destroy(gameObject),销毁该物体;
  • AssetBundle.Unload(false),释放AssetBundle文件内存镜像,不销毁Load创建的Assets对象;
  • AssetBundle.Unload(true),释放AssetBundle文件内存镜像同时销毁所有已经Load的Assets内存镜像;
  • Resources.UnloadAsset(Object),释放已加载的Asset对象;
  • Resources.UnloadUnusedAssets,释放所有没有引用的Asset对象。

    4. 生命周期

    实验篇

    实验中创建了一个简单场景,场景中创建了一个Empty GameObject,上面挂了一个脚本,在Awake函数中通过协程函数来创建资源,具体的Coroutine函数下面都有。
    实验中创建的Prefab是一个坦克车,加入场景中场景内存增加3M左右,同时创建了一个AssetBundle资源供AssetBundle使用。

    1. Resources.Load方式加载一个Prefab, 然后Instantiate GameObject

    代码如下:

   IEnumerator LoadResources()
    {
        // 清除干净以免影响测试结果
        Resources.UnloadUnusedAssets();
        // 等待5秒以看到效果
        yield return new WaitForSeconds(5.0f);

        // 通过Resources.Load加载一个资源
        GameObject tank = Resources.Load("Role/Tank") as GameObject;
        yield return new WaitForSeconds(0.5f);

        // Instantiate一个资源出来
        GameObject tankInst = GameObject.Instantiate(tank, Vector3.zero, Quaternion.identity) as GameObject;
        yield return new WaitForSeconds(0.5f);

        // Destroy一个资源
        GameObject.Destroy(tankInst);
        yield return new WaitForSeconds(0.5f);

        //释放无用资源
        tank = null;
        Resources.UnloadUnusedAssets();

        yield return new WaitForSeconds(0.5f);
    }

执行结果如下:

下面是统计结果:

数据描述 Memory Texture Mesh Material GameObjects Objects in Scene Total Objects
初始 72.8M 1271/8.0M 35/223.0K 25/10.2K 7 211 2187
Resources.Load 72.8M 1271/8.0M 36/0.8M 25/10.2K 7 211 2280
Instantiate 75.3M 1272/9.3M 36/0.8M 26/10.7K 52 303 2375
Destroy 74.7M 1272/9.3M 36/0.8M 26/10.7K 7 211 2283
Resources.UnloadUnusedAssets 72.3M 1271/8.0M 35/223.0K 25/10.2K 7 211 2187

从这里我们得出如下结论:

  • Resouces.Load一个Prefab相对于Instantiate一个资源来说是相对轻量的一个操作,上述过程中,Resources.Load加载一个Prefab几乎没有消耗内存,而Instantiate消耗了2.5M的资源空间。Resources.Load增加了Mesh和Total Object的数量,而Instantiate增加了GameObjects,Objects In Scene和Total Objects的数量;
  • Destroy一个GameObject之后,内存有所减少,但是比较少,本例中减少了0.6M;Instantiate和Destroy前后Material和Texture没有还原,用以后面继续进行Instantiate之用。

若没有调用Resources.UnloadUnusedAssets,则结果如下:

统计结果如下:

数据描述 Memory Texture Mesh Material GameObjects Objects in Scene Total Objects
初始 58.9M 1258/7.5M 34/219.2K 22/9.0K 7 117 2078
Resources.Load 60.0M 1258/7.5M 35/0.8M 22/9.0K 7 117 2171
Instantiate 62.5M 1259/8.9M 36/0.8M 23/9.5K 52 209 2256
Destroy 61.8M 1259/8.9M 35/0.8M 23/9.5K 7 117 2174

得出如下结论:
如果不手动执行Resources.UnloadUnusedAssets,则多余的Mesh,Material和Object不会主动释放。

2. 以AssetBundle.Load的方式加载一个Prefab,然后Instantiate一个GameObject

代码如下:

 IEnumerator LoadAssets(string path)
    {
        // 清除干净以免影响测试结果
        Resources.UnloadUnusedAssets();

        // 等待5秒以看到效果
        yield return new WaitForSeconds(5.0f);

        // 创建一个WWW类
        WWW bundle = new WWW(path);
        yield return bundle;
        yield return new WaitForSeconds(0.5f);

        // AssetBundle.Load一个资源
        Object  obj =  bundle.assetBundle.Load("tank");
        yield return new WaitForSeconds(0.5f);

        // Instantiate一个资源出来
        GameObject tankInst = Instantiate(obj) as GameObject;
        yield return new WaitForSeconds(0.5f);

        // Destroy一个资源
        GameObject.Destroy(tankInst);
        yield return new WaitForSeconds(0.5f);

        // Unload Resources
        bundle.assetBundle.Unload(false);
        yield return new WaitForSeconds(0.5f);

        // 释放无用资源
        //obj = null;
        //Resources.UnloadUnusedAssets();

        yield return new WaitForSeconds(0.5f);
    }

执行结果如下:

统计结果如下:

数据描述 Memory Texture Mesh Material GameObjects Objects in Scene Total Objects
初始 59.9M 1267/7.8M 35/223.0K 25/10.2K 7 127 2099
new WWW 62.0M 1267/7.8M 35/223.0K 25/10.2K 7 127 2099
AssetBundle.Load 64.5M 1268/9.2M 36/0.8M 26/10.5K 7 127 2196
Instantiate 65.6M 1268/9.2M 36/0.8M 26/10.7K 52 219 2288
Destroy 63.9M 1268/9.2M 36/0.8M 26/10.7K 7 127 2196
AssetBundle.Unload 63.7M 1268/9.2M 36/0.8M 26/10.7K 7 127 2196
Resources.UnloadUnusedAssets 61.8M 1267/7.8M 35/223.0K 25/10.2K 7 127 2099

得出如下结论:
通过WWW Load AssetBundle的方式加载一个资源时会自动加载相应的Mesh,Texture和Material,而通过Resouces.Load方式进行加载只会加载Mesh信息。因此通过AssetBundle方式加载后Instantiate一个资源的内存消耗较小,本例中AssetBundle.Load增加了2.5M的内存,而Instantiate增加了1.1M的内存。相比较Resources.Load后Instantiate的内存增量要小很多。

3. 通过静态绑定的方法来Instantiate一个资源

代码如下:

    IEnumerator InstResources()
    {
        Resources.UnloadUnusedAssets();
        yield return new WaitForSeconds(5.0f);

        GameObject inst = GameObject.Instantiate(tank, Vector3.zero, Quaternion.identity) as GameObject;
        yield return new WaitForSeconds(1f);

        GameObject.Destroy(inst);
        yield return new WaitForSeconds(1f);

        //释放无用资源
        tank = null;
        Resources.UnloadUnusedAssets();

        yield return new WaitForSeconds(1f);
    }

执行结果如下:

统计结果如下:

数据描述 Memory Texture Mesh Material GameObjects Objects in Scene Total Objects
初始 62.0M 1268/7.9M 36/0.8M 25/10.2K 7 134 2202
Instantiate 64.4M 1269/9.2M 36/0.8M 26/10.7K 8 137 2207
Destroy 64.0M 1269/9.2M 36/0.8M 26/10.7K 7 134 2204
UnloadUnused Resources 62.3M 1268/7.9M 35/226.3K 25/10.2K 7 134 2107

得出结论如下:
通过静态绑定的方式各种资源的加载顺序和Resources.Load的方式是一样的,一个GameObject创建时,其Component中静态绑定的GameObject只会加载Mesh信息,只有当该GameObject Instantiate出来之后才会加载Texture和Material信息。

理论篇

加载资源的过程可以分为两个阶段,第一阶段是使用Resources.Load或者AssetBundle.Load加载各种资源,第二阶段是使用GameObject.Instantiate克隆出一个新的GameObject。
Load的资源类型包括GameObject, Transform, Mesh, Texture, Material, Shader和Script等各种资源,但是Resources.Load和AssetBundle.Load是有区别的。
使用Resources.Load的时候在第一次Instantiate之前,相应的Asset对象还没有被创建,直到第一次Instantiate时才会真正去读取文件创建这些Assets。它的目的是实现一种OnDemand的使用方式,到该资源真正使用时才会去创建这些资源。
而使用AssetBundle.Load方法时,会直接将资源文件读取出来创建这些Assets,因此第一次Instantiate的代价会相对较小。
上述区别可以帮助我们解释为什么发射第一发子弹时有明显的卡顿现象的出现。

然后我们再来了解一下Instantiate的过程。Instantiate的过程是一个对Assets进行Clone(复制)和引用相结合的过程,Clone的过程需要申请内存存放自己的数据,而引用的过程只需要直接一个简单的指针指向一个已经Load的资源即可。例如Transform是通过Clone出来的,Texture和TerrainData是通过引用复制的,而Mesh,Material,PhysicalMaterial和Script是Clone和引用同时存在的。以Script为例,Script分为代码段和数据段,所有需要使用该Script的GameObject使用的代码是一样的,而大家的数据有所区别,因此对数据段需要使用Clone的方式,而对代码段需要使用引用的方式来复制。
因此Load操作其实Load一些数据源出来,用于创建新对象时被Clone或者被引用。

然后是销毁资源的过程。当Destory一个GameObject或者其他实例时,只是释放实例中那些Clone出来的Assets,而并不会释放那些引用的Assets,因为Destroy不知道是否有其他人在引用这些Assets。等到场景中没有任何物体引用到这些Assets之后,它们就会成为UnusedAssets,此时可以通过Resources.UnloadUnusedAssets来进行释放。AssetBundle.Unload(false)不行,因为它只会释放文件的内存镜像,不会释放资源;AssetBunde.Unload(true)也不行,因为它是暴力的释放,可能有其他对象在引用其中的Assets,暴力释放可能导致程序错误。
另外需要注意,系统在加载新场景时,所有的内存对象都会被自动销毁,这包括了Resources.Load加载的Assets, 静态绑定的Assets,AssetBundle.Load加载的资源和Instantiate实例化的对象。但是AssetBundle.Load本身的文件内存镜像(用于创建各种Asset)不会被自动销毁,这个必须使用AssetBundle.Unload(false)来进行主动销毁。推荐的做法是在加载完资源后立马调用AssetBunble.Unload(false)销毁文件内存镜像。
下图可以帮助理解内存中的Asset和GameObject的关系。

总结篇

  • 为了不出现首次Instantiate时卡顿的现象,推荐使用AssetBundle.Load的方式代替Resources.Load的方式来加载资源;
  • 加载完资源后立马调用AssetBunble.Unload(false)释放文件内存镜像;
  • Unity自身没有提供良好的内存申请和释放管理机制,Destroy一个GameObject会马上释放内存而不是进行内部的缓存,因此应用程序对频繁不用的对象如NPC,FX等进行对象池管理是必要的,减少内存申请次数;
  • 何时进行Resources.UnloadUnusedAssets是需要讨论的一个问题。

Ref:

http://game.ceeger.com/forum/read.php?tid=4394
http://game.ceeger.com/forum/read.php?tid=4466

文/Tnqiang(简书作者)
原文链接:http://www.jianshu.com/p/b37ee8cea04c
著作权归作者所有,转载请联系作者获得授权,并标注“简书作者”。

时间: 2024-10-08 15:16:27

【小松教你手游开发】【unity实用技能】Unity内存申请和释放(转自tnqiang)的相关文章

【小松教你手游开发】【unity实用技能】角色头部跟随镜头旋转

这个在端游上比较场景,在角色展示的时候,当摄像头在角色身边上下左右旋转时,角色头部跟随镜头旋转.如天涯明月刀等. 这个在手游上比较少见,不过实现也没什么区别. 首先一般情况下,找到模型的头部节点,直接用lookAt指向camera就可以了,不过一般需求不会这么简单. 比如说,超过头部扭动极限,头部需要插值回到原始点:当镜头从外部回到极限内,需要插值回来.这时候lookat就没法使用. 更有情况,头部本身坐标系不在世界坐标轴上, 可能旋转了90多或者输出的prefab就是歪的等等,这些情况都没办法

【小松教你手游开发】【unity实用技能】根据上一个GameObject坐标生成的tips界面

开发游戏,特别是mmo手游的时候经常需要开发的一个需求是,点击某个装备,在它附近的位置生成一个tips界面,介绍装备功能和各种信息. 像上面红色框框里的这个. 这个主要的问题是 根据点击的GameObject对应生成这个详情界面时,详情界面位置需要合理摆放(不能显示不到,不能遮挡等) 基本的思路是, 首先找到GameObject的position, 把手机屏幕大概分成四个象限,知道这个GameObject大概在这个屏幕的哪个象限(左上,左下,右上,右下) 根据象限来判断详情界面应该在GameOb

【小松教你手游开发】【unity系统模块开发】热更

现在的手游项目如果没个热更新迭代根本跟不上, 特别是像我们项目做mmo的更是需要经常改动代码. 而现在的项目一般会选择用lua的方式实现热更新 不过我们项目由于历史原因没有使用,用的是另外一种方案 在项目里的所有GameObject都不挂脚本(NGUI脚本就通过代码的方式挂上),自己写的脚本都不继承Mono并打成dll,然后通过一个启动脚本去打开这些dll. 不过这样就有个问题,ios不能热更... 不管怎么样,先来讲讲这种方案要怎么做. 首先有两部分,一部分是打包,一部分是解包. 而包又分为资

【小松教你手游开发】【unity实用技能】InvalidOperationException: out of sync

在unity开发中出现这个bug. 在网上查了下是在迭代器中直接修改引起的.c#是不允许你在迭代器中直接修改. 改了一下确实解决. 原本是这样 [csharp] view plain copy public void Run() { foreach (var item in timerDict) { if (null != item.Value) { item.Value.Run(); } } } 改成这样: [csharp] view plain copy public void Run()

【小松教你手游开发】【unity实用技能】给每个GameObject的打开关闭加上一个渐变

在游戏开发中,经常会因为直接将GameObject,setActive的方式打开关闭,这种方式效果太过生硬而给它加上一个Tween 可能是AlphaTween或者ScaleTween. 再加上一个PlayTween来做控制. 这样子需要在每个GameObject上加上这几个Component不说,还很不好用 所以结合之前用的一个拓展函数的方法,想到一个非常非常方便的方法 (之前的拓展函数文章:http://blog.csdn.net/chrisfxs/article/details/512218

【小松教你手游开发】【unity实用技能】计算目标物体是否在自己的扇形视野范围

在做游戏开发中经常会需要到计算扇形的视野或者是受击范围的时候. 其实这个分为两部分, 第一部分是在扇形距离范围内(也就是不考虑角度,其实是圆形范围内) 第二部分是扇形角度范围内 第一部分很简单,Vector3.Distance(a, b);计算距离 下面讲讲第二部分,扇形角度范围内. 计算怪物是否在你的视野范围内其实可以这么看 Avatar的正方向向量与Avatar到Enemy之间向量的夹角大小是否小于于视线大小的一半. 这样就能判断是否在视线范围内. 所以现在的问题就是这么计算这个夹角 计算的

【小松教你手游开发】【unity实用技能】unity发包优化(android一键发包)

unity本身的发包其实还是挺方便的,国外的游戏基本都用unity本身的发包. 但在国内的游戏有这么多渠道,这个迭代的速度的情况下,就需要一套更高效的发包方式. 接下来讲具体步骤,如果你们项目有热更新会更麻烦一点. 发包优化的目标是做到一键发包,一般发包机会是一台独立的机子,所以 第一步,更新svn 第二步,配置打包信息.根据不同渠道接入不同sdk 第三步,build apk. 因为我们项目暂时还是测试,所以还没做根据不同渠道接入不同sdk.具体思路是写个xml,在上面填写各种配置信息,版本号,

【小松教你手游开发】【unity实用技能】线性差值计算实现

其实这个unity本身就有的函数Mathf.Lerp(),为什么还要自己实现呢. 有一个原因就是这个函数返回的是float型,float型如果数字非常大,转出int时会有精度丢失,也就是转出来的值不对. 而且非常简单. 看下公式 public int Lerp(int a,int b,int v) { return (int)(a - (0 - v) / (0 - 1) * (a - b)): } 原文地址:http://blog.51cto.com/13638120/2084965

【小松教你手游开发】【unity实用技能】unity ios快捷打包

ios打包是比较麻烦的,配通一次流程后需要做个笔记把各种插件各种配置在每次打包的时候重新配置,作为一个程序员当然不能接受这么笨的事情,写个脚本让代码去实现这些. 首先,介绍一个标记 [PostProcessBuild] 在函数前写上这个标记,unity在打完包后便会调用这个函数. 所以我们也将用这个标记让unity在包打成xcode项目以后,去改动xcode里面的配置. 而unity也有一个默认的函数给我们使用 static void OnPostprocessBuild(BuildTarget