菜单

广告探测器-iOS 接入说明

一、简介

核心能力

能力 说明
截图采集 支持开屏 / Banner / 插屏 / 激励视频 / 原生广告
视频合成截图 自动识别 AVPlayerLayer 并合成视频帧 + UIKit 覆盖层
持久化队列 沙盒落盘 + 冷启动恢复 + 重试 + 网络感知
自动上报 内置签名(HMAC-MD5) + multipart 上传 + 限流重试策略
隐私保护 自动遮盖密码输入框 / firstResponder,支持自定义遮盖区域
策略感知 支持按 placementId / networkFirmId / 样式动态启停

二、初始化与关闭

2.1 初始化(必须)

调用时机:建议在 ATT 授权回调之后初始化(确保 IDFA 可用),整个 App 生命周期仅调用一次。

Swift

swift 复制代码
import MaterialMonitorSDK
import AppTrackingTransparency
import AdSupport

class AppDelegate: UIResponder, UIApplicationDelegate {

    // 建议放在 applicationDidBecomeActive 里走 ATT 授权流程
    func applicationDidBecomeActive(_ application: UIApplication) {
        if #available(iOS 14, *) {
            ATTrackingManager.requestTrackingAuthorization { _ in
                self.initMaterialMonitor()
            }
        } else {
            initMaterialMonitor()
        }
    }

    private func initMaterialMonitor() {
        let appId      = "your_app_id"
        let appKey     = "your_app_key"
        let idfa       = ASIdentifierManager.shared().advertisingIdentifier.uuidString
        let sdkVersion = "your_host_sdk_version"                   // 宿主聚合 SDK 版本号

        // MARK: - MaterialMonitor SDK 初始化(完整参数)
        let config = MaterialMonitorConfig.Builder()
            // ── 上传服务地址,使用https://cag.flowpf.com────────────────────────
            .baseUploadUrl("https://cag.flowpf.com")
            // ── 鉴权 ────────────────────────────────────────────────────────
            .appId(appId)
            .appKey(appKey)
            // ── 设备标识符 ───────────────────────────────────────────────────
            .idfa(idfa)                 // ATT 授权后传入 ATTrackingManager.advertisingIdentifier.uuidString
            .idfv(nil)                  // nil 时 SDK 自动从 UIDevice.current.identifierForVendor 读取
            .sdkVersion(sdkVersion)     // 宿主聚合 SDK 版本号,写入 meta device_info.sdk_version
            .caid(nil)                  // CAID(中国广告 ID,可选)
            // ── 网络超时(ms) ──────────────────────────────────────────────
            .connectTimeoutMs(15_000)   // TCP 连接超时,默认 15s,最小 3s
            .readTimeoutMs(30_000)      // 服务端响应超时,默认 30s,最小 3s
            // ── 重试策略 ─────────────────────────────────────────────────────
            .maxUploadAttempts(3)              // 首次 + 最多 2 次重试,最小 1
            .retryDelayMs([5_000, 15_000])     // 各次重试等待时长(Swift 专属)
            // ── 调试(生产环境请保持 false)──────────────────────────────────
            .debugPerfLogging(false)
            .build()

        MaterialMonitor.initialize(config: config)
    }
}

Objective-C

objc 复制代码
#import <MaterialMonitorSDK/MaterialMonitorSDK-Swift.h>
#import <AppTrackingTransparency/AppTrackingTransparency.h>
#import <AdSupport/AdSupport.h>

- (void)applicationDidBecomeActive:(UIApplication *)application {
    if (@available(iOS 14, *)) {
        [ATTrackingManager requestTrackingAuthorizationWithCompletionHandler:^(ATTrackingManagerAuthorizationStatus status) {
            [self initMaterialMonitor];
        }];
    } else {
        [self initMaterialMonitor];
    }
}

