菜单

广告素材探测-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()
            // ── 上传服务地址(留空则使用 SDK 内置默认地址)──────────────────
            .baseUploadUrl("https://your-upload-host.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];
    // ── 上传服务地址(留空使用 SDK 内置默认) ──
    [builder baseUploadUrl:@"https://your-upload-host.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];
}

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

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

提示 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://api.example.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_idupload_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-07-02Powered by