
你的支持對我意義重大!
?? Hi,我是旭銳。本文已收錄到 GitHub · Android-NoteBook 中。這里有 Android 進階成長路線筆記 & 博客,有志同道合的朋友,歡迎跟著我一起成長。(聯(lián)系方式 & 入群方式在 GitHub)
1. 前言
- IMEI 等設備標識符已經(jīng)被認定為用戶隱私的一部分,在非必要的場景獲取甚至頻繁獲取 IMEI,會被認定為違規(guī)獲取用戶信息的行為;
- 從 Android 10 開始,應用無法獲取 IMEI、MAC 等設備唯一標識(申請 READ_PRIVILEGED_PHONE_STATE 權限后也可以獲取,但這個權限只有系統(tǒng)應用能夠獲?。H绻麘脧娦蝎@取,則會獲取到 null 信息或發(fā)生 SecurityException(取決于
targetSdkVersion); - 為了解決這個問題,國家相關部委成立了移動安全聯(lián)盟 MSA,制定了一套《移動智能終端補充設備標識體系》。因為其中最重要的一個標識符是 OAID 匿名設備標識符,因此也有人把這個體系簡稱為 OAID。
2. 補充設備標識體系
補充設備標識體系主要分為四層結構:
- UUID 設備唯一標識符 是不依賴于這個體系的,它們在設備出廠時就固化到硬件信息上了,即時恢復出廠設置也不會重置;
- OAID 匿名設備標識 是 UUID 的替代品,在終端首次啟動時生成。同一設備的 OAID 相同,因此可以在多個應用之間共享,恢復出廠設置會重置 OAID;
- VAID 開發(fā)者匿名設備標識符 是開發(fā)者維度的 ID,在應用安裝時生成。同一設備上且同一開發(fā)者的所有應用 VAID 相同,其他情況 VAID 不同,重新安裝應用會重置 VAID;
- AAID 應用匿名設備標識符 是應用沙盤維度的 ID,在應用安裝時生成。即使是同一設備且同一個開發(fā)者的應用,AAID 也不同,重新安裝、清除用戶數(shù)據(jù)會重置 AAID。
| 英文縮寫 | 英文全稱 | 中文名稱 | 描述 | 重置性 | 應用場景 |
|---|---|---|---|---|---|
| UDID | Unique Device Identifier | 設備唯一標識符 | 設備唯一硬件標識,由設備出廠時的硬件信息生成,如 IMEI | 出廠后無法重置 | 設備唯一標識,如廣告業(yè)務在廣告投放時進行效果歸因 |
| OAID | Open Anonymous Device Identity | 匿名設備標識符 | OAID 是 UUID 的替代品,是設備維度的 ID。在終端首次啟動時生成,同一設備的 OAID 相同 | 恢復出廠設置后,OAID 會重置 | 設備唯一標識,如廣告業(yè)務在廣告投放時進行效果歸因 |
| VAID | Vender Anonymous Device Identity | 開發(fā)者匿名設備標識符 | VAID 是開發(fā)者維度的 ID。在應用安裝時生成,同一設備上且同一開發(fā)者的所有應用 VAID 相同,其他情況 VAID 不同 | 恢復出廠設置、重新安裝應用后,VAID 會重置(例外情況:卸載應用時如果設備中另有相同開發(fā)者的應用且讀取過 VAID,則不會重置) | 開發(fā)者唯一標識,如果統(tǒng)一開發(fā)者不同瑩瑩之間的推薦 |
| AAID | Application Anonymous Device Identity | 應用匿名設備標識符 | AAID 是應用沙盤維度的 ID。在應用安裝時生成,即使是同一設備且同一個開發(fā)者的應用,AAID 也不同 | 恢復出廠設置、重新安裝、清除用戶數(shù)據(jù),AAID 會重置例外情況:卸載應用時如果設備中另有相同開發(fā)者的應用且讀取過 AAID,則不會重置) | 應用唯一標識,可用于用戶統(tǒng)計 |
3. 準備工作
- 注冊 MSA 賬號:
根據(jù) MSA 的要求,下載 SDK 和集成文檔前需要注冊一個企業(yè)賬戶,這一步按照指引提交相關信息和資料即可,一般 1~2 個工作日就可以審核通過。

- 申請 SDK 證書:
從 v1.0.26 開始,SDK 引入了證書校驗機制,每個 APP 都需要申請一個證書文件(包名.cert.pem),并且只有包名與證書匹配的 APP 才能正常獲取補充設備 ID。默認證書的有效期為 1 年,證書過期也會影響獲取補充設備 ID。因此你還需要根據(jù)實際場景需要設計證書更新機制,比如在應用中內(nèi)置一個默認證書,并應用開到期時提前從后臺服務器更新證書。申請證書需要向 msa@caict.ac.cn 發(fā)送申請郵件,并附帶表格 example_batch.csv,例如:

