优化层面:代码、贴图、框架设计
一、代码优化
1、用for代替foreach
原因:Mono下的foreach频繁调用容易触及堆上限,导致GC过早触发,出现卡顿现象。尤其在update中,用for代替foreach。
2、string
连接两个字符串的操作使用StringBuilder.Append来代替string。
String aa="abc";
aa+="def";
StringBuilder text=new StringBuilder("abc",10);
text.Append("def");
原因:每次使用string的时候,都会在内存里创建一个新的字符串对象,需要为该对象分配新的空间。
3、gameObject.tag = xxx
使用gameObject.CompareTag("XXX")来代替gameObject.tag = xxx。
原因:gameObject.tag会在内部循环调用对象分配的标签属性以及拷贝额外的内存。
4、使用ObjectPool对象池来管理对象,避免频繁使用的Instance,Destroy。
二、贴图优化
贴图优化的效果更明显。
1、压缩png、jpg图片 https://tinypng.com/。
2、巧妙通过调整纹理资源,来调整图的大小。如:ugui九宫格,部分缩小后在unity中拉大。
3、IOS平台使用PVRT压缩,Android使用ETC1压缩(ETC1只能支持非Alpha通道的图)。
4、mipMap摄像机距离远近替换不同图片。
5、减少色彩使用(纯色)。
三、框架设计
1、场景切换时加一个loading
当前旧场景内存未释放的时候,加载新的场景,此时前后两个场景的内存叠加,很容易达到内存峰值,导致崩溃。如果在中间加一个loading场景,由于loading场景较小,能够避开内存的大量叠加,当旧场景的内存释放完,新场景的初始化结束之后,再隐藏掉loading场景。
因此loading的两个作用是:
(1)避免场景切换时大量内存叠加导致崩溃。
(2)避免后一个场景太大加载过慢卡住的尴尬。
2、把GUI模块加入生命周期管理
主角、强化、技能、商城、进化、背包、任务等系统如果全部打开,内存很容易达到峰值;
需要有效地管理系统模块生命周期:
(1)将模块进行划分:Cache_10(经常打开的)、Cache_5(偶尔打开的)、Cache_0(只打开一次的)
(2)创建一个MuduleMananger类,内部的render方法每分钟轮询一次,
如果是Cache_0,一关闭就直接destroy释放内存;
如果是Cache_10,10分钟后自动释放内存;
如果是Cache_5,5分钟后自动释放内存;
每次打开模块,该模块都重新计时。
代码占用的内存少,资源占用的内存多。
1.资源类型
GameObject,Transform,Mesh,Texture,Material,Shader,noxss和各种其他Assets
2.资源创建方式
(1)静态引用(初学者):在脚本中加public GameObject变量,然后拖动资源到面板上,然后Instantiate;
(2)Resources.Load(初学者),从Assets/Resources目录下加载资源;
(3)通过AssetBundle.Load加载(最常用),然后Instantiate;
3.资源销毁方式
(1)Destroy
(2)AssetBundle.Unload(false),释放AssetBundle文件内存镜像,不销毁Load创建的Assets对象;
AssetBundle.Unload(true),释放AssetBundle文件内存镜像,销毁Load创建的Assets对象;
(3)Resources.UnloadAsset(Object),释放已加载的Asset对象;
Resources.UnloadUnusedAssets,释放所有未引用的Asset对象;
4.生命周期
例:创建一个场景,场景中创建一个Empty GameObject,上面挂一个脚本来加载tank资源。
在Awake函数中使用协程来创建资源;
(1)使用Resources.Load()加载资源
IEnumerator LoadResources()
{
//释放无用资源
Resources.UnloadUnusedAssets();
//等待5秒
yield return new WaitForSeconds(5.0f);
//加载资源
GameObject tank = Resources.Load("Tank") as GameObject;
yield return new WaitForSeconds(0.5f);
//实例化一个资源
GameObject.Instantiate(tank, Vector3.zero, Quaternion.identity) as GameObject;
yield return new WaitForSeconds(0.5f);
//销毁一个资源
GameObject.Destroy(tankInst);
yield return new WaitForSeconds(0.5f);
//释放无用资源
tank = null;
Resources.UnloadUnusedAssets();
yield return new WaitForSeconds(0.5f);
}
结论:Resources.Load一个资源相对于Instantiate一个资源来说消耗的内存非常少,当Destroy之后,内存占用减少的较少,Material和Texture等都没有还原,以便于之后继续的实例化。
(2)使用AssetBundle.Load加载资源
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);
//实例化一个资源
GameObject tankInst = Instantiate(obj) as GameObject;
yield return new WaitForSeconds(0.5f);
//销毁一个资源
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);
}
结论:
通过Resources.Load加载一个资源的时候,只会加载Mesh;
所以通过AssetBundle的方式加载一个资源,实例化的时候内存消耗较小。
3.通过静态引用的方式加载一个资源
IEnumerator InstResources()
{
//清除干净以免影响
Resources.UnloadUnusedAssets();
//等待5秒
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(0.5f);
}
结论:静态绑定的方式和Resources.Load一样,加载一个资源的时候,只会加载Mesh;