- (void)initMaterialMonitor {
    NSString *appId      = @"your_app_id";
    NSString *appKey     = @"your_app_key";
    NSString *idfa       = [[[ASIdentifierManager sharedManager] advertisingIdentifier] UUIDString];
    NSString *sdkVersion = @"6.5.0";

    MMConfigBuilder *builder = [[MMConfigBuilder alloc] init];
    // ── 上传服务地址 ──
    [builder baseUploadUrl:@"https://cag.flowpf.com"];
    // ── 鉴权 ──
    [builder appId:appId];
    [builder appKey:appKey];
    // ── 设备标识符 ──
    [builder idfa:idfa];
    [builder idfv:nil];
    [builder sdkVersion:sdkVersion];
    [builder caid:nil];
    // ── 网络超时(ms) ──
    [builder connectTimeoutMs:15000];
    [builder readTimeoutMs:30000];
    // ── 重试策略(ObjC 不支持自定义 retryDelayMs,使用默认值 [5000, 15000]) ──
    [builder maxUploadAttempts:3];
    // ── 调试(生产环境请保持 NO) ──
    [builder debugPerfLogging:NO];

    MaterialMonitorConfig *config = [builder build];
    [MaterialMonitor initializeWithConfig:config];
}

提示 1:MaterialMonitor 是单例(MaterialMonitor.shared),初始化后通过 MaterialMonitor.shared.xxx() 调用所有实例方法。

提示 2:所有参数都有合理默认值,最少必填 appId 和 appKey 即可工作;其他参数按需覆盖。

提示 3:ObjC 不支持 retryDelayMs(_:)(Swift 数组桥接限制),ObjC 工程使用内置默认 [5000, 15000]。

2.2 重新初始化

允许在运行中重复调用 initialize(config:) —— SDK 会自动 teardown 旧调度器(取消定时器、中断在途请求、清空回调),再用新 config 重建。冷启动扫描会重做,磁盘已有任务保留。

2.3 关闭 SDK

swift 复制代码
MaterialMonitor.shutdown()
objc 复制代码
[MaterialMonitor shutdown];

关闭后行为:

项 行为
采集 / 上报方法 立即返回 MMErrorCodeUninitialized
在途上传请求 硬取消(URLSession invalidateAndCancel)
重试定时器、网络监听 立即释放
业务回调注册表 清空(已发出回调正常派发,后续回调静默丢弃)
磁盘已落盘记录 保留,下次 initialize 冷启动扫描时继续上报
Config 清空

幂等:重复调用 shutdown() 安全。
可重启:shutdown 后再次 initialize 可恢复全部功能。


三、配置项 MaterialMonitorConfig

通过 Builder 模式构造。所有配置项及默认值如下:

字段 类型 默认值 说明
baseUploadUrl String "" 上传根域名(使用 "https://cag.flowpf.com"),SDK 自动拼接 /v1/creative/upload
appId String "" 应用 ID,写入 multipart 字段 app_id
appKey String "" HMAC-MD5 签名密钥。严禁打印 / 缓存 / 上传明文
idfa String? nil ATT 授权后的 IDFA。SDK 直接透传,不过滤全 0 值
idfv String? nil IDFV。nil 时 SDK 自动从 UIDevice.current.identifierForVendor 读取
sdkVersion String? nil 宿主聚合 SDK 版本号,写入 meta device_info.sdk_version
caid String? nil CAID(中国广告 ID)
connectTimeoutMs Int 15000 TCP 连接超时(ms),最小值 3000
readTimeoutMs Int 30000 服务器响应超时(ms),最小值 3000
maxUploadAttempts Int 3 最大上传尝试次数(首发 + 重试),最小值 1
retryDelayMs [Int] [5000, 15000] 各次重试等待时长(ms)。Swift 专属,ObjC 使用默认值
maxCacheSizeMb Int 100 沙盒 material_monitor/ 总容量上限(MB),最小值 10
debugPerfLogging Bool false 启用详细 Debug 日志。生产环境务必关闭

完整构造示例(Swift)

swift 复制代码
let config = MaterialMonitorConfig.Builder()
    .baseUploadUrl("https://your-upload-host.com")
    .appId(appId)
    .appKey(appKey)
    .idfa(idfa)                          // ATT 授权后传入
    .idfv(nil)                           // nil 时 SDK 自动读取
    .sdkVersion(sdkVersion)
    .connectTimeoutMs(15_000)
    .readTimeoutMs(30_000)
    .maxUploadAttempts(3)
    .retryDelayMs([5_000, 15_000])
    .debugPerfLogging(false)
    .build()

