Unity 使用xLua遇到的坑


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

 
 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檢查就沒有問題了。

 
 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

好的,到這里問題已經完美解決了。當然我們也可以簡單的把拋異常的地方注釋掉,但這肯定不是解決問題的正確方法。當然如果你也遇到這個問題並且有更好的方案也可以一起討論。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM