Unity 使用xLua遇到的坑

在咱們使用xLua做爲Unity中lua集成的解決方案時,遇到了一個問題,就是當咱們使用在lua中把UI中的某個控件綁定相應的事件(如按鈕的onClick事件),xLua綁定這個事件是用委託實現的,具體代碼能夠查看xLua的代碼。而在程序退出的時候xLua會檢查對應的委託有沒有被正確的釋放掉,若是沒有釋放掉的話就會拋出異常。代碼如表所示:git

 
 1         public virtual void Dispose(bool dispose)
 2         {
 3 #if THREAD_SAFE || HOTFIX_ENABLE
 4             lock (luaEnvLock)
 5             {
 6 #endif
 7                 if (disposed) return;
 8                 Tick();
 9 
10                 if (!translator.AllDelegateBridgeReleased())
11                 {
12                     throw new InvalidOperationException("try to dispose a LuaEnv with C# callback!");
13                 }
14 
15                 LuaAPI.lua_close(L);
16 
17                 ObjectTranslatorPool.Instance.Remove(L);
18                 translator = null;
19 
20                 rawL = IntPtr.Zero;
21 
22                 disposed = true;
23 #if THREAD_SAFE || HOTFIX_ENABLE
24             }
25 #endif
26         }

這說明咱們並無把對應的委託給釋放掉。因此咱們須要確保在程序退出以前全部的委託要正確地釋放掉。方案大致以下,每個UI都對應一個實例,這樣在綁定控件的時候建立一個匿名函數,這個函數用於控件把這個控件綁定的事件清除掉,同時把這個匿名函數放到一個數組裏面去,在這個UI銷燬的時候調用一個函數(好比咱們叫作Destroy),這個函數的做用就是負責一些清理工做,其中就包括遍歷前面提到的匿名函數的數組並挨個調用。這樣就把xLua生成的委託的引用去掉了。在程序退出並觸發GC的時候就會把這個委託釋放掉,這樣xLua檢查就沒有問題了。github

 
 1 function UIUtils:AddButtonOnClick(aUIInstance, aButton, aFunc)
 2     aButton.onClick.AddListener(
 3         function ()
 4             aFunc(aUIInstance)
 5         end)
 6 
 7     // 將閉包添加到一個table中用於後面調用
 8     table.insert(aUIInstance.unregisterWidgetClousures, 
 9         function()
10             aButton.onClick:RemoveAllListeners()
11         end)
12 
13 end

可能到這裏你以爲問題已經解決了,但是若是到這的話就不會有這篇文章了。問題是這樣調用了之後在程序退出的時候仍是會拋出異常。按正常來講這樣作了就能夠了,通過一番實驗發現只要這個控件沒有被觸碰過那麼就能夠正常退出,若是觸碰了就會拋出異常。一開始懷疑是xLua的問題但通過看代碼肯定不是它的問題。這個時候想到了可能Unity對這個委託作了緩存,雖然我上面把它清除掉了,可是Unity內部多是作了緩存的。最開始沒有去關注這個問題,而是想了另一個辦法直接把控件對應的事件給黑窯了。示例代碼以下所示:數組

 1 function UIUtils:AddButtonOnClick(aUIInstance, aButton, aFunc)
 2     aButton.onClick.AddListener(
 3         function ()
 4             aFunc(aUIInstance)
 5         end)
 6 
 7     // 將閉包添加到一個table中用於後面調用
 8     table.insert(aUIInstance.unregisterWidgetClousures, 
 9         function()
10             aButton.onClick = nil
11         end)
12 
13 end

 

