|
目標(biāo) 本篇博文的主要目的是講解Unity項(xiàng)目如何進(jìn)行內(nèi)存管理。 當(dāng)我們創(chuàng)建一個array(數(shù)組),string(字符串),object(對象),將會在一個叫做HEAP(堆)的中心池中分配內(nèi)存。當(dāng)這些東西很長時間沒有使用,他們所占用的內(nèi)存將被釋放用以其他用處。以前,都是由程序員使用相關(guān)的函數(shù)調(diào)用明確地分配和釋放堆上的內(nèi)存?,F(xiàn)在,像Unity的Mono開發(fā)引擎的內(nèi)存管理機(jī)制自動地完成上述操作。自動內(nèi)存管理機(jī)制比顯示地分配/釋放內(nèi)存需要更少的代碼,并且減少了內(nèi)存泄露的風(fēng)險。 當(dāng)任何一個函數(shù)被調(diào)用,它的參數(shù)的值將會被拷貝到一段保留的內(nèi)存區(qū)域。占有很少字節(jié)的一些數(shù)據(jù)類型可以很快且很容易地被拷貝。然而,我們知道數(shù)組,對象和字符串體積更大,經(jīng)??截愡@些數(shù)據(jù)是很低效率的。幸運(yùn)的是,不是必須得這樣的。我們在堆上分配了一個(體積小的)指針指向這個實(shí)際占用體積很大的數(shù)據(jù),而堆上的這個值(指針)用來存儲內(nèi)存地址。所以在參數(shù)傳遞的過程中,僅僅需要拷貝這個指針就可以了。在實(shí)時系統(tǒng)運(yùn)行時,被指針?biāo)赶虻臄?shù)據(jù)可以被定位到。函數(shù)每一次調(diào)用都可以使用數(shù)據(jù)的單向拷貝。 值類型是指那些在參數(shù)傳遞過程中直接拷貝和存儲的類型。包括char,floats,integers,Booleans和Unity的結(jié)構(gòu)類型(例如:Color和Vector3)。引用類型指的是那些數(shù)據(jù)存儲在堆上并通過指針指向的類型。因?yàn)閰?shù)變量中的值僅僅是指向?qū)嶋H的數(shù)據(jù)。Strings,objects,arrays是引用類型的例子。 內(nèi)存管理器持續(xù)地跟蹤堆上那些沒有使用的內(nèi)存。當(dāng)有新的內(nèi)存請求,內(nèi)存管理器會分配沒有使用的空間。在后續(xù)的請求被處理被處理之前,這個過程會一直發(fā)生。堆中還在使用的內(nèi)存不太可能被分配。只有當(dāng)還有指針變量指向堆上的數(shù)據(jù),這塊數(shù)據(jù)才可用。如果所有指向堆數(shù)據(jù)的指針都被釋放,那么這塊堆的內(nèi)存就可以安全地被再次分配使用了。 為了確定哪一塊堆內(nèi)存還沒有被使用,內(nèi)存管理器搜索所有激活的引用變量并標(biāo)記他們?yōu)椤發(fā)ive”。在搜索結(jié)束后,內(nèi)存管理器把在那些標(biāo)記為“l(fā)ive”的內(nèi)存塊之間的內(nèi)存區(qū)域認(rèn)為是空的并且可以為隨后的內(nèi)存分配所使用。這個定位和釋放無用內(nèi)存的過程被叫做垃圾回收(GC)。 垃圾回收不是手動的且對程序員是不可見的,但是垃圾回收的過程在后臺是需要耗費(fèi)大量的CPU時間。當(dāng)準(zhǔn)確地使用它時,在所有的性能方面,自動內(nèi)存管理都能等于或者好于手動內(nèi)存管理。然而,無論如何,開發(fā)者有必要避免一些錯誤,比如在執(zhí)行過程中觸發(fā)沒必要的垃圾回收操作和暫停正在回收的操作。有一些第一眼看著沒什么特別的算法其實(shí)是垃圾收集器的夢魘。 這里要知道的主要一點(diǎn)是新的部分不是被一個接一個地添加到字符串中。實(shí)際上,循環(huán)的每一次,myLine變量最后一個元素的內(nèi)容銷毀---一個完整的新的字符串被分配出來,包含了原始的部分并且在末尾加上了新的部分。因?yàn)殡S著變量i的增加,string體積也越來越大,這個值在堆上使用的空間也會增大,每次這個函數(shù)調(diào)用都很容易耗費(fèi)幾千個字節(jié)。為了連接字符串,可以用 System.Text.StringBuilder這個類。 無論如何,即使是重復(fù)的字符串連接也不會引起太大的麻煩,除非頻繁地調(diào)用它。并且在Unity中這個操作通常在幀的更新函數(shù)中。 舉例: 當(dāng)Update函數(shù)被調(diào)用,每一次它都會分配一個新的字符串,并且會產(chǎn)生一定量的新的內(nèi)存垃圾。其中這些垃圾的大部分可以被節(jié)省下來,僅僅當(dāng)myScore改變的時,才會產(chǎn)生新的內(nèi)存垃圾。 5.1 例子1 當(dāng)函數(shù)返回一個數(shù)組時出現(xiàn)另一個問題: 當(dāng)要給新的數(shù)組填充值時,這類函數(shù)是非常簡潔和方便的。無論如何,如果它不斷地被調(diào)用那么每次都會分配新的內(nèi)存。因?yàn)檫@數(shù)組可以非常大。空閑的堆空間會因?yàn)橹貜?fù)的垃圾收集而被快速地耗盡。為了避免這個問題,我們利用數(shù)組是一個引用類型這一事實(shí)。當(dāng)一個數(shù)組作為參數(shù)傳入一個函數(shù)時可以在函數(shù)內(nèi)被改變并且當(dāng)函數(shù)返回時,結(jié)果會保留著。 5.2 例子2 這個函數(shù)簡單地用最新的值替換數(shù)組中存在的值。雖然這要求在函數(shù)調(diào)用這段代碼之前要給數(shù)組分配初始值的內(nèi)存(這看起來比較簡潔)。 垃圾回收正如前面提到的,應(yīng)該盡可能地避免垃圾回收。雖然他們不能被完全消除,有2條策略可以在游戲中減少垃圾回收的執(zhí)行。 這種策略適合那些操作性高的游戲,對于這些游戲來說,平滑的幀率是最重要的事情。這類游戲的典型特點(diǎn)是頻繁分配小塊內(nèi)存,并且這些內(nèi)存存在時間很短暫的。當(dāng)在IOS設(shè)備上使用這個策略時,堆大小大約是200KB,在iPhone 3G上垃圾回收大約需要5ms的時間。如果堆大小增加到1MB,垃圾回收大約要7ms的時間。在一定的幀間隔請求一次垃圾回收是有好處的。這較之嚴(yán)格必須的垃圾回收更為頻繁,但是這個過程會很快并且對于游戲是最小的代價。 無論如何,你使用這個技術(shù),也要觀察profiler統(tǒng)計數(shù)據(jù),確保它真的有減少垃圾回收耗時。 5.4 緩慢且不頻繁的垃圾回收(大的堆空間) 這個策略適合那些內(nèi)存分配不頻繁并且垃圾回收可以在暫停時被處理的游戲。堆空間竟可能地大是有用的,但是也不能太大而造成系統(tǒng)內(nèi)存過低而被操作系統(tǒng)終止掉。無論如何,如果可能的話,Mono運(yùn)行環(huán)境會自動地擴(kuò)大堆空間。你可以在啟動的時候預(yù)先分配一些占位符(例如,實(shí)例化一個無用的對象)來擴(kuò)展堆空間,純粹是為了在內(nèi)存管理器分配對應(yīng)的內(nèi)存。 一個充分大的堆空間不應(yīng)該在要調(diào)用垃圾回收的兩個暫停間被填充滿。當(dāng)暫定時,你可以顯式地調(diào)用一次垃圾回收: 再次強(qiáng)調(diào),你應(yīng)該注意當(dāng)你使用這個策略時,要關(guān)注下profiler統(tǒng)計數(shù)據(jù),而不是假想它會達(dá)到預(yù)期的效果。 有很多例子可以說明,我們可以通過減少對象的創(chuàng)建和銷毀來簡單地避免產(chǎn)生內(nèi)存垃圾。一些游戲中的對象(比如子彈)可能會不斷地出現(xiàn)雖然只有少部分的是有用的。像這樣的例子,重用對象比銷毀舊的再創(chuàng)建一個新的對象要好得多。 我希望這篇博文對你在Unity項(xiàng)目中開發(fā)內(nèi)存管理這一塊是有用的。如果你有問題,可以留言。我希望得到你的回應(yīng)。 原文作者:Nikunj Popat |
|
|