【Unity遊戲開發】記一次解決 LuaFunction has been disposed 的bug的過程

1、引子

  RT,本篇博客記錄的是馬三的一次解決 LuaFunction has been disposed 的bug的全過程,事情還要從馬三的自研框架 ColaFrameWork 提及。最近,馬三在業餘時間維護了一款基於Unity的客戶端自研框架,起名叫 ColaFrameWork ,寓意是但願寫代碼能像喝小可樂同樣享受和輕鬆。爲了在Lua層能夠監聽到UI事件,馬三製做了UGUIEventListener、UGUIDragEventListenner和UGUIMsgHandler等這樣幾個UI組件,其中 UGUIEventListener和UGUIDragEventListenner這種Listener組件實現了IPointerDownHandler、IPointerClickHandler和ISubmitHandler這樣的UGUI IEventSystemHandler UI事件接口,而且實現了接口定義的方法,而後在 UGUIEventListener中暴露出來一些 onClick、onDrag、onSubmit這種委託字段出來。在UI實例化的時候,代碼會把這些監聽器的腳本動態地綁定到UI預製體上面,而後再將Lua層的onClick、onDrag等這些方法動態地與Listener暴露出來的委託字段進行綁定。這樣,當咱們觸發了UI的事件的時候,就會執行Listener中預先實現了相關接口的方法,而咱們又在這些方法中調用了咱們的委託,接着在經過lua虛擬機觸發Lua層的function,從而實現了Lua層對UI事件的監聽,以後咱們也就能夠很方便地在Lua層進行業務邏輯的開發了。html

  大概地工做原理就先講到這裏,畢竟咱們這篇博客主要是記錄如何解決 LuaFunction has been disposed這個bug的,知道一些基本的東西就OK了,關於UGUIEventListener、UGUIDragEventListenner和UGUIMsgHandler等這樣幾個UI組件的一些細節和實現原理等相關內容,馬三會在後續的博客中進行進一步的講解。同時馬三也有計劃待  ColaFrameWork 框架大概成型和穩定之後,將整個框架按照流程與模塊進行分篇地講解與解讀,造成一系列的博客供你們交流學習,好的稍微有點扯遠了,咱們言歸正傳,說說這個bug。git

  上面的組件在實際使用中會偶現  LuaFunction has been disposed 這個bug,它常常出現於咱們在UnityEditor中中止運行遊戲的時候,雖然看起來沒有影響遊戲的正常運行,可是畢竟這個error信息在控制檯看着也很討厭,並且爲了咱們框架的穩定性也應該及時地解決到這個bug。在通過進一步地測試之後,馬三發現了在只點擊UI上面的button組件以後,再執行關閉遊戲並不會出現這個報錯信息,而當在咱們點擊或者使用了InputFiled組件以後,再關閉遊戲則會100%地重現出這個問題。知道了如何復現問題,就好辦了,下一步咱們着手分析一下這個問題是如何出現的,而且嘗試幹掉它。github

  上面說的UGUIEventListener組件的簡化版代碼以下:微信

 1 public class UGUIEventListener : MonoBehaviour,
 2                                   IMoveHandler,
 3         IPointerDownHandler, IPointerUpHandler,
 4         IPointerEnterHandler, IPointerExitHandler,
 5         ISelectHandler, IDeselectHandler, IPointerClickHandler,
 6         ISubmitHandler, ICancelHandler
 7 {
 8     void Start()
 9     {
10 
11     }
12     public delegate void UIEventHandler(GameObject obj);
13     public UIEventHandler onClick;
14     public virtual void OnPointerClick(PointerEventData eventData)
15     {
16         if (CheckNeedHideEvent())
17         {
18             return;
19         }
20         if (null != onEvent)
21         {
22             this.onEvent("onClick");
23         }
24         if (this.onClick != null)
25         {
26             this.onClick(gameObject);
27         }
28     }
29 }

  出現報錯信息時的控制檯截圖:框架

  

2、分析異常出現的緣由

  通常來講在Unity中若是發現控制檯報錯的話,咱們通常會雙擊控制檯中的錯誤信息,它會自動地幫咱們直接定位到發生錯誤的代碼行數,首先就讓咱們來雙擊操做一下,觀察下效果。雙擊之後,發現定位到了以下的這段代碼:ide

 1         public virtual int BeginPCall()
 2         {
 3             if (luaState == null)
 4             {
 5                 throw new LuaException("LuaFunction has been disposed");
 6             }
 7 
 8             stack.Push(new FuncData(oldTop, stackPos));
 9             oldTop = luaState.BeginPCall(reference);
10             stackPos = -1;
11             argCount = 0;
12             return oldTop;
13         }

  能夠觀察到error信息就是第5行的那個拋出異常操做觸發的,經過觀察上下文咱們能夠大概地知道是由於luaState這個Lua虛擬機被銷燬了,可是程序因爲某些未知的緣由仍然調用了某個或者某些LuaFunction所引發的。讓咱們再觀察一下上圖中Unity控制檯的堆棧狀況:函數

