背景
客户端埋点往往需要获取设备的基本信息,比如手机型号、手机系统信息、设备号等等,每次埋点上报都带着这些基本信息,以方便定位用户、识别用户的设备、操作系统等,从而最终帮用户解决问题。鸿蒙提供了封装好的api方便获取手机相关的信息,但是类似安卓或者ios设备的idfa\idfv是不提供的。而相应的替代是 AAID 或者 OAID。
AAID
AAID(Anonymous Application Identifier):应用匿名标识符,标识运行在移动智能终端设备上的应用实例,只有该应用实例才能访问该标识符,它只存在于应用的安装期,总长度36位。与无法重置的设备级硬件ID相比,AAID具有更好的隐私权属性。
AAID具有以下特性:
匿名化、无隐私风险:AAID和已有的任何标识符都不关联,并且每个应用只能访问自己的AAID。 同一个设备上,同一个开发者的多个应用,AAID取值不同。 同一个设备上,不同开发者的应用,AAID取值不同。 不同设备上,同一个开发者的应用,AAID取值不同。 不同设备上,不同开发者的应用,AAID取值不同。
AAID会在包括但不限于下述场景中发生变化:
应用卸载重装。 应用调用删除AAID接口。 用户恢复出厂设置。 用户清除应用数据。
OAID
开放匿名设备标识符,是一种非永久性设备标识符,基于开放匿名设备标识符,可在保护用户个人数据隐私安全的前提下,向用户提供个性化广告,同时三方监测平台也可以向广告主提供转化根因分析。
OAID具有以下特性:
OAID是设备级标识符,同一台设备上不同的App获取到的OAID值一样。 OAID的获取受应用的跟踪开关影响:当应用的跟踪开关开启时,该应用可获取到非全0的有效OAID;当应用的跟踪开关关闭时,该应用仅能获取到全0(00000000-0000-0000-0000-000000000000)的OAID。 同一台设备上首个应用开启应用跟踪开关时,会首次生成OAID。
OAID会在下述场景中发生变化:
用户恢复手机出厂设置。 用户操作重置OAID。
取舍
这两种能力各有优缺点。
- AAID 在许多场景中均会变化,但是它的使用并不需要用户授权,因此我们可以使用鸿蒙的关键资产功能使它在系统层面上进行缓存,这样也能一定程度上保证AAID的唯一和持久性
- OAID 虽然天生具有唯一和持久性,但是需要用户授权,如果用户拒绝授权,根本没有机会拿到它。
思路
我们当然想在应用初始化的时候拿到设备的唯一标识,因此我们可以设计这样的流程:
- 首先申请 OAID,如果用户授权则以此作为设备唯一标识,将其缓存到关键资产中
- 如果用户拒绝授权 OAID,那么我们使用 AAID 作为设备唯一标识,并将其缓存到关键资产中
- 其余时间我们均访问关键资产中的缓存,如果存在则直接返回,不存在则重复 1 和 2,并进行重新缓存
实现
- 构建SystemInfo静态类
export class SystemInfo {
static deviceInfo: string = deviceInfo.displayVersion; // 产品版本
static osInfo: string = deviceInfo.osFullName; // 系统版本
static deviceVersion: string = deviceInfo.versionId; // 版本ID
static deviceId: Promise<string> = SystemInfo.GetSystemDeviceId()
constructor() {
Logger.init()
}
// 获取oaid
static async GetSystemOAID() {
}
// 缓存设备id为关键资产
static SetSystemDeviceId(deviceId: string): Boolean {
}
// 获取aaid
static async GetSystemAAID(): Promise<string> {
}
// 读取关键资产缓存设备id
static async GetSystemDeviceId(): Promise<string> {
}
// 根据定义逻辑获取设备id
static async GetDeviceId() {
}
}
- 实现oaid获取,如果取到则进行关键资产缓存
static async GetSystemOAID() {
try {
// 首先需要获取用户授权
const atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
let authRes = await atManager.requestPermissionsFromUser(getContext(), ["ohos.permission.APP_TRACKING_CONSENT"])
if (authRes.authResults[0] == 0) {
let oaid = await identifier.getOAID();
SystemInfo.SetSystemDeviceId(oaid)
return oaid
} else {
Logger.error('user rejected');
return "";
}
} catch (err) {
Logger.error(`Failed to get oaid by promise, catch error: ${err.code}, ${err.message}`);
return "";
}
}
3.实现aaid获取,如果取到则进行关键资产缓存
static async GetSystemAAID(): Promise<string> {
try {
let aaid = await AAID.getAAID()
SystemInfo.SetSystemDeviceId(aaid);
return aaid
} catch (e) {
let error = e as BusinessError;
Logger.error(`Failed to get aaid ~ code: ${error.code} -·- message: ${error.message}`);
return "";
}
}
4.实现deviceID获取。首先获取oaid,取不到再取aaid
static async GetDeviceId() {
let oaid = await SystemInfo.GetSystemOAID()
if (oaid !== '' && oaid !== '00000000-0000-0000-0000-000000000000') {
return oaid
}
let aaid = await SystemInfo.GetSystemAAID()
return aaid
}
- 实现关键资产缓存
static stringToArray(str: string): Uint8Array {
let textEncoder = new util.TextEncoder();
return textEncoder.encodeInto(str);
}
static SetSystemDeviceId(deviceId: string): Boolean {
try {
if (!canIUse("SystemCapability.Security.Asset")) {
Logger.error(`Asset is not support`);
return false;
}
let attr: asset.AssetMap = new Map();
// 关键资产名称 DEVICE_ID
attr.set(asset.Tag.ALIAS, SystemInfo.stringToArray('DEVICE_ID'));
attr.set(asset.Tag.SECRET, SystemInfo.stringToArray(deviceId));
attr.set(asset.Tag.SYNC_TYPE, asset.SyncType.THIS_DEVICE);
attr.set(asset.Tag.CONFLICT_RESOLUTION, asset.ConflictResolution.OVERWRITE); //新增关键资产时的冲突,覆盖原有的关键资产
attr.set(asset.Tag.IS_PERSISTENT, true); //在应用卸载时是否需要保留关键资产 这里需要授权声明 但不需要用户授权
asset.addSync(attr);
return true;
} catch (err) {
let error = err as BusinessError;
Logger.error(`Asset-addSync device_id error ~ code: ${error.code} -·- message: ${error.message}`);
return false;
}
}
- 关键资产读取
static arrayToString(arr: Uint8Array): string {
let textDecoder = util.TextDecoder.create('utf-8', { ignoreBOM: true })
let result = textDecoder.decodeWithStream(arr, { stream: true });
return result;
}
static async GetSystemDeviceId(): Promise<string> {
try {
if (!canIUse("SystemCapability.Security.Asset")) {
Logger.error(`Asset is not support`);
return "";
}
let query: asset.AssetMap = new Map();
query.set(asset.Tag.ALIAS, SystemInfo.stringToArray('DEVICE_ID'));
query.set(asset.Tag.RETURN_TYPE, asset.ReturnType.ALL);
// **注意 这里如果关键资产不存在 并不会返回空 而是抛出24000002异常**
const assetArr = asset.querySync(query);
if (!assetArr || assetArr.length < 1) {
return "";
}
let map: asset.AssetMap = assetArr[0]
let deviceId = map.get(asset.Tag.SECRET) as Uint8Array;
if (deviceId) {
// 如果取到 直接返回
return SystemInfo.arrayToString(deviceId);
} else {
// 未取到 重新获取并返回
return await SystemInfo.GetDeviceId()
}
} catch (err) {
let error = err as BusinessError;
Logger.error(`Asset-getSync-异常 ~ code: ${error.code} -·- message: ${error.message}`);
if (24000002 == error.code) {
// 未取到 重新获取并返回
return await SystemInfo.GetDeviceId()
}
return "";
}
}
授权
以上涉及的权限有关键资产 ohos.permission.STORE_PERSISTENT_DATA 和 OAID ohos.permission.APP_TRACKING_CONSENT,需要我们在module.json5中进行声明:
"requestPermissions": [
{
"name": "ohos.permission.STORE_PERSISTENT_DATA"
},
{
"name": "ohos.permission.APP_TRACKING_CONSENT",
"reason": "$string:app_tracking_consent_reason",
"usedScene": {
"abilities": [
"EntryAbility"
],
"when": "always",
}
}
]
调用
调用方式很简单,不过由于官方只提供了异步获取方法,所以我们的deviceId也需要异步获取
import { SystemInfo } from '../api/SystemInfo'
let displayVersionInfo: string = SystemInfo.deviceInfo; // 产品版本
let osFullNameInfo: string = SystemInfo.osInfo; // 系统版本
let versionIdInfo: string = SystemInfo.deviceVersion; // 版本ID
SystemInfo.deviceId.then(deviceId => {
console.log(displayVersionInfo, ' | ', osFullNameInfo, ' | ', versionIdInfo, ' | ', deviceId)
})
总结
我们可以通过多种方式来实现设备唯一标识的获取,具体实现可以根据我们的需要进行取舍,官方还提供了一个ODID标识(开发者匿名设备标识符),有需要的可以自行研究一下。