Cocos Creator 2.x手动集成AdMob SDK实战:从架构设计到上架避坑指南 1. 项目概述为什么我们要手动集成AdMob SDK如果你正在用Cocos Creator 2.x开发一款手游并且希望它能通过广告变现那么Google AdMob几乎是绕不开的选择。市面上虽然有现成的插件比如Cocos Store里那个但你会发现它往往只支持最新的Cocos Creator 3.x版本。对于大量仍在使用稳定、成熟的2.4.x或2.3.x版本的项目来说直接使用插件这条路基本就断了。更不用说插件有时会带来额外的兼容性问题、版本锁死或者你根本不想引入一个“黑盒”来增加项目的复杂度。所以手动集成AdMob SDK就成了一个必须掌握的硬核技能。这不仅仅是把几个文件拖进工程那么简单它涉及到原生平台Android/iOS与Cocos JavaScript引擎之间的通信、不同SDK版本的适配、以及最终让游戏成功上架App Store的完整流程。我最近刚完成了一个2.4.10项目的AdMob集成和iOS上架整个过程堪称一部“踩坑百科全书”。从“SDK冲突导致构建失败”到“iOS 14的ATT权限弹窗”再到“上架审核被拒因为广告标识符使用不当”几乎把能遇到的雷都踩了一遍。这篇文章就是我这趟“填坑之旅”的完整记录。我会详细拆解从零开始手动集成Google Mobile Ads SDK到Cocos Creator 2.x项目的每一步重点不是“做什么”而是“为什么这么做”以及“做的时候可能会遇到什么坑”。无论你是独立开发者还是小团队的技术负责人这份避坑指南都能帮你节省大量查文档、搜论坛、熬夜调试的时间。2. 核心思路与架构设计打通JS与原生层的桥梁手动集成SDK核心目标是在Cocos的JavaScript逻辑层与Android/iOS的原生广告SDK之间建立一条稳定、高效的通信通道。Cocos Creator 2.x的架构决定了我们必须通过它的原生扩展机制来实现这一点。2.1 为什么选择手动集成而非等待插件首先我们得明确手动集成的优势。最大的好处是控制力。你可以精确控制使用的AdMob SDK版本避免插件内置的过时版本带来未知问题。其次是灵活性。你可以根据自己游戏的UI框架和交互逻辑定制广告的展示时机、位置和样式而不是被插件的固定接口限制。最后是可维护性。自己写的代码每一行都清楚其作用当出现问题时排查和修复的路径非常清晰。对于需要长期运营的项目这份“家底”的透明度至关重要。2.2 通信架构设计JSBridge是核心整个集成的架构可以概括为“两层桥接”JavaScript层在Cocos的TypeScript/JavaScript脚本中定义一套调用广告功能的接口例如showBannerAd(),loadRewardedVideo()。原生桥接层这是最关键的部分。在Android上我们需要编写Java代码通过Cocos提供的Cocos2dxJavascriptJavaBridge来调用JS函数反之亦然。在iOS上则需要使用Objective-C或Swift和JavaScriptCore框架来实现与JS的相互调用。原生SDK层在桥接层中直接调用Google Mobile Ads SDK的原生API执行加载、展示广告等实际操作。这个架构中JSBridge的稳定性和正确性决定了整个广告功能的成败。一个常见的误区是只实现了单向调用如JS调用原生而忽略了广告事件如加载完成、奖励发放从原生层回调给JS层这会导致广告状态无法同步逻辑混乱。2.3 广告类型与集成策略选择AdMob主要提供横幅广告、插页式广告、激励视频广告和原生广告。对于游戏最常用的是激励视频用于奖励复活、货币等和插页广告关卡间隙。横幅广告可能影响游戏体验需谨慎使用。我的策略是分阶段集成先实现激励视频因为它变现效率高且用户接受度相对较好再实现插页广告最后根据需要添加横幅。在代码结构上我会在JS层封装一个统一的AdManager单例类内部根据平台Android/iOS调用不同的桥接方法这样游戏业务逻辑只需要和AdManager打交道实现了平台无关性。3. Android平台集成实战与避坑指南Android端的集成相对复杂因为涉及Gradle构建、依赖管理以及多版本兼容问题。以下步骤基于Cocos Creator 2.4.10和Google Mobile Ads SDK Android v22.x。3.1 环境准备与SDK获取首先确保你的开发环境有Android SDK和NDK并且Cocos Creator中配置的SDK路径正确。接下来获取AdMob SDK不建议直接下载jar或aar文件这会导致后续依赖管理困难。正确做法是通过Google的Maven仓库引入。打开你的Android项目模块下的build.gradle文件通常位于项目目录/build/jsb-link/frameworks/runtime-src/proj.android-studio/app/。在dependencies块中添加最新版本的AdMob依赖dependencies { implementation com.google.android.gms:play-services-ads:22.6.0 // Cocos和其他依赖... }注意版本号22.6.0请务必查询 Google Maven仓库 以获取最新稳定版。盲目使用旧版本可能导致新设备上的兼容性问题。3.2 修改AndroidManifest.xml与权限配置广告SDK需要网络权限和广告标识符权限。编辑AndroidManifest.xml文件uses-permission android:nameandroid.permission.INTERNET / uses-permission android:nameandroid.permission.ACCESS_NETWORK_STATE / !-- 用于获取广告标识符对于广告归因很重要 -- uses-permission android:namecom.google.android.gms.permission.AD_ID/接下来必须添加AdMob的App ID。这个ID在AdMob后台创建应用时获得。在application标签内添加meta-data android:namecom.google.android.gms.ads.APPLICATION_ID android:valueca-app-pub-xxxxxxxxxxxxxxxx~yyyyyyyyyy/巨坑提示这里的APPLICATION_ID千万不能写成你的广告单元ID如ca-app-pub-xxxxxxxxxxxxxxxx/yyyyyyyyyy。必须使用整个应用的App ID以波浪线~结尾。写错会导致SDK初始化失败广告无法加载且错误日志可能不明显。3.3 实现Java端桥接类JSBridge这是Android集成的核心。我们需要创建一个Java类例如AdMobBridge.java将其放在org.cocos2dx.javascript包下与Cocos默认的包结构一致。这个类主要做三件事初始化AdMob SDK在合适的时机如游戏启动时调用MobileAds.initialize(context)。提供静态方法供JS调用例如loadRewardedVideo(String adUnitId)。在这些方法里创建并配置RewardedAd对象。处理广告回调并通知JS层通过Cocos2dxJavascriptJavaBridge.evalString()方法执行一段JavaScript代码将广告事件如加载完成、用户获得奖励传递回游戏逻辑。关键代码片段示例public class AdMobBridge { private static RewardedAd mRewardedAd; private static Activity mContext; public static void init(Activity activity) { mContext activity; MobileAds.initialize(activity, new OnInitializationCompleteListener() { Override public void onInitializationComplete(InitializationStatus initializationStatus) { // 初始化成功通知JS callJS(window.jsb.reflection.onAdMobInitialized window.jsb.reflection.onAdMobInitialized()); } }); } public static void loadRewardedVideo(final String adUnitId) { if (mRewardedAd ! null) { return; } AdRequest adRequest new AdRequest.Builder().build(); RewardedAd.load(mContext, adUnitId, adRequest, new RewardedAdLoadCallback() { Override public void onAdLoaded(NonNull RewardedAd rewardedAd) { mRewardedAd rewardedAd; // 关键通知JS层广告已加载好 callJS(window.jsb.reflection.onRewardedVideoLoaded window.jsb.reflection.onRewardedVideoLoaded()); // 设置广告展示时的回调 mRewardedAd.setFullScreenContentCallback(...); } Override public void onAdFailedToLoad(NonNull LoadAdError loadAdError) { callJS(String.format(window.jsb.reflection.onRewardedVideoFailed window.jsb.reflection.onRewardedVideoFailed(%s), loadAdError.getMessage())); } }); } private static void callJS(final String script) { mContext.runOnUiThread(new Runnable() { Override public void run() { Cocos2dxJavascriptJavaBridge.evalString(script); } }); } }实操心得所有从原生回调到JS的调用必须放在runOnUiThread中执行。因为广告SDK的回调可能发生在非UI线程而Cocos的JS引擎要求在UI线程中进行交互否则会导致崩溃或界面无响应。3.4 在Cocos启动时初始化Bridge我们需要在Cocos的Android入口处初始化我们的AdMobBridge。修改AppActivity.java位于同一目录在onCreate方法中调用Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // ... Cocos原有代码 AdMobBridge.init(this); }3.5 Gradle构建常见问题排查依赖冲突如果你的项目引入了其他库如Firebase、某些第三方SDK可能会与Play Services Ads库发生版本冲突。使用./gradlew :app:dependencies命令查看依赖树使用exclude或强制指定统一版本号解决。MultiDex问题方法数超过65535限制。在app/build.gradle中启用MultiDexandroid { defaultConfig { multiDexEnabled true } } dependencies { implementation androidx.multidex:multidex:2.0.1 }“Unable to merge dex” 错误通常是重复的类定义。检查依赖确保没有重复引入不同版本的同一库如play-services-ads和play-services-ads-lite。使用Android Studio的“Refactor - Migrate to AndroidX”确保所有库都迁移到AndroidX避免Support库与AndroidX混用。4. iOS平台集成实战与上架前准备iOS端的集成流程与Android类似但环境是Xcode和CocoaPods。最大的挑战往往来自苹果的隐私政策如ATT框架和上架审核。4.1 使用CocoaPods集成SDK这是最推荐的方式能自动处理依赖。在Cocos Creator构建生成的Xcode项目目录下项目目录/build/jsb-link/frameworks/runtime-src/proj.ios_mac/如果还没有Podfile则创建它。编辑Podfile内容如下platform :ios, 12.0 # 根据你的项目最低部署目标调整 target 你的项目名称-mobile do use_frameworks! pod Google-Mobile-Ads-SDK, ~ 11.2 # 使用最新稳定版 end在终端中切换到该目录运行pod install。成功后关闭.xcodeproj文件打开新生成的.xcworkspace文件进行后续开发。4.2 实现Objective-C桥接类创建AdMobBridge.mm文件.mm扩展名支持Objective-C方便与C混合编译Cocos引擎需要。将其加入Xcode工程。核心任务是实现与Android端类似的功能初始化、加载广告、处理回调并通过JavaScriptCore通知JS。关键点在于如何暴露方法给JS。Cocos Creator 2.x的iOS端JS通过jsb对象调用原生。我们需要注册我们的类和方法// AdMobBridge.mm #import GoogleMobileAds/GoogleMobileAds.h #import JavaScriptCore/JavaScriptCore.h #import cocos/scripting/js-bindings/jswrapper/SeApi.h interface AdMobBridge : NSObject GADFullScreenContentDelegate property(nonatomic, strong) GADRewardedAd *rewardedAd; end implementation AdMobBridge (void)initializeAdMob { [[GADMobileAds sharedInstance] startWithCompletionHandler:^(GADInitializationStatus *status) { NSString *script window.jsb.reflection.onAdMobInitialized window.jsb.reflection.onAdMobInitialized();; evalString(script); // evalString是Cocos提供的执行JS字符串的函数 }]; } (void)loadRewardedVideo:(NSString *)adUnitId { AdMobBridge *bridge [AdMobBridge sharedInstance]; if (bridge.rewardedAd) { return; } GADRequest *request [GADRequest request]; [GADRewardedAd loadWithAdUnitID:adUnitId request:request completionHandler:^(GADRewardedAd *ad, NSError *error) { if (error) { NSString *script [NSString stringWithFormat:window.jsb.reflection.onRewardedVideoFailed window.jsb.reflection.onRewardedVideoFailed(%);, error.localizedDescription]; evalString(script); return; } bridge.rewardedAd ad; bridge.rewardedAd.fullScreenContentDelegate bridge; NSString *script window.jsb.reflection.onRewardedVideoLoaded window.jsb.reflection.onRewardedVideoLoaded();; evalString(script); }]; } // 实现GADFullScreenContentDelegate方法处理广告关闭、奖励发放等 - (void)adDidDismissFullScreenContent:(nonnull idGADFullScreenPresentingAd)ad { self.rewardedAd nil; NSString *script window.jsb.reflection.onRewardedVideoClosed window.jsb.reflection.onRewardedVideoClosed();; evalString(script); } // ... 其他回调方法如adWillPresent, adDidRecordImpression等 end // 注册到jsb.reflection命名空间下 void register_admob_bridge(se::Object* global) { se::Value jsbVal; global-getProperty(jsb, jsbVal); se::Object* jsbObj jsbVal.toObject(); se::Value reflectionVal; jsbObj-getProperty(reflection, reflectionVal); se::Object* reflectionObj reflectionVal.toObject(); // 注册静态方法 reflectionObj-defineFunction(initAdMob, _SE(AdMobBridge_initializeAdMob)); reflectionObj-defineFunction(loadRewardedVideo, _SE(AdMobBridge_loadRewardedVideo)); // ... 注册其他方法 }然后需要在Cocos引擎初始化的地方通常是AppDelegate.mm的application:didFinishLaunchingWithOptions:方法末尾调用这个注册函数。4.3 应对ATTApp Tracking Transparency框架这是iOS 14上架审核的必答题。如果应用收集用于广告追踪的数据IDFA必须征得用户同意。在Info.plist中添加NSUserTrackingUsageDescription键填写向用户请求权限的描述文案例如“此标识符将用于向您推送更相关的广告”。在合适的时机通常在游戏主界面加载后展示广告前请求授权。这需要在原生端调用[ATTrackingManager requestTrackingAuthorizationWithCompletionHandler:]。你可以将这个调用封装在桥接方法中由JS层决定何时触发。重要策略即使ATT授权被拒绝AdMob SDK仍然可以工作只是无法使用IDFA进行精准广告投放。你的代码需要能处理这两种情况不能因为用户拒绝就使广告功能完全崩溃。4.4 上架App Store前的终极检查清单隐私政策应用必须提供可访问的隐私政策链接并在应用内或App Store Connect中注明。隐私政策需说明你集成了AdMob收集了哪些数据如广告标识符、设备信息以及数据如何被使用和共享。广告标识符IDFA使用声明在App Store Connect的“App隐私”部分必须如实声明使用了IDFA并勾选所有适用的用途如第三方广告、分析。测试广告单元ID确保上架前将代码中的所有广告单元ID替换为正式的、从AdMob后台申请的ID。使用Google提供的测试ID如ca-app-pub-3940256099942544/5224354917会导致审核被拒。广告展示体验审核员会实际体验你的应用。确保广告不会在非用户主动触发的场景下干扰性弹出比如一进入游戏就弹插页广告激励视频的奖励发放机制必须明确且即时生效。网络权限描述Info.plist中的NSLocalNetworkUsageDescription和NSBonjourServices如果不需要建议清理避免不必要的权限询问。构建版本使用Release模式构建并确保Bitcode设置与你的项目配置一致Cocos项目通常建议禁用Bitcode以减小包体。使用正确的证书和描述文件进行归档。5. Cocos JavaScript层统一封装与管理原生层打通后我们需要在游戏脚本中创建一个好用的、统一的广告管理器。这个AdManager将封装平台差异为游戏业务逻辑提供干净的API。5.1 AdManager单例类的设计// AdManager.ts export default class AdManager { private static _instance: AdManager null; public static get instance(): AdManager { if (!this._instance) { this._instance new AdManager(); } return this._instance; } private _isInitialized: boolean false; private _rewardedAdReady: boolean false; // 初始化由游戏启动脚本调用 public initialize(): void { if (cc.sys.os cc.sys.OS.ANDROID || cc.sys.os cc.sys.OS.IOS) { // 调用原生初始化 if (cc.sys.isNative) { // 通过jsb.reflection调用我们在原生层注册的方法 if (cc.sys.os cc.sys.OS.ANDROID) { jsb.reflection.callStaticMethod(org/cocos2dx/javascript/AdMobBridge, init, ()V); } else if (cc.sys.os cc.sys.OS.IOS) { jsb.reflection.callStaticMethod(AdMobBridge, initAdMob); } // 监听原生回调 this._setupListeners(); } } else { console.log(AdMob not supported on this platform.); } } private _setupListeners(): void { // 将原生回调挂载到window对象供原生层调用 (window as any).jsb.reflection.onAdMobInitialized () { this._isInitialized true; console.log(AdMob SDK initialized.); // 可以开始预加载广告了 this.preloadRewardedVideo(你的激励视频广告单元ID); }; (window as any).jsb.reflection.onRewardedVideoLoaded () { this._rewardedAdReady true; // 可以通知UI更新按钮状态等 cc.systemEvent.emit(rewarded-video-loaded); }; (window as any).jsb.reflection.onRewardedVideoFailed (msg: string) { console.error(Rewarded video failed to load:, msg); // 可以安排重试逻辑 this.scheduleOnce(() { this.preloadRewardedVideo(你的激励视频广告单元ID); }, 30); // 30秒后重试 }; (window as any).jsb.reflection.onUserEarnedReward () { // 最重要的回调发放游戏内奖励 cc.systemEvent.emit(user-earned-reward); // 奖励发放后立即重新预加载下一个广告 this._rewardedAdReady false; this.preloadRewardedVideo(你的激励视频广告单元ID); }; } public preloadRewardedVideo(adUnitId: string): void { if (!this._isInitialized) return; if (cc.sys.os cc.sys.OS.ANDROID) { jsb.reflection.callStaticMethod(org/cocos2dx/javascript/AdMobBridge, loadRewardedVideo, (Ljava/lang/String;)V, adUnitId); } else if (cc.sys.os cc.sys.OS.IOS) { jsb.reflection.callStaticMethod(AdMobBridge, loadRewardedVideo:, adUnitId); } } public showRewardedVideo(): Promiseboolean { return new Promise((resolve) { if (!this._rewardedAdReady) { cc.log(Rewarded video is not ready yet.); resolve(false); return; } // 监听奖励发放事件 const onEarned () { cc.systemEvent.off(user-earned-reward, onEarned); resolve(true); // 用户看完了广告并获得奖励 }; const onClosed () { cc.systemEvent.off(rewarded-video-closed, onClosed); // 如果奖励事件没有触发则resolve(false) // 这里需要一个状态位来判断简化处理 setTimeout(() { if (!rewardGranted) { // rewardGranted 需要在 onEarned 中设为true resolve(false); // 用户关闭了广告但未获得奖励如提前关闭 } }, 500); }; cc.systemEvent.on(user-earned-reward, onEarned); cc.systemEvent.on(rewarded-video-closed, onClosed); // 调用原生展示方法 if (cc.sys.os cc.sys.OS.ANDROID) { jsb.reflection.callStaticMethod(org/cocos2dx/javascript/AdMobBridge, showRewardedVideo, ()V); } else if (cc.sys.os cc.sys.OS.IOS) { jsb.reflection.callStaticMethod(AdMobBridge, showRewardedVideo); } this._rewardedAdReady false; // 展示后立即置为未就绪 }); } }5.2 在游戏中的使用示例在游戏启动场景的onLoad中初始化// GameLaunch.ts import AdManager from ./AdManager; onLoad() { AdManager.instance.initialize(); }在需要展示激励视频的地方如复活按钮点击// RevivalUI.ts async onReviveButtonClick() { const rewardGranted await AdManager.instance.showRewardedVideo(); if (rewardGranted) { // 发放游戏内奖励复活玩家、增加金币等 this.revivePlayer(); cc.tween(this.reviveButton).to(0.3, {scale: 1.2}).to(0.3, {scale: 1}).start(); } else { // 广告未加载或用户未看完 this.showToast(广告未准备好或未完整播放); } }6. 全流程避坑实录与疑难杂症排查即使按照上述步骤操作在实际打包和运行中你依然可能遇到各种奇怪的问题。下面是我遇到的一些典型问题及其解决方案。6.1 Android端常见问题问题1构建失败报错“Program type already present: com.google.ads.xxxx”原因这是典型的依赖冲突或重复。你可能同时引入了完整版play-services-ads和某个子模块如play-services-ads-lite或者与其他包含AdMob旧版本如某些第三方聚合SDK的库冲突。解决执行./gradlew :app:dependencies分析依赖树。在build.gradle中使用exclude排除冲突的模块或使用resolutionStrategy强制指定统一版本。最彻底的方案是如果使用了第三方SDK联系其提供商确认其内置的AdMob版本并尝试让你的主依赖与其保持一致。问题2广告能加载但无法展示Logcat中看到“Ad failed to load : 3”原因错误码3通常对应ERROR_CODE_NO_FILL即广告请求成功但当前没有可投放的广告广告填充率问题。解决测试阶段确保你使用的是Google提供的测试广告单元ID。正式ID在未上线或流量极低时可能无填充。正式环境这是正常现象需要优化广告投放策略如调整广告格式、定位选项或考虑使用广告中介如AdMob Mediation来提高填充率。检查AdMob后台该广告单元是否已正确启用并与应用关联。问题3在部分Android设备特别是华为等上崩溃原因可能是缺少必要的支持库如AndroidX或者SDK版本与设备系统WebView版本不兼容。解决确保项目已完全迁移到AndroidX。在build.gradle中指定支持库版本android { defaultConfig { // ... multiDexEnabled true } } dependencies { implementation androidx.multidex:multidex:2.0.1 // 明确指定核心库版本避免冲突 implementation androidx.appcompat:appcompat:1.6.1 implementation com.google.android.gms:play-services-ads:22.6.0 }提醒用户保持系统和Chrome/WebView为最新版本。6.2 iOS端常见问题问题1pod install失败提示找不到Google-Mobile-Ads-SDK原因CocoaPods源问题或网络问题。解决运行pod repo update更新本地仓库。检查Podfile顶部是否指定了源source https://cdn.cocoapods.org/使用CDN源速度更快。如果使用代理确保终端能正常访问。问题2构建成功但运行时广告不加载Xcode控制台输出Google To get test ads on this device, set: GADMobileAds.sharedInstance.requestConfiguration.testDeviceIdentifiers [ xxx ]原因这是正常提示说明你正在使用测试广告单元ID并且当前设备未被添加到测试设备列表。广告仍会加载但可能是正式广告。解决如果你想确保看到测试广告可以在初始化AdMob后添加以下代码仅限开发调试GADMobileAds.sharedInstance.requestConfiguration.testDeviceIdentifiers [ kGADSimulatorID ]; // 模拟器 // 或者获取真机的测试设备ID运行一次查看控制台输出的 To get test ads... 后面的那串字符 GADMobileAds.sharedInstance.requestConfiguration.testDeviceIdentifiers [ 你的设备ID字符串 ];问题3上架审核被拒理由为“App Tracking Transparency permission is missing”原因审核员认为你的应用使用了IDFA广告标识符但没有正确实现ATT弹窗。解决再次确认Info.plist中已添加NSUserTrackingUsageDescription。确认在代码中调用了requestTrackingAuthorizationWithCompletionHandler:方法。即使你决定在用户拒绝后不展示个性化广告也必须调用此方法并处理回调。在App Store Connect的“App隐私”问卷中关于跟踪的部分必须如实、完整地填写。如果确信自己处理正确可以录制一个屏幕视频展示从安装到触发ATT弹窗的完整流程作为申诉材料提交给审核团队。6.3 跨平台通用问题问题广告显示一次后再次调用展示无效原因激励视频或插页广告对象是“一次性”的。展示后该广告对象就失效了必须重新加载一个新的。解决这是最重要的设计模式。必须在广告关闭回调onAdDismissedFullScreenContent或奖励发放回调中将当前的广告对象置为null并立即触发下一次的预加载。我们的AdManager示例代码中已经体现了这一点在onUserEarnedReward回调中将_rewardedAdReady设为false并调用preloadRewardedVideo。问题在弱网环境下广告加载超时或失败影响用户体验解决实现健壮的重试和降级逻辑。指数退避重试首次加载失败后等待一段时间如2秒重试再次失败则等待更长时间如4秒、8秒并设置最大重试次数。超时控制为广告加载设置一个超时如10秒超时后直接视为失败进行重试或向用户提示“广告暂时不可用”。优雅降级如果多次重试仍失败可以暂时隐藏广告按钮或提供替代的激励获取方式如观看广告失败后允许用户通过分享来获得少量奖励。手动集成AdMob SDK到Cocos Creator 2.x项目是一条需要耐心和细心打通的路径。它没有一键式的便捷但带来的深度控制和对项目架构的清晰理解是使用预制插件无法比拟的。整个过程最磨人的地方往往不是代码本身而是原生环境的配置、依赖冲突的解决以及应对平台政策的变化。希望这份结合了具体操作步骤和大量实战踩坑经验的记录能成为你集成路上的有效参考帮你避开我走过的那些弯路更顺利地将你的游戏推向市场并实现变现。