一個簡單的 lua 對象回收再利用方案

VaughnBarce 7年前發布 | 29K 次閱讀 Lua Lua開發

昨天在 review 我公司一個正在開發的項目客戶端代碼時,發現了一些壞味道。

客戶端框架創建了一個簡單的對象系統,用來組織客戶端用到的對象。這些對象通常是有層級關系的,頂層對象放在一個全局集里,方便遍歷。通常,每幀需要更新這些對象,處理事件等等。

頂層每個對象下,還擁有一些不同類別的子對象,最終成為一個森林結構,森林里每個根對象都是一顆樹。對象間有時有一些引用關系,比如,一個對象可以跟隨另一個對象移動,這個跟隨就不是擁有關系。

這種設計方法或模式,是非常常見的。但是在實現手法上,我聞到了一絲壞味道。

由于類別很多,所以代碼中充斥著模仿靜態語言如 C++/C# 般的大量構造和析構函數。我一直對同事說 Lua 不是 C++ 大部分同事也認同我的觀點,但落到實處,卻逃不出過去的很多經驗。

比如在這次具體問題上,為什么要實現一套帶構造和析構函數的類別系統呢?核心驅動力是因為大部分邏輯對象是和場景關聯在一起的,并且引用了 U3D Engine 中的 C# 對象,依賴 lua 的 gc 系統去回收資源延遲太大。而往往大部分時候,我們都可以明確的知道一個對象從場景中移除,幾乎沒有別的地方在引用它,所以需要立刻釋放資源。

而臨時對象很多,設計人員又想實現一套對象的 table 再利用的方案,讓釋放掉的對象在可能的情況下,能重新在新創建對象時再利用起來,減少 lua gc 的負擔。

為了做到這點,代碼框架模仿了 C++/C# 中的常見手法,在構造函數里建立對象的層級關系(子對象有一個叫 owner 的域指向父對象),在析構函數里調用其擁有的對象的 ondestroy 函數,一級級回收。對于非擁有關系,比如前面舉例的 follow ,再給出 unfollow 函數用于解除引用。

最終的結果是,每個新的類中,都有十幾行雷同的代碼做這些枯燥的事情,而且還偶發 bug 。bug 主要出現在一些引用關系沒有解對,引用了死對象(當對象被重用時,就錯誤引用了新對象),或是不該釋放的對象被提前釋放了,等等。

我認為在代碼基中出現大量雷同的、和具體業務不相關的代碼,還分布在不同的源文件中,這是極壞的味道:因為它相當于制定了一套復雜的約定,讓開發人員遵守,而且這些機械性的代碼內聚性很低,容易出錯。

讓我們重新分析一下需求。

核心問題是:對象的頻繁生成和釋放在實際測試中已經出現了問題:過多的占用臨時內存,以及引擎內資源未能及時釋放。

圍繞這點,設計出來的框架的味道不太好,沒有充分發揮 lua 的動態特性。

而實際上,我們需要的一個工作在 lua 虛擬機中的,更小集合的對象生命期管理系統。這套系統最好是內聚性高,不要侵入真正的業務代碼,它能正確的管理對象樹和弱引用(類 follow)關系。

針對它,我重新設計了一套簡單的類型系統。這套系統支持開發人員預定義對象類型,把引用關系描述在類型定義中,并適當的留出簡單成員變量的位置。讓對象在釋放后,可以盡可能的復用數據結構,避免重復構造新的表,依賴 gc 回收臨時表。

我大約花了 200+ 行代碼來實現它

比如在 test.lua 中我定義了這樣一個類型:

ts.foo {
    _ctor = function(self, a)
        self.a = a
    end,
    _dtor = function(self)
        print("delete", self)
    end,
    a = 0,
    b = true,
    c = "hello",
    f = ts.foo,
    weak_g = ts.foo,
}

這個類型名叫 foo ,它描述了 a b c 三個簡單類型的成員,分別是數字、布爾量、和字符串。并定義了默認值 0 true "hello" ,它們將在構造函數之前被賦值成默認值。

