- 對搜索到的每一個數據進行引用計數並放置在 weak table 中。
- 查找全局變量泄漏:啟動游戲打印一份完整的游戲數據,游戲退出前打印一份完整的內存數據,然后把差異的部分再過濾輸出並且按照引用次數進行排序,然后逐個查找所有可疑或者不該出現的全局變量(一般都在根節點),直接定位修改代碼,直到沒有全局變量泄漏位置。
- 查找游戲邏輯數據未釋放:比如查找戰斗邏輯泄漏,在每次進入主場景打印一份完整的數據,這樣每次戰斗完成都會回到主場景,而且理論上回到主場景戰斗數據都是必須釋放的,然后對比最近兩次主場景中打印的內存數據,將差異部分輸出並且按照引用次數排序,然后根據結果優化或者修改代碼邏輯,將沒有釋放的地方進行釋放。
- 不斷地循環以上方式,直到內存穩定且總量在合理預期范圍內。
collectgarbage("collect") mri.m_cMethods.DumpMemorySnapshot("./", "All", -1)
快照文件的內容,每一行是一個引用對象信息,所有的信息按照引用次數降序排列,每一行被 tab 分成了3列,分別是:對象類型/地址,引用鏈,引用次數。整個文件可以使用 Excel 打開,會自動歸為3列,方便閱讀,重新排序。
文件內容中重點部分是引用鏈的信息,例如 "function: 0x7f85f8e0e3f0 registry.2[_G].Author.Ask[line:33@file:example.lua] 1" 這條信息說明的是:表 "registry" 的成員 "2"(也就是表 "_G")引用了表 "Author",表 "Author" 有一個成員 "Ask" 引用了 "function: 0x7f85f8e0e3f0",函數位置在文件 "example.lua" 中的第33行,一共被引用了1次。這樣就能快速的定位什么對象在哪里被引用,一共被引用了多少次。
"DumpMemorySnapshot" 這個方法最后兩個參數是“根節點對象名稱“和“搜索根節點對象”,默認值為 "registry" 和 "debug.getregistry()",在大多數使用的時候不需要修改使用默認值即可,但是當你想從別的根節點開始搜索來縮小范圍,例如從 "_G" 來搜索,你可以手動設置這兩個參數,例如:
-- Only dump memory snapshot searched from "_G". collectgarbage("collect") mri.m_cMethods.DumpMemorySnapshot("./", "All", -1, "_G", _G)
當整個程序運行一段時間后,再打印一份內存快照(可以打印多份),接下來最重要的工作就是對比快照分析增加的泄露點。在這個工具中,提供了一個名為 “DumpMemorySnapshotComparedFile” 的接口來實現這個對比功能,切記不要自己用文件對比工具來對比兩份快照(有朋友這樣用過),因為快照內容是根據引用計數來降序排序的,時間不同內容也不同,順序也不同,所以普通的文件對比工具在這里是無法生效的。使用方法:
mri.m_cMethods.DumpMemorySnapshotComparedFile("./", "Compared", -1, "./LuaMemRefInfo-All-[1-Before].txt", "./LuaMemRefInfo-All-[2-After].txt")
這個方法會生成一個新文件,里面是出現在第二份快照里但是沒有並出現在第一份快照里的數據,這就是新增內容。
無論是那種類型的數據,如果 dump 后數據過大,但是想查看某個特定的數據,可以使用過濾器來生成一個新文件,可以選擇新文件生成的內容是包含關鍵字,還是排除關鍵字,例如:
-- 輸出文件里所有包含關鍵字 “Author” 的內容。(不區分大小寫) mri.m_cBases.OutputFilteredResult("./LuaMemRefInfo-All-[2-After].txt", "Author", true, true) --輸出文件里所有不包含關鍵字 “Author” 的內容。(不區分大小寫) -- Filter all result exclude keywords: "Author". mri.m_cBases.OutputFilteredResult("./LuaMemRefInfo-All-[2-After].txt", "Author", false, true)
另外,如果想查看某個對象到底被哪些地方引用着,可以使用接口 "DumpMemorySnapshotSingleObject",例如:
--輸出所有引用對象 "_G.Author" 的地方。 collectgarbage("collect") mri.m_cMethods.DumpMemorySnapshotSingleObject("./", "SingleObjRef-Object", -1, "Author", _G.Author) -- 輸出所有引用字符串 "yaukeywang" 的地方。 collectgarbage("collect") mri.m_cMethods.DumpMemorySnapshotSingleObject("./", "SingleObjRef-String", -1, "Author Name", "yaukeywang")
通過以上幾個主要的方法配合使用,就可以快速的查出內存泄漏,即使再手機上也可以使用,比如打印時將保存路徑指向 sd 卡目錄,例如如果使用 Unity 里的 Lua,可以使用:
collectgarbage("collect") mri.m_cMethods.DumpMemorySnapshot(UnityEngine.Application.persistentPath, "All", -1)
它將輸出一份快照文件到 sd 卡目錄下。
現在新加了一個配置選項,一般例如 "DumpMemorySnapshot" 這個方法都是指定一個保存路徑和額外信息,然后保存的文件名最后每次都會加上當前的時間戳,方便根據時間來區分不同的快照,也避免需要頻繁的設置和修改文件名,也避免同一個地方不同時間的快照被不斷覆蓋,這個時間戳選項默認開啟,可以通過 "mri.m_cConfig.m_bAllMemoryRefFileAddTime = false" 來關閉,配置的設置放置在 "require" 后,Dump 之前,其它幾個 Dump 的接口也都有是否附加時間戳到文件名的選項,具體參看源碼。
除了以上的方法,還提供了一些其它的接口可以使用,更詳細的使用請參考 GitHub 上的 ReadMe 和源碼中的接口定義說明,都寫的很詳細了,"Example.lua" 中也演示了常用接口的使用方法。
最后,最近完善了下這個工具,增加了字符串類型的輸出,所以上面的那張搜索路徑圖,路徑上可以再添加一個 "string"。同時需要注意:為了能在同一行顯示所有字符串(以方便其他方法對數據進行處理,例如對比差異增量,Excel 排序統計等),字符串在顯示的時候所有的回車和換行符:'\r', '\n' 都被顯示的替換成了 '\\n',需要閱讀數據的時候注意。