四、采集接口

SDK 提供两种采集接口,按广告是否有可访问的容器视图选择:

广告形态 适用类型 推荐接口
有容器 Banner / 原生广告 collectFrameFromAdContainer
无容器 开屏 / 插屏 / 激励视频 collectFrameFromAdPage

所有采集方法必须在主线程调用。截图采集与上报回调均在主线程派发。

4.1 有容器广告(Banner / 原生广告)

swift 复制代码
public func collectFrameFromAdContainer(
    _ view: UIView,
    adInfo: MMAdInfo,
    timing: AdTimingInput,
    options: FrameOptions,
    callback: FrameCallback
)

参数:

参数 类型 说明
view UIView 广告容器视图
adInfo MMAdInfo 广告信息(见 §7.4)
timing AdTimingInput 广告时间戳(见 §7.1)
options FrameOptions 截图选项(见 §7.2)
callback FrameCallback 采集 + 自动上报回调(弱引用,见 §6)

Swift 示例(Banner):

swift 复制代码
func onBannerAdShow(_ bannerView: UIView, extra: [String: Any]) {
    MaterialMonitor.shared.collectFrameFromAdContainer(
        bannerView,
        adInfo: buildMMAdInfo(extra),
        timing: buildTiming(extra),
        options: FrameOptions(delayMs: 200),  // Banner 延迟可较短
        callback: self
    )
}

4.2 无容器广告(开屏 / 插屏 / 激励视频)

swift 复制代码
public func collectFrameFromAdPage(
    adInfo: MMAdInfo,
    timing: AdTimingInput,
    options: FrameOptions,
    callback: FrameCallback
)

SDK 会自动定位最顶层 presentedViewController 的 view,若不存在则截 keyWindow。

Swift 示例(开屏):

swift 复制代码
func splashAdDidShow() {
    MaterialMonitor.shared.collectFrameFromAdPage(
        adInfo: adInfo,
        timing: timing,
        options: FrameOptions(delayMs: 3000),  // 开屏视频较长,给充足渲染时间 毫秒
        callback: self
    )
}

五、手动上报与队列管理

5.1 手动入队上报

把已有的 FrameRecord 重新入队(场景:业务侧延后上报、跨进程恢复)。

swift 复制代码
public func reportFrameRecord(
    _ record: FrameRecord,
    adInfo: MMAdInfo,
    callback: ReportCallback?
)

说明:

  • record 必须是之前通过 collectFrame* 拿到的(含合法 localFilePath)
  • 触发 show_id 和 upload_task_id 双重去重
  • callback 可选,仅关心上报结果

5.2 立即触发待上传任务

swift 复制代码
MaterialMonitor.shared.flushPendingUploads()

通常无需手动调用 —— SDK 在采集成功 / 网络恢复 / 重试到期时自动触发。

5.3 查询记录

swift 复制代码
// Swift
let records = MaterialMonitor.shared.getRecords(style: .banner)

// ObjC(按 adType 整数)
NSArray<FrameRecord *> *records = [[MaterialMonitor shared] getRecordsForAdType:2];

返回非 dead 状态(pending / uploading / retryWaiting)的所有记录。

5.4 删除记录

swift 复制代码
MaterialMonitor.shared.deleteRecord(record)

行为分支:

当前状态 行为
pending / retryWaiting 立即删除文件对,不触发回调
uploading 标记为 dead,HTTP 完成后静默删除
dead 立即删除文件对

5.5 获取当前配置

swift 复制代码
let config = MaterialMonitor.shared.getConfig()

未初始化或已 shutdown 时返回 nil。


六、回调协议

6.1 FrameCallback(采集 + 上报)

