资源分类

unity的资源类型按照加载方式分为两类:

  • GameObject、Prefab 类需要Instantiate资源
  • Texture、Mesh、Terrain、ShareMaterial等 引用计数的资源

加载机制

Assets加载:

用AssetBundle.Load(同Resources.Load) 这才会从AssetBundle的内存镜像里读取并创建一个Asset对象,创建Asset对象同时也会分配相应内存用于存放(反序列化)

异步读取用AssetBundle.LoadAsync

也可以一次读取多个用AssetBundle.LoadAll

Load出来的Assets其实就是个数据源,用于生成新对象或者被引用,生成的过程可能是复制(clone)也可能是引用(指针)

卸载机制

如下图工程, 我们使用Resources Load一个镜像,然后从中Instantiate一个GameObject, 先测试下Destroy的特性。

private GameObject go;
private bool flag;

void Start()
{
    go = Instantiate(Resources.Load("cube1234")) as GameObject;
    go.name = "instate_8421";
    flag = true;
}

private void Update()
{
    if (Input.GetKey(KeyCode.W))
    {
        GameObject.Destroy(go);
        flag = false;
    }
    if (!flag)
    {
        Debug.Log(go == null);
    }
}

GameObject.Destroy 在下一帧(或者说在本帧的最后)释放内存, go在当前帧看到的值还不是null

GameObject.DestroyImmediate(go);
Debug.Log(go == null);

GameObject.DestroyImmediate会立即释放内存, 对应的GameObject会在当前帧变为null

打开unity的Profile, 查看内存情况,

打开unity的profile, 切换到memory选项, 可以查看到Assets选项栏下的镜像, 也可以看到SceneMemory里的GameObject(Instantiate的对象)。

当使用Resources.UnloadUnusedAssets 卸载的时候, 可以看到cube1234不见了,tex1234的引用计数减少一, 剩余的都是Editor上的引用(手机上引用清零)。


UnityEngine.Object _asset;
GameObject go;
{
    // load 
    _asset = Resources.Load("cube1234");
    go = Instantiate(_asset) as GameObject;
    go.name = "instate_8421";
}
{
    // unload
    GameObject.DestroyImmediate(go);
    _asset = null; 
    Resources.UnloadUnusedAssets();
}

上代码中如果没有_asset置为null, 会出现类似这样的卸载不干净。

关于卸载bunlde中加载出的原生Prefab

如下面代码, obj是Instance之前的从Bundle中加载出来的GameObject:

 var pa = Application.streamingAssetsPath;
var bundle = AssetBundle.LoadFromFile(pa + "/cube");
obj = bundle.LoadAsset<GameObject>("cube");

若想卸载此obj, 编辑器下不能直接通过UnityEngine.Object.Destroy(obj) 接口来卸载,否则会报下图的错误:

但是此方法可以在手机上正常卸载, 所以代码里只用写一个宏区分一下平台就可以了。

#if !UNITY_EDITOR
UnityEngine.Object.Destroy(obj);
AssetBundle.UnloadAllAssetBundles(false);
#endif

关于material和sharematerial的区别

官方对materail的解释

  • Returns the first instantiated Material assigned to the renderer.
  • Modifying material will change the material for this object only.
  • If the material is used by any other renderers, this will clone the shared material and start using it from now on.

当使用Renderer.material的时候,每次调用都会生成一个新的material到内存中去,这在销毁物体的时候需要我们手动去销毁该material,否则会一直存在内存中。
也可以在场景替换的时候使用Resources.UnloadUnusedAssets去统一释放内存。

当使用Renderer.sharedMaterial的时候并不会生成新的material,而是直接在原material上修改,并且修改后的设置就会被保存到项目工程中。一般不推荐使用这个去修改,当某个材质球只被一个gameobject使用的时候可以使用这个去修改,并且最好在修改之前把原属性设置保存,当使用完毕后立即恢复原设置,防止下次加载后的gameobject上还会残留之前的设置信息

实验

Material mat = Resources.Load<Material>("mat1234");
mat.name = "mat_instate8421";
mat.color = Color.black;
go.GetComponent<Renderer>().material = mat;

打开profile, 只在Memoty-Assets栏存在一个material, 此时发现磁盘上的材质也变成了黑色。说明render上的材质和磁盘上的材质是共享的,尽管他们的名字不相同,这里也不会触发clone。在材质的Inspector面板选中材质,也可以看到磁盘上的材质也弹跳。

Material mat = Resources.Load<Material>("mat1234");
mat.name = "mat_instate8421";
var render = go.GetComponent<Renderer>();
render.material = mat;
render.material.color = Color.black;

当我们把材质赋值给render且修改材质的参数时,此时打开profile可以发现两个材质, 其中一个是Instance的, 而且磁盘里的材质并不会因为材质的参数发生改变。

材质带instance

而sharedMaterial就更好理解了,即修改render中的材质, 磁盘里的材质也会发生相应的变化。

Material mat = Resources.Load<Material>("mat1234");
mat.name = "mat_instate8421";
var render = go.GetComponent<Renderer>();
render.sharedMaterial = mat;
render.sharedMaterial.color = Color.black;

