iOS开发:跨开发商的应用数据共享

iOS

iOS上App都是沙盒隔离的,如何做广告效果跟踪、用户拉新邀请奖励?

本文将介绍一种可以跨开发商App进行数据共享的方法,基本原理是通过与Safari共享Cookie来实现。已实现放到github

更新:据闻iOS新版本不支持了,请自行测试确认。

写在前面

说下实际需求场景:

项目组开发的一个系列app,包括一个主应用(暂且称之为AppM)以及一系列子应用(暂且称之为AppX),AppM包含全部功能,而AppX则是另一个代码模板生成的多个应用,上线时AppM使用了公司开发者帐号Acc1,AppX则用了另外的多个帐号Accx。问题来了,产品功能合并,AppM中部分功能(比如攻略资讯)同步到AppC中了,原先分开的业务推送,现在也合并了,(可能)出现的问题是,假如需要对AppX及AppM同时推送同一条资讯,对于安装了多个这样应用的设备而言,用户有可能是接收到多条一模一样的推送消息。

原始需求就是要解决这个,服务端要过滤设备令牌,必须客户端能提供设备识别码。场景虽然不普遍,但有些公司在App Store上上架的多款app,确实会以不同开发者帐号提交的,每个帐号就是独立的开发商。

解决方案,一是能否在不同app中获取/生成相同的设备识别码?二是绕一下弯路,我们提供一个唯一码,但要求能在各个App间共享。我们分别来讨论两种方案。

设备识别码

其实广告商也很关心这个事情。

设备识别码,要求稳定、唯一,通俗讲就是今天跟昨天取到的是一致的、你的设备跟我的设备是不一样的。

MAC地址 && UDID

传统的获取方式,包括其他平台设备中使用的,比如说mac地址啊,确实可以!获取方法就是调用系统库网络相关接口:

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
#include <sys/sysctl.h>
#include <net/if.h>
#include <net/if_dl.h>

- (NSString *)macAddress
{
int mib[6];
size_t len;
char *buf;
unsigned char *ptr;
struct if_msghdr *ifm;
struct sockaddr_dl *sdl;

mib[0] = CTL_NET;
mib[1] = AF_ROUTE;
mib[2] = 0;
mib[3] = AF_LINK;
mib[4] = NET_RT_IFLIST;

if ((mib[5] = if_nametoindex("en0")) == 0) {
printf("Error: if_nametoindex error/n");
return NULL;
}
if (sysctl(mib, 6, NULL, &len, NULL, 0) < 0) {
printf("Error: sysctl, take 1/n");
return NULL;
}
if ((buf = malloc(len)) == NULL) {
printf("Could not allocate memory. error!/n");
return NULL;
}
if (sysctl(mib, 6, buf, &len, NULL, 0) < 0) {
printf("Error: sysctl, take 2");
return NULL;
}

ifm = (struct if_msghdr *)buf;
sdl = (struct sockaddr_dl *)(ifm + 1);
ptr = (unsigned char *)LLADDR(sdl);
NSString *mac = [NSString stringWithFormat:@"%02x:%02x:%02x:%02x:%02x:%02x", *ptr, *(ptr+1), *(ptr+2), *(ptr+3), *(ptr+4), *(ptr+5)];
free(buf);

return [mac uppercaseString];
}

但是Apple家不开心了啊!为什么我家的东西你们知道的一清二楚!所以基于mac地址的几种方案,在Apple屏蔽mac地址获取的高版本系统中都失效了,基于mac地址的OpenUDID都无效

Twolow-level networking APIs that used to return a MAC address now return thefixed value 02:00:00:00:00:00. The APIs in question are sysctl(NET_RT_IFLIST) and ioctl(SIOCGIFCONF).

同样的还有苹果自身提供的UDID:[[UIDevice currentDevice] uniqueIdentifier],40个字符的设备码,在iOS7之后被换成了prefix+IDFV。

IDFV

iOS6之后提供的,同一开发商在同一设备上的不同app能共享相同的IDFV,但对不同开发商是不一致的。获取方法:[[[UIDevice currentDevice] identifierForVendor] UUIDString]。同一开发商的app全部卸载后将重置。

IDFA

这是个提供给广告商的设备识别码,获取方法:[[[ASIdentifierManager sharedManager] advertisingIdentifier] UUIDString];,问题是,苹果继续加强隐私权限,用户可以手动关闭广告跟踪了,这时候拿到的IDFA也是非法值。

以上这些都是适用条件下理论唯一的识别码,除此之外,还有几个开源的设备识别码,不唯一(具体看碰撞率)不稳定但是可以应用在特定条件下,如OpenIDFA、SimuIateIDFA,详细可以看这篇介绍,但在实际业务上操作会复杂。比如OpenIDFA是有日期的加成,每天都变动,SimulateIDFA有启动日期及设备名称等信息加成,重启会变动。

单一的获取设备唯一码,都不太理想,对吧?再来看下数据共享的方案。

iOS应用间通信/数据共享

iOS应用间数据共享有许多应用场景,但实现方式却是屈指可数。首先,iOS中每个app都是独立的单一进程,且受限于iOS的沙盒隔离机制,app进程的访问权限是有限的。我们先看下目前进程间通信及数据共享的几种方式。

传统的IPC方式

  • 管道、共享内存、信号量
  • 本地socket

传统的共享内存(shmget等)等方式,苹果会允许?socket要求两端在线,普通应用进入后台后存活时间很短,现实意义不大。

URLScheme

这是常用的通信方式,目前也广泛使用在各大平台App的授权及OAuth2/SSO登录上。通过URL链接的形式调用,系统可以启动相关配置的应用,参数可以通过URL传递,或者下面介绍的粘贴板。目前的常用形式是,通过URLScheme发起调用,通过粘贴板共享数据。

