置調(diào)試控制臺IngameDebugConsole實(shí)戰(zhàn)指南)
做移動端Unity開發(fā)的朋友大概率都經(jīng)歷過這種時刻包打到真機(jī)上跑邏輯出問題了但Console窗口里的日志根本看不到。以前的做法是接一個無線日志工具或者讓測試幫忙插著數(shù)據(jù)線來回截圖。說實(shí)話效率極低。直到我換上了UnityIngameDebugConsole這個內(nèi)置調(diào)試控制臺插件真機(jī)調(diào)試這件事才算是真正順了。這篇文章就把我實(shí)際使用這個插件的經(jīng)驗(yàn)完整寫出來從怎么集成、怎么配置、到怎么和業(yè)務(wù)日志打通、內(nèi)存怎么控制一次講透。1. 為什么Unity項(xiàng)目需要內(nèi)置調(diào)試控制臺1.1 編輯器Console的真機(jī)盲區(qū)Unity自帶的Console窗口只在編輯器里有用一旦打包到Android或iOS設(shè)備上Debug.Log打出來的內(nèi)容就進(jìn)了系統(tǒng)的logcat或os_log開發(fā)者想看還得連數(shù)據(jù)線、開Android Studio或者Xcode流程又長又別扭。更麻煩的是很多問題是特定機(jī)型、特定網(wǎng)絡(luò)環(huán)境下才出現(xiàn)的現(xiàn)場測試同事反饋一句報錯了但不知道錯在哪你這邊什么都看不到純靠猜。IngameDebugConsole這類插件解決的就是這個盲區(qū)把Console的能力直接搬進(jìn)游戲畫面里在游戲運(yùn)行時就能呼出日志面板看到完整的堆棧信息。它本質(zhì)上做的事情很簡單——監(jiān)聽Unity的日志回調(diào)把日志寫到游戲內(nèi)的UI列表上配合搜索、過濾、折疊重復(fù)條目這些操作讓開發(fā)者不用出游戲就能定位問題。1.2 內(nèi)置控制臺解決的三大痛點(diǎn)我實(shí)際用下來覺得這類工具對下面三個場景的提升最明顯真機(jī)獨(dú)立調(diào)試測試手機(jī)不連電腦也能隨時查看崩潰前的日志上下文復(fù)現(xiàn)Bug時可以當(dāng)場記錄現(xiàn)場信息。優(yōu)化類問題定位很多性能問題只在真機(jī)上暴露比如某些Android機(jī)型GC頻繁、貼圖內(nèi)存峰值在游戲內(nèi)控制臺里配合Memory Profiler的簡版信息可以直接判斷是不是某一幀有異常分配。非技術(shù)同事協(xié)同策劃或測試在操作流程中遇到問題直接截圖游戲畫面里的控制臺日志發(fā)給你溝通成本瞬間降一個量級。所以我說任何一個要上真機(jī)、要發(fā)布移動端的Unity項(xiàng)目都應(yīng)該在開發(fā)期和測試期內(nèi)置一個游戲內(nèi)控制臺。IngameDebugConsole是目前市面上最成熟、配置最靈活的開源方案核心代碼只有幾百行但把日志展示的細(xì)節(jié)做得非常到位。1.3 這個插件適合誰來用如果你屬于下面幾類人這個插件幾乎可以無腦引入做Android/iOS單機(jī)或網(wǎng)游的客戶端開發(fā)日常需要真機(jī)日志排查。維護(hù)線上版本想做一個隱藏調(diào)試入口給內(nèi)部測試包用。處理崩潰或卡死問題時需要玩家/測試在游戲內(nèi)直接反饋日志。項(xiàng)目里已經(jīng)有ngui、ugui等UI系統(tǒng)希望用最少成本接入一個純UI層調(diào)試面板。我用過的幾個商業(yè)項(xiàng)目中有的把IngameDebugConsole直接嵌進(jìn)了發(fā)布包的開發(fā)者模式里平時隱藏連續(xù)點(diǎn)擊Logo五次呼出體驗(yàn)非常好。下面我把完整的集成和定制經(jīng)驗(yàn)拆開講。2. 插件核心機(jī)制與安裝部署2.1 它是怎么工作的先花兩分鐘理解原理后面出問題排查會省很多力氣。IngameDebugConsole的核心機(jī)制其實(shí)就兩個部分第一部分通過Application.logMessageReceived這個靜態(tài)事件拿到Unity引擎拋出的所有日志。這個事件是Unity官方提供的在編輯器、真機(jī)上都可用Debug.Log、Debug.LogWarning、Debug.LogError甚至底層Native代碼打出的日志只要走Unity的日志系統(tǒng)都會被它捕獲。第二部分當(dāng)事件觸發(fā)時插件把日志內(nèi)容封裝成一個sDebugLogEntry對象塞進(jìn)一個環(huán)形緩沖區(qū)默認(rèn)最多存幾千條然后刷新Unity UGUI的列表項(xiàng)。這個緩沖區(qū)就是控制臺數(shù)據(jù)的內(nèi)存核心之后你做的搜索、折疊、復(fù)制都是在緩沖區(qū)內(nèi)做字符串匹配和狀態(tài)標(biāo)記。我最初看源碼時覺得奇怪為什么日志列表能做到幾千條還不卡秘密就在回收機(jī)制它復(fù)用了UGUI的Item對象滾動時不對每條日志都創(chuàng)建文本組件而是把不可見的Item回收到對象池。這種做法跟我做過的一個聊天系統(tǒng)幾乎一模一樣Unity UI滾動場景下的性能基本功。2.2 從導(dǎo)入到第一次呼出把這個插件跑起來的步驟非常簡單我按我習(xí)慣的流程寫一遍直接從GitHub上下載IngameDebugConsole.unitypackage雙擊導(dǎo)入。如果你們項(xiàng)目有專門的第三方插件目錄用菜單里的Assets Import Package Custom Package導(dǎo)入后手動把文件拖進(jìn)去也行。在Project窗口里搜IngameDebugConsole找到IngameDebugConsole的Prefab。這個Prefab已經(jīng)掛好了全部腳本DebugLogManager、DebugLogPopup、DebugLogConsole等。把Prefab拖進(jìn)當(dāng)前場景。考慮到它是單例邏輯最好放進(jìn)一個常駐的初始化場景或DontDestroyOnLoad的管理節(jié)點(diǎn)里。也可以不拖Prefab直接寫一行代碼在運(yùn)行時實(shí)例化。一個最關(guān)鍵的地方如果場景里沒有EventSystem記得加一個。這個控制臺要用UGUI的拖拽和點(diǎn)擊我遇到過幾次面板能彈出來但按鈕點(diǎn)不動的情況一大半是EventSystem缺失。默認(rèn)的呼出手勢是三指雙擊編輯器和真機(jī)都適用彈出后屏幕中央會有一個懸浮小圓球點(diǎn)一下展開完整面板。如果不適應(yīng)這個手勢Inspector里可以改后面集中說配置項(xiàng)。2.3 用代碼主動創(chuàng)建控制臺有些項(xiàng)目的場景結(jié)構(gòu)比較特殊比如用了多場景加載、加載順序不可控這時候直接在場景里放Prefab可能有點(diǎn)別扭。我更推薦的方式是寫一個啟動腳本統(tǒng)一初始化using UnityEngine; public class DebugConsoleLauncher : MonoBehaviour { [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)] private static void AutoCreate() { var prefab Resources.LoadGameObject(IngameDebugConsole); if (prefab ! null) { var instance Object.Instantiate(prefab); Object.DontDestroyOnLoad(instance); } } }把Prefab放到Resources目錄下后這段代碼會在第一個場景加載完成后自動實(shí)例化控制臺并且跨場景不銷毀。用這種方案的好處是熱更代碼也好、普通代碼也好只要啟動時Unity能加載Resources控制臺就一定能創(chuàng)建出來不會因?yàn)閳鼍袄锫┓臥refab導(dǎo)致調(diào)試工具偶爾消失。注意Resources.Load頻繁使用會有性能損耗但這里是啟動時一次性調(diào)用沒有任何問題。如果你的項(xiàng)目已經(jīng)徹底放棄Resources目錄管理也可以改成用AssetBundle加載邏輯一樣。3. 功能拆解與配置項(xiàng)詳解3.1 核心面板會用到的功能呼出控制臺后你會看到類似手機(jī)端日志監(jiān)控工具一樣的界面底部是輸入框上方是滾動的日志流。我梳理一下日常最常用的功能日志過濾頂部有All、Logs、Warnings、Errors四個Tab一鍵只顯示某一級別。搜索輸入關(guān)鍵字實(shí)時過濾。支持簡單的字符串匹配不需要正則夠用。復(fù)制日志單擊某一條日志會把它完整復(fù)制到剪貼板堆棧信息不會丟。注意這里要區(qū)分單擊和拖拽滾動插件用手指輕輕點(diǎn)擊日志Item來實(shí)現(xiàn)復(fù)制剛開始容易誤觸習(xí)慣后效率很高。折疊重復(fù)日志如果某條日志反復(fù)打印插件會自動折疊成一條后面標(biāo)注重復(fù)次數(shù)。對排查周期性日志特別有用。執(zhí)行命令輸入框支持輸入一些DebugLogConsole注冊的命令默認(rèn)有一些系統(tǒng)命令而且最常用的是直接輸入Help查看全部命令。這個功能跟游戲里的GM命令很像后文我會分享如何注冊自己的命令。3.2 關(guān)鍵參數(shù)配置建議選中DebugLogManager后Inspector里配置項(xiàng)非常多但真正值得精細(xì)調(diào)的有這幾個。先說彈出方式。默認(rèn)是PopupGesture綁定三指雙擊我試過一段時間后覺得太容易誤觸特別是手機(jī)游戲里雙指操作很頻繁。最后在DebugLogPopup的參數(shù)中改成了長按屏幕兩秒呼出這個體驗(yàn)對開發(fā)包來說更穩(wěn)。要調(diào)這個手勢找Popup Gesture相關(guān)枚舉選LongPress即可響應(yīng)時間可以自己填。再說日志上限。這是最容易忽略又直接影響性能的參數(shù)。Max Log Count默認(rèn)是1000如果你游戲里日志打得很勤比如每秒幾十條那1000條可能幾分鐘就滿了滿了之后早先的日志會被覆蓋。我一般調(diào)到3000~5000真機(jī)內(nèi)存增加有限但排查問題時的上下文大大增多。但如果你期望長期記錄光調(diào)這個沒用還得靠文件持久化。持久化日志到文件是排查疑難雜癥的利器。Persist Log To File勾選后插件會把日志定期寫入Application.persistentDataPath下的自定義文件。有些項(xiàng)目還要把這個文件和Bugly/Umeng的崩潰日志放一起做聚合上報我在Android上實(shí)測過把CustomLogFileName設(shè)成game_log.txtD級錯誤和崩潰上下文基本能對齊。3.3 通過代碼注冊命令和自定義日志這個插件最亮眼的點(diǎn)是可以像游戲GM工具一樣注冊命令行。比如游戲里有存檔異常我可以注冊一個level_skip命令直接在控制臺輸入跳過關(guān)卡不用重新編譯。using IngameDebugConsole; public static class PlayerConsoleCommands { [ConsoleMethod(level.skip, Skip to a specific level)] public static void SkipLevel(int levelIndex) { var playerData GameData.Instance; playerData.CurrentLevel levelIndex; playerData.Save(); Debug.Log($Level has been set to {levelIndex}); } }ConsoleMethod特性是插件提供的用這個方法標(biāo)注的靜態(tài)方法會自動注冊成控制臺命令。我推薦的另外一個實(shí)用命令是清除本地存檔[ConsoleMethod(player.clear_save, Clear all saved data)] public static void ClearSave() { PlayerPrefs.DeleteAll(); Debug.Log(All saved data cleared.); }前端加一個[ConsoleMethod]后端自動就在控制臺里可用了比自己在UI上搓一個調(diào)試按鈕省事太多。這里我吃了不少虧才體會到與其把調(diào)試入口寫到游戲UI里不如全部統(tǒng)一收斂到控制臺命令里性能也好、代碼也干凈。3.4 自定義UI皮膚和圖文混排的嘗試插件默認(rèn)皮膚是深色半透明底、綠黃紅三色日志辨識度很高。如果你們游戲風(fēng)格偏白色UI可以在DebugLogManager上調(diào)整配色也可以直接改Prefab里Text組件的顏色。但有一點(diǎn)建議不要換字體、不要加圖片表情因?yàn)檫@個控制臺的核心是快速讀取信息花哨的裝飾只會增加渲染壓力。真要給日志加圖標(biāo)推薦用TextMeshPro的sprite來做比如在日志前用sprite0標(biāo)記一個箭頭上標(biāo)壓力很小。這里延伸說一句熱詞里常有人搜Unity 圖文混排其實(shí)在控制臺日志里做圖文混排是不劃算的。你真想在日志里顯示道具圖標(biāo)就讓圖標(biāo)走DebugLogManager.Log的富文本參數(shù)Unity的Debug.Log本身支持富文本插件展示層也支持部分UILabel富文本但真機(jī)上字符串開銷會增大。我的經(jīng)驗(yàn)是控制臺的日志保持純文本最穩(wěn)妥。4. 性能開銷與內(nèi)存泄露隱患排查4.1 日志字符串隱藏的GC Alloc很多人引入游戲內(nèi)控制臺后擔(dān)心的第一個問題都是它會不會拖累游戲性能我的實(shí)測結(jié)論是插件本身的Update邏輯幾乎不消耗CPU但你打日志的方式會。UWA和Unity官方都反復(fù)警告過Debug.Log在真機(jī)上是有GC Alloc的字符串拼接越頻繁分配越好。插件充其量是把這些日志顯示出來它不會憑空減少你代碼里分配的內(nèi)存。拿熱詞里提到的粒子特效內(nèi)存泄露來呼應(yīng)一下粒子系統(tǒng)不停實(shí)例化又不停打日志日志系統(tǒng)本身的高頻字符串分配就會疊加進(jìn)內(nèi)存峰值。我見過一個項(xiàng)目ParticleSystem每幀都Log一次當(dāng)前粒子數(shù)控制臺一開Memory Profiler里的GC Alloc直接飆升。4.2 控制臺自身的資源控制那么我們能不能讓控制臺這個面板盡量少占資源可以幾個關(guān)鍵點(diǎn)不用時保持隱藏不要讓它整個面板常駐。UI的Canvas只要激活哪怕沒有可見更新也要付出重建代價。IngameDebugConsole的彈出面板默認(rèn)是隱藏的保持默認(rèn)就行。調(diào)整日志刷新頻率。插件有一個LogUpdateInterval之類的參數(shù)控制UI刷新間隔默認(rèn)每幀刷新。如果你的游戲?qū)PU敏感把它改成每0.1秒刷新一次日志吞吐不變但UI重建次數(shù)大大降低。降低單條日志最大長度。有的錯誤堆棧特別長整條塞進(jìn)UGUI的Text里RichText解析和網(wǎng)格重建都很吃力。建議把MaxLogLength設(shè)成200~300超長部分截?cái)嗤暾畔⒖繌?fù)制接口取。我在一個中端Android機(jī)驍龍6系上做過粗測開著控制臺、日志每秒30條、面板懸浮小圓球常駐幀率基本無感知下降。但如果你把面板完全展開并且保持滾動讓幾千條日志同時參與渲染布局幀率會掉5~10幀這是UGUI本身的瓶頸不是插件的問題。4.3 和Memory Profiler等工具配合有一些內(nèi)存問題并不是控制臺導(dǎo)致的只是控制臺給了你觀察窗口。比如項(xiàng)目同時掛著粒子特效內(nèi)存泄露的問題你用控制臺只能看到Log報錯看不到具體是哪個資源沒釋放。這時候正確流程是先在控制臺里看到異常日志再到Unity Profiler里抓Memory Snapshot對比兩次快照。我自己常做的一個操作是在控制臺命令里注冊一個mem_dump命令執(zhí)行時調(diào)UnityEngine.Profiling.Profiler或者Resources.FindObjectsOfTypeAll把當(dāng)前活動對象數(shù)量、紋理內(nèi)存粗略打出來。這樣在真機(jī)環(huán)境不接Profiler也能快速確認(rèn)是不是有對象沒釋放。代碼很簡單[ConsoleMethod(mem.dump, Dump basic memory info)] public static void DumpMemory() { var allTextures Resources.FindObjectsOfTypeAllTexture(); long textureMemory 0; foreach (var tex in allTextures) { // 粗略估算不包含mipmap壓縮等細(xì)節(jié) textureMemory tex.width * tex.height * 4; } Debug.Log($Active textures: {allTextures.Length}, estimated memory: {textureMemory / (1024 * 1024)} MB); }有經(jīng)驗(yàn)之后你會發(fā)現(xiàn)游戲內(nèi)控制臺的意義不只是看日志它更像一個簡易的運(yùn)行時診斷終端很多以前必須連電腦才能做的事現(xiàn)在一臺手機(jī)就能完成。5. 常見問題與排查技巧實(shí)錄5.1 為什么我的真機(jī)上看不到日志這個問題排第一因?yàn)閹缀跞巳硕紩龅?。先說最簡單的排查鏈檢查DebugLogManager有沒有被創(chuàng)建出來。有時你只在場景里放了Prefab但是被它自身的Awake邏輯銷毀了或者場景卸載時一起銷毀了而下一個場景沒有重新實(shí)例化。建議用我前文寫的DontDestroyOnLoad方式。檢查有沒有其他插件把Application.logMessageReceived的事件處理給接管或屏蔽了。比如部分崩潰統(tǒng)計(jì)SDK會上報日志理論上不會屏蔽事件但如果你自己在Awake里又忘記-委托鏈就可能異常。檢查DebugLogManager上某個ReceiveLogsInReleaseBuild的開關(guān)。如果沒勾Release包是不接收日志的日志不會顯示這其實(shí)是個安全設(shè)計(jì)。還有一個我踩過的坑在Android上如果Player Settings里勾了Development BuildDebug.Log輸出沒問題但如果是Release包且沒勾Use Player Log日志默認(rèn)不出。這個跟控制臺插件無關(guān)是Unity打包配置的問題勾上就好。5.2 點(diǎn)擊面板時游戲角色也跟著移動這是UI穿透問題??刂婆_面板本身用了全屏的Image做背景遮擋正常情況下不會點(diǎn)穿。但如果你是手動改過Prefab層級或者用了某些全局的Input轉(zhuǎn)發(fā)監(jiān)聽就可能在面板打開時把輸入事件穿透到游戲身上。我的處理方式是在面板打開的時候暫停游戲主邏輯。DebugLogManager有一個OnEnablePanel回調(diào)你可以注冊進(jìn)去private void OnEnable() { DebugLogManager.Instance.OnLogWindowShown OnWindowShown; DebugLogManager.Instance.OnLogWindowHidden OnWindowHidden; } private void OnWindowShown() { Time.timeScale 0f; } private void OnWindowHidden() { Time.timeScale 1f; }這個做法要小心如果游戲邏輯本身對timeScale有依賴比如動畫、音游、倒計(jì)時那就別一刀切暫停改成切一個輸入屏蔽層只屏蔽角色的移動輸入。5.3 日志文件占滿存儲空間勾選了Persist Log To File后如果游戲長時間掛機(jī)、日志又打得很兇文件可能膨脹到幾百M(fèi)B。我在正式發(fā)布包里執(zhí)行過一個清理策略每次啟動控制臺時如果日志文件超過5MB直接刪除重建。對排查幾天前的崩潰日志來說5MB足夠但如果你要保留歷史那就得做日志輪轉(zhuǎn)。插件默認(rèn)沒有內(nèi)置輪轉(zhuǎn)所以我寫了一個簡單的啟動清理[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] private static void CleanupOldLog() { var path Path.Combine(Application.persistentDataPath, game_log.txt); if (File.Exists(path)) { var info new FileInfo(path); if (info.Length 5 * 1024 * 1024) { File.Delete(path); } } }這個技巧不復(fù)雜但能避免線上包存儲失控的尷尬。同理如果你發(fā)現(xiàn)Android上persistentDataPath里文件很多記得在設(shè)置界面里提供清除調(diào)試日志的入口。5.4 與第三方的IL2CPP/混淆沖突這個坑比較新項(xiàng)目從Mono切到IL2CPP后控制臺大部分功能正常但類型信息不全。因?yàn)镮L2CPP的AOT編譯會裁剪掉一些反射用不到的元數(shù)據(jù)而插件里某些命令依賴反射獲取[ConsoleMethod]標(biāo)注的方法。解決辦法有兩個在Player Settings的IL2CPP Code Generation選項(xiàng)里勾選Faster (smaller) builds一般能保留更多元數(shù)據(jù)。但這樣包體會增大。更精準(zhǔn)的做法是寫一個[Preserve]標(biāo)注把帶命令的類或方法強(qiáng)制保留。如果你用代碼裁剪strip engine code務(wù)必要加上。using AOT; using UnityEngine.Scripting; [Preserve] public static class PlayerConsoleCommands { [Preserve] [ConsoleMethod(player.clear_save, Clear all saved data)] public static void ClearSave() { } }這個坑最典型的表現(xiàn)是編輯器里一切正常打出來的包提示Command not found。只要看到這個提示十有八九就是裁剪導(dǎo)致的反射失效。5.5 快速定位堆棧信息過深移動端報錯堆棧往往被Unity裁剪或壓縮看起來只有一行NullReferenceException沒有具體哪個腳本。這種情況不能全指望控制臺。我先說插件能做的把日志以文件形式持久化然后用VS Code或?qū)iT工具打開把堆棧整理成可讀格式。插件解決的是現(xiàn)場獲取的問題拿到文件后的分析還是得配合常規(guī)工具鏈。再補(bǔ)充一個我認(rèn)為很實(shí)用的技巧給所有業(yè)務(wù)模塊增加一個模塊名前綴日志格式統(tǒng)一成[UI]、[NET]、[AUDIO]。在控制臺搜索時直接搜前綴幾秒內(nèi)就能過濾出整個模塊的日志流。這是很多人忽略的日志規(guī)范卻是一個大型項(xiàng)目最值錢的基礎(chǔ)建設(shè)。社區(qū)里不少質(zhì)量較高的Unity項(xiàng)目日志規(guī)范做得都非常系統(tǒng)化。6. 從插件到一套完整的真機(jī)調(diào)試流程IngameDebugConsole只是一個大拼圖里的一塊。如果你真想提高排查效率我建議把下面這套流程在自己的項(xiàng)目里搭起來開發(fā)期和測試期打包Debug包開啟控制臺、開啟日志持久化并且讓測試人員知道怎么呼出面板。每次提測版本里寫一個/version命令顯示當(dāng)前SVN/Git提交號、構(gòu)建時間、宏開關(guān)狀態(tài)。測試反饋問題時要求提供控制臺截圖最好帶上展開面板后的日志上下文。截圖比口頭描述可靠十倍。版本發(fā)布前默認(rèn)關(guān)閉控制臺入口但內(nèi)部OTA包保留打開入口的方式建議通過配置文件或服務(wù)器遠(yuǎn)程開關(guān)。遠(yuǎn)程開關(guān)這個主意我是從一個上線后出了詭異Bug的項(xiàng)目里悟出來的。線上包不暴露入口但服務(wù)器下發(fā)一個標(biāo)志玩家在設(shè)置頁連點(diǎn)版本號控制臺就能在Release包里呼出。這時它的價值已經(jīng)遠(yuǎn)超開發(fā)插件的范疇直接變成了線上問題診斷終端?;氐綐?biāo)題本身UnityIngameDebugConsole這個插件看起來是一個小工具實(shí)際用好了卻能在整個研發(fā)流程中扮演不可替代的角色。它不依賴平臺SDK、集成成本低、源碼清晰如果你想基于自己的需求魔改也非常方便。我在實(shí)際項(xiàng)目里已經(jīng)把它從一個緊急救火工具用成了日常開發(fā)標(biāo)配如果你還沒接強(qiáng)烈建議下一個版本就加上。最后再分享一個我個人的使用習(xí)慣我會專門在控制臺注冊一個log_memory_limit命令實(shí)時調(diào)整日志上限在內(nèi)存吃緊的真機(jī)上把日志降到一個低水位讓性能分析更干凈。這個習(xí)慣讓我在幾次線上性能事故中只用一臺手機(jī)就完成了初步定位效率比我預(yù)想的高得多。你項(xiàng)目里接上了這個插件也不妨先給自己注冊幾個用得順手的命令把調(diào)試主動權(quán)握在手里。