這樣就解決了問題。可是後面發現咱們要重用UI的時候因爲咱們重用的規則所致(UI的C#對象沒有回收可是會回收,可是lua對象會回收),上面的這個地方就出問題了。當咱們下次再要從新使用這個UI的時候,由於上面被置空了,接下來使用就有問題了。咱們也想過其它的方法來解決,但總感受破壞了原有簡單的結構。這樣作不太好。這個時候就想看看Unity到底哪裏出了問題了,不過幸運的是很快就發現了問題。咱們使用ILSpy打開UnityEngine.dll查看了一下UnityEvent的代碼,發如今它的基類裏面作了一個簡單的優化,就是這個優化致使了上面問題的發生。咱們來看下代碼片段: 緩存

1 public abstract class UnityEventBase : ISerializationCallbackReceiver
2 {
3     private InvokableCallList m_Calls;
4 }

Unity用這個來保存須要調用函數,咱們再來看看它的具體實現片斷:閉包

 1 namespace UnityEngine.Events
 2 {
 3     internal class InvokableCallList
 4     {
 5         private readonly List<BaseInvokableCall> m_PersistentCalls = new List<BaseInvokableCall>();
 6 
 7         private readonly List<BaseInvokableCall> m_RuntimeCalls = new List<BaseInvokableCall>();
 8 
 9         private readonly List<BaseInvokableCall> m_ExecutingCalls = new List<BaseInvokableCall>();
10 
11         private bool m_NeedsUpdate = true;
12 
13         public void AddListener(BaseInvokableCall call)
14         {
15             this.m_RuntimeCalls.Add(call);
16             this.m_NeedsUpdate = true;
17         }
18 
19         public void RemoveListener(object targetObj, MethodInfo method)
20         {
21             List<BaseInvokableCall> list = new List<BaseInvokableCall>();
22             for (int i = 0; i < this.m_RuntimeCalls.Count; i++)
23             {
24                 if (this.m_RuntimeCalls[i].Find(targetObj, method))
25                 {
26                     list.Add(this.m_RuntimeCalls[i]);
27                 }
28             }
29             this.m_RuntimeCalls.RemoveAll(new Predicate<BaseInvokableCall>(list.Contains));
30             this.m_NeedsUpdate = true;
31         }
32 
33         public void Clear()
34         {
35             this.m_RuntimeCalls.Clear();
36             this.m_NeedsUpdate = true;
37         }
38 
39         public void Invoke(object[] parameters)
40         {
41             if (this.m_NeedsUpdate)
42             {
43                 this.m_ExecutingCalls.Clear();
44                 this.m_ExecutingCalls.AddRange(this.m_PersistentCalls);
45                 this.m_ExecutingCalls.AddRange(this.m_RuntimeCalls);
46                 this.m_NeedsUpdate = false;
47             }
48             for (int i = 0; i < this.m_ExecutingCalls.Count; i++)
49             {
50                 this.m_ExecutingCalls[i].Invoke(parameters);
51             }
52         }
53     }
54 }
咱們看到有m_RuntimeCalls這個變量,它是用來作什麼的呢?就是爲了作一個優化的,爲了只在添加或移除了Listener以後才更新它作的一個優化。對於原來Unity自己的設計來講,是沒有問題的。可是,咱們看一下不管是RemoveListener或者Clear的時候都沒有清掉m_RuntimeCalls裏面的值,按理說它在Clear()的時候是應該清掉的。因此就有了咱們前面提到的問題。知道了緣由,這裏就有了兩個解決方法:
  1. 直接必UnityEngine.dll的代碼,由於咱們沒有源碼,因此只能經過一些工做來改。但這帶來一個問題,就是須要給開發組的每一個人替換修改後的dll,另一個問題就是若是升級Unity的話又會帶來沒必要要的麻煩。因此這個方案就放棄了。
  2. 咱們能夠看到雖然Clear()沒有調用m_ExecutingCalls.Clear(),可是咱們能夠再調用一次Invoke()函數,這個時候它就會把m_ExecutingCalls的內容清掉了,這個時候就沒有對象引用着xLua生成的委託了。這個方案目前來講仍是比較好的。由於畢竟多調用一次的開銷是能夠接受的。

  因而代碼變成了以下代碼示例的樣子:函數

 1 function UIUtils:AddButtonOnClick(aUIInstance, aButton, aFunc)
 2     aButton.onClick.AddListener(
 3         function ()
 4             aFunc(aUIInstance)
 5         end)
 6 
 7     // 將閉包添加到一個table中用於後面調用
 8     table.insert(aUIInstance.unregisterWidgetClousures, 
 9         function()
10             aButton.onClick:RemoveAllListeners()
11             aButton.onClick()
12         end)
13 
14 end

好的,到這裏問題已經完美解決了。固然咱們也能夠簡單的把拋異常的地方註釋掉,但這確定不是解決問題的正確方法。固然若是你也遇到這個問題而且有更好的方案也能夠一塊兒討論。優化

相關文章
相關標籤/搜索