- Unity3D高級(jí)編程:主程手記
- 陸澤西
- 1713字
- 2022-01-07 14:46:20
2.8.3 盡可能地使用對(duì)象池
說到類實(shí)例,我們應(yīng)該明白,內(nèi)存分配和內(nèi)存消耗會(huì)對(duì)我們的程序產(chǎn)生影響,這也是提高程序效率的關(guān)鍵所在,我們不但要減少內(nèi)存分配次數(shù)和內(nèi)存碎片,還要避免內(nèi)存卸載帶來的性能損耗。Unity3D使用的是C#語言,因此它使用垃圾回收機(jī)制回收內(nèi)存,即使Unity3D在發(fā)布后將C#轉(zhuǎn)換為C++,也依然會(huì)使用垃圾回收機(jī)制來執(zhí)行分配和銷毀內(nèi)存。作為高級(jí)程序員,我們應(yīng)該能感受到,在創(chuàng)建類實(shí)例時(shí)內(nèi)存分配時(shí)的性能損耗以及垃圾回收時(shí)的艱難。
垃圾回收有多難呢?下面進(jìn)行解釋。我們?cè)贑#中可隨意地新建類實(shí)例,由于不用管它們的死活,所以可丟棄或空置引用變量。類實(shí)例不斷地被引用和間接引用,又不斷地被拋棄,垃圾回收器就要負(fù)責(zé)仔仔細(xì)細(xì)地收拾我們的爛攤子。內(nèi)存不可能永遠(yuǎn)被分配而不回收,于是垃圾回收只能在內(nèi)存不夠用的時(shí)候到處詢問和檢查(即遍歷所有已分配的內(nèi)存塊),看看哪個(gè)類實(shí)例完全被遺棄就撿回來(意思是完全沒有人引用了),并將內(nèi)存回收。因此,當(dāng)業(yè)務(wù)邏輯越大、數(shù)據(jù)量越多時(shí),垃圾回收需要檢查的內(nèi)容也越多,如果回收后依然內(nèi)存不足,就得向系統(tǒng)請(qǐng)求分配更多內(nèi)存。
垃圾回收過程如此艱難,它每次回收時(shí)都會(huì)占用大量CPU算力,因此,我們應(yīng)該盡可能地使用對(duì)象池來重復(fù)利用已經(jīng)創(chuàng)建的對(duì)象,這有助于減少內(nèi)存分配時(shí)的消耗,也減少了堆內(nèi)存的內(nèi)存塊數(shù)量,最終減少了垃圾回收時(shí)帶來的CPU損耗。
除了通過new操作創(chuàng)建某個(gè)類內(nèi)存導(dǎo)致GC單元耗時(shí)增加外,以我的經(jīng)驗(yàn)來看,很容易被忽略的還有new List這種類型的使用,我們?cè)谄綍r(shí)編程時(shí)會(huì)大量使用動(dòng)態(tài)數(shù)組,并且隨時(shí)將它拋棄。類似的Dictionary<int,List>也是眾多被忽略的內(nèi)存分配消耗之一,被裝進(jìn)Dictionary字典中的List常被隨意地丟棄,且我們不會(huì)注意它是否能被再次利用。
C#中一個(gè)簡(jiǎn)單的通用對(duì)象池就能解決這些問題,但我們常常嫌棄它,覺得麻煩。以我的編程經(jīng)驗(yàn)來看,圖方便、好用往往要付出性能損耗的代價(jià)性能高的代碼通常都有點(diǎn)反人性,我們應(yīng)該盡量找到一個(gè)平衡點(diǎn),既有高的代碼可讀性,又盡量不要被人性所驅(qū)使而去做一些圖方便的事情,這在任何時(shí)候都是很有價(jià)值的,對(duì)象池源碼如下:
internal class ObjectPool<T>where T : new() { private readonly Stack<T>m_Stack = new Stack<T>(); private readonly UnityAction<T>m_ActionOnGet; private readonly UnityAction<T>m_ActionOnRelease; public int countAll { get; private set; } public int countActive { get { return countAll - countInactive; } } public int countInactive { get { return m_Stack.Count; } } public ObjectPool(UnityAction<T>actionOnGet, UnityAction<T>actionOnRelease) { m_ActionOnGet = actionOnGet; m_ActionOnRelease = actionOnRelease; } public T Get() { T element; if (m_Stack.Count == 0) { element = new T(); countAll++; } else { element = m_Stack.Pop(); } if (m_ActionOnGet != null) m_ActionOnGet(element); return element; } public void Release(T element) { if (m_Stack.Count>0 && ReferenceEquals(m_Stack.Peek(), element)) Debug.LogError("Internal error. Trying to destroy object that is already released to pool."); if (m_ActionOnRelease != null) m_ActionOnRelease(element); m_Stack.Push(element); } } internal static class ListPool<T> { // 避免分配對(duì)象池 private static readonly ObjectPool<List<T>>s_ListPool = new ObjectPool<List<T>>(null, l =>l.Clear()); public static List<T>Get() { return s_ListPool.Get(); } public static void Release(List<T>toRelease) { s_ListPool.Release(toRelease); } }
這兩個(gè)對(duì)象池的類都是從Unity的UI庫中提取出來的,都是非常實(shí)用的對(duì)象池工具,我們應(yīng)該盡可能地使用它們。上述對(duì)象池使用棧隊(duì)列將廢棄的對(duì)象存儲(chǔ)起來,并在需要時(shí)從棧隊(duì)列中推出實(shí)例交給使用者。對(duì)象池并不復(fù)雜,麻煩的是使用,程序中所有創(chuàng)建對(duì)象實(shí)例、銷毀對(duì)象實(shí)例、移除對(duì)象實(shí)例的部分都需要用對(duì)象池去調(diào)用。
我們來舉幾個(gè)使用ObjectPool和ListPool對(duì)象池的例子,源碼如下:
public class A { public int a; public float b; } public void Main() { Dictionary<int,A>dic2 = new Dictionary<int, A>(16); for(int i = 0 ; i<1000 ; i++) { A a = ObjectPool<A>.Get(); // 從對(duì)象池中獲取對(duì)象 a.a = i; a.b = 3.5f; A item = null; if(dic.TryGetValue(a.a, out item)) { ObjectPool<A>.Release(item); // 值會(huì)被覆蓋,所以覆蓋前收回對(duì)象 } dic[a.a] = a; int removeKey = Random.RangeInt(0,10); if(dic.TryGetValue(removeKey, out item)) { ObjectPool<A>.Release(item); // 移除時(shí)收回對(duì)象 dic.Remove(removeKey); } } Dictionary<int,List<A>>dic2 = new Dictionary<int, List<A>>(1000); for(int i = 0 ; i<1000 ; i++) { List<A>arrayA = ListPool<A>.Get(); // 從對(duì)象池中分配List內(nèi)存空間 dic2.Add(i,arrayA); List<A>item = null; int removeKey = Random.RangeInt(0,1000); if(dic.TryGetValue(removeKey, out item)) { ListPool<A>.Release(item); // 移除時(shí)收回對(duì)象 dic.Remove(removeKey); } } }
上述代碼中,A類和List需要?jiǎng)?chuàng)建1000次,每次創(chuàng)建都使用對(duì)象池,并在字典Dictionary移除時(shí)會(huì)將對(duì)象送回對(duì)象池。這樣我們就可以不斷利用被回收的對(duì)象池,自然也就不用總是創(chuàng)建新的對(duì)象了,所有被遺棄的對(duì)象都會(huì)被存儲(chǔ)起來,并不會(huì)被垃圾回收程序回收,內(nèi)存不斷被重復(fù)利用,減少了內(nèi)存分配和釋放所帶來的消耗。
減少內(nèi)存分配除了使用對(duì)象池外,還可以在對(duì)象池上使用預(yù)加載來優(yōu)化,在程序運(yùn)行前讓對(duì)象池中的對(duì)象分配得多一些,這樣在我們需要實(shí)例對(duì)象時(shí)就不再需要臨時(shí)分配內(nèi)存了。此方法可以擴(kuò)展到資源內(nèi)存,類實(shí)例對(duì)象有對(duì)象池,資源也可以有對(duì)象池,在核心程序運(yùn)行前,如果能提前知道后面要加載的內(nèi)容,那么提前將資源內(nèi)容加載到內(nèi)存中可以讓內(nèi)存分配次數(shù)減少,甚至完全避免臨時(shí)的加載和分配,因此很多優(yōu)化技巧會(huì)圍繞如何預(yù)測(cè)后面內(nèi)容需要的實(shí)例對(duì)象和資源內(nèi)容展開,例如,統(tǒng)計(jì)每個(gè)角色需要的資源和實(shí)例對(duì)象在下一個(gè)場(chǎng)景中的數(shù)量并提前加載,或者接近某個(gè)出口或入口時(shí)就開始預(yù)測(cè)即將進(jìn)入的場(chǎng)景的資源內(nèi)容等。
- 數(shù)據(jù)庫程序員面試筆試真題與解析
- ReSharper Essentials
- Java面向?qū)ο蟪绦蜷_發(fā)及實(shí)戰(zhàn)
- Bootstrap Essentials
- HTML5+CSS3+JavaScript Web開發(fā)案例教程(在線實(shí)訓(xùn)版)
- Android Native Development Kit Cookbook
- 用戶體驗(yàn)增長(zhǎng):數(shù)字化·智能化·綠色化
- Python之光:Python編程入門與實(shí)戰(zhàn)
- Android應(yīng)用案例開發(fā)大全(第二版)
- ExtJS Web應(yīng)用程序開發(fā)指南第2版
- Raspberry Pi Robotic Projects(Third Edition)
- JavaScript從入門到精通(視頻實(shí)戰(zhàn)版)
- 貫通Tomcat開發(fā)
- H5+移動(dòng)營(yíng)銷設(shè)計(jì)寶典
- Android嵌入式系統(tǒng)程序開發(fā)(基于Cortex-A8)