粘贴板(UIPasteboard)

提供了三种粘贴板:

1
2
3
4
5
6
//获取系统级别的剪切板
+ (UIPasteboard *)generalPasteboard;
//获取一个自定义的剪切板 name参数为此剪切板的名称 create参数用于设置当这个剪切板不存在时 是否进行创建
+ (nullable UIPasteboard *)pasteboardWithName:(NSString *)pasteboardName create:(BOOL)create;
//获取一个应用内可用的剪切板
+ (UIPasteboard *)pasteboardWithUniqueName;

第1种粘贴板是系统级别,各应用均可访问修改;第2种,可以在同一开发者开发的其他应用程序中共享数据;第3种是第2中特例。有个关键方法:- (void)setData:(NSData *)data forPasteboardType:(NSString *)pasteboardType;,支持现有UTI类型数据或任意二进制,往粘贴板的第一个item写入数据。

这样的话,岂不是可以利用keychain(下文介绍)+ pasteboard的方式实现应用间数据共享?试了一下,使用keychain持久化,使用pasteboard共享,差点就完美了,

但存在问题:

  1. 微信、微博、QQ等平台分享会导致粘贴板清空,共享失效
  2. 即使粘贴板未被清空,【VendorA_APP_A】将数据共享到粘贴板(及keychain)后,设备重启,下次启动直接使用【VendorB_APP_B】,无法拿到共享数据,将提供与【VendorA_APP_A】完全不一致的设备标识信息

没有保证的,一口老血。

Keychain(Access Group)

Keychain是Apple提供的又一套数据共享、数据持久化的方案,实质上是对存放在/private/var/Keychains/keychain-2.db上的一个sqlite数据库的操作。当然这么好用的东西,苹果怎么可能没有权限控制。 苹果通过内置到app中的权限文件限制应用可访问的共享数据,还有所有访问自然都是限制在同一开发商中的。

之前我们也通过Keychain提供了一个设备识别码获取保存的框架,主要思路是,将IDFV及设备参数等发到后台获取一个GUID,将GUID写入keychain特定Access-Group中,这样同一个开发商便能够通过配置相同的Access-Group在设备上始终获取到同一个设备码,即使是原先所有app都被卸载后(IDFV会重置,见上文描述)。

可以通过Security框架进行keychain中内容的CURD操作,具体配置及操作其实已经有很多文章介绍就不啰嗦了。

实际上可以通过组合以上的几种方式来实现自己的需求的。微信、微博平台的授权登录、分享等是URLScheme+UIPasteboard组合的经典。在下面的介绍的SFCookie中,同样是多种方式组合。

SFCookie

这个方案主要是因为iOS9新增的SafariServices框架。SafariServices新增类SFSafariViewController允许app内使用Safari访问Web页面,数据互通(关于Safari数据互通,还有个“共享Web凭证”)。我们将通过访问Safari的Cookie来实现数据共享。

但是它也是受限的,SFSafariViewController不允许访问本地html(所以必须将html配置到服务端站点),且iOS10之后应用审核规范中冒出了一条,明确不允许隐藏Safari视图。实际测试发现,如果SFSafariViewController的视图被hidden或者alpha值过小,它是不会加载页面内容的,其他方面限制暂未发现。适当规避这个还是可以利用它来实现我们的功能的。

SFCookie提供的是,通过SFCookie来访问存放在Safari中的Cookie,至于Cookie内容是什么根据业务而定。就文章开头所举的例子的话,可以是一个简单的设备令牌。提供的接口如下:

1
2
3
4
5
6
7
8
9
10
11
12
// 设置appScheme及web页面路径
+ (void)setNTL_SafariCookie_AppScheme:(NSString *)appScheme;
+ (void)setNTL_SafariCookie_WebURL:(NSString *)webUrl;

// 获取cookie
- (void)getCookies:(NSArray<NSString *> *)names complete:(NTLSafariCookieCompletion)completion;

// 删除cookie
- (void)delCookies:(NSArray<NSString *> *)names complete:(NTLSafariCookieCompletion)completion;

// 设置cookie
- (void)setCookies:(NSDictionary *)nameValues complete:(NTLSafariCookieCompletion)completion;

以上调用均为异步回调,默认回调到主线程。

SFCookie实现比较简单,仅提供了UIApplication+SafariCookie这样一个拓展方法分类,以及一个简单的包含内嵌JS处理Cookie的html文件“sfc.html”。给项目加入SFCookie的支持,需要

  1. 将“sfc.html”放到服务端站点上,并使用setNTL_SafariCookie_WebURL配置好加载的地址
  2. 配置appScheme,以确保能从SFSafariViewController得到回调
  3. 将UIApplication+SafariCookie分类拓展的代码加入项目,或使用pod的方式亦可

回调正是通过URLScheme的方式实现。AppScheme是可以各个客户端动态配置的,并作为参数传递给Web页面来保证正确的回调。需注意的几点:

  1. 参数尽量不要掺杂特殊符号,虽然内部已经做了URL编解码处理;如果有特殊符号需测试检验;
  2. AppScheme在设备上唯一(默认是safaricookie),否则从Web页面发起调用后,由系统决定跳转;

回到文章开头所提的需求,我们可以结合keychain,基本流程是:

  1. SFCookie获取特定键的cookie,若能取到并检验有效,便是所需要的设备码,结束
  2. 获取keychain保存的共享的设备码,若存在便是所需设备码,并将结果写入SFCookie,结束
  3. 生成/获取设备码,写入keychain及SFCookie,结束

具体查看源码

Author: Jason

Permalink: http://blog.knpc21.com/ios/ios-safari-cookie/

文章默认使用 CC BY-NC-SA 4.0 协议进行许可,使用时请注意遵守协议。

Comments