尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Unity WebGL异常处理实战:三种日志捕获与远程监控方案

Unity WebGL异常处理实战:三种日志捕获与远程监控方案 在Unity所有发布平台里WebGL是我认为最需要重新学习“异常处理”思维的一个。以前做Windows或Android端异常直接打在Console窗口变量值、堆栈、调用链一目了然一旦切到WebGL你会发现Console经常一片安静浏览器控制台倒是抛出一堆看不懂的错误甚至整个页面直接白屏。这背后不是Unity出了问题而是WebGL平台本身的运行机制决定了异常捕获必须换一套玩法。这篇博文就是要解决这件事。我会从UnityWebGL的报错产生原理讲起把C#层和JavaScript层的异常分别会流向哪里拆清楚然后给出3种我已经在实际项目里验证过的异常捕获配置方案第一套适合开发期快速定位第二套适合联调和轻度生产环境第三套适合正式上线后的远程监控。看完之后你能拿到可直接抄走的代码模板、构建配置参数和一份常见报错排查速查表省掉自己从头踩坑的时间。1. 为什么WebGL平台的报错总让人摸不着头脑1.1 编译目标和运行环境的基本差异先明确一个基础认知Unity WebGL不是把C#代码直接跑在浏览器里而是先把C#编译成IL再由IL2CPP转成C最后通过Emscripten编译成WebAssemblywasm。这个过程中的每一层转换都会影响异常信息的形态。在Windows平台Unity的Mono或IL2CPP运行时可以直接抛出托管异常异常对象的类型、Message、StackTrace都结构清晰。但在WebGL平台C#异常要先穿过IL2CPP转换成C层的处理再通过Emscripten的运行时与JavaScript环境交互。很多底层错误到了浏览器这边就变成了一个不痛不痒的RuntimeError或者干脆被浏览器安全模型吞掉。更麻烦的是浏览器本身有几套独立的错误机制window.onerror能捕获未处理的JavaScript异常unhandledrejection能捕获Promise异常而WebAssembly运行时内部的异常不一定走这两个通道。所以你在浏览器控制台看到的报错可能跟你游戏里实际的问题根本不沾边。1.2 异常信息的三种最终去向我在实际项目里观察下来WebGL项目的异常最终会流向三个地方第一Unity Console窗口。前提是你在构建时勾选了Development Build并且代码里有Debug.LogError等输出。这种情况下C#层的异常会通过Console窗口展示但堆栈信息可能被压缩行号对应的是C转换后的位置而不是原始C#代码。第二浏览器DevTools的Console面板。这部分包含两类信息Unity通过jslib或者Emscripten打印的日志以及浏览器运行时抛出的原生JavaScript错误。实际开发中WebAssembly的崩溃、加载失败、内存溢出等问题只能在这里看到。第三玩家/用户看到的页面表现。比如点击按钮没反应、画面卡死、白屏。这类是最终用户能感知的“异常”但它们没有具体的报错文本需要我们去主动捕获底层异常才能反推原因。所以我很早就得出一个结论在WebGL平台只依赖Unity Console或只依赖浏览器控制台都不够必须把两条链路全都接进来才有可能还原一个错误的完整面貌。1.3 常见报错类型与现象速查在你开始设计捕获方案之前建议先对WebGL常见的异常类型有个整体印象。我用一张表整理一下方便你排查时对照参考异常类型表现常见原因白屏/黑屏页面加载后没有任何画面资源加载失败、WebAssembly编译失败、初始化逻辑异常运行时JS错误浏览器控制台出现红色异常jslib插件代码问题、第三方JS库冲突、浏览器API兼容性C#未捕获异常游戏逻辑中断画面卡住空引用、数组越界、加载的资源为空内存耗尽崩溃页面直接卡死或浏览器崩溃内存泄漏、资源过大、64位内存分配失败WebSocket/网络错误联机功能无响应服务器连接失败、协议不兼容、浏览器跨域限制当你写好了异常捕获配置其实做的是三件事把隐藏的C#异常捞出来、把JS异常接进来、把所有信息统一整理到一个可控的出口。2. 动手之前先把异常捕获的整体思路理顺2.1 捕获链路的两端C#日志与JS运行时在设计方案之前你得先理解异常捕获链路的两端。一端是Unity C#运行时。Unity自身提供了一个静态事件Application.logMessageReceived所有通过Debug.Log、Debug.LogWarning、Debug.LogError输出的日志都会进入这个回调。这是C#层异常捕获最容易接入的入口但它只能捕获被日志打出来的信息捕获不了被Unity内部吞掉的错误。另外还有一个Application.logMessageReceivedThreaded功能基本相同但会在非主线程调用UI操作需要小心线程问题。另一端是JavaScript运行时。浏览器原生提供了window.onerror和unhandledrejection这两个全局事件可以拿到未处理的JS异常。此外还有Emscripten的Module对象提供了onAbort、onRuntimeInitialized等回调WebAssembly初始化失败时能在这里看到原因。比较理想的做法是两条链路都打通并且把日志尽量统一成同一种格式这样无论是查本地还是查线上你面对的都是同一套日志语言。2.2 日志分级与格式化先把信息整理干净捕获异常的第一步不是写捕获代码而是定日志格式。很多项目在各种日志回调里print了一堆东西结果排查时根本无法区分哪条是崩溃前的关键日志哪条是普通信息。我个人的建议是至少保留四个级别Info、Warn、Error、Fatal。每个级别对应不同的显示颜色和上报策略。在C#里直接用Debug.Log、LogWarning、LogError天然对应前三级Fatal可以用LogError加自定义标识来实现。格式上我习惯统一成这样的结构[时间戳] [级别] [场景/模块] 日志内容比如[2025-03-10 14:22:31] [Error] [LoginPanel] NullReferenceException: 用户名输入框未初始化。别小看这个细节当你有几千条日志要筛选时带时间戳和模块名的日志会让排查效率翻倍。2.3 本地调试时最高效的观测姿势本地调试阶段我的经验是Unity Console窗口优先浏览器控制台次之游戏内日志面板仅在需要模拟生产环境时才打开。为什么不是优先用浏览器控制台因为Unity Console里的日志格式经过Unity引擎格式化堆栈信息更直观而且双击可以直接定位到脚本代码。但你在Unity编辑器里看到的Console日志并不代表WebGL运行时就完全一致尤其是WebGL特有的JS错误编辑器预览模式下根本不会出现。所以我的本地调试流程通常是先用Unity编辑器把C#层逻辑调通再构建一个Development Build放到浏览器里跑同时打开浏览器DevTools的Console面板与Unity Console对照着看。这个流程下90%以上的问题都能在开发阶段暴露出来。3. 三种可落地的异常捕获配置方案3.1 方案一C#日志回调游戏内日志面板这是成本最低、见效最快的一套方案适合中小型项目在开发期和测试期使用。核心思路是通过Application.logMessageReceived把所有C#日志集中到一个全局容器里然后在游戏场景里做一块简单的日志UI平时隐藏按快捷键或点按钮时弹出。先写一个最简版本的日志管理器using System; using System.Collections.Generic; using UnityEngine; public class LogCapture : MonoBehaviour { public static LogCapture Instance { get; private set; } public ListLogEntry Logs new ListLogEntry(); public int MaxLogCount 500; public event ActionLogEntry OnLogAdded; private void Awake() { if (Instance ! null Instance ! this) { Destroy(gameObject); return; } Instance this; DontDestroyOnLoad(gameObject); Application.logMessageReceived HandleLog; } private void OnDestroy() { Application.logMessageReceived - HandleLog; } private void HandleLog(string logString, string stackTrace, LogType type) { LogEntry entry new LogEntry { Timestamp DateTime.Now.ToString(yyyy-MM-dd HH:mm:ss), Message logString, StackTrace stackTrace, LogType type }; Logs.Add(entry); if (Logs.Count MaxLogCount) { Logs.RemoveRange(0, Logs.Count - MaxLogCount); } OnLogAdded?.Invoke(entry); } } public class LogEntry { public string Timestamp; public string Message; public string StackTrace; public LogType LogType; }这个管理器的关键点是DontDestroyOnLoad保证切换场景时日志不会丢。MaxLogCount限制数量避免运行时间长了之后内存增长失控。OnLogAdded事件可以给UI面板做实时刷新。日志面板可以单独做一个Canvas挂在同一个物体上。面板里用一个ScrollView显示最近的日志Error以上标红Warning标黄Info保持白色。我还会加一个复制按钮一键把日志拷贝到剪贴板方便发给同事或贴到问卷里。不过这套方案有个天然短板它只能捕获C#层打印出来的日志捕获不了浏览器JS层面的异常也捕获不了WebAssembly崩溃。所以它适合当“游戏内自查”的工具但撑不起完整的问题排查。3.2 方案二jslib插件打通JS与C#双向通道当浏览器控制台不断抛JS错误而Unity Console里一片安静时你就需要方案二了通过.jslib插件模板建立一个C#与JavaScript之间的双向通信通道。Unity官方支持在Plugins文件夹下放.jslib文件里面可以写JavaScript函数通过[DllImport(__Internal)]暴露给C#调用。反过来JS也能调用Unity的SendMessage或者CallFunction来触发C#方法。先看一个最基础的.jslib模板mergeInto(LibraryManager.library, { JSLog: function (level, messagePtr) { var message UTF8ToString(messagePtr); var prefix ; if (level 0) prefix [INFO]; else if (level 1) prefix [WARN]; else if (level 2) prefix [ERROR]; console.log(prefix message); }, InstallJSErrorHook: function () { window.onerror function (msg, source, line, col, error) { var message [JS_ERROR] msg at source : line : col \n (error error.stack ? error.stack : ); Module.SendMessage(GlobalObject, OnJSError, message); return false; }; window.addEventListener(unhandledrejection, function (event) { var reason event.reason; var message [JS_PROMISE_ERROR] (reason reason.stack ? reason.stack : reason); Module.SendMessage(GlobalObject, OnJSError, message); }); Module.onAbort function (what) { Module.SendMessage(GlobalObject, OnFatalError, WebAssembly Abort: what); }; } });对应C#端的声明using System.Runtime.InteropServices; using UnityEngine; public class JSBridge : MonoBehaviour { [DllImport(__Internal)] private static extern void JSLog(int level, string message); [DllImport(__Internal)] private static extern void InstallJSErrorHook(); public void Awake() { #if UNITY_WEBGL !UNITY_EDITOR InstallJSErrorHook(); #endif } public void OnJSError(string message) { Debug.LogError(message); } public void OnFatalError(string message) { Debug.LogError([FATAL] message); } }这里有几个细节很容易踩坑。第一jslib函数的参数传递。字符串不能直接传必须用UTF8ToString转换指针。反过来从JS调C#时用Module.SendMessage传字符串可以直接传Unity会自动转换。第二编译条件必须加UNITY_WEBGL !UNITY_EDITOR。因为jslib方法在编辑器里没有对应实现直接调用会报EntryPointNotFoundException。第三window.onerror回调里要return false否则部分浏览器会认为错误已被处理不再打印原始日志反而影响排查。这套方案真正解决了C#和JS两边日志割裂的问题。我在一个上线项目里用这套方案线上玩家反馈“卡死了”后台日志能看到最后一条C#日志同时也能看到玩家浏览器抛出的JS异常很多时候就能拼出完整的事故现场。3.3 方案三生产环境级上报体系前两套方案解决的都是“本地能看到异常”但做上线项目你需要的是“即使没有开发者在场也能把异常收集回来”。这就需要引入生产环境级的远程上报体系。整体架构分三层第一层是Unity C#日志统一出口。沿用方案一的LogCapture但把OnLogAdded的回调里加一段上报逻辑根据日志级别决定是否上报。我一般只上报Error和FatalWarn和Info在本地保留即可否则上报量会很大。第二层是JS全局错误捕获。沿用方案二的jslib安装逻辑但在JS端把捕获到的信息同时上报给远端服务或者统一交给后端接口转发。我建议在JS端直接上报路径更短而且可以带上浏览器类型、版本、页面URL、用户操作路径等WebGL开发更需要关注的上下文。第三层是上报通道。最简单的做法是Unity发HTTP请求到自己的后端后端记录日志后写入日志系统或数据库。也可以接入第三方错误监控平台它们通常提供了JavaScript SDK可以直接上报。很多团队会在WebGL构建里集成Sentry我实际用下来也不错但要注意在jslib插件的资源加载阶段提前初始化避免早期错误丢失。下面是JS端上报的片段function reportError(level, message, stack) { var payload { level: level, message: message, stack: stack, userAgent: navigator.userAgent, url: location.href, timestamp: Date.now() }; fetch(https://your-backend-api.example.com/log, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify(payload), keepalive: true }).catch(function () {}); }keepalive属性很关键它保证页面在崩溃或刷新前发出的请求尽量能被送达。WebGL游戏很多是单页应用玩家卡死后可能直接刷新页面如果不加keepalive上报请求很可能被中断。还有一点是隐私合规。如果你要上报用户ID、设备信息、操作路径务必确认自己符合相关隐私政策建议上报前做脱敏或者明确告知用户。这套体系一旦跑起来线上问题就不再需要靠运气发现。我在项目里会额外做一个“日志仪表盘”按错误类型分组展示每周看一下Top错误排行很多隐藏的兼容性问题就能提前暴露。3.4 三种方案的选型建议光看方案容易晕我直接按项目类型给你参考项目阶段推荐方案原因开发期独立调试方案一轻量、无外部依赖、快速定位C#逻辑问题小规模测试/联调方案二能排查JS层异常适合内测群体验反馈正式上线/大规模用户方案三远程可观测能持续监控线上兼容性实际项目中方案一和方案二往往可以合并使用保留游戏内日志面板方便测试人员反馈时截图同时开启jslib捕获JS错误。等产品稳定后再决定是否正式部署方案三的完整上报链路。4. 实战阶段配置过程中的常见问题与排查4.1 日志面板有输出但浏览器控制台为空这个问题我遇到太多次了。游戏内日志面板正常滚动但打开浏览器DevTools却什么日志都没有尤其当你在自己电脑上调试时还以为是浏览器缓存了旧版本。最常见的原因是构建参数的设置。打开Player Settings确认Scripting Backend选择的是IL2CPP然后在Compression Format里如果选了Brotli或Gzip浏览器控制台的JS报错信息可能因为压缩变形而变短或丢失行号。本地调试时我习惯把Compression Format设为Disabled发布时再启用压缩。另外一个原因是Development Build没有被勾选。非开发构建默认会剥离大量调试信息和Console日志所以浏览器控制台看不到是正常的。这也是为什么我强烈建议所有WebGL测试构建都勾选Development Build。4.2 构建产物打开白屏资源加载失败白屏在WebGL项目里非常让人头疼因为没有报错弹窗玩家只会说“打不开”。但白屏的根源基本都可以在浏览器Network面板里找到线索。先打开DevTools的Network面板刷新页面看有几个请求是红色或者状态码异常的。最常见的是403通常是服务器没有正确配置wasm或data文件的MIME类型。.wasm文件需要配置为application/wasm.data文件通常是application/octet-stream如果你用Nginx托管一定要在配置里加上。另外一个高发点是CORS跨域问题。如果你把WebGL构建放在一个域名而资源或API请求指向另一个域名浏览器会拦截请求控制台会出现CORS错误。解决方案是在服务器响应头里加Access-Control-Allow-Origin或者把所有请求都改为同域。白屏还有一个容易忽略的因素是WebAssembly内存分配失败。WebGL构建默认分配的内存大小可以在构建时配置如果游戏场景比较大或者运行时动态加载了较多资源内存不足就会导致Wasm初始化失败。出现这种情况时可以尝试在Player Settings里降低最大内存或改用64位构建并在代码层面优化资源释放。4.3 堆栈信息、符号映射与发布版本排错线上版本最难受的问题是没有调试符号。打包出来的wasm是编译后的二进制你只能拿到一个WebAssembly地址根本看不出对应哪个C#方法。Unity官方提供了解析wasm堆栈的方法。构建时勾选Player Settings里的“Debug Symbols”相关选项会生成符号文件。再配合浏览器DevTools的Source Map或者Unity的Symbol Server功能可以把wasm地址映射回C#函数名。不过这个映射过程比较繁琐我之前就被坑过。后来我发现一个更实用的思路不要只依赖堆栈信息而是在代码的关键路径上主动打日志。尤其是网络请求开始/结束、资源加载完成、场景切换前后这些节点一旦异常日志可以帮你确定问题发生的阶段比单纯偶发崩溃后看一堆堆栈有效得多。发布版本还要注意一个细节URL参数对日志级别的影响。我在项目里做了一个按URL参数控制日志上报级别的功能例如?logLevelinfo可以开启所有日志上报?logLevelerror只上报错误。这样当线上玩家反馈问题时可以让TA把带参数的URL分享给你通过这个参数临时开启详细日志定位问题后再恢复不用重新打包。4.4 一些容易被忽略的浏览器侧因素WebGL项目跑在浏览器里就要尊重浏览器的规则。我踩过几个记忆深刻的坑写出来提醒你。第一移动端浏览器的兼容性差异很大。ISO Safari对WebAssembly的支持和桌面Chrome不完全一致尤其在低内存设备上更容易崩溃。我建议在测试覆盖列表里同时包含iOS Safari和Android Chrome它们的行为差异比你想象中更大。第二浏览器的自动播放策略会影响音频初始化。如果项目在启动时自动播放背景音乐而浏览器还没发生用户交互音频不会正常播放相关的异常也可能被静默吞掉。解决方案是让音频系统在用户第一次点击后再初始化或者在初始化时捕获NotAllowedError。第三GPU显存限制。WebGL上下文默认使用显存多个WebGL页面同时打开时可能因为显存耗尽导致所有页面都崩溃。这个问题在监控平台里几乎看不到只能靠代码层面对纹理和Framebuffer做及时释放以及引导用户不要同时开太多重负载页面。第四不要忽略浏览器版本。WebGL2已经是主流但部分旧版浏览器只支持WebGL1。如果你的项目用了WebGL2专属特性又碰到玩家浏览器不支持页面不会直接报错而是各种奇怪的渲染异常。构建时勾选合适的Graphics APIs并在启动时做特性检测信息要尽早暴露。写在最后的一点经验这三套配置方案的核心思路其实就一句话在WebGL世界里你的异常信息来源是双通道的必须把C#和JavaScript两边都接上才能拿到完整的问题画面。方案一帮你快速解决C#逻辑问题方案二帮你补上JS层的信息盲区方案三让你在线上版本里也能持续监控。我个人做项目时还有一个习惯每次处理完一个WebGL线上的疑难杂症都会顺手把问题现象、排查链路、最终原因整理成一篇简短的记录放在团队知识库里。几个月下来就能形成一份非常宝贵的问题库后来遇到类似报错基本不用重新排查直接翻记录就能对上。这部分投入看起来不起眼却是提升团队排障效率最快的方式。最后再分享一个小技巧构建完WebGL后建议先把产物放到任意一个静态服务器上而不是用Unity的Build And Run直接打开。独立托管方式更接近线上用户的访问环境Network请求、CORS策略、压缩格式的行为都和正式部署一致很多在编辑器预览里发现不了的问题在独立服务器上跑一遍就能暴露出来。把这个步骤养成习惯你的WebGL版本稳定性会明显上一个大台阶。
返回列表