漫談惟一設備ID

1、前言

設備ID,簡單來講就是一串符號(或者數字),映射現實中一臺設備。
若是這些符號和現實中的設備是一一對應的,可稱之爲「惟一設備ID(Unique Device Identifier)」。
html

不幸的是,對於Android平臺而言,沒有穩定的API可讓開發者獲取到這樣的設備ID。
開發者一般會遇到這樣的困境:
隨着項目的演進, 愈來愈多的地方須要用到設備ID;
然而隨着Android版本的升級,獲取設備ID卻愈來愈難了。
加上Android平臺碎片化的問題,獲取設備ID之路,能夠說是寸步難行。java

2、設備ID的做用

關於設備ID的做用,大概能夠分爲下面幾點:
android

  • 統計需求
    統計需求是設備ID最多見的用途,包括DAU, MAU的統計,行爲統計,廣告激活的統計等。git

  • 業務需求
    設備ID一般也用於業務中。 好比結合行爲統計作用戶畫像,覺得用戶提供個性化的服務,你們感覺比較明顯的就是新聞類和電商類的APP了。
    這類操做,有利有弊,仁者見仁智者見智,咱們這裏就很少作討論了。
    又如,定向推送,不必定是廣告推送,錯誤修復,內測推送等也會用到設備ID。
    還有是一些和特定業務結合的用途,好比構造分佈式ID等,就不一一列舉了。github

  • 風控需求
    設備ID還可用於防刷單,反做弊等。
    固然,風控需求單靠設備ID是沒法完成的,一般須要創建一套反做弊系統。
    關於這方面的內容,難以一言以蔽之,這裏咱們很少做展開。算法

3、獲取設備ID的API

獲取設備標識的API屈指可數,並且都或多或少有一些問題。
常規的API有如下這些:sql

IMEI

IMEI本該最理想的設備ID,具有惟一性,恢復出廠設置不會變化(真正的設備相關)。
然而,獲取IMEI須要 READ_PHONE_STATE 權限,估計你們也知道這個權限有多麻煩了。
尤爲是Android 6.0之後, 這類權限要動態申請,不少用戶可能會選擇拒絕受權。
咱們看到,有的APP不受權這個權限就沒法使用, 這可能會下降用戶對APP的好感度。
並且,Android 10.0 將完全禁止第三方應用獲取設備的IMEI, 即便申請了 READ_PHONE_STATE 權限。
因此,若是是新APP,不建議用IMEI做爲設備標識;
若是已經用IMEI做爲標識,要趕忙作兼容工做了,尤爲是作新設備標識和IMEI的映射。網絡

設備序列號

經過android.os.Build.SERIAL得到,由廠商提供。
若是廠商比較規範的話,設備序列號+Build.MANUFACTURER應該能惟一標識設備。
但現實是並不是全部廠商都按規範來,尤爲是早期的設備。
最致命的是,Android 8.0 以上,android.os.Build.SERIAL 總返回 「unknown」;
若要獲取序列號,可調用Build.getSerial() ,可是須要申請 READ_PHONE_STATE 權限。
到了Android 10.0以上,則和IMEI同樣,也被禁止獲取了。 整體來講,設備序列號有點雞肋:食之無味,棄之惋惜。dom

MAC地址

獲取MAC地址也是愈來愈困難了,
Android 6.0之後經過 WifiManager 獲取到的mac將是固定的:02:00:00:00:00:00, 再後來連讀取 /sys/class/net/wlan0/address 也獲取不到了。
現在只剩下面這種方法能夠獲取(沒有開啓wifi也能夠獲取到):分佈式

public static String getWifiMac() {
    try {
        Enumeration<NetworkInterface> enumeration = NetworkInterface.getNetworkInterfaces();
        if (enumeration == null) {
            return "";
        }
        while (enumeration.hasMoreElements()) {
            NetworkInterface netInterface = enumeration.nextElement();
            if (netInterface.getName().equals("wlan0")) {
                return formatMac(netInterface.getHardwareAddress());
            }
        }
    } catch (Exception e) {
        Log.e("tag", e.getMessage(), e);
    }
    return "";
}
複製代碼

再日後說不許這種方法也行不通了,且用且珍惜~

ANDROID_ID

Android ID 是獲取門檻最低的,不須要任何權限,64bit 的取值範圍,惟一性算是很好的了。
可是不足之處也很明顯:
一、刷機、root、恢復出廠設置等會使得 Android ID 改變;
二、Android 8.0以後,Android ID的規則發生了變化

  • 對於升級到8.0以前安裝的應用,ANDROID_ID會保持不變。若是卸載後從新安裝的話,ANDROID_ID將會改變。
  • 對於安裝在8.0系統的應用來講,ANDROID_ID根據應用簽名和用戶的不一樣而不一樣。ANDROID_ID的惟一決定於應用簽名、用戶和設備三者的組合。