材质不带instance

与此对应的是mesh, 给meshfilter赋值的时候,也区分mesh和shareMesh。例如下面例子中改变顶点色:

Mesh mesh = Resources.Load<Mesh>();
var filter = go.GetComponent<MeshFilter>();
filter.sharedMesh = mesh;
mesh.colors[0] = Color.black;

API

Resources.UnloadAsset

Resources.UnloadAsset仅能释放非GameObject和Component的资源,比如Texture、Mesh等真正的资源。对于由Prefab加载出来的Object或Component,则不能通过该函数来进行释放。

用Resources.UnloadAsset 释放未Instance的Object 会出现这样的错误 :Unload Assets may only be used on individual assets and can not be used on GameObject’s/Components or AssetBundles.

AssetBundle.Unload

对于AssetBundle.Unload(false)只是删掉索引结构自身;

AssetBundle.Unload(true)会对自身和由它创建的Asset删除(不管场景是否引用不推荐)

Resource.UnloadUnuseAsset

用于释放所有没有引用的Asset对象

Destroy

主要用于销毁克隆对象,也可以用于场景内的静态物体,不会自动释放该对象的所有引用。虽然也可以用于Asset,但是概念不一样要小心,如果用于销毁从文 件加载的Asset对象会销毁相应的资源文件!但是如果销毁的Asset是Copy的或者用脚本动态生成的,只会销毁内存对象。

GC.Collect

GC.Collect()强制垃圾收集器立即释放内存 Unity的GC功能不算好,没把握的时候就强制调用一下

UnityStudio

unitystudio 源码地址

  • 查看AssetBundle内资源【File->LoadFile】
  • 提取AssetBundle内资源【Export】

点击菜单file中的Load file,选择一个AssetBundle,在Asset List可以看到ab包内所有资源,包括纹理、shader、音频,在Scene Hierarchy中可以看到树状结构(Prefab)。

材质不带instance

WebExtract & Binary2Text

AssetBundle对于大家来说会是一个黑盒子,其实在Unity的安装目录下有WebExtract & Binary2Text这二个工具,可以帮你把AssetBundle这个黑盒子打开。例如:升级版本AssetBundle变大了,二次构建AssetBundle出现差异了,AssetBundle内到底包含了哪些资源等。
对于构建出来的AssetBundle,我们先通过WebExtract来解开,这时候可以得到一个文件夹,里面包含着一些文件。

cd  /Applications/Unity/Unity.app/Contents/Tools
ls -al

场景的AssetBundle解开为BuildPlayer-和BuildPlayer-.sharedAssets。普通的AssetBundle解开为一个CAB-的文件。BuildPlayer-和CAB-对应的就是Profiler里面Others/SerializedFile里面的名字。

当调用WebExtract工具的时候,控制台还打印出来了一些信息。这里需要注意的是Size的组成。Bundle的Size是有header的信和blocks数据块、额外的一些数据Data组成。

Blocks根据不同的压缩方式会有不同的组织形式,譬如下图LZ4,它会产生三个压缩的Blocks,所以在读取资源的时候会先找到资源被压缩在哪个Blocks上,然后把Blocks解压并且Seek到对应位置去读对应的数据。而LZMA只有一个Block,需要把整个Blocks都解压后在读取对应的数据。

材质不带instance

WebExtract解开的文件都是二进制文件,并不是明文,通过使用Binary2Text的工具可以把这些二进制文件直接反序列化成明文。

-detailed这个参数可以让序列化出来的文本带上更多详细的信息,包括这个资源占用了大小是多少,哪些大哪些小。

-hexfloat这个参数是把浮点数都以16进制的格式来输出,这样能够保证浮点精度的输出。我们曾经遇到过两次构建有差异的问题,通过WebExtract跟Binary2Text解开后发现文件还是一致的,但后面细查发现是因为float的输出的问题。所以加入了这个参数。

binary2text inputbinaryfile [outputfile] [-detailed] [-largebinaryhashonly] [-hexfloat]

解开后文本内不同资源需要关注的一些点:

  • Assetbundle块:记录着当前AB的Assets,而Asset又会有PreloadIndex以及PreloadSzie来定义如何能把Asset给组织起来
  • PreloadData块:当前AB的Assets的依赖的Asset资源
  • External References块:引用外部的Assetbundle的列表,m_FileID & m_PathID: m_FileID为0表示资源在当前包内,不为0所以引用这外部的资源。其ID值对应着External Referecnes的列表。m_PathID为当前包内的唯一ID
  • Material:可以确认其ShaderKeyword的数量是否是符合预期的,还可以看到ShaderProperty数值是否是正确
  • Texture:可以检查是否被重复打包了,其大小占用了多少
  • Shader:可以检查是否含有了默认的Standard 或者额外的变体,通过SubProgram的数量来大致判断一下是否符合变体组合的数量。另外还可以有编译后的二进制大小。这些都直接影响到项目中ShaderLab的内存占用
  • MonoScript: 我们脚本的关联,另外还会存有一些该脚本的一些数据

类似的工具还有:

  • disunity
  • UnityAssetsExplorer

参考资料