- MaterialMonitor 是一款广告素材监测 SDK,用于在广告展示时采集广告画面截图(含视频帧合成)+ 元数据,并自动上传到素材审计服务端,用于素材合规审计与质量监测。
- 下载地址:http://info.appsmartsite.com/Material_monitor/iOS_MaterialMonitor.zip
| 能力 | 说明 |
|---|---|
| 截图采集 | 支持开屏 / Banner / 插屏 / 激励视频 / 原生广告 |
| 视频合成截图 | 自动识别 AVPlayerLayer 并合成视频帧 + UIKit 覆盖层 |
| 持久化队列 | 沙盒落盘 + 冷启动恢复 + 重试 + 网络感知 |
| 自动上报 | 内置签名(HMAC-MD5) + multipart 上传 + 限流重试策略 |
| 隐私保护 | 自动遮盖密码输入框 / firstResponder,支持自定义遮盖区域 |
| 策略感知 | 支持按 placementId / networkFirmId / 样式动态启停 |
调用时机:建议在 ATT 授权回调之后初始化(确保 IDFA 可用),整个 App 生命周期仅调用一次。
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)
}
}
#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];
}
提示 1:
MaterialMonitor是单例(MaterialMonitor.shared),初始化后通过MaterialMonitor.shared.xxx()调用所有实例方法。提示 2:所有参数都有合理默认值,最少必填
appId和appKey即可工作;其他参数按需覆盖。提示 3:ObjC 不支持
retryDelayMs(_:)(Swift 数组桥接限制),ObjC 工程使用内置默认[5000, 15000]。
允许在运行中重复调用 initialize(config:) —— SDK 会自动 teardown 旧调度器(取消定时器、中断在途请求、清空回调),再用新 config 重建。冷启动扫描会重做,磁盘已有任务保留。
MaterialMonitor.shutdown()
[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 日志。生产环境务必关闭 |
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 |
所有采集方法必须在主线程调用。截图采集与上报回调均在主线程派发。
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):
func onBannerAdShow(_ bannerView: UIView, extra: [String: Any]) {
MaterialMonitor.shared.collectFrameFromAdContainer(
bannerView,
adInfo: buildMMAdInfo(extra),
timing: buildTiming(extra),
options: FrameOptions(delayMs: 200), // Banner 延迟可较短
callback: self
)
}
public func collectFrameFromAdPage(
adInfo: MMAdInfo,
timing: AdTimingInput,
options: FrameOptions,
callback: FrameCallback
)
SDK 会自动定位最顶层 presentedViewController 的 view,若不存在则截 keyWindow。
Swift 示例(开屏):
func splashAdDidShow() {
MaterialMonitor.shared.collectFrameFromAdPage(
adInfo: adInfo,
timing: timing,
options: FrameOptions(delayMs: 3000), // 开屏视频较长,给充足渲染时间 毫秒
callback: self
)
}
把已有的 FrameRecord 重新入队(场景:业务侧延后上报、跨进程恢复)。
public func reportFrameRecord(
_ record: FrameRecord,
adInfo: MMAdInfo,
callback: ReportCallback?
)
说明:
record 必须是之前通过 collectFrame* 拿到的(含合法 localFilePath)show_id 和 upload_task_id 双重去重MaterialMonitor.shared.flushPendingUploads()
通常无需手动调用 —— SDK 在采集成功 / 网络恢复 / 重试到期时自动触发。
// Swift
let records = MaterialMonitor.shared.getRecords(style: .banner)
// ObjC(按 adType 整数)
NSArray<FrameRecord *> *records = [[MaterialMonitor shared] getRecordsForAdType:2];
返回非 dead 状态(pending / uploading / retryWaiting)的所有记录。
MaterialMonitor.shared.deleteRecord(record)
行为分支:
| 当前状态 | 行为 |
|---|---|
| pending / retryWaiting | 立即删除文件对,不触发回调 |
| uploading | 标记为 dead,HTTP 完成后静默删除 |
| dead | 立即删除文件对 |
let config = MaterialMonitor.shared.getConfig()
未初始化或已 shutdown 时返回 nil。
@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 |
重试耗尽 / 鉴权失败 / 参数错误 / 配额耗尽等终态 |
reportFrameRecord 使用,签名同 FrameCallback 的上报部分。
weak),ViewController 释放后回调静默丢弃,不会泄漏AdTimingInputpublic init(requestTimeMs: Int64, fillTimeMs: Int64, showTimeMs: Int64)
广告各阶段时间戳(毫秒)。便利构造:
let timing = AdTimingInput.sameWallClockMs(Int64(Date().timeIntervalSince1970 * 1000))
FrameOptionspublic 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 |
[] | 需要遮盖的子视图(隐私区域,如自定义密码框) |
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 | 采集时间戳(毫秒) |
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 |
MaterialStyle| 枚举值 | adType | rawValue |
|---|---|---|
.native |
0 | "native" |
.rewarded |
1 | "rewarded" |
.banner |
2 | "banner" |
.interstitial |
3 | "interstitial" |
.splash |
4 | "splash" |
MMErrorCode 枚举(ObjC 用 MMErrorCodeXxx 前缀):
| 数字值 | errorName | 含义 | 是否可重试 |
|---|---|---|---|
| 1001 | VIEW_INVALID |
View 为 nil 或未 attached 到 window | ❌ |
| 1002 | SCENE_INVALID |
keyWindow 找不到或顶层 VC 已 dismiss | ❌ |
| 1003 | COLLECT_FAILED |
截图失败(如 view 已释放) | ❌ |
| 1004 | COMPRESS_FAILED |
JPEG 压缩失败 | ❌ |
| 数字值 | 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 |
当前广告位被策略关闭 | ❌ |
是。SDK 内部 assert(Thread.isMainThread),非主线程调用会触发 assert(DEBUG 包崩溃,Release 包行为未定义)。
delayMs)该怎么设?| 广告类型 | 推荐延迟 |
|---|---|
| Banner | 200ms |
| 插屏 | 500ms |
| 激励视频 | 1000ms |
| 开屏 | 2000~3000ms(视频开屏给足渲染时间) |
| Native | 200~500ms(业务方渲染较快) |
record.localFilePath 何时可用?onCollectSuccess 之后 sandbox 文件已存在;onReportSuccess / onReportFailure 之后文件可能已被清理,不要在上报回调里读 localFilePath。
maxCacheSizeMb),超出后冷启动扫描会按 createdAt 排序删除最老的 dead 记录iOS 系统级限制:
对于后三种情况,iOS 系统级别无法从应用进程内抽帧。若需视频素材监测,建议由服务端通过广告网络下发的素材 URL 直接拉取分析。
仅在联调期开启。开启后会通过 print 输出大量 Debug 日志:
app_id / sign / meta JSON 等字段会,直到系统挂起 App。SDK 使用普通 URLSession,不支持后台传输。若 App 被杀死,未上传的任务会在下次冷启动时恢复(pending 状态)。
SDK 全面 @objc 标注。需要:
Bridging-Header.h 中导入:#import <MaterialMonitorSDK/MaterialMonitorSDK-Swift.h>MMConfigBuilder@interface MyVC : UIViewController <FrameCallback>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)
}
}
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)")
}
}
#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
原生广告(业务方自渲染)与 Banner 共用 collectFrameFromAdContainer,把渲染好的容器 view 传入即可:
func nativeAdDidShow(_ nativeView: UIView) {
MaterialMonitor.shared.collectFrameFromAdContainer(
nativeView,
adInfo: adInfo,
timing: timing,
options: FrameOptions(delayMs: 300), // 原生素材渲染稍慢,给 300ms
callback: self
)
}