LuaException: LuaFunction has been disposed
LuaInterface.LuaFunction:BeginPCall() (at Assets/3rd/ToLua/Core/LuaFunction.cs:73)
System_Action_string_Event:Call(String) (at Assets/Scripts/Generate/DelegateFactory.cs:1364)
UGUIEventListener:OnDeselect(BaseEventData) (at Assets/Scripts/UIBase/UIEventListeners/UGUIEventListener.cs:239)
UnityEngine.EventSystems.EventSystem:OnDisable()

  能夠看到這個調用是由UGUIEventListener.cs的239行的代碼觸發的,讓咱們繼續看看UGUIEventListener.cs的239行代碼作了什麼操做:oop

  

  在第239行咱們嘗試調用了 onEvent 這個委託,可是按道理咱們在遊戲退出的時候並無操做UI,應該不會觸發到這個方法纔對啊。按照之前的基本套路,咱們能夠嘗試着在這裏下個斷點觀察一下調用堆棧,這樣就能知道是什麼觸發這個方法的了而且還能夠觀察一下局部變量的值與狀態。可是馬三發現當遊戲退出運行的時候,這個斷點是並不生效的,根本斷不住,由於當遊戲中止運行的時候,咱們所Attach得進程也就結束了,因此VS並不會在這個斷點停住。可是操蛋的是,這個bug只有在遊戲退出運行的時候纔會出現,簡直陷入了僵局,怎麼辦呢?別急咱們繼續看Unity控制檯打印出來的調用堆棧的最後一行:UnityEngine.EventSystems.EventSystem:OnDisable(),由此咱們能夠得知是Unity底層的EventSystem:OnDisable()觸發的這段代碼。學習

  看來不閱讀分析一下UGUI的源代碼是不行了,幸虧Unity官方將大部分的UGUI代碼進行了開源操做,咱們能夠很方便地閱讀,以便深刻地瞭解UGUI的運行機理,遇到問題時也能夠更好地定位源頭,UGUI源代碼的傳送門。首先咱們定位到 EventSystem的OnDisable方法,由於最後的堆棧信息指向了這裏:測試

 1         protected override void OnDisable()
 2         {
 3             if (m_CurrentInputModule != null)
 4             {
 5                 m_CurrentInputModule.DeactivateModule();
 6                 m_CurrentInputModule = null;
 7             }
 8 
 9             m_EventSystems.Remove(this);
10 
11             base.OnDisable();
12         }

  在EventSystem的OnDisable方法中,調用了m_CurrentInputModule.DeactivateModule()這個方法,它是 BaseInputModule 這個基類中的一個虛方法,繼承自它的子類負責了重寫。咱們所處的平臺是PC平臺,所以使用的是 StandaloneInputModule 這個子類,找到它的 DeactivateModule 方法,內容很簡單就是兩行,先調用了基類的方法,而後執行了ClearSelection這個方法:

  

  繼續觀察一下 ClearSelection 這個方法的實現,發現最後關鍵代碼主要是調用了eventSystem.SetSelectedGameObject(null, baseEventData)這個方法:

 1         protected void ClearSelection()
 2         {
 3             var baseEventData = GetBaseEventData();
 4 
 5             foreach (var pointer in m_PointerData.Values)
 6             {
 7                 // clear all selection
 8                 HandlePointerExitAndEnter(pointer, null);
 9             }
10 
11             m_PointerData.Clear();
12             eventSystem.SetSelectedGameObject(null, baseEventData);
13         }

  繼續分析  SetSelectedGameObject 這段代碼:

  終於看到點苗頭了,問題就出如今125行這裏,讓咱們再看看 ExecuteEvents.deselectHandler 這個委託究竟是何方神聖?

  在上面的 ExecuteEvents.deselectHandler 實現代碼中,咱們看到了熟悉的 OnDeselect ,咱們的錯誤調用就是由這裏直接發起的,本質上來說它會在Unity MonoBehavior腳本的生命週期函數 OnDisable中觸發。

  看完了UGUI 的源碼以後,讓咱們再來分析一下ToLua的源碼,看看Lua虛擬機是在什麼時候被銷燬的,在ToLua框架中,LuaClient是一個很是重要的類,它掌管着Lua虛擬機的建立、啓動和銷燬,咱們能夠在這裏找到咱們想要的答案:

  其中LuaClient的Destroy方法,就是負責銷燬Lua虛擬機的函數,它的實現以下:

 1     public virtual void Destroy()
 2     {
 3         if (luaState != null)
 4         {
 5 #if UNITY_5_4_OR_NEWER
 6             SceneManager.sceneLoaded -= OnSceneLoaded;
 7 #endif    
 8             luaState.Call("OnApplicationQuit", false);
 9             DetachProfiler();
10             LuaState state = luaState;
11             luaState = null;
12 
13             if (levelLoaded != null)
14             {
15                 levelLoaded.Dispose();
16                 levelLoaded = null;
17             }
18 
19             if (loop != null)
20             {
21                 loop.Destroy();
22                 loop = null;
23             }
24 
25             state.Dispose();
26             Instance = null;
27         }
28     }

  能夠看到在這個方法中,ToLua對Lua虛擬機進行了Dispose釋放的騷操做,而後將虛擬機引用從新置空,若是執行完這步之後,咱們再經過 luaState.BeginPCall 去嘗試調用一個LuaFunction的話就會出現上文中的 LuaFunction has been disposed 的異常了。咱們繼續往下看,觀察一下這個銷燬的方法是在遊戲中的哪一個生命週期被調用的:

  

  能夠看到分別是在重寫過MonoBehavior的OnDestroy和OnApplicationQuit函數中調用的,這兩個函數處在整個MonoBehavior腳本的哪一個聲明週期呢?是時候祭出咱們珍藏已久的了Unity MonoBehavior腳本執行順序和生命週期圖了:

  經過觀察上圖,咱們知道了,首先會執行腳本中的 OnApplicationQuit 而後再執行 OnDisable 最後執行腳本的OnDestroy函數。通過這一系列還不算太複雜地分析與追蹤,咱們終於理清了以下的這麼一個bug出現的機制和流程:

  1. 在遊戲退出的時候,根據Unity腳本函數的生命週期,首先觸發了 LuaClinet的 OnApplicationQuit 函數,Lua虛擬機在此處被銷燬,引用被置空;
  2. 緊接着執行了腳本的OnDisable函數,觸發了EventSystem 的 OnDisable() 函數;
  3. 該函數執行了 BaseInputModule 及其子類的 DeactivateModule() 方法;
  4. 在  StandaloneInputModule 這個子類對 DeactivateModule() 方法的實現中,調用了 ClearSelection() 方法;
  5. ClearSelection 方法中調用了 EventSystem 的 SetSelectedGameObject(),這個方法用於觸發激活/非激活 GameObject的選中狀態;
  6. SetSelectedGameObject中會執行咱們UGUIEventListener的OnSelect和OnDeselect這兩個函數;
  7. UGUIEventListener 中的 OnSelect 和 OnDeselect函數會嘗試調用綁定過LuaFunction的委託;
  8. 經過 luaState.BeginPCall 去嘗試調用一個LuaFunction的時候,發現 LuaState 已經被提早釋放掉了,因此就會拋出 「LuaFunction has been disposed」的異常了

