在我們使用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 }
- 直接必UnityEngine.dll的代碼,因為我們沒有源碼,所以只能通過一些工作來改。但這帶來一個問題,就是需要給開發組的每個人替換修改后的dll,另外一個問題就是如果升級Unity的話又會帶來不必要的麻煩。所以這個方案就放棄了。
- 我們可以看到雖然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
好的,到這里問題已經完美解決了。當然我們也可以簡單的把拋異常的地方注釋掉,但這肯定不是解決問題的正確方法。當然如果你也遇到這個問題並且有更好的方案也可以一起討論。