Post

iOS UUID 解析|Swift装置唯一识别与IDFV替代方案全攻略

针对iOS开发者面临UUID限制与封锁问题,解析Swift中UUID取得方法及IDFV替代方案,并提供Key-Chain持久化UUID实作,助你稳定辨识装置唯一值,避免重装APP导致识别码变动,提升用户追踪与资料一致性。

iOS UUID 解析|Swift装置唯一识别与IDFV替代方案全攻略

Click here to view the English version of this article.

點擊這裡查看本文章正體中文版本。

基于 SEO 考量,本文标题与描述经 AI 调整,原始版本请参考内文。


iOS UUID 的那些事 (Swift/iOS ≥ 6)

iPlayground 2018 回来 & UUID那些事

前言:

上周六、日跑去参加 iPlayground Apple 软体开发者研讨会,这个活动讯息是同事PASS过来的,去之前我也不清楚这个活动。

两天下来,整题活动跟时程安排流畅,议程内容:

  1. 趣味的:脚踏车、凋零的Code、iOS/API 演进史、威利在哪里(CoreML Vision)

  2. 实用的:测试类 (XCUITest、依赖注入)、SpriteKit 做动画效果的替代方案、GraphQL

  3. 真功夫:深入拆解Swift、iOS 越狱/Tweak开发、Redux

脚踏车Project 印象深刻,用iPhone手机当感测器感测脚踏车踏板转动,直接在台上骑脚踏车切换投影片(前辈主要目标是要做开源版zwift,也分享了许多地雷,例如Client/Sever通信、延迟问题、磁场干扰)

凋零的Dirty Code;听得心有戚戚,在心里会心一笑;技术债就是这样一直累积下来的,开发时程赶,所以用架构性较差的快速做法,后人接手改也没时间重构,就越积越多;到最后可能真的只有打掉这条路了

测试类(Design Patterns in XCUITest) KKBOX的前辈 ,完全没藏私直接公开他们的作法及程式范例细节还有遇到的雷、解决办法,这堂也是对我们工作上最有帮助的项目;测试这块是我一直想加强的部分,可以回去好好研究研究

Lighting Talk的部分在台下听得也好想上去分享😂 下次要提早做好准备了!

会后的offical party,酒水食物场地都很有诚意,听前辈们的真心话吐露,很轻松有趣之外还吸收许多职场软实力.

台大后台咖啡

台大后台咖啡

我才知道原来这是第一届,真的有荣幸能够参加,所有工作人员跟讲者辛苦了!

去参加研讨会的目的不外乎就是要: 增加广度 ,吸收新知、了解生态、碰一些平常不会接触的项目跟 增加深度 ,如果是自己已经摸过的项目就是去听听看有没有遗漏的地方或是还有其他做法没发现.

抄了许多笔记可以回来慢慢研究回味。

UUID的那些事

因为我听完回去后马上实际应用到APP上;这堂是由Zonble前辈主讲,我听到从iPhone OS 2写到iOS 12我就跪了;由于入行较晚,我是从iOS 11/Swift 4 才开始写,所以没碰到那些因为苹果修改API的动乱时期。

想想UUID从可以取得到封锁也是蛮合理的;如果是用在良善的地方:辨识使用者装置、广告或第三方运用唯一性去做广告操作;但如果有厂商想做恶,也可以透过这个机制反查,知道你这只手机的主人是怎么样的人?(例如有装旅游+台北等公车+BMW APP+婴儿照护 就能推测你很常出国家里有小孩而且住在台北 之类的资讯)再加上你在APP上输入的个资,能拿去做什么应用不敢想像

但这其中也波及到很多正当守法的用户,像是本来用UUID当使用者的资料解密KEY或用UUID当装置判断都受到很大的影响;真佩服那个时期的工程师前辈们,这些影响老板跟使用者一定会狂骂,要急中生智找其他替代办法.

替代方案:

