Unity资源管理机制
资源分类
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的, 而且磁盘里的材质并不会因为材质的参数发生改变。
而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;
与此对应的是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)。
WebExtract & Binary2Text
AssetBundle对于大家来说会是一个黑盒子,其实在Unity的安装目录下有WebExtract & Binary2Text这二个工具,可以帮你把AssetBundle这个黑盒子打开。例如:升级版本AssetBundle变大了,二次构建AssetBundle出现差异了,AssetBundle内到底包含了哪些资源等。
对于构建出来的AssetBundle,我们先通过WebExtract来解开,这时候可以得到一个文件夹,里面包含着一些文件。
cd /Applications/Unity/Unity.app/Contents/Tools
ls -al
场景的AssetBundle解开为BuildPlayer-
当调用WebExtract工具的时候,控制台还打印出来了一些信息。这里需要注意的是Size的组成。Bundle的Size是有header的信和blocks数据块、额外的一些数据Data组成。
Blocks根据不同的压缩方式会有不同的组织形式,譬如下图LZ4,它会产生三个压缩的Blocks,所以在读取资源的时候会先找到资源被压缩在哪个Blocks上,然后把Blocks解压并且Seek到对应位置去读对应的数据。而LZMA只有一个Block,需要把整个Blocks都解压后在读取对应的数据。
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