一、概述
本项目使用 AnyThink(Taku/安速) 作为广告聚合平台。AnyThink 是一个国内广告聚合中间件,可以同时聚合多家广告 SDK(Sigmob、CSJ/Pangle、Kuying/SDM),由广告主/运营通过后台配置流量分配,客户端统一接入。
广告系统分为三个层次:
应用层 (lib/ad/) → 管理广告生命周期,决定展示时机
桥梁层 (packages/anythink_sdk/) → Flutter Plugin,桥接 MethodChannel
原生层 (Android/iOS SDK) → 各广告 SDK 原生实现
当前项目仅使用了 开屏广告(Splash Ad) 一种广告形式。广告配置完全由服务端下发,支持按平台(Android/iOS)区分。
二、整体架构
┌─────────────────────────────────────────────────────────────┐
│ AppBootstrapper │
│ lib/startup/app_bootstrapper.dart │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ ① initSDKAsync(initAdSDK: true) │ │
│ │ ├── 基础 SDK 初始化 │ │
│ │ └── _initATT() → _initAdSDK() │ │
│ │ └── 服务端获取 AdConf 配置 → AdTakuSdk.init() │ │
│ │ │ │
│ │ ② _resolveStartPage() │ │
│ │ └── ATT + adConf → AdTakuSplashWidget │ │
│ └──────────────────────────────────────────────────────┘ │
└────────────────────┬────────────────────────────────────────┘
│
┌────────────────────▼────────────────────────────────────────┐
│ AdTakuSplashWidget (Widget) │
│ lib/ad/ad_taku_splash_widget.dart │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ ① 展示静态开屏背景图 │ │
│ │ ② AdTakuSplash.instance.showIfReadyOtherwiseLoad() │ │
│ │ ③ 任意失败 → _enterAppOnce() 进入主页 │ │
│ └──────────────────────────────────────────────────────┘ │
└────────────────────┬────────────────────────────────────────┘
│
┌────────────────────▼────────────────────────────────────────┐
│ AdTakuSplash (控制器) │
│ lib/ad/ad_taku_splash.dart │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ ① initListener() → 订阅 ATListenerManager │ │
│ │ ② load() → ATSplashManager.loadSplash() │ │
│ │ ③ show() → ATSplashManager.showSplash() │ │
│ │ ④ 通过 Completer 实现加载等待 + 超时 │ │
│ │ ⑤ 路由事件到 SplashCallbacks │ │
│ └──────────────────────────────────────────────────────┘ │
└────────────────────┬────────────────────────────────────────┘
│
┌────────────────────▼────────────────────────────────────────┐
│ anythink_sdk (Flutter Plugin) │
│ packages/anythink_sdk-1.0.8 │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ MethodChannel('anythink_sdk') │ │
│ │ │ │
│ │ Dart 端: │ │
│ │ ├── ATInit → initAnyThinkSDK() │ │
│ │ ├── ATSplash → loadSplash() / showSplash() │ │
│ │ └── ATListener → 事件回调分发 (splashEventHandler) │ │
│ │ │ │
│ │ ↓ MethodChannel invoke / ↑ MethodCallHandler │ │
│ │ │ │
│ │ 原生端: │ │
│ │ ├── Android: ATSplashHelper + FrameLayout 渲染 │ │
│ │ └── iOS: ATFSplashAdManger + ATFSplashDelegate │ │
│ └──────────────────────────────────────────────────────┘ │
└────────────────────┬────────────────────────────────────────┘
│
┌────────────────────▼────────────────────────────────────────┐
│ 原生广告 SDK (聚合网络) │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Taku Core 6.5.70 │ │
│ │ ├── Sigmob Adapter 4.25.9 │ │
│ │ ├── CSJ/Pangle Adapter 7.3.0.8 │ │
│ │ └── Kuying/SDM Adapter 6.5.54 │ │
│ └──────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
三、anythink_sdk 包详解(桥梁层)
packages/anythink_sdk-1.0.8/ 是一个本地 Flutter 插件包,本质上是原生 AnyThink SDK 的 Flutter 桥接。
3.1 通信方式
使用单个 MethodChannel('anythink_sdk') 通道,双向通信:
Flutter → Native: 通过 channel.invokeMethod() 调用原生方法
Native → Flutter: 通过 channel.setMethodCallHandler() 回调结果
3.2 Dart 端 API 结构
所有类集中在 at_index.dart 中导出:
全局实例
类
用途
ATFlutterChannel
AnythinkSdk
原始 MethodChannel 实例(仅 platformVersion)
ATInitManger
ATInit
SDK 初始化、GDPR、渠道配置
ATListenerManager
ATListener
所有广告格式的回调分发器
ATSplashManager
ATSplash
开屏广告加载/展示/状态查询
ATBannerManager
ATBanner
横幅广告(未使用)
ATInterstitialManager
ATInterstitial
插屏广告(未使用)
ATRewardedManager
ATRewarded
激励视频(未使用)
ATNativeManager
ATNative
原生广告(未使用)
3.3 回调系统
ATListener 注册为 MethodChannel 的 setMethodCallHandler,解析 methodName 分发到对应的 StreamController:
原生 invokeMethod("SplashCall", data)
→ ATListener._adMethodHandler()
→ 识别 "SplashCall"
→ splashEventController.add(ATSplashResponse.fromMap(data))
→ 订阅者(AdTakuSplash.initListener)收到事件
每个广告格式都有独立的响应模型:
响应类
状态枚举
事件数量
ATSplashResponse
SplashStatus
18 种(含广告源级别)
ATBannerResponse
BannerStatus
15 种
ATInterstitialResponse
InterstitialStatus
17 种
ATRewardResponse
RewardedStatus
23 种
ATNativeResponse
NativeStatus
21 种
ATInitResponse
-
GDPR/位置回调
ATDownloadResponse
DownloadStatus
仅 Android
3.4 平台视图
横幅广告和原生广告支持 AndroidView/UiKitView 嵌入 Flutter Widget 树:
PlatformBannerWidget→at_banner_platform_viewPlatformNativeWidget→at_native_platform_view
开屏广告不使用平台视图,而是由原生端直接在 Activity 的 android.R.id.content 上覆盖 FrameLayout 渲染。
四、lib/ad/ 管理层详解
4.1 文件依赖关系
callback_ad_splash.dart ← 纯数据类型,定义回调和事件模型
↑
ad_taku_splash.dart ← 开屏广告控制器(加载/展示/超时/释放)
↑
ad_taku_splash_widget.dart ← Flutter Widget,UI 层桥接
│
ad_taku_sdk.dart ← SDK 初始化(与 splash 无依赖)
4.2 AdTakuSdk — SDK 初始化
文件: lib/ad/ad_taku_sdk.dart
静态工具类,封装 ATInitManger 的方法:
- 防重复初始化(
_sdkInited标志位) setupAndInit():编排方法,依次配置日志/渠道 → 注册 GDPR 监听器 → 调用initTaku()(即ATInitManger.initAnyThinkSDK)- GDPR 合规处理:获取用户位置 → 如在欧盟且未设置 → 弹出 GDPR 弹窗
- 提供各种配置快捷方法(日志、渠道、自定义数据、设备信息限制)
4.3 AdTakuSplash — 开屏广告控制器
文件: lib/ad/ad_taku_splash.dart
单例模式,管理开屏广告完整生命周期:
核心机制:
load() → _loadOnceAndWait()
├── 防并发加载(_isLoading 标志)
├── 创建 Completer<bool>
├── 发起 ATSplashManager.loadSplash()(不 await,防 MethodChannel 死锁)
├── 等待 Completer(超时时间 = tolerateTimeoutMs + 1500ms)
└── Completer 由事件监听器中的 splashDidFinishLoading/fail/timeout 完成
→ load() 返回 true/false
状态追踪:
_isLoading— 防止并发加载_isShowing— 防止重复展示_attemptSeq— 单调递增的尝试序号,用于日志关联_lastAttemptEndByTimeout— 上次是否因超时结束_lastStatus— 最近一次 SplashStatus
回调路由: initListener() 订阅 ATListenerManager.splashEventHandler,通过 SplashStatus 映射到 SplashCallbacks 中对应的回调。
4.4 AdTakuSplashWidget — 开屏 UI
文件: lib/ad/ad_taku_splash_widget.dart
StatefulWidget,开屏页面的实现:
- 展示
Assets.images.imageSplash作为背景图 _showAdSplashAsync()在initState的addPostFrameCallback中执行- 容错机制(全部 fallthrough):
- 无可用视图 → 直接进应用
- 应用不在前台 → 直接进应用
- SDK 未初始化 → 直接进应用
- 广告加载失败 → 进应用
- 广告展示失败 → 进应用
- 广告超时 → 进应用
- 广告被用户关闭 → 进应用
MissingPluginException→ 进应用
_enterAppOnce()通过_hasEnteredApp标志位确保只会进入应用一次- 进入目标:已注册用户 →
NavigationPage;未注册 →LoginPage
4.5 SplashCallbacks — 事件回调模型
文件: lib/ad/callback/callback_ad_splash.dart
回调字段
触发条件
onFailToLoad
广告加载失败
onFinishLoading
广告加载完成(准备展示)
onTimeout
广告超时
onShowSuccess
广告展示成功
onShowFailed
广告展示失败
onClick
用户点击广告
onDeepLink
DeepLink 跳转
onWillClose
广告即将关闭
onClose
广告已关闭
onUnknown
未识别的状态
五、启动集成流程
文件: lib/startup/app_bootstrapper.dart
5.1 整体时序
main()
│
▼
AppBootstrapper.bootstrap()
│
├── ① _initBase() — 竖屏锁定、状态栏、包信息
├── ② _initProviders() — Riverpod 容器、Dio、网络
├── ③ _initEnvironment() — 读取 API 环境
│
├── ④ _isAgreePrivacy()?
│ NO ──→ AppLauncherWidget(未同意隐私,跳过所有 SDK 初始化)
│ YES ──→ 继续
│
├── ⑤ initSDKAsync(initAdSDK: true)
│ ├── 阿里一键登录 SDK
│ ├── 微信 SDK
│ ├── Flutter Foreground Task
│ └── _initATT() — iOS 请求 ATT 权限
│ └── 授权后 ──→ _initAdSDK()
│ ├── 请求服务端: BasicsServer.getAdConfList()
│ │ 参数: { platform: android/ios, adType: "taku" }
│ ├── 过滤: platform 匹配 + adType="splash" + isEnable=1
│ ├── 提取 appId, appKey
│ └── AdTakuSdk.setupAndInit(appId, appKey)
│ → ATInitManger.initAnyThinkSDK()
│
├── ⑥ _resolveStartPage()
│ ├── 刷新用户信息
│ ├── 未登录 → LoginPage
│ ├── 已登录:
│ │ └── 无 ATT 权限 OR 无 adConf → NavigationPage/LoginPage
│ └── 已登录 + ATT + 有 adConf → AdTakuSplashWidget
│
└── ⑦ runApp(startPage)
5.2 关键设计点
隐私优先: 用户未同意隐私协议时,完全不初始化广告 SDK,也不请求任何广告配置。
ATT 依赖(iOS 特有): iOS 上必须在用户授予 AppTrackingTransparency 权限后才会初始化广告 SDK。Android 上始终返回 true。
服务端驱动配置: 所有广告参数(appId、appKey、placementId、isEnable)均从服务端下发,运营可远程控制开屏广告的展示。
六、服务端配置模型
模型字段
类型
说明
id
int
主键
platform
String
平台标识:android / ios
appId
String
Taku SDK 的应用 ID
appKey
String
Taku SDK 的应用 Key
adType
String
广告类型:splash
placementId
String
广告位 ID
isEnable
int
启用标志:0=禁用, 1=启用
source
String?
来源标识
请求接口: BasicsApi.adConfList — POST 请求,参数 platform(当前设备平台)和 adType: "taku"。
过滤逻辑(app_bootstrapper.dart lines 197-201):
result.where((element) =>
element.platform == platform &&
element.adType == "splash" &&
element.isEnable == 1
).toList();
七、原生平台集成要点
7.1 Android
- Gradle 依赖: 通过插件的
build.gradle声明api("com.anythink.sdk:core-taku:6.5.70")及多家适配器 - AAR 文件: 插件内嵌
anythink_core.aar、anythink_splash.aar等 - 开屏渲染:
ATSplashHelper.java创建ATSplashAd,通过activity.addContentView()将FrameLayout添加到android.R.id.content - 聚合网络: Sigmob、CSJ/Pangle、Kuying/SDM 三家广告 SDK
- 应用层 AndroidManifest: 无任何广告相关声明,全部由插件 Manifest 合并提供
7.2 iOS
- 语言: 全部 Objective-C(未使用 Swift)
- 架构:
ATFAdManger使用 Category 按广告格式拆分(+Initchannel、+SplashAd、+RewardedVideo等) - 回调:
ATFSendSignalManger单例通过[self.methodChannel invokeMethod:methodName arguments:arguments]回传事件 - ATT: 应用层在
_initATT()中弹出 ATT 授权请求(iOS 14+)
八、架构特点与设计模式
8.1 设计模式总结
模式
应用位置
单例模式
AdTakuSplash.instance、AdTakuSdk(静态类)、ATInitManger、ATSplashManager 等所有管理器
观察者模式
ATListener 使用 StreamController 分发事件,AdTakuSplash.initListener() 订阅
回调模式
SplashCallbacks 定义 10 种可选回调
编排模式
AdTakuSdk.setupAndInit() 编排日志→渠道→监听器→初始化的顺序
Promise/Future
_loadOnceAndWait() 使用 Completer<bool> + 超时实现异步等待
守卫模式
防重复初始化、防并发加载、防重复进入应用(_hasEnteredApp)
8.2 容错设计
广告系统采用 "优雅降级" 原则:广告是增强体验,不是必要功能。
- SDK 初始化失败 → 不展示广告,正常进入应用
- 广告加载失败 → 立即进入应用
- 广告展示失败/超时 → 立即进入应用
- 用户关闭广告 → 进入应用
- 插件未安装/未注册(
MissingPluginException)→ 进入应用 - 隐私未同意 → 不初始化 SDK,直接进入应用
8.3 安全与合规
- 隐私协议前置: 用户不同意隐私协议不初始化任何广告 SDK
- GDPR 支持: 提供
getUserLocation()(欧盟检测)和showGDPRConsentDialog()(UMP 弹窗) - ATT 授权(iOS 14+): 需要用户授权后才能初始化广告 SDK
- 设备信息限制:
deniedUploadDeviceInfo()可限制上报特定设备标识(OAID、IMEI 等)
8.4 可扩展性
虽然当前仅使用开屏广告,但 anythink_sdk 包完整支持:
- 横幅广告(Banner)
- 插屏广告(Interstitial,含自动加载模式)
- 激励视频(Rewarded Video,含自动加载模式)
- 原生广告(Native,含平台视图嵌入)
每种广告格式的 Flutter 端 API、原生端实现和回调模型均已就绪,可在服务端配置后快速启用。
九、数据流完整链路
用户打开 App
│
▼
AppBootstrapper.bootstrap()
│
├── 隐私同意?
│ ├── NO → AppLauncherWidget(停止)
│ └── YES
│
├── _initAdSDK()
│ ├── HTTP 请求: BasicsApi.adConfList(platform, adType="taku")
│ ├── 服务端返回: [{appId, appKey, adType: "splash", placementId, isEnable: 1}]
│ ├── 过滤匹配项
│ ├── AdTakuSdk.setupAndInit(appId, appKey)
│ │ ├── ATInitManger.setLogEnabled()
│ │ ├── ATInitManger.setChannelStr()
│ │ ├── AdTakuSdk.addSDKListener()
│ │ │ └── 监听 userLocation 事件 → 在欧盟 → showGDPRConsentDialog()
│ │ └── ATInitManger.initAnyThinkSDK(appid, appkey)
│ │ └── MethodChannel → 原生 ATAdInitManger → Taku Core 初始化
│ └── 保存 adConfModel(含 placementId)
│
├── _resolveStartPage()
│ └── 已登录 + ATT + adConf → AdTakuSplashWidget(placementId)
│
▼
AdTakuSplashWidget.initState()
│
├── addPostFrameCallback → _showAdSplashAsync()
│
▼
_showAdSplashAsync()
│
├── 前置检查(视图/生命周期/SDK已初始化)
│
├── AdTakuSplash.instance
│ .on(failToLoad→_enterAppOnce, close→_enterAppOnce, ...)
│ .setPlacementID(placementId)
│ .showIfReadyOtherwiseLoad(autoShowAfterLoad: true)
│
├── AdTakuSplash.initListener()
│ └── ATListenerManager.splashEventHandler.listen()
│
├── ATSplashManager.splashReady()?
│ ├── YES → show()
│ │ └── ATSplashManager.showSplash()
│ │ └── MethodChannel → 原生展示(FrameLayout覆盖内容)
│ │ 用户观看广告 → 点击/关闭 → 原生回调
│ │ → MethodChannel("SplashCall") → ATListener
│ │ → SplashStatus.splashDidClose
│ │ → SplashCallbacks.onClose → _enterAppOnce()
│ │
│ └── NO → load()
│ └── _loadOnceAndWait()
│ ├── 创建 Completer
│ ├── ATSplashManager.loadSplash(placementID, tolerateTimeout)
│ │ └── MethodChannel → 原生加载广告
│ └── 等待 Completer(超时 tolerateTimeoutMs + 1500ms)
│ │
│ 原生加载完成 → MethodChannel("SplashCall")
│ → SplashStatus.splashDidFinishLoading
│ → _completeAttemptIfNeeded(true) → Completer.complete(true)
│ → load() 返回 true
│ → autoShow → show()
│ └── 同上方 YES 分支
│
└── 任意失败 → _enterAppOnce()
└── CommonNavigator.pushAndRemoveUntil(NavigationPage/LoginPage)
十、关键文件索引
文件路径
职责
行数
lib/ad/ad_taku_sdk.dart
Taku SDK 初始化封装
~130
lib/ad/ad_taku_splash.dart
开屏广告控制器
~280
lib/ad/ad_taku_splash_widget.dart
开屏页面 UI
~100
lib/ad/callback/callback_ad_splash.dart
开屏事件回调模型
~35
lib/startup/app_bootstrapper.dart
启动编排(含广告初始化)
~230
packages/anythink_sdk-1.0.8/lib/at_splash.dart
Plugin 开屏 API
~55
packages/anythink_sdk-1.0.8/lib/at_init.dart
Plugin 初始化 API
~100
packages/anythink_sdk-1.0.8/lib/at_listener.dart
Plugin 事件分发器
~80
packages/anythink_sdk-1.0.8/lib/at_splash_response.dart
开屏回调数据模型
~65
packages/anythink_sdk-1.0.8/lib/at_index.dart
Plugin API 统一导出
~15
packages/anythink_sdk-1.0.8/android/.../splash/ATSplashHelper.java
Android 开屏原生实现
-
packages/anythink_sdk-1.0.8/ios/.../splash/ATFSplashAdManger.m
iOS 开屏原生实现
-
lib/modules/basics/model/mode_ad.dart
广告配置数据模型
~20
lib/modules/basics/server/server_basics.dart
广告配置 API 调用
~20
十一、验证方式
- 检查 SDK 初始化日志: 打开 SDK 调试日志(
AdTakuSdk.setLogEnabled(true)),观察[AdTakuSdk]前缀的输出 - 验证服务端配置: 通过
/basics/adConfList接口确认返回的appId/appKey/placementId是否正确 - 原生端调试: AnyThink 提供
ATInitManger.showDebuggerUI()打开调试面板,可查看各广告平台的加载情况 - 开屏广告展示测试: 冷启动应用,观察开屏广告是否正常加载和展示,确认关闭后正确进入主页面
- 容错测试: 断网启动 → 确认不进广告直接进入主页面;关闭广告 SDK 配置 → 确认不进广告直接进入主页面