兩個規則致使的結果就是:
第一,若是用戶安裝APP設備是8.0如下,後來卸載了,升級到8.0以後又重裝了應用,Android ID不同;
第二,不一樣簽名的APP,獲取到的Android ID不同。
其中第二點可能對於廣告聯盟之類的有所影響(若是彼此是用Android ID對比數據的話),因此Google文檔中說「請使用Advertising ID」,
不過你們都知道,Google的服務在國內用不了。

對Android ID作了約束,對隱私保護起到必定做用,而且用來作APP本身的活躍統計也仍是沒有問題的。

4、 設備ID的特性分析

筆者以前寫過一篇文章《Android設備惟一標識的獲取和構造》, 文中提到設備ID的兩個概念:惟一性和穩定性。
惟一性:兩臺不一樣的設備獲取到相同的設備ID不相同;
穩定性:同一臺設備在不一樣的時間, 獲取到設備ID相同。

分析惟一性,咱們能夠從ID的分配來入手:

  • 一、按規則構造
    好比自增ID(包括分步自增),分段構造的ID(如snowflake算法)等,此類ID能保證惟一性。
    設備ID中的IMEI,設備序列號,MAC等,都是按照規則構造的,理論上能保證惟一性。
    設備序列號是對廠商自己惟一,全局惟一須要在加上 Build.MANUFACTURER
    不過,設備序列號和MAC的惟一要打個問號,由於要看廠商是否遵照規則。
    但隨着手機產業的日漸成熟,傳統意義上的山寨設備已愈來愈少,因此大多數狀況下仍是惟一的。

  • 二、隨機生成
    好比UUID和Android ID,這類ID有必定的機率會重複,關鍵是看ID的長度(有多少bit)。
    有人作了這樣一張隨機數的衝突機率表:

左邊第一欄是bit數量,第二欄是對應的取值範圍,再後面是元素個數以及對應的衝突機率。
例如,假若有50000個32bit的隨機數,則這些隨機數中,至少有兩個相同數字的機率爲25%;
換一種說法,就是假若有四組數,每組都有50000個隨機數,則大約其中一組會有重複的數字。
32bit有43億的取值範圍,怎麼50000個隨機數就有這麼高的出現重複的機率?
若是對此感到困惑,能夠了解一下生日悖論

Android ID是長度爲16的十六進制字符串,其實就是64bit,咱們來分析一下其重複的機率:
假如APP累計激活量達到50億的APP,則每兩個這樣的APP就大約有一個會有重複的Android ID。
不過這裏的「重複」不是大量的重複,而是「至少有兩個相同」,也就是,若是設備激活量有50億(不少APP達不到-_-),那麼有可能會有少許的重複的Android ID。
整體而言, Android ID的惟一性仍是不錯的。
JDK的randomUUID,大體能夠認爲是128bit的隨機數(其中有6bit是固定的),即便到達200億的數量,有重複的機率也僅僅是10的負18次方,微乎其微。

穩定性有兩個層面:

  • 一、ID的生命週期
    IMEI,序列號,MAC等都是硬件相關,即便刷機也不會改變;
    Android ID則穩定性較弱,恢復出廠設置和刷機都會改變Android ID。
  • 二、受版本的變化的影響
    隨着Android版本的提高,Google對權限是越收越緊了。
    獲取設備ID的API,要麼收起不給用(IMEI), 要麼獲取變得困難(SERIAL ),要麼不一樣簽名的APP獲取的值不同(Android ID)。
    同時,Android 10中存儲權限也收縮了,以前的那種生成惟一ID寫到SD卡的某個角落的,以求卸載重裝後讀以前的ID等方法也不奏效了。
    增強隱私方面的權限,對用戶而言是好事,但對開發者而言就比較難受了。
    尤爲是有的API原本能夠用,升級後就獲取不到了,這種斷崖式的變化,可能會對數據統計形成影響。

5、設備ID的構造

不管是統計需求,仍是業務需求,都要求設備ID是惟一的,穩定的。
若是設備ID有重複,則活躍統計,用戶畫像,定向推送等通通都不許確了;
其中,影響最深是定向推送,送錯快遞還有可能追回,推送錯了就很差說了,若是推送內容又比較重要,後果不堪設想。
若是設備ID不穩定(ID變化),會影響到活躍統計(會認爲是新用戶),對用戶畫像也有較大影響(以前的ID關聯的行爲數據沒法跟蹤了)。

