官术网_书友最值得收藏!

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)容等。

主站蜘蛛池模板: 宜兴市| 合作市| 新宁县| 莱芜市| 阜康市| 海安县| 临澧县| 洪湖市| 繁昌县| 平南县| 西昌市| 阿勒泰市| 利川市| 武胜县| 宜章县| 凌云县| 武义县| 兴和县| 大宁县| 施甸县| 宁陵县| 响水县| 景宁| 德阳市| 铜陵市| 咸阳市| 田阳县| 山西省| 渝中区| 金昌市| 洛宁县| 库尔勒市| 日照市| 台东市| 天长市| 芷江| 沛县| 福鼎市| 永登县| 拜城县| 开原市|