swift 复制代码
@objc public protocol FrameCallback: AnyObject {
    func onCollectSuccess(_ record: FrameRecord)
    func onCollectFailure(errorCode: Int, errorName: String, message: String)
    func onReportSuccess(_ record: FrameRecord)
    func onReportSkip(_ record: FrameRecord, reason: String)
    func onReportFailure(errorCode: Int, errorName: String, message: String, record: FrameRecord?)
}
回调 触发时机
onCollectSuccess 截图成功 + 落盘 + 入队
onCollectFailure 截图或入队失败(不会再触发后续上报回调)
onReportSuccess 服务端返回 code=0
onReportSkip show_id 已存在(duplicated_show_id)
onReportFailure 重试耗尽 / 鉴权失败 / 参数错误 / 配额耗尽等终态

6.2 ReportCallback(仅上报)

reportFrameRecord 使用,签名同 FrameCallback 的上报部分。

6.3 关键特性 ⚠️

  • 所有回调在主线程派发,可直接访问 UI
  • 回调采用弱引用(weak),ViewController 释放后回调静默丢弃,不会泄漏
  • 采集流程进行中(delay + 截图 + 压缩 + 落盘)callback 被强引用持有,VC 在此期间不会被释放(最长 ~3 秒 + 处理时间),需注意

七、数据模型

7.1 AdTimingInput

swift 复制代码
public init(requestTimeMs: Int64, fillTimeMs: Int64, showTimeMs: Int64)

广告各阶段时间戳(毫秒)。便利构造:

swift 复制代码
let timing = AdTimingInput.sameWallClockMs(Int64(Date().timeIntervalSince1970 * 1000))

7.2 FrameOptions

swift 复制代码
public init(
    delayMs: Int           = 500,
    maxSizeKb: Int         = 300,
    maxLongEdge: Int       = 1080,
    keepQualityFirst: Bool = true,
    excludeViews: [UIView] = []
)
字段 默认 说明
delayMs 500 等待广告素材加载完成的延迟(ms)。视频广告建议 2000+,Banner、原生 200~300 即可
maxSizeKb 300 JPEG 压缩目标最大体积(KB)
maxLongEdge 1080 落盘 JPEG 最长边像素上限
keepQualityFirst true 是否优先保证图像质量(从 0.7 起降质量)
excludeViews [] 需要遮盖的子视图(隐私区域,如自定义密码框)

7.3 FrameRecord

代表一次采集结果。业务方一般只读使用。

字段 类型 说明
showId String 广告展示 ID(服务端幂等去重键)
uploadTaskId String SDK 内部 UUID
placementId String 广告位 ID
style MaterialStyle 广告样式(Swift)
adType Int 广告类型整数(ObjC 友好)
requestTimeMs / fillTimeMs / showTimeMs Int64 时间戳(毫秒)
imageUrl String? 原生素材 URL(仅 native 场景)
localFilePath String 沙盒绝对路径
imageHash String pHash(16 字符小写 hex)
createdAt Int64 采集时间戳(毫秒)

7.4 MMAdInfo

广告信息,业务方在 onAdShow 回调中构造:

字段 类型 必填 说明
requestId String ✅ 请求 ID
showId String ✅ 广告展示唯一 ID(幂等去重键)
placementId String ✅ 广告位 ID
adSourceId String ✅ 广告源 ID
adNetworkFirmId Int ✅ 广告网络 ID
adType Int ✅ 广告类型整数:0=Native / 1=RewardedVideo / 2=Banner / 3=Interstitial / 4=Splash
requestTimestamp Int64 ✅ 请求时间戳(毫秒)
fillTimestamp Int64 ✅ 填充时间戳(毫秒)
showTimestamp Int64 ✅ 展示时间戳(毫秒)
creativeId String? 选 创意 ID(部分广告网络不返回)
ecpm NSNumber? 选 eCPM
sceneId String? 选 场景 ID

7.5 MaterialStyle

枚举值 adType rawValue
.native 0 "native"
.rewarded 1 "rewarded"
.banner 2 "banner"
.interstitial 3 "interstitial"
.splash 4 "splash"

八、错误码

MMErrorCode 枚举(ObjC 用 MMErrorCodeXxx 前缀):

采集错误(1xxx)