爲此,有必要設計一套方案,提供相對定穩定的,惟一的設備ID。
首先要明確兩個前提條件:
前面分析的設備ID中,在可用的前提下,出現重複的機率較小;
若是必定的頻率去觀察,好比說天天,整體而言,觀察到和昨天不同的機率也是較小的。
如何在原本就較小的機率的前提下,繼續下降機率呢?

5.1 方案分析

一種方案是組合設備ID(直接拼接,或者拼接後計算摘要)。
舉個例子,假如出現重複的機率和發生變化的的機率都是千分之一,
則對於兩臺不一樣設備,兩個設備ID同時重複的機率是百萬分之一,兩個設備ID至少有一個發生變化約爲千分之二。
也就是,拼接ID的效果是大大提升惟一性,可是必定程度上下降穩定性(只要其中一個要素變化,拼接的ID就變了)。
但事實上,現在能拿到的設備ID,最突出的矛盾是不穩定,因此,咱們不能爲了提升惟一性而犧牲穩定性。

要提升穩定性,能夠引入容錯方案。
容錯方案有不少,好比網絡傳輸,用checksum去校驗報文,若是出錯了則重發;
再如磁盤陣列,數據寫入兩個磁盤,只有當兩個磁盤同時出錯時纔會丟失數據,從而大大下降丟失數據的機率。
可是對於設備ID,以上兩種方案都不合適,由於上面的方案須要經過checksum來確認原信息是否被修改,設備ID沒有這樣的條件。

因此,能夠引入相似虛擬貨幣用到的"拜占庭容錯"方案。
簡單地說,就是要採集三個設備ID到雲端,若是有兩個(包括兩個以上)的設備ID和以前的記錄相同,則認爲是同一臺設備。
一樣假設出現重複的機率和發生變化的的機率都是千分之一,則:
同一臺設備的兩次採集,認不出是同一臺設備的條件爲「至少兩個設備ID都和上次不同」,機率約爲百萬分之三。
兩臺不一樣的設備,認爲是同一臺的條件是爲「三個設備ID中,至少有兩個設備ID和另外一臺設備相同」,機率一樣約爲百萬分之三。
因此,用此方案,惟一性和穩定性都獲得了提升。

5.2 具體實現

基本思想是:服務端有一張設備 ID 的表,核心的屬性(Column)有:

id | did_1 | did_2 | did_3

客戶請求時,上傳三個設備 ID,服務端檢索:

SELECT * from t_device_id WHERE did_1=? or did_2=? or did_3=?
複製代碼

若是檢索到記錄,其中至少兩個did和上傳的相同,則返回 id;
不然,插入上傳的三個設備 ID,並將新插入記錄的 id 返回。

一般狀況下,服務端表的主鍵爲自增序列(爲了確保插入的有序性),
因此咱們不能直接返回表的主鍵,不然容易被他人推測其餘的設備 ID,以及知曉用戶數量。
所以,在主鍵 ID 以外,咱們須要另一個惟一 ID。
有兩種思路:

  • 隨機化,好比用randomUUID 這種方案優勢是具備隱蔽性,從UUID徹底不可能得知主鍵ID,可是佔空間,檢索效率通常。
  • 根據主鍵 id 加密(混淆)出另外一個Long類型的id。
    此方案優勢是節省空間,檢索快,可是要求和主鍵ID一一映射,以確保不會重複,
    同時要求計算結果有離散性(計算結果和原ID相差很大;以及原ID較少的改變,比方說+1,會引發結果的巨大變化)。
    這些要求有方法實現,不過在此咱們很少作展開。

而後就是,須要三個設備ID……

  • Android ID 和 MAC地址都還能夠取到,再加一個,實施方案的條件就湊齊了。
  • IMEI 須要 READ_PHONE_STATE 權限,因此若是不想申請READ_PHONE_STATE 權限,能夠不採集IMEI了;
    並且,即便申請了 READ_PHONE_STATE,Android 10.0之後也獲取不到了。
  • 可是設備序列號只有在Android 8.0以前才能夠免權限獲取;
    在8.0以後,10.0以前,需READ_PHONE_STATE 權限;
    10.0以後, 有READ_PHONE_STATE權限也獲取不到了。

