资讯动态

Cocos Creator 接入 Bugly:原生与 JS/TS 桥接的崩溃监控实战

发布时间:2026/10/15 12:57:49 来源:尧图企业网站定制
一个 Cocos Creator 项目在进入真机测试之后最让我心里没底的往往不是玩法做没做完而是用户设备上崩了我完全不知道。崩溃监控这件事业界的通用答案就是接 Bugly。Bugly 负责把崩溃日志收回到后台开发者再根据堆栈定位问题。不过它跟 Cocos Creator 的关系有点微妙Bugly 是典型的原生 SDK而 Cocos Creator 的项目里既有 Android/iOS 原生壳又有 JS/TS 逻辑层接入的时候不能只做原生侧的初始化还要把两层之间的桥接打通。这篇文章就是记录我自己在一个模拟项目X里完成这套接入的过程重点讲 Android/iOS 各自的初始化、JS/TS 层如何把异常主动喂给 Bugly以及在验证阶段怎么判断自己到底接没接对。适合正在给 Cocos Creator 项目补崩溃监控的同学参考不管你的项目是 2.x 的老工程还是 3.x 的新工程核心思路都一样。1. 为什么崩溃监控要提前装以及 Cocos 接入 Bugly 的真正难点在哪1.1 一条崩溃日志比玩家写的十句反馈都有用游戏上线之后玩家可能因为闪退直接卸载也可能去应用商店刷一星差评。玩家骂人往往描述不清楚场景但崩溃日志不一样它会告诉我崩在哪个方法、哪一行、哪个调用栈。Bugly 这类工具真正的价值不是接上就完事而是把崩溃自动分类甚至能帮你做符号还原。Android 的 so 库崩溃、iOS 的 Objective-C 异常后台都能按堆栈聚合省掉大量对日志、查版本的时间。我在模拟项目X里刚开始接 Bugly 时以为只是加一个依赖、调一行代码的事。真的深入进去才发现这事的复杂度完全取决于你的游戏逻辑写在哪个层。Cocos Creator 项目跟纯原生 App 最大的区别是它至少有两条崩溃线要管。1.2 Cocos Creator 天然分成两层Bugly 只能抓住其中一层Cocos Creator 出的包外皮还是标准的 Android/iOS 原生工程里面跑的却是 Cocos 引擎和你的 TypeScript 游戏逻辑。Bugly 扎在这一层原生壳里所以它能自动抓到的是AppActivity、Objective-C 文件、底层渲染库这些原生代码发生的崩溃。至于你 TS 代码里的空指针、数组越界、某个回调里抛出来的TypeError绝大多数情况下不会让整个原生进程崩溃自然也就不会出现在 Bugly 后台里。这就是 Cocos 项目接 Bugly 的特殊之处原生崩溃能自动上报但 JS/TS 层的异常必须由你主动捕获、主动上报。不少团队集成完 Bugly原生崩溃测出来没问题就收工了结果线上玩家反馈游戏卡在一个界面不动、点按钮没反应后台一条数据都没有就是因为 JS 异常没被喂进去。1.3 这次要打通的完整链路我这次在模拟项目X里要验证的目标有三条Android 原生崩溃自动上报到 BuglyiOS 原生崩溃自动上报到 BuglyTS 层捕获到的 JS 异常通过桥接调用原生方法手动塞给 Bugly。前两条是 SDK 的基本能力配置好了就是自动的。第三条需要我在原生代码里暴露一个静态方法再在 TS 层做一个全局错误监听器。整体工作量不大但每一步都容易出小岔子下面按平台拆开讲。2. Android 侧依赖、初始化、混淆Release 也能正常上报2.1 先找到正确的 build.gradle 文件Cocos Creator 2.x 生成的 Android 工程一般在proj.android-studio目录3.x 则在native/engine/android目录。接入时很多人会犯一个错误把依赖加到最外层的build.gradle那个是工程根配置加错地方会导致 Android Studio 同步后根本找不到库。要改的是app模块下面的build.gradle文件里通常能看到applicationId、compileSdkVersion这些配置。我在模拟项目X里用的依赖写法是这样的dependencies { implementation com.tencent.bugly:crashreport:latest.release }latest.release是 Bugly 官方支持的一种版本写法会自动拉取最新版。生产环境我更建议锁一个具体版本比如4.1.9.6这种免得哪天底层 SDK 更新后行为变了你排查半天才发现是升级带的坑。不过如果你一时拿不准最新版本用latest.release快速跑通验证是没问题的。2.2 初始化代码放进 AppActivity比放别处同致少很多Cocos 2.x 的默认工程里入口 Activity 一般是AppActivity继承自Cocos2dxActivity文件位置通常在src/org/cocos2dx/javascript/AppActivity.java。Cocos 3.x 的入口类位置会有点不一样但同样找 onCreate。我选择在AppActivity.onCreate里初始化而不是单独去建一个 Application。原因很简单默认工程没有自定义 Application建一个还得去 Manifest 里注册多一步还容易漏。如果你项目里本来就有 Application那放在 Application 里更好毕竟初始化时间越早越能抓到早期崩溃。public class AppActivity extends Cocos2dxActivity { Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); CrashReport.initCrashReport(this, 你的BuglyAppId, false); } }第三个参数是是否开启 debug 模式。联调阶段传true日志会打得更全便于用 logcat 直接过滤Bugly关键字看初始化结果准备上架 release 包时一定要改成false否则会有重复上报、日志过多的问题。我见过有人提交包之前忘了改后台同一崩溃被刷出几十条非常干扰判断。2.3 Manifest 权限和混淆规则release 包没数据先查这两样权限方面Bugly 至少需要联网权限。模拟项目X的AndroidManifest.xml里我加了两条uses-permission android:nameandroid.permission.INTERNET / uses-permission android:nameandroid.permission.ACCESS_NETWORK_STATE /如果不加ACCESS_NETWORK_STATESDK 无法准确判断当前网络状态上报时机和失败重试都会受影响。然后是混淆。如果你自己开启了 minifyEnabled特别是 release 包务必要在 ProGuard 或 R8 规则文件里加这样一段-dontwarn com.tencent.bugly.** -keep class com.tencent.bugly.** { *; }Warn 甚至 keep 住 SDK 的类是为了不让混淆把 Bugly 内部类重新命名。混淆后类名变成a.b.c再传回后台做不了符号还原而崩溃信息很可能因为反射调用受到影响直接传不出去。很多debug 包有日志release 包没数据的案子八成出在这里。2.4 用一行代码主动制造原生崩溃来验证接入完别急着打包先在代码里埋一个测试入口。我用的是调试菜单里藏了一个按钮点击之后执行throw new RuntimeException(bugly native crash test);然后打包到真机运行点击按钮应用闪退过几分钟打开 Bugly 后台看有没有这条崩溃。如果后台能出现bugly native crash test字样说明从采集到上传的整条链路是通的。测完记得把这个入口摘掉别带着崩溃测试按钮上架。3. iOS 侧Podfile 和 AppDelegate跟 Android 是两条平行路线3.1 没有 Podfile 的 Cocos 工程先补一个Cocos Creator 生成的 iOS 工程有些默认带 Podfile有些老工程是纯手工管理依赖的。模拟项目X用的方案是 CocoaPods在ios工程目录下建一个Podfile内容大致是这样platform :ios, 10.0 target GameName do pod Bugly end写完之后执行pod install以后打开工程就别再点.xcodeproj了要打开.xcworkspace。这一步有人会漏改了一堆代码发现编译不过其实就是打开错了工程文件。如果你的项目不用 CocoaPods也可以手动把Bugly.framework拖进工程在 Build Settings 里设置 Other Linker Flags 加上-ObjC。两种方式本质上一样但我个人更倾向于 Podfile版本管理干净升级就是改一行pod Bugly, ~ 2.2.5。3.2 在 AppDelegate 里初始化调试时能看到本地日志Cocos 2.x 的 iOS 工程里有一个AppDelegate.m3.x 通常也有对应的 AppDelegate 文件。初始化代码放在didFinishLaunchingWithOptions最前面确保 Bugly 尽早运行#import Bugly/Bugly.h - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { [Bugly startWithAppId:你的iOS端BuglyAppId]; // 其他初始化... return YES; }iOS 的 SDK 没有像 Android 那样在代码里放一个 debug 开关而是通过工程配置和宏来控制。联调阶段可以在启动函数附近临时加一行日志输出用 Xcode 控制台过滤 Bugly 相关输出看是否能打印出初始化成功的信息。我在模拟项目X里试过能打出带有当前版本、设备标识的日志基本就说明 SDK 已经跑起来了。3.3 同一个 Bugly 控制台Android 和 iOS 是两套 AppID这里要特别提醒Bugly 后台创建产品时Android 和 iOS 会分别生成 AppID。很多第一次接的人以为随便拿一个平台 ID 两边都能用结果 iOS 端填了 Android 的 AppID控制台里自然什么都收不到。我的做法是先在后台建一个某游戏的产品在里面分别添加 Android 应用和 iOS 应用然后把两个 AppID 分开记Android 端配置写在 gradle 或 Java 文件里iOS 端配置写在 AppDelegate 里。两端可以共用一个崩溃分析后台但 AppID 绝不混用。4. 打通 JS/TS 到原生这是 Cocos 接 Bugly 最容易被忽略的部分4.1 不接桥接你会漏掉最危险的线上问题原生崩溃确实严重但玩家实际遇到更多的可能是点了某个按钮完全没反应、背包打开是空的、切场景黑屏这类 JS/TS 逻辑错误。它们不崩原生进程却直接影响流失。Bugly 本身捕获不到这些所以必须让 TS 层在捕获到全局未处理错误时主动调用原生上报。我在模拟项目X里给 Bugly 后台起的名字叫JS异常跟原生崩溃分开看这样能判断到底是引擎层的问题还是自己业务逻辑的问题。如果什么都不做线上出问题全靠玩家录屏和客服转述排查效率会低很多。4.2 原生端暴露静态上报方法Android 端我写了一个NativeBuglyHelper.java作为 JS 和 Bugly 之间的中转站package com.example.game; import com.tencent.bugly.crashreport.CrashReport; public class NativeBuglyHelper { public static void setPlayerId(String playerId) { CrashReport.setUserId(playerId); } public static void reportJsException(String message, String stack) { // 将 JS 的堆栈信息拼进异常描述方便后台直接展示 RuntimeException e new RuntimeException(JSException: message \n stack); CrashReport.postException(e); } }这里用的是CrashReport.postException(Throwable)这条 API把 JS 侧拿到的错误信息包装成一个 RuntimeException 上报。注意模拟项目X实际跑下来后台展示的时候调用栈信息是我 Java 侧这一层的原始 JS 堆栈会出现在异常 message 里所以 message 里的内容宁长勿短尽量把 source、line、column 都拼进去。iOS 端我在 AppDelegate 里加了一个同名的类方法逻辑类似 (void)setPlayerId:(NSString *)playerId { [Bugly setUserIdentifier:playerId]; } (void)reportJsException:(NSString *)message stack:(NSString *)stack { NSException *exception [NSException exceptionWithName:JSException reason:[NSString stringWithFormat:%\n%, message, stack] userInfo:nil]; [Bugly reportException:exception]; }不同版本 Bugly 提供的自定义上报接口名称可能有差异在你自己的头文件里搜reportException或reportError就能找到对应方法。思路是把它包进去而不是自己去模拟崩溃。4.3 TS 侧挂上全局错误监听在 TS 层我用了一个独立的工具模块名字叫BuglyBridge.ts。核心逻辑是const jsb (globalThis as any).jsb; // 判断是否原生环境 export function isNativePlatform(): boolean { return typeof jsb ! undefined !!jsb.reflection; } export function setPlayerId(playerId: string): void { if (!isNativePlatform()) return; if (cc.sys.platform cc.sys.Platform.ANDROID) { jsb.reflection.callStaticMethod( com/example/game/NativeBuglyHelper, setPlayerId, (Ljava/lang/String;)V, playerId ); } else if (cc.sys.platform cc.sys.Platform.APPLE) { // iOS 上原生类不分包名方法带冒号 jsb.reflection.callStaticMethod( NativeBuglyHelper, setPlayerId:, playerId ); } } export function reportJsError(message: string, stack: string): void { if (!isNativePlatform()) return; if (cc.sys.platform cc.sys.Platform.ANDROID) { jsb.reflection.callStaticMethod( com/example/game/NativeBuglyHelper, reportJsException, (Ljava/lang/String;Ljava/lang/String;)V, message, stack ); } else if (cc.sys.platform cc.sys.Platform.APPLE) { jsb.reflection.callStaticMethod( NativeBuglyHelper, reportJsException:stack:, message, stack ); } }然后在游戏入口初始化时挂上全局监听export function installGlobalErrorReporter(): void { if (typeof window undefined) return; window.addEventListener(error, (event) { const message event.message || unknown js error; const stack event.error?.stack ? event.error.stack : message; reportJsError(message, stack); }); window.addEventListener(unhandledrejection, (event) { let reason event.reason || ; if (typeof reason object) { reason JSON.stringify(reason); } reportJsError(UnhandledPromiseRejection, String(reason)); }); }要注意的是如果你用的是 Cocos Creator 3.x 且版本较新jsb.reflection在新版本里可能被调整或移除。我在模拟项目X里主要跑的是 2.x 和 3.x 早期版本这条路径有效。新版本如果发现jsb.reflection不存在就去读官方文档里最新的原生桥接方案思路完全一样原生暴露方法JS 侧通过反射或消息通道调用。别被 API 名字变化吓到本质就是两扇门之间的钥匙。4.4 顺手把玩家标识和场景标签传上去崩溃日志里如果能带上玩家 ID、服务器 ID、渠道名复现问题的效率会翻倍。Bugly 本身提供了用户维度 API所以我在桥接层里顺便封装了setPlayerId。业务代码里只要在登录成功后调一次后续所有崩溃都会自动关联到这个玩家 ID。场景标签同样有价值。比如玩家在看新手引导时崩溃、在打开商城时崩溃给那个模块打一个 tag后台就能按标签聚合。Android 端使用CrashReport.setUserSceneTagiOS 端对应接口在头文件里能找到。这部分不是必须的但强烈建议至少把玩家 ID 传上去否则后台只能看到一个模糊的崩溃设备无法对应到具体用户会话。5. 验证是否真生效产物、日志、后台三对账5.1 先检查打包产物别上来就怀疑后台接入之后最怕的是我明明加了怎么后台没有。我现在的习惯是先看产物再做测试最后才去后台。Android 打包完 APK用解压工具打开在classes.dex或包结构里找com/tencent/bugly目录是否存在。如果连这个目录都没有说明编译时依赖就没进去别浪费时间测了。iOS 打包完看工程里是不是包含Bugly.framework或者去.app/Frameworks目录里搜一下。5.2 三种崩溃测试分别验证三条链路我在模拟项目X里固定会做三种主动崩溃测试原生崩溃调前面写的throw new RuntimeException(bugly native crash test)验证自动上报JS 异常在 TS 代码里写一行null.abc()触发TypeError验证全局监听和桥接上报混合场景在原生代码里加一段CrashReport.postException再在后台看自定义异常是否出现。前两种测试做完后台大概五到十分钟内能看到新崩溃上传。如果后台迟迟没动静问题多半不在 SDK而在初始化时机、AppID、混淆、权限这几项。记得测试时让手机切一次网络某些网络环境下 SDK 会延迟上报不切换容易误判。5.3 后台查不到数据的排查清单我把模拟项目X里排查没数据问题用到的检查项整理成了一张表排查点检查方式AppID 是否正确Android/iOS 分别核对后台生成的产品 ID别串用debug 模式是否开启Android 初始化第三参数为 true 时上报可能受干扰Manifest 是否缺权限确认 INTERNET 和 ACCESS_NETWORK_STATE 都加了混淆是否挡住 SDKrelease 包用解压工具确认类存在规则是否 keep初始化是否太晚Application 或入口 Activity 的 onCreate 最安全上报后是否切网络等 5-10 分钟切换 Wi-Fi/4G 再次打开应用iOS 端是否错误关闭异常捕获查看 AppDelegate 里初始化前后是否有异常处理逻辑干预这套清单基本覆盖了我踩过的坑照着查能省大半天时间。6. 一个少踩两步坑的接入顺序我的实际操作习惯模拟项目X跑完这套流程后我个人总结出一个固定的接入顺序以后每个游戏项目我都打算这么干先把 Android 原生初始化、权限、混淆配好用主动崩溃验证后台能收到再去做 iOS 同样的事情。两端原生跑通后才写 TS 桥接层和全局错误监听。最后做一次 release 包又做一次 debug 包的对比验证。顺序不要反过来。如果先写了 TS 桥接再配原生初始化一旦后台老没数据你会分不清是桥接问题、初始化问题还是混淆问题排查范围一下大好几倍。我早期接过一次就是因为反过来做最后发现只是混淆规则漏了一条。联调阶段可以在installGlobalErrorReporter里加一行日志在发布到原生前先用浏览器预览版或模拟器确认window.onerror能正确触发。这样能把 JS 层的问题隔离在原生接入之前解决。上线前记得把 Android 的 debug 参数关掉、把崩溃测试按钮移除、把 AppID 从占位符换成正式值。现在团队里无论谁接手新游戏我都是把这几步写成一个 check list 发过去。照着走一遍原生崩溃和 JS 异常两条线都能在后台看到持续有效的数据。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