本篇文章以取得UUID辨识装置唯一值为主,如果是要找知道使用者装了哪些APP的替代方案可参考以下关键字搜寻做法: UIPasteboard pasteboardWithName: create: (运用剪贴簿在APP间共享) 、canOpenURL: info.plist LSApplicationQueriesSchmes (运用canOpenURL检查APO有无安装,要在info.plist列举,最多50笔)

  1. 用MAC Address当UUID,但后来也被BAN了

  2. Finger Printing (Canvas/User-Agent…) :没研究,不过这项目主要拿来让safari跟app能产生同样的UUID, Deferred Deep Linking (延迟深度连结)用 AmIUnique?

  3. ID entifier F or V endor (IDFV):目前主流的解决方案🏆 概念是苹果会根据你的Bundle ID前辍为使用者产生UUID,相同的Bundle ID前辍会产生相同的UUID,例如:com.518.work/com.518.job 同个装置会得到相同的UUID 如同原文ID For Vendor,相同的前辍苹果认为即是相同厂商的APP,所以共享UUID是允许的。

ID entifier F or V endor (IDFV):

1
let DEVICE_UUID:String = UIDevice.current.identifierForVendor?.uuidString ?? UUID().uuidString

唯需注意:当所有同Vendor的APP都移除后再重装就会产生新的UUID ( com.518.work跟com.518.job都被删除,再装回com.518.work这时就会产生新的UUID ) 同理如果你只有一个APP,删掉重装就会产生新的UUID

因为这个特性,我们公司的其他APP是使用Key-Chain来解决这个问题,听了讲者前辈的指点也验证了这个做法是正确的!

流程如下:

Key-Chain UUID栏位有值时取值,无则取IDFA的UUID值并回写

Key-Chain UUID栏位有值时取值,无则取IDFA的UUID值并回写

Key-Chain 写入方式:

1
2
3
4
5
6
7
8
9
if let data = DEVICE_UUID.data(using: .utf8) {
    let query = [
        kSecClass as String       : kSecClassGenericPassword as String,
        kSecAttrAccount as String : "DEVICE_UUID",
        kSecValueData as String   : data ] as [String : Any]
    
    SecItemDelete(query as CFDictionary)
    SecItemAdd(query as CFDictionary, nil)
}

Key-Chain 读取方式:

1
2
3
4
5
6
7
8
9
10
11
let query = [
    kSecClass as String       : kSecClassGenericPassword,
    kSecAttrAccount as String : "DEVICE_UUID",
    kSecReturnData as String  : kCFBooleanTrue,
    kSecMatchLimit as String  : kSecMatchLimitOne ] as [String : Any]

var dataTypeRef: AnyObject? = nil
let status: OSStatus = SecItemCopyMatching(query as CFDictionary, &dataTypeRef)
if status == noErr,let dataTypeRef = dataTypeRef as? Data,let uuid = String(data:dataTypeRef, encoding: .utf8) {
   //uuid
} 

如果嫌 Key-Chain 操作太繁琐可以自行封装或使用第三方套件。

完整CODE:

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
let DEVICE_UUID:String = {
    let query = [
        kSecClass as String       : kSecClassGenericPassword,
        kSecAttrAccount as String : "DEVICE_UUID",
        kSecReturnData as String  : kCFBooleanTrue,
        kSecMatchLimit as String  : kSecMatchLimitOne ] as [String : Any]
    
    var dataTypeRef: AnyObject? = nil
    let status: OSStatus = SecItemCopyMatching(query as CFDictionary, &dataTypeRef)
    if status == noErr,let dataTypeRef = dataTypeRef as? Data,let uuid = String(data:dataTypeRef, encoding: .utf8) {
        return uuid
    } else {
        let DEVICE_UUID:String = UIDevice.current.identifierForVendor?.uuidString ?? UUID().uuidString
        if let data = DEVICE_UUID.data(using: .utf8) {
            let query = [
                kSecClass as String       : kSecClassGenericPassword as String,
                kSecAttrAccount as String : "DEVICE_UUID",
                kSecValueData as String   : data ] as [String : Any]
        
            SecItemDelete(query as CFDictionary)
            SecItemAdd(query as CFDictionary, nil)
        }
        return DEVICE_UUID
    }
}()

因为我在其他Extension Target也需要参照所以直接包成一个闭包参数使用

有任何问题及指教欢迎 与我联络


Buy me a beer

本文首次发表于 Medium (点击查看原始版本),由 ZMediumToMarkdown 提供自动转换与同步技术。

Improve this page on Github.

This post is licensed under CC BY 4.0 by the author.