那麼,若是在沒有 READ_PHONE_STATE 權限的狀況下,以及Android 10.0以後,如何處理?
首先,設備序列號仍是要採集的,畢竟還有部分舊版本的設備能夠獲取到,能區分一點是一點;
而後,採集一些設備相關的信息,機型,硬件信息等(相同的機型,可能有多種配置,因此同時也採集一下硬件信息)。
最終匹配規則以下:

private fun matchDeviceId(deviceIdList: List<DeviceId>, r: DeviceId): DeviceId? {
	if (deviceIdList.isEmpty()) {
		return null
	}
	var maxPriorityDid : DeviceId? = null
	var priority = 0
	deviceIdList.forEach { did ->
		val s = idMatch(did.serial_no, r.serial_no)
		val a = idMatch(did.android_id, r.android_id)
		val m = idMatch(did.mac, r.mac)
		if (s && m && a) {
			return did
		}

		if(priority == 3) return@forEach
		if ((s && (a || m)) || (a && m)) {
			priority = 3
			maxPriorityDid = did
		}

		if(priority >= 2) return@forEach
		val p = idMatch(did.physics_info, r.physics_info)
				|| idMatch(did.dark_physics_info, r.dark_physics_info)
		if (p && a) {
			priority = 2
			maxPriorityDid = did
		}

		if(priority >= 1) return@forEach
		if (p && m) {
			priority = 1
			maxPriorityDid = did
		}
	}
	return maxPriorityDid
}

複製代碼
  • 若是設備序列號、Android ID、MAC全都不等,則前面的SQL查詢不會返回記錄(也就是沒有匹配的設備)。
  • 若是設備序列號,Android ID 和 MAC 所有相同,直接返回。
  • 不然,遍歷列表,取優先級最高的deviceId返回。
  • 若是隻有Android ID 或 MAC 之一相等,可是設備信息都匹配不上的話,也認爲不是同一個設備。

若是沒有匹配的設備,則認爲是新設備;
此時,生成新的udid返回,同時插入新設備的相關信息(設備ID,硬件信息)。

關於硬件信息,需知足一個要求:在設備重啓、恢復出廠設置等操做以後,不會變化。
常規信息有CPU核心數,RAM/ROM大小(以Gb爲單位採集,而不是精確到比特,不然容易變化),屏幕分辨率和dpi等,結合機型,保守估計有上千甚至上萬種可能性,相對Android ID 的 2^64 固然相差很遠了,可是仍可做爲輔助的參考信息。
試想在設備序列號獲取不到,Android ID 和 MAC 地址其中一個發生變化時,檢索到的都是隻有Android ID 或者 MAC 其中一個匹配的記錄,茫茫機海,說不許就有一兩臺的Android ID 或 MAC是相同的。
這時候選哪個呢? 再加上設備信息,或許就區分開了。
常規的設備信息容易遭到篡改,因此,在常規信息以外,咱們能夠挖掘一些冷門的設備特徵,好比 NetworkInterface 和 傳感器 的相關信息。
當常規信息被篡改時,若是冷門的設備信息還沒變,仍可識別出是同一臺設備。
至於如何挖掘,那就各顯神通了,一般作手機硬件或者ROM的朋友可能會知道更多的API。
爲了方便檢索,咱們能夠用MurmurHash將信息壓縮到64bit(Long的長度)。

再者,在獲取到udid以後,能夠定時(好比每隔兩天)就上傳udid和設備信息給雲端,雲端比較一下存儲的信息和上傳的信息,不相同則更新,這樣能夠提升udid的穩定性。
比方說,用戶在設備是Android 7.0 的時候卸載了APP,在Android 8.0以後安裝回來,這時候Android ID 是變化了的,可是憑着MAC和設備信息咱們能夠認出這臺設備,同時更新其 Android ID;
若是哪一天輪到MAC獲取不到了,這時候咱們仍能夠根據 Android ID和設備信息識別出這臺設備。

6、總結

本文介紹了設備ID的用途,現狀,並分析了現有設備ID的特性,最後提出了一種設備ID的構造方案。
按照這幾年的趨勢,各類設備ID的API或許還會越收越緊,單從客戶端去構造可靠的設備ID 是比較困難的,而基於信息採集和雲端綜合計算則相對容易。
具體實現,筆者編寫了一個Demo,已發佈的到github,謹供參考。
項目地址:github.com/No89757/Udi…

參考資料:
www.jianshu.com/p/9d828d259…
www.jianshu.com/p/ad9756fe2…
blog.csdn.net/andoop/arti…
blog.csdn.net/renlonggg/a…
developer.android.com/about/versi…
en.wikipedia.org/wiki/Birthd…

相關文章
相關標籤/搜索