- 下載 SDK 與集成文檔:
企業(yè)賬號注冊并審核通過后,就可以從官網(wǎng)下載到相關資料了(因為 MSA 禁止第三方違規(guī)分發(fā) SDK,所以小伙伴們還是得自己去下載)。

-
準備 vivo 商城 AppID:
因為 vivo 手機的方案不同,在配置 SDK 時需要用到你的應用在應用商城上申請的 APPID。
4. 集成與封裝
-
集成 aar: 依賴
oaid_sdk_x.x.x.aar,具體方法你肯定會了; -
APPID 配置: 將
supplierconfig.json復制到項目 /main/assets 目錄下,并修改里面的 APPID 配置,主要是修改 vivo 的配置,例如:

-
證書配置: 將
包名.cert.pem證書文件復制到項目 /main/assets 目錄下,例如:

設置混淆: 配置 SDK 集成文檔中列舉的混淆配置,具體方法你肯定會了;
-
設置 Gradle 編譯選項: 根據(jù)你的項目需要,配置 ndk 編譯配置,例如:
// ndk { // abiFilters 'armeabi-v7a', 'x86', 'arm64-v8a', 'x86_64' // } splits { abi { enable true reset() include 'armeabi-v7a', 'arm64-v8a' universalApk true } } packagingOptions { doNotStrip "*/armeabi-v7a/*.so" doNotStrip "*/arm64-v8a/*.so" } -
代碼封裝: 完成以上集成和配置步驟后,剩下就是調用 SDK 接口獲取 OAID 了。官方提供的 Demo 中
DemoHelper代碼質量一般,我簡單整理了一版,代碼不難,你直接看吧。有一些點要注意下:- 1、隱私政策授權: 需要確保用戶同意《隱私政策》后,再初始化 SDK,懂得都懂;
-
2、初始化時機: 加固版本在調用前需要先
loadLibrary(”msaoaidsec”),因為加載有延遲,所以官方推薦盡早提前初始化; -
3、回調時機:
MidSdkHelper#InitSdk()的結果回調可能是同步的,也可能是從異步子線程回調,與 SDK 內(nèi)部的判斷也有關。
IOAIDApi.kt
interface IOAIDApi {
/**
* 1. 初始化,確保用戶同意《隱私政策》之后,再初始化 OAID SDK
* 2. 加固版本在調用前必須載入SDK安全庫,因為加載有延遲,推薦在 Application 中調用 loadLibrary 方法
*
* @param debug 是否調試,debug 狀態(tài)會開啟 SDK 日志輸出
*/
fun init(debug: Boolean)
/**
* 獲取 ID,回調可能是同步的,也可能是異步的
*/
@AnyThread
fun fetchDeviceIds(callback: (OAIDResult) -> Unit)
}
OAID.kt
internal class OAID private constructor(
context: Context
) : IOAIDApi {
// ApplicationContext
private val context: Context = context.applicationContext
/**
* 證書初始化標記,true:已經(jīng)初始化
*/
@Volatile
private var isCertInit: Boolean = false
/**
* 證書是否有效,true:有效
*/
private var isCertValid: Boolean = false
/**
* 證書過期時間,null:證書無效
*/
private var certExpDate: Date? = null
/**
* 調試開關
*/
private var debug: Boolean = false
companion object {
@Volatile
private var _oaid: IOAID? = null
@AnyThread
fun oaid(context: Context): IOAID {
if (null == _oaid) {
synchronized(IOAID::class.java) {
if (null == _oaid) {
_oaid = OAIDImpl(context)
}
}
}
return _oaid!!
}
}
override fun init(debug: Boolean) {
System.loadLibrary("msaoaidsec")
this.debug = debug
}
override fun fetchDeviceIds(callback: (OAIDResult) -> Unit) {
// 1. 驗證與初始化證書
checkCertValidity()
// 2.1 提供空信息的 ID 提供器
val unsupportedIdSupplier = IdSupplierImpl()
// 2.2 統(tǒng)一的回調接收器
val listener = IIdentifierListener { supplier: IdSupplier? ->
supplier?.let {
// 回調
callback(
OAIDResult(
supplier.isSupported, supplier.isLimited, supplier.oaid, supplier.vaid, supplier.aaid
)
)
}
}
if (!isCertValid) {
// 證書無效,直接回調空信息
listener.onSupport(unsupportedIdSupplier)
return
}
// 3. 調用 SDK 接口獲取 OAID
val code = try {
MdidSdkHelper.InitSdk(context, debug, listener)
} catch (error: Error) {
error.printStackTrace()
-1
}
// 4. 處理異常情況
when (code) {
InfoCode.INIT_ERROR_CERT_ERROR, // 證書未初始化或證書無效,SDK 內(nèi)部不會回調 onSupport
InfoCode.INIT_ERROR_DEVICE_NOSUPPORT, // 不支持的設備, SDK 內(nèi)部不會回調 onSupport
InfoCode.INIT_ERROR_LOAD_CONFIGFILE, // 加載配置文件出錯, SDK 內(nèi)部不會回調 onSupport
InfoCode.INIT_ERROR_MANUFACTURER_NOSUPPORT, // 不支持的設備廠商, SDK 內(nèi)部不會回調 onSupport
InfoCode.INIT_ERROR_SDK_CALL_ERROR // SDK 調用出錯, SDK 內(nèi)部不會回調 onSupport
-> {
// 異常情況,直接回調空信息
listener.onSupport(unsupportedIdSupplier)
}
InfoCode.INIT_INFO_RESULT_DELAY, // 獲取接口是異步的,SDK 內(nèi)部會回調 onSupport
InfoCode.INIT_INFO_RESULT_OK // 獲取接口是同步的,SDK 內(nèi)部會回調 onSupport
-> {
// do nothing
}
else -> {
// do nothing
}
}
}
/**
* 驗證與初始化證書
* @return true:證書初始化成功;false:證書初始化失敗
*/
private fun checkCertValidity() {
/**
* 從asset文件讀取證書內(nèi)容
*
* @return 證書字符串
*/
fun loadPemFromAssetFile(): String {
return try {
// 證書文件名
val certFileName = context.packageName + ".cert.pem"
val inputStream = context.assets.open(certFileName)
val bufferReader = BufferedReader(InputStreamReader(inputStream))
val builder = StringBuilder()
var line: String?
while (bufferReader.readLine().also { line = it } != null) {
builder.append(line)
builder.append('\n')
}
builder.toString()
} catch (e: IOException) {
""
}
}
/**
* 解析證書過期時間
* @return 過期時間,證書不合法時返回 null
*/
fun getCertExpDate(certStr: String): Date? {
return try {
// 證書實體
val cert = CertificateFactory.getInstance("X.509").generateCertificate(certStr.byteInputStream()) as X509Certificate
// 驗證證書有效性,如果證書過期會拋出異常
cert.checkValidity()
cert.notAfter
} catch (ex: Exception) {
// 證書無效
null
}
}
// DCL
if (isCertInit) {
// 初始化只需要進行一次,返回上一次的結果
return
}
synchronized(IOAID::class) {
if (isCertInit) {
return
}
isCertInit = true
// 證書文件名
val certStr = loadPemFromAssetFile()
// 證書過期時間
certExpDate = getCertExpDate(certStr)
// TODO 如果你的應用場景對證書有效性要求非常高,可以在這個時機提前下載更新證書
// 初始化證書,證書只需要初始化一次
isCertValid = MdidSdkHelper.InitCert(context, certStr)
}
}
}
5. 其他細節(jié)
- 隱私政策(重要): 因為 OAID 屬于第三方 SDK,所以你需要制定使用 SDK 獲取 ID 涉及的隱私政策,例如第三方 SDK 列表:
| SDK 名稱 | 場景描述及特別說明 | 涉及收集的主要個人信息類型 | 數(shù)據(jù)接收方名稱 | 隱私政策鏈接 |
|---|---|---|---|---|
| OAID | 獲取 OAID,在廣告投放時進行效果歸因 | 設備信息(設備唯一標識) | 中國信息通信研究院 | http://www.msa-alliance.cn/ |
-
SDK 提供的接口:
- IdSupplier#isSupported(): 判斷設備是否支持補充設備標識符
- IdSupplier#isLimited(): 判斷設備是否限制應用獲取補充設備標識符
- IdSupplier#getOAID(Context): 獲取 OAID
- IdSupplier#getVAID(Context): 獲取 VAID
- IdSupplier#getAAID(Context): 獲取 AAID
SDK 是否需要聯(lián)網(wǎng)?
多數(shù)廠商在調用接口時會要求聯(lián)網(wǎng),比如獲取 VAID、AAID 時需要去廠商后臺校驗和計算獲得
- SDK 內(nèi)部如何判斷是否同一開發(fā)者的應用?
VAID 的定義是設備 + 開發(fā)者維度的 ID,同一設備上且同一開發(fā)者的所有應用 VAID 相同,其他情況 VAID 不同。不同手機廠商在判斷是否同一開發(fā)者的方式不同,有些是直接通過 AppId 判斷,如 vivo;有些是通過應用簽名信息判斷,如 oppo?;?AppID 的方案,要求我們在配置文件中填寫在應用商城中分配的 AppId(并配置到 assets/supplierconfig.json),基于應用簽名的方案,對我們會友好些。
- SDK 是否支持模擬器?
補充設備標識符本質上是廠商提供的能力,因此只有真機支持,模擬器是不支持的。支持的真機列表見 SDK 文檔:

—— 圖片截圖自 MSA 官方文檔
- 如何判斷證書的有效期?
可以通過代碼解析,也可以把證書內(nèi)容復制到 https://www.pianyissl.com/tools/cer_decoder 在線解析,例如:

不了解數(shù)字證書的小伙伴,可以回顧下我們之前的討論:《加密、摘要、簽名、證書,一次說明白!》
參考資料
最近,我建了一個 Android 交流群,希望大家可以一起討論技術,找到志同道合的朋友,需要的可以掃碼進群!