ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Unity WebGL异常捕获与日志上报:三种配置方案从原理到实战

Unity WebGL异常捕获与日志上报:三种配置方案从原理到实战 先说一个我自己的经历第一次把Unity游戏构建成WebGL版本丢到浏览器里玩家反馈进不去我看后台数据加载率直接掉了一半。最痛苦的是没有任何报错日志控制台干干净净连个红色感叹号都没有。后来才知道WebGL的异常捕获和原生平台完全是两码事如果不按浏览器的方式去接日志你连报错都看不到。这篇文章就把我踩过的坑和最终沉淀下来的3种配置方案完整讲清楚从原理到代码、从开发调试到线上排查适合正在做Unity WebGL项目、或者准备把现有Unity项目搬到浏览器的开发者。1. 先搞清楚UnityWebGL的报错为什么总是看不懂、抓不到1.1 浏览器环境与原生平台的根本差异很多人拿到报错的第一反应是去看Unity Console在原生平台Windows、Android这个思路没问题但WebGL完全不是这么回事。Unity在WebGL平台不是跑在独立进程里而是被编译成了WebAssemblyWasm跑在浏览器的JavaScript虚拟机里。这个架构带来两个直接后果第一Unity的Debug.Log虽然默认会输出到浏览器控制台但输出的格式和内容都经过了C#到JS的跨语言桥接很多底层信息会丢。比如你在C#里输出一个Debug.Log(test)浏览器控制台确实能看见但如果你想拿到C#堆栈和JS堆栈的完整链路默认配置是给不出来的。第二除了Unity托管代码自己的异常还有大量JS层的运行时错误、资源加载错误、浏览器API调用失败。这些错误Unity Console根本看不见只会显示在浏览器DevTools的Console面板里。我见过很多团队排查一天一夜最后发现是WebGL构建产物里某个JS文件被CDN缓存了旧版本导致的加载崩溃。所以在WebGL下做异常捕获思路必须从C#统一处理切换成浏览器统一处理。这就像你在自己家原生平台水管坏了知道总闸在哪到了别人家浏览器宿主环境先得找到别人家的水电入户点。1.2 常见的Unity WebGL报错类型速览从我的项目经验来看WebGL报错集中在这么几类加载阶段错误Unable to load file、CompileError、Aborted这类错误集中出现在初始化WebAssembly模块、加载数据文件.data时。大多是CDN配置、压缩格式gzip/brotli、跨域访问CORS引起的。运行时JS错误TypeError: Cannot read properties of undefined、ReferenceError: xxx is not defined这类通常是集成第三方JS SDK、或者调用浏览器API时机不对。Unity托管异常C#层面的NullReferenceException、ArgumentException等这类异常会出现在浏览器控制台但堆栈经过了Wasm翻译不是原始C#路径格式比较怪。内存问题Out of memory、abort(Error: System.OutOfMemoryException)。WebGL有严格的内存上限32位寻址通常建议控制在2GB以内资源加载过多直接崩。这几类错误的发生时机不同、捕获方式也不同靠单一方案根本全覆盖。1.3 报错信息为什么会静默丢失这里有个非常关键的机制浏览器对未捕获的JS异常默认只在Console输出一行日志并不会主动通知你的代码。如果用户不按F12打开控制台错误就永远看不见。更麻烦的是Unity的Wasm模块内部发生无法恢复的错误时会调用abort()页面表现可能是直接卡死、白屏也可能在Console里只输出一句Aborted(...)原因被截断得面目全非。我后来在排查一个移动端白屏问题时发现游戏实际是因为Wasm内存申请失败触发了RuntimeError整个Wasm实例直接终止但Unity侧没有任何回调暴露给开发者只能靠浏览器全局错误监听去接。这就是为什么要做多层捕获而不是依赖Unity单方面输出。2. 方案一Unity侧日志回调3分钟接入最短路径2.1 核心原理Application.logMessageReceived这是Unity官方提供的最基础的日志回调接口。只要在游戏启动时注册监听Unity的所有日志输出Log、Warning、Error、Exception、Assert都会同步到你的C#方法里。代码很简单using UnityEngine; public class UnityLogCatcher : MonoBehaviour { [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)] private static void Register() { Application.logMessageReceived HandleLog; } private static void HandleLog(string logString, string stackTrace, LogType type) { if (type LogType.Error || type LogType.Exception || type LogType.Assert) { // 沉淀到本地缓存或者通过jslib传给JS层 Debug.Log($[UnityLogCatcher] {type}: {logString}\n{stackTrace}); } } }注意[RuntimeInitializeOnLoadMethod]的使用它保证这段代码在场景加载后自动执行不需要手动挂载到某个GameObject上。如果你用的是老版本Unity或者想把监听挂到特定对象也可以写Awake。2.2 从C#把日志递交给JS层如果在浏览器环境光是C#内部打印一遍意义不大。很多项目希望把Unity日志统一交给JS层的监控体系这时候要挂一个jslib插件做桥接。首先在Assets/Plugins/WebGL/目录下新建一个WebGLLogBridge.jslib文件mergeInto(LibraryManager.library, { PushUnityLog: function (logString, stackTrace, type) { var msg UTF8ToString(logString); var stack UTF8ToString(stackTrace); if (typeof window ! undefined window.__unityLogBridge) { window.__unityLogBridge(msg, stack, type); } } });然后在C#侧声明外部方法并调用using System.Runtime.InteropServices; using UnityEngine; public class UnityLogBridge { [DllImport(__Internal)] private static extern void PushUnityLog(string logString, string stackTrace, int type); public static void Push(string logString, string stackTrace, LogType type) { #if UNITY_WEBGL !UNITY_EDITOR PushUnityLog(logString, stackTrace, (int)type); #endif } }这样当C#层出现Exception时HandleLog回调里调用UnityLogBridge.Push(...)日志就会通过jslib桥接到浏览器全局的window.__unityLogBridge函数里。这个函数你可以自由定义比如统一上报或者弹窗展示。注意jslib的mergeInto、UTF8ToString这些API在Unity 2020都还兼容但Unity 2023起官方推荐使用LibraryManager.library的新式写法或Unity Built-in JS API老接口依然能用后续大版本升级再适配即可。2.3 方案一的边界在哪里这个方案的优势是接入快、纯C#代码搞定拿到的是Unity托管层的日志和字符串堆栈对大部分代码层面的问题够用。但它有两个明显缺陷捕获不到JS层错误。比如你的项目调用了第三方JS SDKSDK内部抛异常Unity的logMessageReceived完全无感。拿不到完整的浏览器运行时堆栈。C#堆栈经过Wasm编译后stackTrace字符串往往是at UnityEngine.MonoBehaviour...这种虽然能定位到大致代码但和浏览器DevTools里的原生态JS堆栈不是一回事。如果你只做开发期自测方案一足够。但如果是线上项目想靠它做用户报错收集你会发现漏网率非常高。3. 方案二浏览器全局监听把JS层的漏网之鱼捞回来3.1 核心原理window.onerror与unhandledrejection在浏览器里要捕获全局未处理异常和未处理的Promise异常标准做法是监听两个事件window.onerror或window.addEventListener(error)和unhandledrejection。前者捕获同步异常和资源加载错误后者捕获异步Promise里抛出的错误。这段代码可以直接放在构建产物index.html的head里或者单独抽一个error-catch.js文件加载(function () { function normalizeMessage(message, source, lineno, colno, error) { return { type: js_error, message: message, source: source : lineno : colno, stack: error error.stack ? error.stack : , url: location.href, ua: navigator.userAgent, time: Date.now() }; } window.addEventListener(error, function (event) { var payload normalizeMessage( event.message, event.filename, event.lineno, event.colno, event.error ); // 交给统一上报函数 if (window.__reportError) { window.__reportError(payload); } }); window.addEventListener(unhandledrejection, function (event) { var reason event.reason; var payload { type: unhandledrejection, message: reason reason.message ? reason.message : String(reason), stack: reason reason.stack ? reason.stack : , url: location.href, ua: navigator.userAgent, time: Date.now() }; if (window.__reportError) { window.__reportError(payload); } }); })();这段代码的价值在于它监听的是浏览器运行时层面不管错误来自Unity的Wasm层还是第三方SDK只要在页面里抛出来、没有被捕获都能被记录。3.2 怎么拿到更完整的Wasm堆栈浏览器DevTools里Wasm函数通常显示成wasm-function[123]:0x1a2b3c这种没有语义的形式。要想还原成有意义的函数名需要在构建Unity项目时开启Debug Symbols并配合Unity生成的.symbols.json文件做映射。这个后面第5章会讲实操。关键点是event.error.stack里Wasm堆栈虽然不美观但包含了十六进制的函数偏移地址这些数据是后续符号还原的基础。所以这里收集原始堆栈时一定不要截断尽量存全。很多上报平台默认只截取前几十行WebGL项目如果要还原符号得把这个限制去掉或者调大。3.3 把方案一和方案二串起来理想状态是C#层的异常通过jslib桥接传到JSJS层的异常由全局监听兜底两边都汇总到同一个window.__reportError函数。在index.html或单独的JS文件里定义window.__reportError function (payload) { // 这里是统一出口可以打点、可以远程上报、可以在页面上绘制一个泡 console.error([GlobalErrorCatcher], payload); // 上报到你的日志服务 if (navigator.sendBeacon) { var blob new Blob([JSON.stringify(payload)], { type: application/json }); navigator.sendBeacon(/api/logs/error, blob); } else { fetch(/api/logs/error, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify(payload) }); } };同时修改world里的jslibPushUnityLog方法让它把C#日志也转换成__reportError格式调出去PushUnityLog: function (logString, stackTrace, type) { var msg UTF8ToString(logString); var stack UTF8ToString(stackTrace); if (window.__reportError) { window.__reportError({ type: unity_error, message: msg, stack: stack, url: location.href, ua: navigator.userAgent, time: Date.now() }); } }这样Unity C#异常和JS异常就统一走一条链路了。注意navigator.sendBeacon在页面卸载unload时会保证发出请求这是它比fetch更适合做崩溃日志上报的原因。不过sendBeacon对Payload大小有上限一般建议不超过64KB。如果堆栈非常大建议先压缩或截断再用fetch保底。4. 方案三构建期注入上报脚本线上问题不再靠用户截图4.1 核心思路改造构建产物index.html前两个方案都是运行时接入但WebGL项目的构建产物是自动生成的每次重新Build都会被覆盖。方案三的思路是把上报逻辑固化到构建流程里每次Build后自动注入。具体有两种做法做法一手动改index.html。在文件里加入全局异常捕获脚本这种方法一劳永逸但每次构建完都得改一次容易漏。做法二更推荐写一个Node脚本在构建后处理阶段自动修改index.html。核心步骤是Unity构建完成后生成目录Build/其中index.html是入口文件。用Node读取index.html字符串在head标签后插入上报脚本内容。写回文件。这个脚本可以放在项目的BuildPipeline里调用Unity的IPostprocessBuildWithReport接口来实现自动化。不必引入复杂的构建工具链一段Node脚本就够。4.2 远程上报的Payload设计线上报错收集的价值取决于你收集到的上下文够不够。我目前的Payload大致长这样{ type: unity_error, message: NullReferenceException: Object reference not set to an instance of an object, stack: at ExampleClass.DoSomething (Int32 id) [0x0001a] in ..., url: https://game.example.com/index.html, ua: Mozilla/5.0 ... Chrome/124.0, timestamp: 1715000000000, project: webgl-demo, version: 1.0.3, level: error }字段设计建议url和ua帮你判断是哪个入口、哪个浏览器出的问题WebGL的浏览器兼容性问题很常见这两项必填。version帮助你定位是不是某个发布版本才出现的问题。project当多个项目共用一套日志服务时区分来源。level可按LogType的Error、Exception、Assert区分严重程度。4.3 线上环境还需要注意的事线上上报会暴露一个问题上报请求跨域。如果你的游戏部署在game.example.com日志服务在log.example.com那么上报接口必须支持CORS否则请求发不出去。另外一个容易踩的坑是上报接口本身挂掉或者请求超时不能阻塞游戏主流程。所以上报必须做异步和容错处理一般用navigator.sendBeacon天然异步不阻塞页面用fetch时记得加.catch(() {})避免上报失败又产生新的JS错误形成错误嵌套。这个坑我真实遇到过上报接口超时直接滚雪球控制台刷了几百行错误。5. 高频报错速查表与排查技巧实录5.1 我遇到过的经典报错和处理方式下面是我在Unity WebGL项目里实际遇到过的报错整理成一个速查表遇到直接可以对照着查报错信息常见原因首选排查方向UnityLoader is not definedUnity 2020 的构建产物里不再默认挂全局UnityLoader对象改用createUnityInstance加载检查loader.js是否正常引用CompileError: WasmCompileError: Compiling wasm function failed浏览器版本过低或资源损坏检查浏览器版本清CDN缓存确认Unity WebGL最低版本要求abort(Error: System.OutOfMemoryException)Wam内存超过浏览器限制检查Asset加载策略调低内存预算用Addressables做按需加载TypeError: Cannot read properties of undefined (reading onProgress)加载脚本顺序不对传入的配置对象为空检查Build/config.js是否正常加载参数名是否拼错Unable to load file (Internal error).data文件加载失败或CORS未配置确认CDN服务器响应头Access-Control-Allow-OriginRuntimeError: abort(undefined)运行时遇到未知的致命错误开启Decompression Fallback检查Unity 2021是否开启Brotli压缩兼容我在本地调试时最高频的其实是第二类CompileError尤其在低版本Chrome和部分国产浏览器上Wasm编译兼容性参差不齐。遇到这种我是直接引导用户升级浏览器同时把构建产物里的Wasm做分层加载先出可交互的加载页再拉Wasm包体验上会好很多。5.2 为什么我捕获到的堆栈是乱码符号映射问题第一次把方案三的上报数据拉到后台时我看到的堆栈长这样at wasm-function[1427]:0x10235e at wasm-function[31]:0x8ab2c1 at wasm-function[258]:0x1a3f5b完全没有函数名根本没法定位。原因在于Unity WebGL默认不做符号保留要把Wasm堆栈映射回可读的C#函数名需要先开启构建选项里的Debug Symbols然后拿到Build/xxx.symbols.json文件。拿到symbols文件后写一个简单的映射工具把堆栈里的十六进制地址和函数名对应起来。如果只是偶尔排查几次手动在同目录下用脚本查也行。如果是线上长期监控建议接第三方日志平台他们对Wasm符号还原的支持比自研来得省事。这里推荐一个折中做法本地调试时用Unity的Development BuildAuto Connect Profiler跑看到的是可读堆栈线上环境收集原始十六进制堆栈不截断然后保留构建记录和symbols文件出了线上问题再批量还原。这个流程我用了小半年效率比对着十六进制堆栈发呆高得多。5.3 调试组合拳一套开发期联调的完整流程综合三个方案我日常开发期的调试流程是这样的本地跑Unity Editor时直接用Console看C#日志此时方案一不启用也无所谓因为Editor自带日志窗口。冲一次WebGL构建做联调时打开浏览器DevTools正式加载方案一的jslib桥接 方案二的全局监听。这个时候报错既能看到Unity日志也能看到JS侧的完整调用栈。准备发版时跑构建期注入脚本方案三让每次构建产物都自动带上线上上报能力。这套组合拳打通后我后来接到线上反馈基本不再需要用户开DevTools截图后台能直接看到错误堆栈、浏览器版本、执行到哪个关卡。尤其是在做一些营销页H5游戏时跨团队协作的时候直接把日志链接丢给前端负责人省掉大量复现不了、本地正常的循环沟通。最后分享一个小技巧在Unity WebGL中如果你用了第三方JS SDK比如统计SDK、广告SDKSDK自己可能也会捕获并吞掉错误导致你的全局监听拿不到。遇到这种情况可以在DevTools的Source面板里给SDK的catch地方打断点或者在集成SDK时主动关闭SDK的自动上报错误选项把错误统一交给自己的上报体系来处理。这是我被广告SDK坑了一整天后总结出来的希望对你有用。
返回列表