本文講解針對GC的優(yōu)化具體的操作补箍,針對GC的產(chǎn)生以及原理不清晰的可以查看
文章懊蒸。本文分析的幾個點:
1 緩存
2 定時器
3?對象池
4 數(shù)組
5 減少不必要內(nèi)存垃圾
6 減少單次GC的運行時間
緩存
? ? 在函數(shù)中,如果使用數(shù)組并且不需要返回該數(shù)組作為函數(shù)結(jié)果磅摹,就會產(chǎn)生不必要的堆內(nèi)存垃圾,這時可以在函數(shù)外對函數(shù)進行緩存操作。
例如如下代碼渣淤,每次調(diào)用都會產(chǎn)生一個新的數(shù)組,造成內(nèi)存分配吉嫩,使用完該函數(shù)會導致產(chǎn)生內(nèi)存垃圾价认,主要是每次都用都產(chǎn)生一個。
void OnTriggerEnter()
{
Transform[ ] ?objLists = Transform.GetChildrens();
ExampleFunction2(objList);
}
然而如下代碼自娩,通過緩存一個數(shù)組用踩,則每次調(diào)用函數(shù)都不會進行新的內(nèi)存分配,重復使用緩存的數(shù)組忙迁,從而減少了內(nèi)存垃圾的產(chǎn)生脐彩。
public ?Transform[ ] objList;
void Start()
{
objList = Transform.GetChildrens();
}
void OnTriggerEnter()
{
ExampleFunction(objList);
}
另外在update()/LateUpdate()等函數(shù)中,添加條件判斷以及計時器用來減少堆內(nèi)存分配姊扔。
void Update()
{
Test(transform.postion);
}
void Test()
{
GameObject ?box = Instanist("Prefabname") as gameObject;
box.tranfrom.positon = transfrom.postion;
}
定時器
上述代碼表示惠奸,在綁定該腳本的物體的運行軌跡上,不斷的創(chuàng)建box物體恰梢,又可以是為了畫出物體的運動軌跡或者汽車的車胤之類的功能佛南,當然寫法是不好的,此處只為示例嵌言。update中不斷的創(chuàng)建box,不斷的產(chǎn)生堆內(nèi)存垃圾嗅回。改為如下代碼:
public Vector3 PreviousPostion;
Update()
{
? ? if(tranform.postion ! = PreviousPostion)
{Test(tranform.positon);}
}
void Test()
{?
GameObject? box = Instanist("Prefabname") as gameObject;}
box.tranfrom.positon = transfrom.postion;
PreviousPostion = transform.postion;
}
上述代碼保證了,當物體位置不斷的時候呀页,不會創(chuàng)建新的box妈拌,有效的降低了內(nèi)存垃圾的產(chǎn)生頻率。如果再加上一個定時器如下:
public float Time =2;
Update()
{
Time -= time.deltaTime;
if(Time <= 0)
{
if(transform.postion != PreviousPostion)
{
Test(transform.postion);
}
Time = 2;
}
}
通過條件判斷蓬蝶,定時器來限制Update()函數(shù)中某些容易產(chǎn)生對內(nèi)存分配的執(zhí)行頻率尘分,可以有效的降低內(nèi)存垃圾的產(chǎn)生。
對象池
另外一點丸氛,對于unity3d中生命周期函數(shù)update()中培愁,盡量避免創(chuàng)建引用類型,可以在變量聲明處進行緩存缓窜,此時如果必須要進行對象的重復創(chuàng)建銷毀定续,例如王者榮耀中谍咆,英雄不斷的出生死亡,暴君小兵的銷毀創(chuàng)建私股,針對此類問題摹察,需要我們經(jīng)常使用的對象池概念object-Pool。不理解可查看unity中對象池(引用文章)對象池的意義在于倡鲸,減少對內(nèi)存的垃圾供嚎,減少GC,提升游戲性能峭状。對象池對應(yīng)的單例設(shè)計模式在上文中有詳細介紹克滴,再次不做細致應(yīng)用講解。另一篇官方的對象池相關(guān)的文章Unity:Object Pooling,是一篇不錯的從本質(zhì)上理解對象池概念的文章优床。
List.Clear()
這個是老生常談的問題了劝赔。游戲開發(fā)中很多時候有些不注意的地方容易產(chǎn)生多余的數(shù)組分配,導致不必要的內(nèi)存垃圾。下列代碼中m_objList列表每次使用都需要重新創(chuàng)建一個胆敞,重新分配一段堆內(nèi)存着帽,產(chǎn)生巨大垃圾。
Update()
{ ? ? ? ?m_objList = new List();?
? ? ? ? ?Test(m_objList); ? ? ?
?}竿秆、
改為如下代碼启摄,只需要創(chuàng)建一個列表,每次需要使用的時候幽钢,Clear()一些就可以當作新的列表使用了歉备,類似于對象池的概念,重復利用率達到最高匪燕,盡量減少內(nèi)存垃圾蕾羊,減少GC調(diào)用的次數(shù),單次GC的運行時間帽驯。
public List m_objListe;
Update()
{ ?
? m_objList.Clear()
? ?Test(m_objListe);
}
減少不必要內(nèi)存垃圾
string字符串
C#中string為引用類型龟再,分配到heap上,所以需要GC來清理回收尼变。項目開發(fā)中對string的操作非常多利凑,不注意會導致內(nèi)存垃圾。string變量定義賦值后嫌术,如string name = "xiaoTang"哀澈,name的值“xiaoTang”是不可更改的,如name +=" 3 years old"度气;這樣會產(chǎn)生一個新的string name = "xiaoTang 3 years old";而“xiaoTang”這個舊的string還是存在于內(nèi)存中割按,作為一個單獨的變量,沒有被使用磷籍,變成了內(nèi)存垃圾适荣。這就是字符串容易產(chǎn)生內(nèi)存垃圾的原理现柠。那么如果游戲中Text總的內(nèi)容是一個顯示變化的標簽,就更容產(chǎn)生巨大的垃圾了弛矛。如下面代碼所示够吩。
public Text timerText;
private float timer;
void Update()
{
timer += Time.deltaTime;
timerText.text = "Time:"+ timer.ToString();
}、
更改后汪诉,變化的部分和不變的部分分離废恋,代碼得到很大優(yōu)化。
public Text timerHeaderText;
public Text timerValueText;
private float timer;
void Start()
{
timerHeaderText.text = "TIME:";
}
void Update()
{
timerValueText.text = timer.ToString();
}
PS:針對string的Key:
1 降低字符串創(chuàng)建次數(shù):1個string被多次使用就要緩存起來扒寄,降低string多次創(chuàng)建。
2 降低字符串操作次數(shù):Text中的不變部分與可變部分分離開拟烫,作為2個單獨的Text该编。
3 StringBiulderClasss:代替string進行字符串實時創(chuàng)建。stringBuilderCalss專門針對不需要內(nèi)存分配設(shè)計硕淑。
4 Debug.Log()函數(shù):即使輸出為空课竣,也會創(chuàng)建至少1個空字符串,項目中大量使用輸出置媳,需注意于樟。
Unity3d函數(shù)調(diào)用
編寫代碼時,調(diào)用插件/工具/引擎代碼中函數(shù)時拇囊,容易產(chǎn)生內(nèi)存garbage迂曲。本節(jié)列舉部分:
Mesh.normals:迭代器中不斷使用myMesh.normals,函數(shù)每次返回會創(chuàng)建一個數(shù)組用以存放結(jié)果寥袭。
voidExampleFunction()
{for(inti=0; i < myMesh.normals.Length;i++)
{
Vector3 normal=myMesh.normals[i];
}
}
同樣功能路捧,替代代碼:只產(chǎn)生了一個垃圾,替代了迭代器中多個垃圾传黄。
voidExampleFunction()
{
Vector3[] meshNormals=myMesh.normals;
for(inti=0; i < meshNormals.Length;i++)
{
Vector3 normal=meshNormals[i];
}
}
GameObject.name/GameObject.tag/input.touches/Phycis.PhereCastNonAlloc():這些函數(shù)都會產(chǎn)生用以返回結(jié)果的應(yīng)用類型內(nèi)存垃圾杰扫,unity提供了替代函數(shù)避免垃圾產(chǎn)生;
privatestringplayerTag="Player";
void OnTriggerEnter(Collider other)
{
boolisPlayer = other.gameObject.tag ==playerTag;
}
替代代碼:gameObject.CompareTag()函數(shù)替代GameObject.Tag進行比較膘掰,避免垃圾產(chǎn)生章姓。
privatestringplayerTag ="Player";
voidOnTriggerEnter(Collider other)
{
boolisPlayer =other.gameObject.CompareTag(playerTag);
}
用Input.GetTouch()和Input.touchCount()來代替Input.touches
Physics.SphereCastNonAlloc()來代替Physics.SphereCastAll()。
裝箱
裝箱操作指:值類型被用作應(yīng)用類型時內(nèi)部變化過程识埋。如:int類型被當作參數(shù)傳入需要object類型參數(shù)的函數(shù)凡伊。string.Format(string,Object);
voidExampleFunction()
{int cost =5;
string displayString = String.Format("Price:{0} gold",cost);
}
cost變量進行了裝箱操作。裝箱操作會在堆內(nèi)存上分配一個system.Object空間來緩存變量惭聂,這個緩存變量就是垃圾窗声。當然,函數(shù)調(diào)用中辜纲,很多插件工具閉包中的代碼我們不知道笨觅,調(diào)用的時候很容易產(chǎn)生垃圾拦耐,需要我們盡量避免。
協(xié)程(StartCoroutine())
yield return 0见剩;會產(chǎn)生垃圾杀糯,0作為int值類型傳入了需要object引用類型個的函數(shù)中,產(chǎn)生了裝箱操作苍苞。yield return null替代就不會有問題固翰。
對于協(xié)程,unity用來控制主線程之外的獨立任務(wù)羹呵,或者用以控制代碼執(zhí)行順序骂际。替代方法很多,多個事件之間可以利用委托事件方法實現(xiàn)控制代碼執(zhí)行順序冈欢。
Foreach()
unity5版本之間歉铝,每一次迭代會產(chǎn)生一個object引用對象垃圾,是由于裝箱操作引起的凑耻。5版本之后太示,unity修復了此問題杭措,再次不在贅述啦扬。
如何降低單次GC的運行時間?
該部分主要講重構(gòu)代碼:
每次GC文兢,需要檢查所有的堆內(nèi)存上的對象邻吭,引用類型對象的數(shù)量要降低餐弱。
struct類型是值類型,如果里面包含引用類型镜盯,GC就必須檢查struct里面所有的對象岸裙,包含值類型。如下速缆,name導致GC 的時候struct結(jié)構(gòu)體中所有對象都必須進行檢查降允。
public struct ItemData ? ? ?//值類型
{
public ? ? ?string ? ? ?name; ? //引用類型
public int cost;
public Vector3 position;
}
private ItemData[] itemData;
重構(gòu)代碼的方式,是把struct拆分開艺糜,這里根據(jù)具體需要做決定剧董,有些時候這樣的重構(gòu)方式導致程序可讀性差,讀者自己權(quán)衡破停。
如下代碼翅楼,GC檢查dialogData的時候,會檢查nextDialoglog對象真慢。替代代碼中用ID替代了對象實體的引用毅臊,避免了檢查次數(shù)的增加。
publicclassDialogData
{privateDialogData nextDialog;publicDialogData GetNextDialog()
{returnnextDialog;
}
}
替代代碼:
publicclassDialogData
{privateintnextDialogID;publicintGetNextDialogID()
{returnnextDialogID;
}
}