其中還定義了 f 和 g 兩個引用成員。f 是一個強引用,引用類型也是 foo ;g 是一個弱引用,用 weak_ 前綴修飾。

我們可以為 foo 定義出構造函數 ctor ,這個 ctor 會傳入 table self ,使用者不必關系 self 從哪里來,到底是新構造的表,還是過去釋放的對象的表的再利用。框架會保證在 ctor 被調用前,其成員都賦值為默認值;其中的引用變量都將被賦為 false ,可以在構造函數里進一步賦值。注:這里是 false 不是 nil ,是希望可以在 self 中保留一個 slot 。

框架不會給 self 附加 metatable ,這樣對使用者最為靈活,如果需要,可以在 ctor 中加上自己需要的 metatable 。

構造 foo 類的對象可以調用 ts.foo:new(...) ,它將在框架內的 root 集內添加一個新的 foo 對象,并返回。

如果想繼續構造 foo 下的 f ,可以使用 ts[f].f(...) 。這個函數調用會調用對應的構造函數,并不需要指明構造類型,這是因為類型定義中已經指明了 foo.f 的類型。

這里使用了一個比較奇怪的語法:ts[f].f ,我們應該理解成對 f.f 的引用進行操作。這里并沒有對 foo 對象設置 metatable 來提供更漂亮的語法糖,這是因為希望把 metatable 的彈性留給使用者。而且明確寫 ts[對象].字段 可以顯示的提示這段代碼將對 “對象.字段” 的引用進行修改。

修改引用,讓其引用到一個新對象可以用 ts[f].g = f 。這會把 f 的一個弱引用賦給 f.g 。強引用也能這樣寫 ts[f].f = f 。

那么,寫 ts[f].f = ts.foo:new(...) 和寫 ts[f].f(...) 有什么區別呢?

前者通過調用 ts.foo:new 構造出一個 foo 對象,然后賦給了 f.f 。但是它會在 root 集內也添加一個這個新對象,當日后從 root 集移除 f 時,這個新對象依舊被 root 集引用。

而后者也是構造了一個新的 foo 對象賦給 f.f ,但它不會在 root 集添加這個對象,并且新對象中會自動生成一個 .owner 字段指向 f 。

如果想清除一個引用,可以寫 ts[f].f = nil 。不過再次讀 f.f 的時候,會發現值是 false 而不是 nil 。這是為了在數據結構中保持一個 slot ,也可以確保用戶加的 metatable 可以正確工作。

如果只想用讀取 f 下屬對象,就直接寫 f.f 或 f.g 即可。不過這里 g 是一個弱引用,所以通常使用前應該做一次判斷 if f.g then ... end 。

從 root 集移除一個對象,可以用 ts.delete(f) 。但是這個 delete 操作絕對不會觸發對象的終結函數 dtor ,它做的僅僅是把對象從 root 集中移除。

上面反復談到了 root 集。這是個很有用的集合,比如,你可以簡單理解為,它就是場景,而構造出來的對象都默認放在了場景中。

我們可以用 for obj in ts.each() do ... end 來遍歷這個集合,取出所有的對象處理。也可以單獨刪選一種類型的對象遍歷: for obj in ts.each(ts.foo) do ... end 。

當一些對象移除 root 集,或是對象樹內部的引用關系改變后,你可以調用 ts.collectgarbage() 來尋找哪些對象已經不再被引用,框架會用一個 mark-sweep 算法把不再被 root 集引用的對象回收再利用。在回收前,如果對象有 dtor ,也會調用。

另外,每個對象都有一個唯一的數字 id ,可以用 obj._id 獲得。及時對象被收回,id 也不會重復。所以、當你在這個系統外想引用系統內的對象時,就應該用 id 來保持一個弱引用。之后,可以通過 ts.get(id) 來轉換為真正的對象。如果對象已經被回收, ts.get 會返回 nil 。

ts.type(obj) 可以獲得一個對象的類型名字,如果對象不是這個系統內的對象,則返回 nil 。

 

來自:http://www.udpwork.com/item/16074.html

 

 本文由用戶 VaughnBarce 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!