
1. 項目概述為什么我們需要關注IMEI和OAID在Android開發領域設備標識符的獲取一直是個既基礎又敏感的話題。幾年前你可能只需要調用TelephonyManager.getDeviceId()就能輕松拿到IMEI國際移動設備識別碼用它來做用戶唯一性識別、風控或者簡單的數據統計簡單粗暴。但如今隨著全球范圍內對用戶隱私保護的法規日益嚴格比如歐盟的GDPR、國內的《個人信息保護法》以及Android系統自身的權限收緊這條路已經走不通了。Android 10API 29開始對非系統應用獲取不可重置的設備標識符如IMEI、序列號施加了嚴格限制普通應用在沒有特殊權限的情況下基本無法獲取。這就引出了我們今天要深入探討的核心OAID匿名設備標識符。它是由移動安全聯盟MSA牽頭國內各大手機廠商共同推行的一套替代方案。簡單理解OAID是一個可以由用戶重置的、用于廣告等業務場景的設備標識符。它平衡了“設備識別”的業務需求和“用戶隱私”的保護要求正在成為國內Android生態中新的“事實標準”。所以當你的項目涉及到用戶設備識別、廣告歸因、反作弊、數據統計等場景時理解并正確獲取IMEI在有限條件下和OAID就從一個可選項變成了一個必選項。這不僅僅是調用一個API那么簡單它涉及到不同Android版本的適配、國內各廠商私有實現的兼容、以及如何優雅地處理獲取失敗的情況。接下來我將結合多年的踩坑經驗為你拆解其中的門道。2. 核心概念與法規背景深度解析在動手寫代碼之前我們必須把幾個關鍵概念和背后的“游戲規則”搞清楚。這能幫你避免很多合規上的雷區也能讓你理解為什么代碼要那么寫。2.1 IMEI昔日的王者與今日的枷鎖IMEI是一串15位的數字對于手機而言它就像身份證號理論上全球唯一。在過去的App開發中它是用戶設備識別的黃金標準。它的核心特點與當前困境唯一性與持久性通常一部手機一個IMEI雙卡雙待手機有兩個刷機也不會改變。高權限門檻從Android 6.0API 23開始獲取IMEI需要READ_PHONE_STATE運行時權限。用戶拒絕授權你就拿不到。Android 10的致命一擊即使應用擁有READ_PHONE_STATE權限對于非系統應用或設備所有者應用TelephonyManager.getDeviceId()等方法也會返回空值或拋出一串零。官方徹底收緊了口袋。僅限手機IMEI是蜂窩網絡設備的標識對于Wi-Fi版的平板或電視盒子等設備它不存在。注意現在依然有些“教程”教人通過反射等黑科技去挖IMEI我強烈不建議你這么做。這違反了Google Play的開發者政策應用有被下架的風險在國內它也觸碰了合規紅線。我們的目標是在合規的前提下解決問題而不是制造問題。2.2 OAID隱私時代的新共識面對IMEI的不可用國內安卓生態自己找到了一條出路——OAID。它不是一個Android原生API而是一套由移動安全聯盟MSA制定規范各手機廠商華為、小米、OPPO、vivo等在自己的系統層面實現的方案。你需要理解的OAID關鍵特性可重置性這是OAID與IMEI最本質的區別。用戶可以在手機設置中通常在“隱私”或“廣告”選項里手動重置OAID。重置后App獲取到的將是一個全新的標識符。這賦予了用戶控制權。分場景有效期開發者側在用戶明確同意隱私政策前你只能獲取到一個臨時的、無效的OAID全零或特定格式。只有在用戶同意后才能獲取到有效的、可用于業務標識的OAID。用戶側如前所述用戶可隨時重置。廠商依賴性強OAID的獲取依賴于手機系統內集成的MSA SDK或廠商自己的兼容實現。這意味著不同品牌、不同系統版本的手機其獲取方式、穩定性和速度可能有差異。這就是我們后面要處理的主要兼容性問題。主要用途官方定義是用于廣告、統計、反作弊等業務場景。明確禁止用于追蹤個人身份信息或任何可能關聯到自然人的活動。2.3 法律與平臺合規紅線開發時腦子里必須繃緊這根弦隱私政策必須在App的隱私政策中清晰、明確地告知用戶你收集了OAID并說明其用途例如“用于識別設備以防止欺詐行為”或“用于廣告投放效果分析”。必須在用戶同意你的隱私政策后才能去獲取有效的OAID。權限聲明獲取OAID本身通常不需要額外的Android權限因為它不通過TelephonyManager。但如果你同時嘗試在允許的條件下獲取IMEI則仍需聲明并申請READ_PHONE_STATE權限。數據使用與存儲獲取到的標識符不能明文上傳或存儲建議進行不可逆的哈希處理如SHA-256并加鹽Salt以增加安全性。絕對不要將OAID/IMEI與用戶的手機號、身份證號等個人敏感信息關聯存儲。Google Play政策Google明確限制對不可重置設備標識符的收集。如果你的應用要上架Google Play必須將OAID作為主要或備選方案并且要處理好IMEI在海外設備上無法獲取的情況。3. 工程實現方案設計與選型理論清楚了我們進入實戰環節。一個健壯的設備標識符獲取模塊不能只寫一種方法必須設計一個具有優先級和降級策略的完整方案。3.1 整體架構設計思路我的策略是構建一個“獲取鏈”或“回退策略”。核心思想是優先嘗試獲取最理想、最合規的標識符如果失敗則依次嘗試備選方案最終確保總能返回一個可用于業務邏輯的標識哪怕是自定義生成的。一個典型的優先級順序是OAID最高優先級國內環境下的首選合規且受支持廣泛。Android ID或AAIDAndroid系統提供的另一個標識符但恢復出廠設置或應用簽名變更時會改變。可作為OAID不可用時的補充。Google Advertising ID (GAID)主要用于Google廣告生態用戶也可重置。在海外設備上是重要選擇。有限條件下的IMEI/MEID在低版本Android10且獲得權限的情況下可作為備選。必須做好版本判斷和權限檢查。自定義UUID最后防線如果以上所有方法都失敗則在本地生成一個UUID并持久化存儲。這個UUID只在本設備本次安裝中有效卸載重裝會變化。你的業務邏輯應該基于這個鏈條中最先成功獲取到的有效標識符來運行。3.2 核心工具庫MSA統一SDK要獲取OAID我們繞不開移動安全聯盟的官方SDK。這是與各廠商系統服務通信的橋梁。集成步驟與關鍵點獲取SDK從MSA官方渠道如官網或GitHub下載最新版的oaid_sdk_x.x.x.aar文件。引入項目將aar文件放入你項目的libs目錄并在模塊的build.gradle文件中添加依賴dependencies { implementation files(libs/oaid_sdk_x.x.x.aar) // 其他依賴... }權限與組件聲明在AndroidManifest.xml中需要添加SDK要求的權限和組件。這一步非常關鍵遺漏會導致在某些機型上無法調起服務。!-- 網絡權限用于SDK可能的網絡校驗雖然主要走本地Binder -- uses-permission android:nameandroid.permission.INTERNET / uses-permission android:nameandroid.permission.ACCESS_NETWORK_STATE / uses-permission android:nameandroid.permission.ACCESS_WIFI_STATE / !-- 聲明SDK需要的Provider路徑務必與aar包內一致 -- application ... provider android:namecom.bun.miitmdid.core.MdidProvider android:authorities${applicationId}.mid.mdid android:exportedtrue android:enabledtrue android:multiprocesstrue / ... /application實操心得android:authorities中的${applicationId}是Gradle的占位符編譯時會自動替換為你的包名。確保這一致性可以避免FileUriExposedException等詭異問題。另外exportedtrue是必須的因為需要跨進程通信。4. 核心代碼實現與分步詳解現在我們來編寫核心的獲取類。我會將代碼分塊并解釋每一部分的意圖和注意事項。4.1 構建設備標識符獲取器我們創建一個DeviceIdHelper單例類來統一管理。import android.content.Context; import android.os.Build; import android.provider.Settings; import android.telephony.TelephonyManager; import android.text.TextUtils; import com.bun.miitmdid.core.MdidSdkHelper; import com.bun.miitmdid.interfaces.IIdentifierListener; import com.bun.miitmdid.interfaces.IdSupplier; import java.util.UUID; public class DeviceIdHelper { private static volatile DeviceIdHelper instance; private Context appContext; private String cachedOaid null; private boolean isOaidFetching false; private DeviceIdHelper(Context context) { this.appContext context.getApplicationContext(); // 使用Application Context避免內存泄漏 } public static DeviceIdHelper getInstance(Context context) { if (instance null) { synchronized (DeviceIdHelper.class) { if (instance null) { instance new DeviceIdHelper(context); } } } return instance; } }4.2 實現OAID的異步獲取OAID的獲取是異步的因為它需要跨進程與系統服務通信。我們使用MSA SDK的回調接口。public interface OnDeviceIdFetchListener { void onSuccess(String deviceId, int source); // source用于標識來源OAID, IMEI等 void onError(String errorMsg); } public void getDeviceId(final OnDeviceIdFetchListener listener) { // 優先嘗試使用緩存的OAID如果之前成功獲取過 if (!TextUtils.isEmpty(cachedOaid)) { if (listener ! null) { listener.onSuccess(cachedOaid, SOURCE_OAID); } return; } // 防止重復發起請求 if (isOaidFetching) { if (listener ! null) { listener.onError(OAID is already being fetched.); } return; } isOaidFetching true; // 在子線程中調用MSA SDK避免可能的主線程阻塞 new Thread(() - { try { int code MdidSdkHelper.InitSdk(appContext, true, new IIdentifierListener() { Override public void OnSupport(boolean isSupport, IdSupplier supplier) { isOaidFetching false; if (isSupport supplier ! null) { String oaid supplier.getOAID(); // 這是核心獲取方法 String vaid supplier.getVAID(); String aaid supplier.getAAID(); if (!TextUtils.isEmpty(oaid)) { cachedOaid oaid; if (listener ! null) { listener.onSuccess(oaid, SOURCE_OAID); } } else { // OAID為空嘗試降級方案 fallbackToOtherId(listener); } // 記得調用supplier的銷毀方法釋放資源 supplier.shutDown(); } else { // 設備不支持MSA方案如老舊機型或非大陸ROM fallbackToOtherId(listener); } } }); // InitSdk的返回值0表示成功調起非0表示失敗 if (code ! 0) { isOaidFetching false; // 初始化錯誤常見于Manifest配置錯誤或系統服務異常 fallbackToOtherId(listener); } } catch (Exception e) { isOaidFetching false; e.printStackTrace(); fallbackToOtherId(listener); } }).start(); }代碼解讀與坑點異步與線程MdidSdkHelper.InitSdk內部涉及Binder調用雖然本身不耗時但為保險起見我習慣放在子線程。回調OnSupport也發生在子線程如果你要更新UI記得用Handler或runOnUiThread。緩存機制OAID在用戶重置前是不變的所以本地緩存一次是合理的能提升后續獲取速度。緩存鍵可以考慮加上用戶是否同意隱私政策的狀態。isSupport為false這并不絕對意味著失敗。有些廠商的較新系統如部分小米MIUI 12.5可能換了實現方式或者用戶限制了廣告標識符。此時必須無縫切換到降級方案。資源釋放supplier.shutDown()一定要調用這是個好習慣避免潛在的資源泄露。4.3 降級方案Android ID、GAID與IMEI當OAID不可用時我們啟動降級鏈條。private void fallbackToOtherId(OnDeviceIdFetchListener listener) { // 1. 嘗試Android ID String androidId Settings.Secure.getString(appContext.getContentResolver(), Settings.Secure.ANDROID_ID); if (!TextUtils.isEmpty(androidId) !9774d56d682e549c.equals(androidId)) { // 注意已知的一個bug有些設備會返回這個固定值需要排除 if (listener ! null) { listener.onSuccess(androidId, SOURCE_ANDROID_ID); } return; } // 2. 嘗試Google Advertising ID (GAID) - 需要Google Play服務 String gaid getGoogleAdvertisingId(); if (!TextUtils.isEmpty(gaid)) { if (listener ! null) { listener.onSuccess(gaid, SOURCE_GAID); } return; } // 3. 嘗試IMEI/MEID (需要權限和版本檢查) if (Build.VERSION.SDK_INT Build.VERSION_CODES.Q) { // Android 10以下 if (checkReadPhoneStatePermission()) { String imei getImeiOrMeid(); if (!TextUtils.isEmpty(imei)) { if (listener ! null) { listener.onSuccess(imei, SOURCE_IMEI); } return; } } } // 4. 終極方案生成并保存自定義UUID String customUUID getOrCreateCustomUUID(); if (listener ! null) { listener.onSuccess(customUUID, SOURCE_CUSTOM_UUID); } } private String getGoogleAdvertisingId() { // 簡化示例實際使用需要集成Google Play services-ads // 并且需要在子線程中調用因為可能涉及網絡請求 try { Class? gpsClass Class.forName(com.google.android.gms.ads.identifier.AdvertisingIdClient); Method getAdvertisingIdInfo gpsClass.getMethod(getAdvertisingIdInfo, Context.class); Object adInfo getAdvertisingIdInfo.invoke(null, appContext); if (adInfo ! null) { Method getIdMethod adInfo.getClass().getMethod(getId); return (String) getIdMethod.invoke(adInfo); } } catch (Exception e) { // Google Play服務不可用或版本不兼容 } return null; } private String getImeiOrMeid() { try { TelephonyManager tm (TelephonyManager) appContext.getSystemService(Context.TELEPHONY_SERVICE); if (Build.VERSION.SDK_INT Build.VERSION_CODES.O) { String imei tm.getImei(0); // 對于雙卡設備傳入slotIndex if (TextUtils.isEmpty(imei)) { imei tm.getMeid(0); // 嘗試獲取MEID (CDMA設備) } return imei; } else { // 舊版本API return tm.getDeviceId(); } } catch (SecurityException e) { // 權限被拒絕 return null; } catch (Exception e) { return null; } } private String getOrCreateCustomUUID() { SharedPreferences sp appContext.getSharedPreferences(device_id, Context.MODE_PRIVATE); String uuid sp.getString(custom_uuid, null); if (TextUtils.isEmpty(uuid)) { uuid UUID.randomUUID().toString().replace(-, ); sp.edit().putString(custom_uuid, uuid).apply(); } return uuid; }5. 兼容性、調試與線上問題排查實錄即使代碼寫完了真正的挑戰才剛剛開始。各品牌安卓手機的“魔改”系統會讓你遇到各種意想不到的情況。5.1 主流機型兼容性實測與應對策略我整理了一份在主流品牌機型上測試OAID獲取的經驗表手機品牌系統版本OAID獲取情況常見問題與解決方案華為EMUI 10 / HarmonyOS 2良好基本無問題。注意舊機型EMUI 9.x可能需要檢查系統是否支持。小米MIUI 12情況復雜MIUI 12.5以上用戶可在“設置-隱私保護-特殊權限-獲取設備標識”中完全關閉OAID提供。此時isSupport可能為true但返回空OAID。必須做好空值判斷并降級。OPPOColorOS 11良好相對穩定。部分海外版ROM可能不支持。vivoFuntouch OS 10 / OriginOS良好無特殊問題。三星國行系統支持國行ROM集成了MSA。海外版One UI絕對不支持OAID必須依賴GAID或Android ID。一加/realme等基于ColorOS同OPPO兼容性通常跟隨其底層系統。其他品牌或原生Android各類大概率不支持如中興、聯想或刷了原生AOSP的設備。必須立即降級到Android ID/GAID方案。通用調試技巧日志輸出在OnSupport回調中不僅打印OAID也打印supplier.getVAID(),supplier.getAAID()。有時VAID或AAID可能有值可以幫助判斷是系統問題還是OAID本身被限制。用戶引導對于小米等可關閉OAID的用戶可以在獲取失敗時友好地提示用戶“為了提供更好的服務請在系統設置中允許應用獲取設備標識”并給出跳轉設置頁的指引如果可能。注意措辭不能強制要求。5.2 線上問題排查清單當線上監控發現設備標識獲取失敗率升高時可以按以下步驟排查確認失敗分布看失敗用戶集中在哪些機型、哪些系統版本。如果是某個品牌特定版本集中失敗基本可以斷定是該系統的兼容性問題。檢查Manifest配置線上問題有時源于構建變體Flavors或混淆導致Provider的authorities不正確。確保最終APK中的AndroidManifest.xml里Provider的authorities屬性是你的應用包名。檢查隱私政策觸發時機確保是在用戶點擊同意隱私政策后才調用getDeviceId()。提前調用可能只能拿到臨時ID。查看SDK版本是否使用了過時的MSA SDK去官網更新到最新版本可能修復了某些機型的兼容性問題。網絡與權限雖然OAID獲取不依賴網絡但SDK初始化可能需要網絡校驗。確保應用有網絡權限且在首次獲取時網絡通暢。雙進程問題如果你的應用有多進程例如推送服務單獨一個進程每個進程都需要獨立初始化MSA SDK。確保在Application的onCreate或每個進程的入口處進行初始化。5.3 隱私合規自檢要點在上線前務必和法務或合規同事一起核對[ ] 隱私政策中是否明確列出了“收集設備標識符OAID/Android ID等”及其用途[ ] 獲取設備標識符的代碼調用時機是否嚴格在用戶同意隱私政策之后[ ] 是否提供了用戶拒絕提供設備標識符后應用的基本功能仍可使用的路徑雖然標識符對很多功能很重要但設計上應有降級體驗[ ] 收集到的標識符在傳輸和存儲時是否做了哈希脫敏處理[ ] 是否避免了不必要的權限申請如只為獲取IMEI才申請READ_PHONE_STATE6. 進階思考與最佳實踐最后分享一些更深層的思考和優化方向這些往往決定了方案的健壯性和可維護性。1. 標識符的統一處理與上報你可能會從不同渠道OAID、IMEI、Android ID拿到不同格式的標識符。建議在本地或服務端做一個“歸一化”處理。例如對所有獲取到的原始標識符進行統一的SHA-256哈希加鹽再上報給服務器。這樣服務器端存儲和比對的是統一的哈希值既安全又一致。2. 時效性與刷新機制OAID和GAID是可變的。你需要建立監聽或定期刷新的機制。對于OAIDMSA SDK沒有提供直接的變更回調一個實踐方案是在App每次冷啟動或定期如每24小時重新獲取一次并與本地緩存對比如果發生變化則通知服務器更新關聯關系。對于GAID可以使用AdvertisingIdClient.Info.isLimitAdTrackingEnabled()來監聽用戶是否開啟了限制廣告跟蹤。3. 海外市場的策略如果你的App面向全球市場那么OAID主要適用于中國大陸市場。在海外你的策略應該是優先獲取Google Advertising ID (GAID)。備選Android ID。徹底放棄對IMEI的依賴除非是針對特定運營商的深度定制應用。 需要構建一個根據設備區域、系統語言或是否安裝Google Play服務來動態選擇主策略的智能獲取鏈。4. 模塊化與可測試性將DeviceIdHelper這類工具類設計為接口驅動。定義一個IDeviceIdFetcher接口然后為OAID、GAID等提供不同的實現類。這樣不僅代碼結構清晰也便于編寫單元測試你可以輕松Mock各種獲取成功或失敗的情況來測試你的降級邏輯是否正確。5. 監控與告警在應用的監控體系中加入設備標識符獲取的相關指標。例如各標識符來源OAID/Android ID/IMEI/Custom的占比。OAID獲取的平均耗時、失敗率。特定機型/系統版本的獲取失敗集中度。 當這些指標出現異常波動時能夠第一時間發出告警讓你能主動發現兼容性問題而不是等到用戶投訴。設備標識符的獲取從一個簡單的API調用演變成了一個需要綜合考慮兼容性、合規性、安全性和用戶體驗的復雜工程問題。它沒有一勞永逸的銀彈唯有多測試、多觀察、多思考構建一個足夠魯棒和靈活的方案才能在各種復雜的真實環境中穩穩落地。希望這份結合了多年實戰經驗的拆解能幫你少走些彎路。