近段时间,苹果终于在大陆区开放了应用商店的竞价广告。毫无疑问又开启了苹果应用导量的新玩法,各大厂商都紧跟脚步吃螃蟹。本篇讲解苹果广告中的归因部分。
苹果广告其实在海外已运行多年,而因为IDFA的政策变动,现在苹果有新旧两套归因框架,通常我们都要接入。

一、iAd 和 AdServices 框架概述

iAd 框架:适用于iOS14.3以下版本,基于IDFA,需要用户允许使用IDFA。尤其iOS14.0起,IDFA的政策变动,要接入ATT追踪框架。
归因流程:打开APP -> 调用iAd框架 -> 读取广告因素(JSON) -> 发送广告因素到后端 -> 发送激活日志到后端
AdServices 框架:适用于iOS14.3及以上版本,不需用户授权。
归因流程:打开APP -> 调用AdServices框架 -> 读取token -> 发送token到苹果后端换取广告因素(JSON) -> 发送广告因素到后端 -> 发送激活日志到后端
两框架在流程上区别不大,得到数据也类似,主要区别是AdServices只有ID,没有具体的名字。详细JSON字段:
两套框架都要接,详细区别不大,主要是跟iOS版本相关

区别 iAd AdServices
归因窗口期 30天 30天
ATT影响 支持ATT Opt-In 不影响
归因误差率 15%-70%不等 10%左右或更低
数据延迟 三方MMP数据称3秒内返回结果比例大于50% 三方MMP数据称延迟0.5-1秒
参数丰富度 较全 较少(只返回ID)
是否支持展示归因和指纹信息归因 不支持 不支持
是否支持非AppStore上架APP(越狱包) 不支持 不支持

▲▲▲高版本(14.5+)的idfa获取,要等待弹窗被用户授权后才能得到,所以需要延迟调用广告归因和激活日志:

二、与第三方广告的归因的区别

1、第三方广告(如头条快手)使用的是广告点击的监测短链,由广告商回传给我们后台;苹果采用的是iAd和AdServices,由接入的苹果SDK发送参数到我们后台;
第三广告的监测短链示例:
https://api.myhost.com/ad/toutiao/click?adkey=abcde&idfa=__IDFA__&ip=__IP__&os=__OS__&callback=__CALLBACK__
苹果ASA的客户端回传示例:
https://api.myhost.com/ad/asa/click?idfa=xxxx&orgId=1234&campaignId=123456&adGroupId=123456&keywordId=12345678
(注意客户端回传的参数,若没有增加IP地址的参数,则需要服务端读取请求者的IP地址,以作归因参数)
2、第三方广告在点击时回传,苹果广告在应用打开时回传;
3、第三方广告使用自定义的adkey作为广告依据,苹果广告建议使用苹果的广告组ID(adGroupId)作为后台广告依据;
广告组ID可以在苹果投放后台右上角查看,如图

自己BI后台的广告列表示例:
无法复制加载中的内容

三、苹果端SDK的接入

1、准备条件,开发环境 Xcode12.3+,MacOS11+。如果版本不满足,则需更新开发软件(和系统)。
2、接入方法,添加iAd到Xcode项目:
1)选择项目主文件 > TARGETS > General

2)引入 iAd.framework、AdServices.framwork、AdSupport.framwork

3)进入 Link Binary With Libraries,将上述3个框架都改为Optional

4、代码 MySDK.m(Objective-C)

至此,苹果ASA的广告归因接入告一段落
待后台对接好广告归因逻辑代码,苹果包上架应用商店,就可以了。
附:IDFA的新旧版本的接入

四、引用

官方链接: https://ads.apple.com/cn/help/advanced/0028-apple-ads-attribution-api/
苹果搜索广告ASA提审经验和投放技巧: https://blog.csdn.net/testflight121/article/details/119177975

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
+(void)initSDK{

//... ...

//苹果ASA;延迟4秒再发送,等ATT用户操作结果,可能有IDFA

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(4 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

[MySDK LogAds];

});

//激活日志;延迟6秒再发送,先让Ads发送完再发

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(6 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

[MySDK LogOpen];

});

//... ...

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
/** 导入上述3个框架*/

#import <iAd/iAd.h>#import <AdServices/AdServices.h>#import <AppTrackingTransparency/AppTrackingTransparency.h>

/** 苹果Ads广告*/

/** TODO:有些旧设备新系统(iPhone8),会出现token为空的问题*/

+(void)LogAds{// 14.3之后

if (@available(iOS 14.3, *)) {

NSError *error;

NSString *token = [AAAttribution attributionTokenWithError:&error];

NSLog(@"LogAds:AdServces,Token: %@", token);

if (token != nil) {

// 1、发送POST给苹果得到归因数据

[MySDK sendToken:[MySDK getANullableString:@"token" content:token] completeBlock:^(NSDictionary *attrData) {

//异步,会延后

NSLog(@"LogAds:14.3+ Dict: %@", attrData);

//TODO::发送数据给服务端

// ... ...

}];

}

// 14.3之前

} else {

if ([[ADClient sharedClient] respondsToSelector:@selector(requestAttributionDetailsWithBlock:)]) {

NSLog(@"LogAds:iAd called");

[[ADClient sharedClient] requestAttributionDetailsWithBlock:^(NSDictionary *attrData, NSError *error) {

//异步,会延后

NSLog(@"LogAds:14- Dict: %@", attrData);

//TODO::发送数据给服务端

// ... ...

}];

}

}

}

/** 读取可能为空的字符串*/

+(nullable NSString *)getANullableString:(NSString *)desc content:(NSString *)content{

if(content == nil){

return @"";

}

return [NSString stringWithFormat:@"%@", content];

}

/** 发送归因token得到数据 */

+(void)sendToken:(NSString *)token completeBlock:(void(^)(NSDictionary* data))completeBlock{

NSString *url = [NSString stringWithFormat:@"https://api-adservices.apple.com/api/v1/"];

NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:url]];

request.HTTPMethod = @"POST";

[request addValue:@"text/plain" forHTTPHeaderField:@"Content-Type"];

NSData* postData = [token dataUsingEncoding:NSUTF8StringEncoding];

[request setHTTPBody:postData];

NSURLSession *session = [NSURLSession sharedSession];

NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {

NSDictionary * result = NULL;

if (error) {

//请求失败

NSLog(@"LogAds:sendToken ERR");

if (completeBlock) {

NSMutableDictionary *nulldict = [NSMutableDictionary dictionary];

completeBlock(nulldict);

}

}else{

// 请求成功

NSError *resError;

NSMutableDictionary *resDic = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&resError];

result = [[NSDictionary alloc] initWithDictionary:resDic];

if (completeBlock) {

completeBlock(result);

}

}

}];

[dataTask resume];

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#import <AppTrackingTransparency/AppTrackingTransparency.h>



-(void)initSDK{

//... ...

//IDFA iOS14不同方式

if (@available(iOS 14, *)) {// iOS14及以上版本需要先请求权限

[ATTrackingManager requestTrackingAuthorizationWithCompletionHandler:^(ATTrackingManagerAuthorizationStatus status) {

// 获取到权限后,依然使用老方法获取idfa

// iOS14以后,idfa在回调之后才能获得,应当等回调后再发送日志

if (status == ATTrackingManagerAuthorizationStatusAuthorized) {

self->_idfa = [[ASIdentifierManager sharedManager].advertisingIdentifier UUIDString];

}

}];

}else{

_idfa = [[[ASIdentifierManager sharedManager] advertisingIdentifier] UUIDString];

}

//... ...

}