在咱們使用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 }
因而代碼變成了以下代碼示例的樣子:函數
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
好的,到這裏問題已經完美解決了。固然咱們也能夠簡單的把拋異常的地方註釋掉,但這確定不是解決問題的正確方法。固然若是你也遇到這個問題而且有更好的方案也能夠一塊兒討論。優化