资讯动态

Unity接入原生Android穿山甲广告SDK:从工程配置到桥接实现

发布时间:2026/10/17 10:33:15 来源:尧图企业网站定制
1. 项目背景与整体接入思路1.1 为什么Unity项目要接原生Android穿山甲做Unity开发的朋友应该都有同感引擎自带的广告SDK出海还行但回到国内安卓生态填充率、ecpm和适配度都差点意思。尤其你要跑信息流、激励视频这类主流变现形式绕不开的就是穿山甲这类国内头部广告平台。但穿山甲的官方SDK又没做正式的Unity插件包所以我们需要自己动手把原生Android端的SDK接进Unity工程里。这个“自己动手”听起来有点麻烦实际操作下来其实就是一套固定的适配流程把SDK放进Gradle工程、用Java/Kotlin写一层桥接类、再通过Unity的AndroidJavaObject机制暴露C#接口给Unity调用。核心工作量不在原生端而在两边的生命周期和回调怎么对齐。搞清楚这个后面所有问题都顺了。1.2 原生接入和封装SDK该怎么选我见过不少团队在这卡住先纠结要不要用第三方打包好的Unity SDK。说实话那些打包好的Unity插件包也存在一些现实问题——版本更新慢、源码不透明、广告位模板不好定制偶尔出问题还得求着维护方改。而原生接入看着费劲但每一步自己都能控制平台出什么新功能你只要升级原生依赖就行不用等二次封装。本方案适合的人群比较明确想要精细控制广告展示时机的Unity开发者担心第三方Unity封装SDK更新不及时、问题定位困难的小团队有成熟Android工程希望Unity模块和原生模块共存的项目组需要同时接入多家广告平台做聚合对比想要统一管理回调的开发者只要你的Unity项目最终是导出Android工程或按Android Library模式打包这套原生接入方案就完全适用。今天这篇文章我会从工程配置、底层桥接、代码实现、问题排查几个维度把这个过程完整走一遍。2. 环境准备与基础工程配置2.1 Unity工程需要提前做哪些设置Unity端接入原生Android SDK第一个坑往往不是代码而是导出配置没做对。我建议在工程一开始就检查三个关键项包名一定要和穿山甲后台注册的包名一致别等到提审前才改那样会牵扯到各种渠道配置脚本后端推荐用IL2CPP ARM64当前主流应用商店基本都要求支持64位架构用Mono在Android 14以上的新机子上容易碰到兼容问题Gradle导出的模式建议选“Export Project”方便你直接在Android Studio里看依赖树、查冲突这几项花五分钟就能确认但能省掉后面一大半的排查时间。尤其是IL2CPP穿山甲SDK本身是Java层代码和Scripting Backend没有直接关系但打包后需要处理的符号裁剪、native崩溃堆栈的问题IL2CPP环境下会更容易定位。Unity版本方面我自己的实践是Unity 2020.3 LTS以上都没有问题更高版本反而少踩一些AndroidX的坑。如果你还在用Unity 2018不支持完整Gradle工程接入的复杂度会明显上升建议趁早升级。2.2 穿山甲SDK怎么集成到Gradle工程现在平台的接入方式已经比较统一通过Gradle依赖或者直接下载aar包。Gradle依赖更推荐因为后续升级版本只需改一行版本号不用每次手动替换aar文件。在Unity导出的原生工程里打开根目录的build.gradle在allprojects或dependencyResolutionManagement的仓库列表里加上穿山甲的maven仓库地址。随后在launcher模块的build.gradle中添加SDK依赖。如果是按依赖库模式接入就在unityLibrary的build.gradle里加同样的依赖这里要特别注意依赖模块的位置。还有一个容易忽略的地方穿山甲SDK会依赖AndroidX相关库。如果你的Unity工程没有开启AndroidX需要在gradle.properties里加上一句android.useAndroidXtrue android.enableJetifiertrueJetifier主要用来兼容那些还停留在support库时代的第三方SDK开了它会让打包时间稍微变长但必要性高。如果你不确定当前工程里有没有老support库依赖直接用这组配置最稳妥。2.3 动态申请权限与隐私弹窗的前置检查Android 6.0之后运行时权限是要在代码里动态申请的。穿山甲SDK的大部分能力比如广告拉取并不强制要求你在启动时就申请所有权限但不申请定位权限可能会导致部分广告填充率下降这个和广告主定向投放策略有关。我自己的项目处理方式是启动时只申请必要的存储权限定位权限放在用户进入“个性化推荐设置”页面时再去申请。游戏类App如果一上来就是一堆授权弹窗玩家流失率很直观所以能缓就缓。穿山甲也提供了隐私弹窗配置接口这个建议认真接部分审核渠道会检查隐私合规情况。SDK初始化时可以直接把用户隐私同意状态传进去让SDK在合规前提下拉取广告。3. 核心桥接层设计与回调链路3.1 Unity调用Android方法的几种通道Unity和原生Android之间通信最常用的就是AndroidJavaClass和AndroidJavaObject。原理简述一下Unity在C#层通过这几个工具类以JNI的方式直接调用Java层的静态方法或实例方法。第一种方式是写一个静态工具类C#里通过AndroidJavaClass直接调用静态方法比如传入广告位ID然后拉取广告。好处是简单直接适合不需要保持状态的场景。第二种方式是创建原生Java的对象实例把对象引用保存在C#层后续通过这个实例调用方法、访问属性。适用于需要管理回调、或对象本身持有大量状态的场景广告管理类就属于这种。但要注意JNI调用在主线程和子线程是有区别的。Unity的AndroidJavaObject调用必须发生在Unity主线程所以C#侧的调用点尽量放在Update或通过回调驱动的地方不要在加载场景的异步线程里乱调。3.2 原生回调怎么安全地传回Unity穿山甲SDK的回调基本都在Android主线程触发。但Unity的UnitySendMessage要求方法被调用时Unity引擎处于可执行脚本的状态如果恰好在场景切换的半中间回调消息可能会丢失甚至导致后续行为异常。所以我的做法是在原生桥接层里不直接调UnitySendMessage而是先把回调数据暂存在一个本地队列里再通过UnityPlayer.currentActivity.runOnUiThread转发。转发前判断Unity是否处于paused状态如果页面暂时不可见就等恢复后再补发。这里贴一段核心思路private void postToUnity(String gameObjectName, String methodName, String message) { final String go gameObjectName; final String mn methodName; final String msg message; // 确保在主线程再用UnityPlayer.UnitySendMessage Activity activity UnityPlayer.currentActivity; if (activity null) return; activity.runOnUiThread(new Runnable() { Override public void run() { UnityPlayer.UnitySendMessage(go, mn, msg); } }); }这么做一方面保证了线程安全另一方面也避免在Unity忙碌期间强制注入回调造成卡顿。实测下来这种模式非常稳尤其是在激励视频关闭、游戏要恢复计时的场景回调丢失率基本为零。3.3 生命周期初始化时机和场景关系穿山甲SDK的初始化建议放在Application创建时或Activity启动早期。如果是Unity导出的工程UnityPlayerActivity是启动Activity你可以在Unity的C#侧尽早调用初始化也可以选择直接在原生层的Application.onCreate里初始化。我推荐在原生层初始化通过自定义Application类在应用一启动就把SDK初始化好等Unity场景加载完广告位随时可以拉广告。如果在Unity侧的Start里才初始化App启动到Unity场景就绪这段时间原生侧的广告预加载时机就被浪费了。初始化那段代码大概是这样的注意传参是否完整// 自定义Application public class DemoApp extends Application { Override public void onCreate() { super.onCreate(); if (TTAdSdk.checkIsInitSuccess()) { return; } TTAdConfig config new TTAdConfig.Builder() .appId(APP_ID) .appName(示例应用) .debug(false) .build(); TTAdSdk.init(this, config); } }不要小看“预加载”这个概念。广告加载本身有网络请求一次完整加载可能耗时一到三秒。你提前初始化、提前load用户看到广告位时通常已经缓存完成展示成功率和体验都好很多。这个细节对ecpm的影响非常直接。4. 从零到一完整接入实操过程4.1 后台创建应用与广告位开始写代码之前先去穿山甲开发者后台把应用和广告位注册好。应用包名、应用名称按实际填写就行需要注意一点如果项目有多个渠道包建议每个渠道包单独注册应用或至少用不同广告位方便后续看分渠道数据。广告位的类型按需求选择。游戏类产品常见的组合是“开屏广告 激励视频 偶尔插屏”。激励视频适合发放游戏货币/复活机会开屏广告相当于用户进入App的第一个曝光位插屏则要控制频率不然很容易引起用户反感。广告位创建完成之后后台会给你一个广告位ID字符串。这串ID后续代码里会用到建议放到配置表里别硬编码在C#脚本中。为了后续运营调整方便我把广告位ID都放在一个独立的AdsConfig类里换ID只改一处。4.2 写原生桥接层广告管理类真正干活的是这个原生广告管理类。这里以激励视频为例因为激励视频是游戏里最常接的广告形态生命周期也相对复杂。public class RewardAdManager { private static volatile RewardAdManager sInstance; private TTAdNative mTTAdNative; private TT rewardedAd; private String mAdUnitId; public RewardAdManager(String adUnitId) { mAdUnitId adUnitId; TTAdSdk.init(UnityPlayer.currentActivity, new TTAdConfig.Builder() .appId(你的APP_ID) .build()); mTTAdNative TTAdSdk.getAdManager().createAdNative(UnityPlayer.currentActivity.getApplicationContext()); } public void loadAd() { AdSlot adSlot new AdSlot.Builder() .setCodeId(mAdUnitId) .setSupportDeepLink(true) .setOrientation(TTAdConstant.VERTICAL) .build(); mTTAdNative.loadRewardVideoAd(adSlot, new TTAdNative.RewardVideoAdListener() { Override public void onError(int code, String message) { postToUnity(AdsListener, OnError, code | message); } Override public void onRewardVideoAdLoad(TT rewardVideoAd) { rewardedAd rewardVideoAd; postToUnity(AdsListener, OnLoad, ); } Override public void onRewardVideoCached() { postToUnity(AdsListener, OnCached, ); } // ... 其余回调 }); } public void showAd() { if (rewardedAd null) return; rewardedAd.setRewardAdInteractionListener(new TT.RewardAdInteractionListener() { Override public void onRewardArrive(boolean isVerified, int rewardType, Bundle extraInfo) { postToUnity(AdsListener, OnReward, ); } // ... 展示、关闭、点击、跳过等回调 }); rewardedAd.showRewardVideoAd(UnityPlayer.currentActivity); } }代码里有个细节值得留意每次showAd前都要检查rewardedAd是否为null因为激励视频是一次性资源展示完就失效了。正确流程是“加载成功后再展示”展示完成或失败后立刻发起下一次预加载。还有一个坑不要用一个实例反复show某些版本SDK在show完之后没有把对象置空你再次调用会直接没反应。我是在onAdClose回调里把rewardedAd置空然后重新走load流程。4.3 C#侧的封装控制类原生层写好了Unity侧要做的就是把调用关系封装优雅一点。这里用AndroidJavaObject直接和原生管理类对话。public class NativeAdsManager : MonoBehaviour { private AndroidJavaObject _adManager; // 激励视频广告位ID private const string RewardAdUnitId 你的激励视频广告位ID; void Start() { // 构造原生广告管理对象 AndroidJavaClass unityPlayerClass new AndroidJavaClass(com.unity3d.player.UnityPlayer); AndroidJavaObject activity unityPlayerClass.GetStaticAndroidJavaObject(currentActivity); AndroidJavaClass managerClass new AndroidJavaClass(com.your.packagename.RewardAdManager); _adManager managerClass.CallStaticAndroidJavaObject(getInstance, activity); } public void LoadRewardAd() { _adManager?.Call(loadRewardAd, RewardAdUnitId); } public void ShowRewardAd() { _adManager?.Call(showRewardAd); } // 这个方法是给UnitySendMessage回调用的 public void OnRewardAdLoaded(string message) { // 广告就绪可以允许玩家点击“观看视频领奖励”按钮 } }C#侧要做的还有CallStatic和Call的区分。如果你原生类用的是单例模式通过静态方法getInstance获取实例那第一次调用用CallStatic后续操作使用Call。AndroidJavaObject的引用在场景切换时要注意如果在DontDestroyOnLoad的GameObject上持有基本上可以全程复用。4.4 AndroidManifest和混淆配置Unity导出的Android工程AndroidManifest主要在launcher模块。SDK要求的权限比如INTERNET、ACCESS_NETWORK_STATE这些在Unity默认manifest里可能没有需要补上。这里有一份保守权限配置按需使用uses-permission android:nameandroid.permission.INTERNET / uses-permission android:nameandroid.permission.ACCESS_NETWORK_STATE / uses-permission android:nameandroid.permission.ACCESS_WIFI_STATE / uses-permission android:nameandroid.permission.READ_PHONE_STATE / uses-permission android:nameandroid.permission.ACCESS_COARSE_LOCATION /沃爱德不要一次性把SDK文档里的权限全加上。部分敏感权限会增加审核风险能不加就不加。比如定位权限如果App不依赖位置那就不声明顶多损失一些LBS类广告填充。混淆配置这边如果你是开启minify打包的需要添加穿山甲SDK的keep规则。大意是SDK内部类保持原样不做混淆-keep class com.bytedance.sdk.** { *; } -keep class com.pangle.** { *; } -keep class **.R$* { *; } -dontwarn com.bytedance.sdk.**Unity导出的工程里这些规则可以在unityLibrary的proguard-unity.txt里追加。设置完之后我习惯用gradlew assembleRelease打一个包在反编译工具里确认类名没有变成a.b.c这种再做下发测试。4.5 打包验证和日志确认集成完成的标志是第一次跑Release包打开调试日志能够看到SDK初始化成功的日志广告拉取返回了加载成功并且手动触发展示能看到广告画面。穿山甲SDK自己带了debug开关。我调试阶段一律开着能看到ad request状态、加载耗时、失败原因。但发布前一定记得关掉debug否则会严重影响线上日志的隐私合规。用Android Studio的Logcat观察UnityPlayer和穿山甲的日志Tag。正常情况下初始化完成后拉取请求会打出类似广告拉取成功的日志。如果出现error code用文档里错误码表对照即可后面章节我会整理高频错误。5. 常见问题与排查心得5.1 穿山甲广告错误码速查接触过一段时间后我整理了一份高频错误码遇到先别慌对照处理错误码常见含义排查建议-5001初始化失败检查App ID确认网络连通确认初始化调用在合适时机-5004广告拉取超时多为网络或运营商问题可重试-5005广告位无填充检查广告位ID是否有效是否开启了测试模式-5006广告拉取频率过高降低请求频率不要无限自动刷新-5100广告位ID与App ID不匹配去后台核对广告位归属-5200广告展示失败检查设备系统版本、Activity状态、SDK版本-5300广告被拦截检查是否在非主线程操作是否被动态权限拦截这套表最实用的地方在于前期调试时频繁出现的错误码大多集中在-5001和-5005。前者是初始化时机和App ID填错后者是广告位配置没生效。把这两个排查掉接入工作基本就完成80%。5.2 广告加载成功但无法展示这个问题在接入中很典型。加载回调明明触发了调用show也返回true屏幕上就是不出广告。绝大多数情况和UnityPlayerActivity的window状态有关。Unity的UnityPlayerActivity本身是一个特殊的Activity子类化之后如果不小心在onPause或onStop里调用了不恰当的方法会影响SDK展示广告时申请窗口焦点。另外部分广告形式如开屏是抢占式展示需要App前台的时机正确你可以尝试延迟100到300毫秒再调用show。我还在某个机型上遇到过一种情况广告内容加载完毕但没缓存show时本地视频还没准备好。这种就需要监听SDK的回调中的缓存状态确保缓存完成后再允许玩家触发展示。在激励视频全部换成等缓存完成再展示后这种问题就绝迹了。5.3 UnitySendMessage回调不触发回调和线程有关。穿山甲SDK请求广告的网络回调和展示回调在不同版本的SDK里线程来源并不完全一致。如果你直接拿业务线程去调UnitySendMessage大概率在某些机型上就丢失了。解决方案就是前文提到的postToUnity双层转发机制。先把消息转到Android主线程再由主线程调用UnitySendMessage。同时确保C#侧接收回调的GameObject没有被销毁。我见过有人把监听对象挂在临时场景里场景一切换Unity那边自然就收不到任何东西了。另外UnitySendMessage的字符串消息长度也有讲究。虽然可以传JSON但SDK和系统对binder消息有大小限制超大回调数据要拆分处理。咱们广告回调的message字段一般都很短这个场景问题不大但心里有个数。5.4 依赖冲突和构建失败Gradle构建失败最常见的原因穿山甲SDK依赖的AndroidX版本和Unity工程里其他插件依赖的版本冲突。Library模块的依赖不会直接暴露给你但通过Android Studio里Gradle面板的dependency insight可以看到冲突细节。常规处理方式有三种在gradle.properties里强制指定统一版本号让所有依赖平级将某些冲突的传递依赖用exclude排除前提是确认SDK内部不依赖特定类提升Unity导出工程的Gradle版本让依赖仲裁更灵活尽量避免的做法是直接把aar包拖进Unity Plugins/Android目录。这样虽然也能跑起来但一旦SDK有更新替换aar还要处理旧版本的残留文件还不如Gradle依赖可控。5.5 旧机型适配的几个实测细节广告SDK的整体性能在主流机型上都比较稳定但老款中低端安卓机上我遇到不少有意思的现象视频广告播放时音量和游戏背景音乐冲突。处理办法是给音频管理器做一下暂停/恢复消息的接收广告开始播就调Unity的AudioListener.pause。部分华为/小米定制系统在后台保活策略激进广告打开的过程中如果切到后台回调可能延迟很久。测试时尽量别锁定后台限制选项。部分低内存机型加载广告后出现Unity资源被系统回收、回场景加载慢。这种时候建议通过Application.targetFrameRate降帧率缓解瞬时压力。这些问题的共性是广告SDK本身运行正常但和Unity引擎的音频、内存机制互相影响需要应用层做联动处理。确保广告层和游戏核心循环解耦能达到更好体验。6. 收益最大化的几个运营小技巧6.1 广告预加载与时机策略广告加载和展示是两个独立步骤。优秀的接入方案一定会在适当时间做好预加载。分享一套我实测稳定的策略激励视频进入主界面后立刻预加载第一个玩家看完激励视频后马上预加载下一个插屏广告不要和关卡结束弹窗同时出现放在返回主界面2~3秒之后降低玩家反感度Banner只在有固定UI容器的界面显示避免遮挡关键操作按钮如果广告位加载失败配置自动重试机制但重试间隔不要低于30秒。频繁请求会触发平台的限频机制得不偿失。6.2 收益数据怎么分析和优化穿山甲后台的收益数据有一定延迟但后台提供的主要维度基本上能覆盖你的分析需求。我平时主要关注三个指标变化ecpm不同广告位的价格变化提示需不要调整展示频率填充率填充率持续偏低可能是广告位被限流或配置异常展示点击率点击率过高可能吸引的是低质量流量影响广告后续竞争力按每千次展示收益来分配广告位展示权重会很有效。开屏和激励视频的ecpm通常较高插屏和Banner适当降低频率。为运营同事增加一个远程开关可以快速控制某个广告位的开启和关闭做A/B调优。6.3 测试模式与线上模式切换穿山甲SDK有测试模式打开后返回的都是测试广告方便开发期验证流程。这里说一个很多人踩过的坑测试模式下的广告位ID和正式广告位ID是分开配置的正式环境的广告位ID换成测试ID返回的结果没有参考价值。上线之前我习惯在代码里专门留一个编译开关public static bool IsTestMode false;通过这个开关控制SDK初始化时的debug字段以及广告位ID的选择。千万不要忘记切回正式模式就发版否则线上广告迟迟不生效还容易排查半天找不到原因。7. 写在最后的经验总结Unity接入原生穿山甲广告这件事本质上不算难难在两套引擎思维的切换。Android原生开发者习惯考虑生命周期、线程、依赖树Unity开发者习惯组件化、场景管理、脚本回调。两者要协同关键就是把广告SDK的生命周期管理交给原生层把消息回调转换成Unity熟悉的事件驱动模式。真正扎实的方案就是这层转译工作做得好。折腾过几次之后我现在再做类似的广告接入项目已经能在一两天内完成从配置到验证的整个流程。这套方式的另一个好处是它不只是能接穿山甲对国内其他几家头部广告平台的Unity接入同样适用——接入架构是通用的换SDK只是换一层的实现细节。如果你正打算给Unity项目接入穿山甲又有点犹豫我个人建议是别绕弯直接按原生接入的方式走。初期多花的那半天一天时间后面大概率会赚回来。遇到问题也欢迎交流干这行的经验本来就是踩坑踩出来的少踩几个坑就等于多赚了几段时间。

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

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

免费获取报价 →
↑