3、解決bug

  在理清了bug出現的機制後,只要對症下藥,就不難解決問題了。上文中分析出來最根本的緣由其實就是調用時機的問題,UGUI的源碼咱們是最好不要去隨便改的,能改得只有咱們本身的工程代碼。其實只要在執行 UGUIEventListener 的那些回調以前,將UGUIEventListener 中綁定LuaFunction的那些委託執行置空操做就能夠了,經過再次觀察Unity MonoBehavior腳本生命週期圖,咱們發現了 OnApplicationQuit 函數先於OnDisable 函數被調用而且在整個腳本的生命週期中只會被調用一次,那麼置空操做放在這裏再合適不過了:

 1     public virtual void OnApplicationQuit()
 2     {
 3         this.onClick = null;
 4         this.onDown = null;
 5         this.onUp = null;
 6         this.onDownDetail = null;
 7         this.onUpDetail = null; ;
 8         this.onDrag = null;
 9         this.onExit = null;
10         this.onDrop = null;
11         this.onSelect = null;
12         this.onDeSelect = null;
13         this.onMove = null;
14         this.onBeginDrag = null;
15         this.onEndDrag = null;
16         this.onEnter = null;
17         this.onSubmit = null;
18         this.onScroll = null;
19         this.onCancel = null;
20         this.onUpdateSelected = null;
21         this.onInitializePotentialDrag = null;
22         this.onEvent = null;
23     }

  添加了上面的置空步驟之後,咱們再次按照bug復現的流程進行屢次測試,發現不會拋出 「LuaFunction has been disposed」 的異常了。

4、總結

  在本篇博客中,你們跟着馬三一塊兒經歷了出現bug、尋找復現bug的步驟、經過調試和分析源碼定位問題出現的位置和緣由、根據分析對症下藥解決bug 的一整套流程,能夠說在實際的Unity遊戲開發工做中,大部分的bug修復流程都與上述相似。在遇到咱們沒有見過的疑難bug的時候,首先千萬不要慌張,不妨抽根菸或者喝杯小可樂壓壓驚,以後再從斷點調試和分析運行原理入手定能解決大多數的bug。

  驚現Bug不要慌

  斷點調試來幫忙

  理性分析看源碼

  寫好程序奔小康

  解決完了Bug,馬三內心美滋滋,哼着本身瞎編的打油詩,又開始寫起了下一個Bug...

 

 

 

 

若是以爲本篇博客對您有幫助,能夠掃碼小小地鼓勵下馬三,馬三會寫出更多的好文章,支持微信和支付寶喲!

       

 

做者:馬三小夥兒
出處:http://www.javashuo.com/article/p-vtpatbjr-ba.html 請尊重別人的勞動成果,讓分享成爲一種美德,歡迎轉載。另外,文章在表述和代碼方面若有不妥之處,歡迎批評指正。留下你的腳印,歡迎評論!

相關文章
相關標籤/搜索