Unity 的 Mono 引擎等運(yùn)行時(shí)系統(tǒng)會(huì)自動(dòng)管理內(nèi)存端仰,不僅書(shū)寫(xiě)方便而且大大降低了內(nèi)存泄漏的可能性(即分配了內(nèi)存但后續(xù)從未釋放的情況),不過(guò)要使用得當(dāng)跌前,否則會(huì)導(dǎo)致不必要的頻繁觸發(fā)垃圾回收器并在執(zhí)行中引起暫停钮糖。
優(yōu)化案例1:重復(fù)的字符串連接
string str = "";
for (int i = 0; i < 10000; i++)
{
str += "," + i;
}
Debug.Log(str);
上面這段代碼看上去沒(méi)問(wèn)題,但可能成為垃圾回收的噩夢(mèng)枢析。每次循環(huán)時(shí)玉掸,str 變量的先前內(nèi)容變?yōu)樗劳鰻顟B(tài):分配的整個(gè)新字符串將包含原始部分加上末尾的新部分。由于字符串隨著 i 值的增加而變長(zhǎng)醒叁,因此消耗的堆空間量也會(huì)增加司浪,所以每次調(diào)用此函數(shù)時(shí)都很容易用掉數(shù)百個(gè)字節(jié)的空閑堆空間。如果需要將大量字符串連接在一起把沼,StringBuilder 是更好的選擇啊易。上面的算法可以優(yōu)化為:
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < 10000; i++)
{
stringBuilder.Append(",");
stringBuilder.Append(i);
}
Debug.Log(stringBuilder.ToString());
我們可以借助Unity的Profiler對(duì)比結(jié)果:發(fā)現(xiàn)GC減少了一半。
優(yōu)化案例2:字符串頻繁連接
有些小伙伴喜歡在Unity的Update函數(shù)中處理邏輯饮睬,比如:
void Update() {
string scoreText = "Score: " + score.ToString();
scoreBoard.text = scoreText;
}
這樣寫(xiě)在每次調(diào)用 Update 時(shí)都會(huì)分配新的字符串租谈,并生成源源不斷的垃圾⌒妫可以優(yōu)化為僅在score值發(fā)生變化的時(shí)候更新 text垦垂。
優(yōu)化案例3:函數(shù)返回?cái)?shù)組值
public class ExampleScript : MonoBehaviour {
float[] RandomList(int numElements) {
var result = new float[numElements];
for (int i = 0; i < numElements; i++) {
result[i] = Random.value;
}
return result;
}
}
在新建包含值的數(shù)組時(shí)宦搬,這種類型的函數(shù)非常從容和方便。但是劫拗,如果重復(fù)調(diào)用這種函數(shù)间校,則每次都會(huì)分配全新的內(nèi)存。由于數(shù)組可能非常大页慷,因此空閑堆空間可能會(huì)迅速耗盡憔足,導(dǎo)致頻繁進(jìn)行垃圾收集。避免此問(wèn)題的一種方法是利用數(shù)組為引用類型這一特點(diǎn)酒繁。作為參數(shù)傳入該函數(shù)的數(shù)組可在該函數(shù)內(nèi)予以修改滓彰,且結(jié)果在函數(shù)返回后仍然保留。像上面這樣的函數(shù)通持萏唬可替換為如下所示的函數(shù):
public class ExampleScript : MonoBehaviour {
void RandomList(float[] arrayToFill) {
for (int i = 0; i < arrayToFill.Length; i++) {
arrayToFill[i] = Random.value;
}
}
}
優(yōu)化案例4:對(duì)象的重復(fù)使用
在許多情況下揭绑,通過(guò)減少創(chuàng)建和銷毀的對(duì)象數(shù)量即可避免生成垃圾。游戲中存在某些類型的對(duì)象郎哭,例如飛彈他匪,TableView的Cell等,這些對(duì)象可能會(huì)多次反復(fù)遇到夸研,但是只有少數(shù)對(duì)象會(huì)同時(shí)處于游戲中邦蜜。在這種情況下,通澈ブ粒可以重用對(duì)象悼沈,而不是銷毀舊對(duì)象并替換為新對(duì)象。
優(yōu)化案例5:減少Debug.Log的使用
移除游戲中的Debug.Log()函數(shù)的代碼姐扮,盡管該函數(shù)可能輸出為空絮供,對(duì)該函數(shù)的調(diào)用依然會(huì)執(zhí)行,該函數(shù)會(huì)創(chuàng)建至少一個(gè)字符(空字符)的字符串溶握。如果游戲中有大量的該函數(shù)的調(diào)用杯缺,這會(huì)造成內(nèi)存垃圾的增加。在游戲上線時(shí)我們可以禁用日志:
Debug.unityLogger.logEnabled = false;
優(yōu)化案例6:Unity函數(shù)調(diào)用
調(diào)用GameObject.name 或者 GameObject.tag也會(huì)造成預(yù)想不到的堆內(nèi)存分配睡榆,這兩個(gè)函數(shù)都會(huì)將結(jié)果存為新的字符串返回萍肆,這就會(huì)造成不必要的內(nèi)存垃圾,對(duì)結(jié)果進(jìn)行緩存是一種有效的辦法胀屿,但是在Unity中都對(duì)應(yīng)的有相關(guān)的函數(shù)來(lái)替代塘揣。對(duì)于比較gameObject的tag,可以采用GameObject.CompareTag()來(lái)替代宿崭。
bool isPlayer = other.gameObject.CompareTag(playerTag);
不只是GameObject.CompareTag亲铡,Unity中許多其他的函數(shù)也可以避免內(nèi)存垃圾的生成。比如我們可以用Input.GetTouch()和Input.touchCount()來(lái)代替Input.touches,或者用Physics.SphereCastNonAlloc()來(lái)代替Physics.SphereCastAll()奖蔓。
優(yōu)化案例7:裝箱操作
裝箱操作是指一個(gè)值類型變量被用作引用類型變量時(shí)候的內(nèi)部變換過(guò)程赞草,如果我們向帶有對(duì)象類型參數(shù)的函數(shù)傳入值類型,這就會(huì)觸發(fā)裝箱操作吆鹤。比如String.Format()函數(shù)需要傳入字符串和對(duì)象類型參數(shù)厨疙,如果傳入字符串和int類型數(shù)據(jù),就會(huì)觸發(fā)裝箱操作疑务。如下面代碼所示:
void ExampleFunction()
{
int cost = 5;
string displayString = String.Format("Price:{0} gold",cost);
}
在Unity的裝箱操作中沾凄,對(duì)于值類型會(huì)在堆內(nèi)存上分配一個(gè)System.Object類型的引用來(lái)封裝該值類型變量,其對(duì)應(yīng)的緩存就會(huì)產(chǎn)生內(nèi)存垃圾知允。裝箱操作是非常普遍的一種產(chǎn)生內(nèi)存垃圾的行為撒蟀,即使代碼中沒(méi)有直接的對(duì)變量進(jìn)行裝箱操作,在插件或者其他的函數(shù)中也有可能會(huì)產(chǎn)生温鸽。最好的解決辦法是盡可能避免或者移除造成裝箱操作的代碼保屯。
優(yōu)化案例8:主動(dòng)GC
Unity允許開(kāi)發(fā)者禁用垃圾回收和請(qǐng)求垃圾回收,不過(guò)都需要根據(jù)情況謹(jǐn)慎處理涤垫。
//請(qǐng)求垃圾回收
System.GC.Collect();
優(yōu)化案例9:增量垃圾收集
增量式垃圾收集將垃圾收集過(guò)程分散到多個(gè)幀中配椭。
增量式垃圾收集是 Unity 使用的默認(rèn)垃圾收集方法。Unity 仍然使用 Boehm–Demers–Weiser 垃圾收集器雹姊,但是以增量模式運(yùn)行。Unity 不會(huì)在每次運(yùn)行時(shí)進(jìn)行完整的垃圾收集衡楞,而是將垃圾收集工作負(fù)載拆分到多個(gè)幀中吱雏。這意味著,不必單次長(zhǎng)時(shí)間中斷程序的執(zhí)行來(lái)讓垃圾收集器完成工作瘾境,Unity 會(huì)進(jìn)行多次短時(shí)間的中斷歧杏。雖然這不能整體上加快垃圾收集速度,但將工作負(fù)載分布到多個(gè)幀可以極大減少垃圾收集“尖峰”破壞應(yīng)用程序流暢性的問(wèn)題迷守。
環(huán)境:
Unity:2020.3.12f