数字值 errorName 含义 是否可重试
1001 VIEW_INVALID View 为 nil 或未 attached 到 window ❌
1002 SCENE_INVALID keyWindow 找不到或顶层 VC 已 dismiss ❌
1003 COLLECT_FAILED 截图失败(如 view 已释放) ❌
1004 COMPRESS_FAILED JPEG 压缩失败 ❌

上报错误(2xxx)

数字值 errorName 含义 是否可重试
2001 PARAMS_INVALID 必填参数缺失或非法 ❌
2002 NETWORK_ERROR 网络请求失败 / 超时 ✅
2003 AUTH_FAILED 鉴权失败(检查 appId / appKey) ❌
2004 RETRY_EXHAUSTED 重试次数耗尽 ❌
2005 UNINITIALIZED SDK 未初始化或已 shutdown ❌
2006 NOT_READY 上传配置不全(appId / appKey 缺失) ❌
2007 STRATEGY_DISABLED 当前广告位被策略关闭 ❌

九、常见问题、注意事项与推荐配置

Q1:必须在主线程调用采集吗?

是。SDK 内部 assert(Thread.isMainThread),非主线程调用会触发 assert(DEBUG 包崩溃,Release 包行为未定义)。

Q2:采集延迟(delayMs)该怎么设?

广告类型 推荐延迟
Banner 200ms
插屏 500ms
激励视频 1000ms
开屏 2000~3000ms(视频开屏给足渲染时间)
Native 200~500ms(业务方渲染较快)

Q3:回调里 record.localFilePath 何时可用?

onCollectSuccess 之后 sandbox 文件已存在;onReportSuccess / onReportFailure 之后文件可能已被清理,不要在上报回调里读 localFilePath。

Q4:内存占用与磁盘空间?

  • 内存:每次采集峰值约 = 截图原图体积(4×宽×高 字节,1080p 约 8MB)+ JPEG 临时 buffer。压缩完成后释放
  • 磁盘:默认上限 100MB(maxCacheSizeMb),超出后冷启动扫描会按 createdAt 排序删除最老的 dead 记录

Q5:视频广告截图是黑屏 / 视频画面缺失?

iOS 系统级限制:

  • ✅ AVPlayerLayer 渲染的视频 —— SDK 已实现合成
  • ❌ AVSampleBufferDisplayLayer —— 无法抽帧
  • ❌ CAMetalLayer / CAEAGLLayer —— 无法抽帧
  • ❌ DRM 保护内容 —— 系统禁止抽帧

对于后三种情况,iOS 系统级别无法从应用进程内抽帧。若需视频素材监测,建议由服务端通过广告网络下发的素材 URL 直接拉取分析。

Q6:debugPerfLogging 该开启吗?

仅在联调期开启。开启后会通过 print 输出大量 Debug 日志:

  • 性能损耗:每次截图 / 上传增加约 5ms
  • 隐私风险:日志中含 app_id / sign / meta JSON 等字段
  • 生产环境必须关闭

Q7:APP 进入后台后还会继续上传吗?

会,直到系统挂起 App。SDK 使用普通 URLSession,不支持后台传输。若 App 被杀死,未上传的任务会在下次冷启动时恢复(pending 状态)。

Q8:如何对接 ObjC 工程?

SDK 全面 @objc 标注。需要:

  1. 在 Bridging-Header.h 中导入:#import <MaterialMonitorSDK/MaterialMonitorSDK-Swift.h>
  2. ObjC Builder 类名:MMConfigBuilder
  3. 实现协议:@interface MyVC : UIViewController <FrameCallback>

附录 A:完整接入示例

A.1 AppDelegate 初始化

swift 复制代码
import MaterialMonitorSDK
import AppTrackingTransparency
import AdSupport

@main
class AppDelegate: UIResponder, UIApplicationDelegate {

