# UnityCodeDescriptive **Repository Path**: assassinlx/unity-code-descriptive ## Basic Information - **Project Name**: UnityCodeDescriptive - **Description**: 小工具 - **Primary Language**: Unknown - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-03-20 - **Last Updated**: 2026-03-25 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Unity Code Recorder — 运行期排查录制器 一套**零 DLL 修改、零外部依赖**的 Unity 运行期代码执行追踪与排查工具。 在 Play Mode 下自动采样所有业务类和方法的调用情况,结合手动会话式排查,最终一键导出 HTML / JSON / CSV 可视化报告。 --- ## 1. 解决什么问题 在 Unity 项目的日常开发中,当运行结果不符合预期时,开发者通常面临以下困境: | 痛点 | 具体表现 | |------|----------| | **执行路径不可见** | 不知道 Play Mode 下到底哪些脚本被执行了、哪些方法被调用了,只能靠经验猜测或逐行加 `Debug.Log` | | **状态快照缺失** | 出问题的瞬间,关键变量值、列表数量等上下文已随帧更新被覆盖,无法回溯"案发现场" | | **异常上下文断裂** | Unity 抛出 Error / Exception 时,虽有调用栈,但缺少异常前的业务操作链条与状态快照,难以复现 | | **日志散乱难分析** | `Debug.Log` 产生的日志散落在 Console 中,无法按"操作会话"组织,也无法直观看到文件/方法命中热点 | | **调试手段侵入性高** | 传统方案要么需要 IL 织入(引入 Mono.Cecil 依赖,有 JIT 崩溃风险),要么需要在源码中大量埋点 | **本工具的核心价值**: 1. **自动追踪**:进入 Play Mode 后,通过 PlayerLoop 采样自动发现所有被执行的业务类和方法,**无需修改任何业务代码**即可获得全局执行视图。 2. **会话式排查**:以"操作会话 → 关键步骤 → 状态快照 → 异常冻结"的结构组织排查数据,形成因果链条。 3. **异常冻结**:当检测到异常或结果不符合预期时,立即冻结当前上下文(调用栈 + 状态快照),保留完整现场。 4. **一键报告**:将所有数据导出为 HTML 可视化报告(含类/方法热点排行、会话卡片、事件时间线),以及 JSON / CSV 供二次分析。 --- ## 2. 怎么使用 ### 2.1 导入工程 将本仓库中的文件放入 Unity 项目中(**注意文件放置位置,详见第 3 节**): ``` YourUnityProject/Assets/Plugins/CodeRecorder/ ├── CodeRecorder.cs ← 必须在非 Editor 目录 ├── CodeTrace.cs ← 必须在非 Editor 目录 ├── CodeTraceExample.cs ← 示例脚本,可选 ├── Editor/ │ └── CodeRecorderWeaver.cs ← 必须在 Editor 目录 └── CodeRecorderWindow.cs ← 必须在 Editor 目录(有 #if UNITY_EDITOR 包裹) ``` **不需要安装 Mono.Cecil 或任何其他第三方库。** ### 2.2 打开控制面板 菜单栏 → `Window` → `代码排查录制器`,即可打开编辑器窗口。 ### 2.3 基本工作流(推荐) #### 第一步:启用运行时追踪 在控制面板的"🔍 运行时追踪"面板中,勾选 **启用运行时追踪**。 或通过菜单 `Tools → Code Recorder → Toggle Runtime Tracing`。 #### 第二步:开始录制 & 进入 Play Mode 1. 在控制面板中点击 **▶ 开始录制**。 2. 进入 Play Mode,正常操作你要排查的业务流程。 3. 系统会自动采样追踪所有被执行的业务类和方法。 #### 第三步:观察面板数据 在窗口中可实时看到: - **追踪到的类**:按命中次数排序,可展开查看各类下的方法列表 - **方法热点 Top 30**:快速定位高频调用方法 - **场景存活 MonoBehaviour**:当前场景中实际存在的业务组件 #### 第四步:(可选)手动记录关键信息 在"手动排查面板"中,可以: - **开始会话**:为当前排查流程命名 - **记录步骤**:标记关键操作节点 - **记录状态**:快照变量值、列表数量等 - **标记异常并冻结上下文**:当结果不符合预期时,保留完整现场 #### 第五步:停止录制 & 查看报告 1. 点击 **■ 停止录制**(默认自动导出报告)。 2. 点击 **📂 打开报告目录**,在浏览器中打开 `.html` 文件查看可视化报告。 报告包含: - **重点会话卡片**(含冻结 / 异常标记) - **运行命中文件热点排行** - **运行命中方法热点排行** - **运行时追踪:类热点 & 方法热点**(来自 PlayerLoop 采样) - **详细事件时间线**(前 500 条,含源码行定位) ### 2.4 业务代码接入(进阶) 在关键业务逻辑中使用 `CodeTrace` 静态类,可将自动采样升级为更清晰的因果链: ```csharp // 方式一:using Scope 自动管理会话生命周期 using (var scope = CodeTrace.Scope("奖励结算", "从战斗结束触发")) { CodeTrace.Step("读取配置", "解析奖励表"); CodeTrace.State("rewardCount", rewardCount.ToString()); int finalReward = Calculate(); CodeTrace.State("finalReward", finalReward.ToString()); if (finalReward < 0) { // 标记异常 → 冻结上下文 → 异常结束会话 scope.Fail("奖励数量非法负数", ">=0", finalReward.ToString(), "检查配置读取逻辑"); return; } scope.Success("结算完成"); } // 方式二:手动控制会话 CodeTrace.BeginOperation("打开背包面板"); CodeTrace.Step("请求服务端数据"); CodeTrace.State("itemCount", items.Count.ToString()); CodeTrace.EndOperation("面板打开成功", true); // 方式三:捕获异常 try { DangerousOperation(); } catch (Exception ex) { CodeTrace.Exception(ex, "执行危险操作时出错"); } ``` ### 2.5 录制配置说明 | 配置项 | 说明 | 默认值 | |--------|------|--------| | 录制名称 | 导出报告的文件名前缀 | `Trace` | | 停止时自动导出 | 停止录制后是否自动生成报告 | ✅ | | 输出控制台日志 | 是否将每条事件实时打印到 Console | ❌ | | 捕获 Unity Error/Exception | 是否监听 `Application.logMessageReceived` | ✅ | | 异常时冻结会话 | Exception 发生时自动冻结当前会话 | ✅ | | Error/Assert 时冻结会话 | Error/Assert 发生时自动冻结当前会话 | ✅ | | 录制编辑器信号 | 是否记录窗口焦点变化、Selection 变化等编辑器事件 | ❌ | | 采样间隔(帧) | 运行时追踪的 PlayerLoop 采样频率(越小越全面,开销越大) | 2 | | MonoBehaviour 枚举间隔(帧) | 场景存活 MonoBehaviour 的枚举频率 | 60 | | 最大保留事件数 | 超出后自动裁剪最早的非活动会话事件 | 25,000 | | 最大保留会话数 | 超出后自动裁剪最早的非活动会话 | 200 | --- ## 3. 操作时的注意事项 ### ⚠️ 文件放置位置(最常见的坑) - **`CodeRecorder.cs` 和 `CodeTrace.cs` 必须放在非 Editor 目录**(如 `Assets/Plugins/CodeRecorder/`)。因为 `CodeRecorder` 继承自 `MonoBehaviour`,需要在运行时可访问;`CodeTrace` 是业务代码调用的静态入口。如果放进 Editor 目录,运行时将无法实例化,工具完全不可用。 - **`CodeRecorderWeaver.cs` 必须放在 Editor 目录**(如 `Assets/Plugins/CodeRecorder/Editor/`)。它使用了 `UnityEditor` API(`EditorPrefs`、`InitializeOnLoad`、`MenuItem`),打包时不能被包含。 - **`CodeRecorderWindow.cs`** 虽然在文件内部已用 `#if UNITY_EDITOR` 包裹,放在 Editor 目录更安全。 ### ⚠️ 仅限开发调试期使用 - PlayerLoop 采样和 StackTrace 获取会带来性能开销。**正式打包时应关闭运行时追踪,或从构建中排除相关脚本**。 - 采样间隔设为 1 帧时采集最全面但开销最大,建议排查期间设为 1~3 帧,平时设为 5~10 帧。 ### ⚠️ 运行时追踪需要 Play Mode - 运行时追踪(类 & 方法采样)**只在 Play Mode 下工作**。在编辑器模式下面板会提示"运行时追踪仅在 Play Mode 下工作"。 - 进入 Play Mode 时,如果已启用追踪,会自动清空上次数据并重新开始采样。 ### ⚠️ 源码行号定位需要调试符号 - 要让报告中的方法热点落回具体源码文件和行号,需要项目启用 **Script Debugging**(Player Settings → Other Settings → Configuration → Script Debugging)。 - 没有 PDB / 调试符号时,仍能追踪到类名和方法名,但无法定位到具体行号。 ### ⚠️ 录制生命周期 - **先开始录制,再操作业务**。手动面板的会话 / 步骤 / 状态按钮只有在录制中才可用。 - 停止录制时,未结束的活动会话会自动以 `Interrupted` 状态关闭。 - 报告导出到 `Application.persistentDataPath` 目录,可通过面板的"📂 打开报告目录"按钮快速找到。 ### ⚠️ 自动会话创建 - 当 Unity 抛出 Error/Exception 时(且已启用"捕获 Unity Error/Exception"),如果当前没有活动会话,工具会**自动创建一个名为"自动异常捕获"的会话**。这确保异常不会因为没有会话而丢失。 ### ⚠️ 跨程序集反射 - `CodeRecorder.cs`(运行时程序集)通过反射访问 `CodeRecorderWeaver`(Editor 程序集)的数据。如果你的项目使用了自定义 Assembly Definition,需确保 Editor 程序集名称与代码中的查找逻辑匹配(当前代码查找 `NssEditor` 程序集名)。如不匹配,需修改 `SnapshotRuntimeTraceData()` 和 `CodeTrace` 中相关的程序集名查找逻辑。 --- ## 4. 原理剖析 ### 4.1 整体架构 本工具由四个核心模块组成: ``` ┌────────────────────────────────────────────────────────────────────┐ │ CodeRecorderWindow (Editor) │ │ 编辑器窗口 — 管理录制、显示追踪数据、操作会话 │ └────────────────────┬─────────────────────────┬─────────────────────┘ │ │ ┌────────────▼──────────┐ ┌──────────▼──────────────┐ │ CodeRecorder │ │ CodeRecorderWeaver │ │ (Runtime MonoBehaviour)│ │ (Editor Static Class) │ │ │ │ │ │ • 会话 / 步骤 / 状态 │ │ • PlayerLoop 注入采样 │ │ • 异常冻结 │◄──│ • StackTrace 调用栈解析 │ │ • 调用栈采集 │ │ • MonoBehaviour 枚举 │ │ • HTML/JSON/CSV 导出 │ │ • 类 & 方法聚合统计 │ └────────────▲──────────┘ └──────────────────────────┘ │ ┌────────────┴──────────┐ │ CodeTrace │ │ (Static Facade) │ │ │ │ • 业务代码的统一入口 │ │ • Scope / Step / │ │ State / Problem │ └───────────────────────┘ ``` ### 4.2 核心原理:PlayerLoop 注入 + StackTrace 采样 **这是本工具最关键的技术实现,也是它能做到"零 DLL 修改、零外部依赖"的原因。** #### 什么是 PlayerLoop? Unity 的每一帧由一系列固定阶段组成,称为 **PlayerLoop**:`EarlyUpdate → FixedUpdate → Update → PreLateUpdate → PostLateUpdate → ...`。Unity 在 `UnityEngine.LowLevel` 命名空间下开放了 `PlayerLoop.GetCurrentPlayerLoop()` 和 `PlayerLoop.SetPlayerLoop()` API,允许开发者向任意阶段**插入自定义子系统**。 #### 本工具如何利用 PlayerLoop? `CodeRecorderWeaver` 在进入 Play Mode 时,向 PlayerLoop 的四个阶段注入采样回调: ``` EarlyUpdate → OnSampleEarlyUpdate() : 递增帧计数器,定期枚举场景存活 MonoBehaviour Update → OnSampleUpdate() : 采集当前调用栈 PreLateUpdate → OnSamplePreLateUpdate(): 采集当前调用栈 PostLateUpdate → OnSamplePostLateUpdate(): 采集当前调用栈 + 向 Recorder 推送聚合数据 ``` 每个采样回调做的核心操作是: 1. **`new StackTrace(false)`** — 调用 `System.Diagnostics.StackTrace` 获取当前线程的完整调用栈。 2. **遍历栈帧** — 通过 `StackFrame.GetMethod()` 获取 `MethodBase`,再通过 `DeclaringType` 获取类信息。 3. **过滤系统类** — 排除 `System.*`、`UnityEngine.*`、`UnityEditor.*` 等引擎/框架类,只保留用户业务代码。 4. **聚合统计** — 用字典(`Dictionary` / `Dictionary`)记录每个类/方法的命中次数、首次/最后发现时间。 #### 为什么 StackTrace 能采到用户代码? 在 Unity 的帧循环中,`Update` 等阶段执行时,调用栈从引擎底层(C++ 侧的 `PlayerLoop`)一路上到 C# 侧的 `MonoBehaviour.Update()`。当我们在 `Update` 阶段的末尾插入采样器时,虽然各个 MonoBehaviour 的 Update 已经执行完毕,但 **StackTrace 仍然能捕获到当前 PlayerLoop 阶段本身的调用链**,并且在此之前被执行的方法信息(通过上一帧的采样窗口)可以被统计。 更重要的是,对于**协程(Coroutine)**和**事件回调**等异步调用路径,它们在 `PreLateUpdate` 或 `PostLateUpdate` 阶段被 Unity 调度执行,多阶段采样确保了更高的覆盖率。 #### MonoBehaviour 存活枚举的补充作用 单靠 StackTrace 采样可能遗漏某些低频调用的类。因此,`CodeRecorderWeaver` 每隔 N 帧(默认 60 帧)调用 `FindObjectsOfType()` 枚举当前场景中所有存活的 MonoBehaviour 实例。这能发现: - 场景中存在但本帧未被调用的业务类 - 通过**反射检查** (`BindingFlags.DeclaredOnly`) 发现用户重写了哪些 Unity 回调(`Awake`、`Start`、`Update`、`OnTriggerEnter` 等),即使这些方法尚未被 StackTrace 捕获 #### 首次发现时的源码定位 为了平衡性能和信息完整性,常规采样使用 `new StackTrace(false)`(不加载 PDB 信息,开销极低)。仅在方法**首次被发现**时,才执行一次 `new StackTrace(true)`(加载 PDB 信息,获取源码文件路径和行号)。这意味着: - 高频方法只有一次额外的 PDB 查询开销 - 后续命中直接增加计数器,近乎零开销 ### 4.3 会话式排查模型 `CodeRecorder` 的数据模型围绕"操作会话"组织: ``` TraceSession(会话) ├── sessionId 唯一标识 ├── operationName 操作名称(如"打开背包面板") ├── context 上下文说明 ├── status Running → Success / Abnormal / Exception / Interrupted ├── frozen 是否已冻结上下文 └── [统计] eventCount, stackFrameCount, stateSnapshotCount, uniqueFileCount, uniqueMethodCount TraceEvent(事件) ├── sessionId 归属会话 ├── recordType SessionStart / Step / State / Issue / Exception / StackFrame / UnityLog / EditorSignal ├── trigger 触发源 ├── severity Info / Warning / Error ├── className 类名(由 StackTrace 解析) ├── methodName 方法名 ├── fileName 文件名 ├── lineNumber 行号 ├── codeSnippet 对应源码行内容(从文件中读取) └── timestamp 时间戳 ``` 当调用 `CodeTrace.Problem()` 或 Unity 抛出异常时,工具会: 1. 将当前会话标记为 `Abnormal` 或 `Exception` 2. 设置 `frozen = true` 3. 立即采集完整调用栈并附带源码行内容 4. 这些"冻结上下文"事件在 HTML 报告中会被高亮显示 ### 4.4 跨程序集数据桥接 Unity 的 Editor 代码和 Runtime 代码分属不同程序集(Assembly)。`CodeRecorderWeaver` 在 Editor 程序集中持有采样数据(静态字典),而 `CodeRecorder` 在 Runtime 程序集中需要将这些数据导出到报告中。 解决方案是**反射桥接**: 1. **停止录制时**:`CodeRecorder.SnapshotRuntimeTraceData()` 通过 `AppDomain.CurrentDomain.GetAssemblies()` 查找 Editor 程序集,反射调用 `GetClassesSortedByHits()` / `GetMethodsSortedByHits()` 等方法,将数据拷贝到 Runtime 侧的快照列表中。 2. **导出报告时**:优先使用已快照的数据,如果快照为空则降级通过反射实时读取。 3. `CodeTrace` 中的 `TrackedClassCount` / `IsClassTracked()` 等查询 API,也是通过反射实现跨程序集访问。 ### 4.5 Unity 日志拦截 通过 `Application.logMessageReceived` 事件监听 Unity 运行时的 Error / Assert / Exception 日志。当捕获到此类日志时: - 如果当前没有活动会话,自动创建一个"自动异常捕获"会话 - 根据配置决定是否冻结会话上下文 - 使用 `StackTraceUtility.ExtractStackTrace()` 获取调用栈(兼容 IL2CPP 后端) ### 4.6 涉及的关键知识点 | 知识领域 | 具体技术 | 在本工具中的应用 | |----------|----------|-----------------| | **Unity PlayerLoop** | `UnityEngine.LowLevel.PlayerLoop` API | 在帧循环中注入自定义采样器,无需修改任何 DLL | | **System.Diagnostics.StackTrace** | .NET 调用栈反射 API | 运行时采样当前调用栈,解析出类名、方法名、源码定位 | | **Unity StackTraceUtility** | Unity 内置的调用栈格式化工具 | 兼容 IL2CPP 后端的调用栈获取 | | **C# 反射 (Reflection)** | `Assembly` / `Type` / `MethodInfo` / `FieldInfo` | 跨 Runtime/Editor 程序集桥接数据;检查 MonoBehaviour 重写的回调方法 | | **Unity Editor API** | `EditorWindow` / `EditorPrefs` / `InitializeOnLoad` / `MenuItem` | 编辑器窗口 UI、持久化配置、菜单集成 | | **Application.logMessageReceived** | Unity 全局日志回调 | 捕获运行时 Error / Exception,自动创建异常会话 | | **FindObjectsOfType** | Unity 场景对象查询 | 枚举当前场景存活的所有 MonoBehaviour 实例 | | **IDisposable / using 模式** | C# 资源管理模式 | `CodeTraceScope` 实现自动开始/结束会话 | ### 4.7 为什么不需要 IL 织入(Mono.Cecil)? 早期版本曾采用 IL 织入方案(通过 `CompilationPipeline.assemblyCompilationFinished` 回调 + Mono.Cecil 修改编译后的 DLL)。但这种方案存在: - **JIT 崩溃风险**:修改 IL 字节码后,Mono JIT 在某些边界情况下会崩溃 - **外部依赖**:需要安装 `com.unity.nuget.mono-cecil` 包 - **编译链路脆弱**:织入失败可能导致 DLL 损坏,需要手动还原 当前版本完全放弃了 IL 织入,改用 **PlayerLoop 注入 + StackTrace 采样**。这种方案: - ✅ 零 DLL 修改,不触碰 Mono 内部 - ✅ 兼容 Unity 2019+ Mono 后端 - ✅ 无需任何第三方库 - ✅ 可捕获 Update 链、协程、事件回调等多种调用路径 - ⚠️ 采样式(非全量),极低频调用可能偶尔遗漏,但通过 MonoBehaviour 枚举 + 多阶段采样大幅降低遗漏率 --- ## 5. 文件清单 | 文件 | 位置要求 | 说明 | |------|----------|------| | `CodeRecorder.cs` | **非 Editor 目录** | 核心录制器,MonoBehaviour,管理会话/事件/导出 | | `CodeTrace.cs` | **非 Editor 目录** | 业务代码调用的静态入口(Facade 模式) | | `CodeTraceExample.cs` | 非 Editor 目录 | 示例脚本,展示 Scope / Step / State / Problem 用法 | | `Editor/CodeRecorderWeaver.cs` | **Editor 目录** | 运行时追踪器,PlayerLoop 注入 + StackTrace 采样 | | `CodeRecorderWindow.cs` | Editor 目录(推荐) | 编辑器窗口 UI | --- ## 6. 兼容性 - **Unity 版本**:2019.4+(依赖 `UnityEngine.LowLevel.PlayerLoop` API,2019 起可用) - **脚本后端**:Mono / IL2CPP 均可(调用栈采集使用 `StackTraceUtility.ExtractStackTrace()` 兼容 IL2CPP) - **平台**:Editor only(运行时追踪和窗口均依赖 Editor API) --- ## License 见 [LICENSE](./LICENSE) 文件。