    func application(_ application: UIApplication,
                     didFinishLaunchingWithOptions launchOptions: [...]?) -> Bool {
        // ATT 授权后再初始化(推荐)
        if #available(iOS 14, *) {
            ATTrackingManager.requestTrackingAuthorization { status in
                DispatchQueue.main.async {
                    self.setupMaterialMonitor()
                }
            }
        } else {
            setupMaterialMonitor()
        }
        return true
    }

    private func setupMaterialMonitor() {
        let idfa = ASIdentifierManager.shared().advertisingIdentifier.uuidString

        let config = MaterialMonitorConfig.Builder()
            .baseUploadUrl("https://your-upload-host.com")
            .appId("your_app_id")
            .appKey("your_app_key")
            .idfa(idfa)
            .sdkVersion("6.5.0")
            .debugPerfLogging(false)
            .build()

        MaterialMonitor.initialize(config: config)
    }
}
swift 复制代码
extension MyViewController: FrameCallback {

    func onBannerAdShow(_ bannerView: UIView, extra: [String: Any]) {
        let adInfo = buildMMAdInfo(from: extra)

        MaterialMonitor.shared.collectFrameFromAdContainer(
            bannerView,
            adInfo: adInfo,
            timing: adInfo.timingInput,
            options: FrameOptions(delayMs: 200),
            callback: self
        )
    }

    // MARK: FrameCallback
    func onCollectSuccess(_ record: FrameRecord) {
        print("✅ 采集成功 taskId=\(record.uploadTaskId)")
    }
    func onCollectFailure(errorCode: Int, errorName: String, message: String) {
        print("❌ 采集失败 [\(errorName)] \(message)")
    }
    func onReportSuccess(_ record: FrameRecord) {
        print("✅ 上报成功 showId=\(record.showId)")
    }
    func onReportSkip(_ record: FrameRecord, reason: String) {
        print("⏭ 上报跳过:\(reason)")
    }
    func onReportFailure(errorCode: Int, errorName: String,
                        message: String, record: FrameRecord?) {
        print("❌ 上报失败 [\(errorName)] \(message)")
    }
}
objc 复制代码
#import <MaterialMonitorSDK/MaterialMonitorSDK-Swift.h>

@interface MyViewController () <FrameCallback>
@end

@implementation MyViewController

- (void)onBannerAdShow:(UIView *)bannerView extra:(NSDictionary *)extra {
    MMAdInfo *adInfo = [self buildMMAdInfoFromExtra:extra];
    AdTimingInput *timing = [[AdTimingInput alloc]
        initWithRequestTimeMs:adInfo.requestTimestamp
                   fillTimeMs:adInfo.fillTimestamp
                   showTimeMs:adInfo.showTimestamp];
    FrameOptions *options = [[FrameOptions alloc] init];

    [[MaterialMonitor shared] collectFrameFromAdContainer:bannerView
                                                   adInfo:adInfo
                                                   timing:timing
                                                  options:options
                                                 callback:self];
}

#pragma mark - FrameCallback
- (void)onCollectSuccess:(FrameRecord *)record {
    NSLog(@"✅ 采集成功 %@", record.uploadTaskId);
}
- (void)onCollectFailureWithErrorCode:(NSInteger)code
                            errorName:(NSString *)name
                              message:(NSString *)msg {
    NSLog(@"❌ 采集失败 [%@] %@", name, msg);
}
- (void)onReportSuccess:(FrameRecord *)record { /* ... */ }
- (void)onReportSkip:(FrameRecord *)record reason:(NSString *)reason { /* ... */ }
- (void)onReportFailureWithErrorCode:(NSInteger)code
                           errorName:(NSString *)name
                             message:(NSString *)msg
                              record:(FrameRecord *)record { /* ... */ }
@end

A.4 原生广告接入

原生广告(业务方自渲染)与 Banner 共用 collectFrameFromAdContainer,把渲染好的容器 view 传入即可:

swift 复制代码
func nativeAdDidShow(_ nativeView: UIView) {
    MaterialMonitor.shared.collectFrameFromAdContainer(
        nativeView,
        adInfo: adInfo,
        timing: timing,
        options: FrameOptions(delayMs: 300),   // 原生素材渲染稍慢,给 300ms
        callback: self
    )
}

最近修改: 2026-09-08Powered by