android手机唯一id方案总结

google对隐私管理越来越严格了,华为也出了个OAID来保护用户隐私。对于生成android设备唯一id一直没有个绝对完美的方案,只能说做到尽量唯一吧,这里做一下总结。

一、设备系统版本为Android Q

Android Q 隐私权:数据和标识符变更

从 Android Q 开始,应用必须具有 READ_PRIVILEGED_PHONE_STATE 特许权限才能访问设备的不可重置标识符(包含 IMEI 和序列号)。许多用例不需要不可重置的设备标识符。如果您的应用没有该权限,但您仍尝试查询标识符的相关信息,则平台的响应会因目标 SDK 版本而异:

  • 如果应用以 Android Q 为目标平台,则会发生 SecurityException
  • 如果应用以 Android 9(API 级别 28)或更低版本为目标平台,则相应方法会返回 null 或占位符数据(如果应用具有 READ_PHONE_STATE 权限)。否则,会发生 SecurityException

注意:如果您的应用是设备所有者或配置文件所有者应用,那么即使您的应用以 Android Q 为目标平台,您也只需 READ_PHONE_STATE 权限即可访问不可重置的设备标识符。此外,如果您的应用具有特殊运营商权限,则无需任何权限即可访问这些标识符。

如果您的应用将不可重置的设备标识符用于广告跟踪或用户分析目的,请为这些特定用例创建 Android 广告 ID。要了解详情,请参阅唯一标识符的最佳做法

应用市场合规检测越来越严格了,Android Q以上都用OAID吧

二、设备系统版本为Android P及其以下

1. google官方唯一标识符最佳做法

2. 友盟生成唯一id的方案

反编译了友盟统计analytics-6.1.4.jar,友盟生成唯一id的方案可以总结为:

  • SDK_INT<23:imei>mac地址(直接api获取)>android_id>serial number
  • SDK_INT=23:imei>mac地址(api获取,读取系统文件)>android_id>serial number
  • SDK_INT>23:imei>serial number>android_id>mac地址(api获取,读取系统文件)

反编译的代码如下,稍微修改了下方法名

public class DeviceIdUtil {
    private static FileReader fileReader;
    private static BufferedReader bufferedReader;

    private static String getDeviceUniqueId(Context paramContext) {
        String str = "";
        if (Build.VERSION.SDK_INT < 23) {
            str = getDeviceId(paramContext);
            if (TextUtils.isEmpty(str)) {
                str = getMacAddressFromWifiManager(paramContext);
                if (TextUtils.isEmpty(str)) {
                    str = Settings.Secure.getString(paramContext.getContentResolver(), "android_id");
                    if (TextUtils.isEmpty(str)) {
                        str = getSerial();
                    }
                }
            }
        } else if (Build.VERSION.SDK_INT == 23) {
            str = getDeviceId(paramContext);
            if (TextUtils.isEmpty(str)) {
                str = getMacAddressFromNetworkInterface();
                if (TextUtils.isEmpty(str)) {
                    if (a.d) {//反编译看代码默认是true
                        str = getMacAddressFromFile();
                    } else {
                        str = getMacAddressFromWifiManager(paramContext);
                    }
                }
                if (TextUtils.isEmpty(str)) {
                    str = Settings.Secure.getString(paramContext.getContentResolver(), "android_id");
                    if (TextUtils.isEmpty(str)) {
                        str = getSerial();
                    }
                }
            }
        } else {
            str = getDeviceId(paramContext);
            if (TextUtils.isEmpty(str)) {
                str = getSerial();
                if (TextUtils.isEmpty(str)) {
                    str = Settings.Secure.getString(paramContext.getContentResolver(), "android_id");
                    if (TextUtils.isEmpty(str)) {
                        str = getMacAddressFromNetworkInterface();
                        if (TextUtils.isEmpty(str)) {
                            str = getMacAddressFromWifiManager(paramContext);
                        }
                    }
                }
            }
        }
        return str;
    }
    private static String getMacAddressFromFile() {
        try {
            String[] arrayOfString = {"/sys/class/net/wlan0/address", "/sys/class/net/eth0/address", "/sys/devices/virtual/net/wlan0/address"};
            for (byte b1 = 0; b1 < arrayOfString.length; b1++) {
                try {
                    String str = a(arrayOfString[b1]);
                    if (str != null) {
                        return str;
                    }
                } catch (Throwable throwable) {
                }
            }
        } catch (Throwable throwable) {
        }
        return null;
    }

    private static String a(String paramString) {
        String str = null;
        try {
            fileReader = new FileReader(paramString);
            bufferedReader = null;
            if (fileReader != null) {
                try {
                    bufferedReader = new BufferedReader(fileReader, 1024);
                    str = bufferedReader.readLine();
                } finally {
                    if (fileReader != null) {
                        try {
                            fileReader.close();
                        } catch (Throwable throwable) {
                        }
                    }
                    if (bufferedReader != null) {
                        try {
                            bufferedReader.close();
                        } catch (Throwable throwable) {
                        }
                    }
                }
            }
        } catch (Throwable throwable) {
        }

        return str;
    }


    private static String getMacAddressFromNetworkInterface() {
        try {
            Enumeration enumeration = NetworkInterface.getNetworkInterfaces();
            while (enumeration.hasMoreElements()) {
                NetworkInterface networkInterface = (NetworkInterface) enumeration.nextElement();
                if ("wlan0".equals(networkInterface.getName()) || "eth0".equals(networkInterface.getName())) {
                    byte[] arrayOfByte = networkInterface.getHardwareAddress();
                    if (arrayOfByte == null || arrayOfByte.length == 0) {
                        return null;
                    }
                    StringBuilder stringBuilder = new StringBuilder();
                    for (byte b1 : arrayOfByte) {
                        stringBuilder.append(String.format("%02X:", new Object[]{Byte.valueOf(b1)}));
                    }
                    if (stringBuilder.length() > 0) {
                        stringBuilder.deleteCharAt(stringBuilder.length() - 1);
                    }
                    return stringBuilder.toString().toLowerCase(Locale.getDefault());
                }
            }
        } catch (Throwable throwable) {
        }
        return null;
    }


    private static String getSerial() {
        String str = "";
        if (Build.VERSION.SDK_INT >= 9 && Build.VERSION.SDK_INT < 26) {
            str = Build.SERIAL;
        } else if (Build.VERSION.SDK_INT >= 26) {
            try {
                Class clazz = Class.forName("android.os.Build");
                Method method = clazz.getMethod("getSerial", new Class[0]);
                str = (String) method.invoke(clazz, new Object[0]);
            } catch (Throwable throwable) {
            }
        }
        return str;
    }

    private static String getDeviceId(Context paramContext) {
        String str = "";
        TelephonyManager telephonyManager = (TelephonyManager) paramContext.getSystemService("phone");
        if (telephonyManager != null) {
            try {
                if (a(paramContext, "android.permission.READ_PHONE_STATE")) {
                    if (Build.VERSION.SDK_INT > 26) {
                        Class clazz = Class.forName("android.telephony.TelephonyManager");
                        Method method = clazz.getMethod("getImei", new Class[]{Integer.class});
                        str = (String) method.invoke(telephonyManager, new Object[]{method, Integer.valueOf(0)});
                        if (TextUtils.isEmpty(str)) {
                            method = clazz.getMethod("getMeid", new Class[]{Integer.class});
                            str = (String) method.invoke(telephonyManager, new Object[]{method, Integer.valueOf(0)});
                            if (TextUtils.isEmpty(str)) {
                                str = telephonyManager.getDeviceId();
                            }
                        }
                    } else {
                        str = telephonyManager.getDeviceId();
                    }
                }
            } catch (Throwable throwable) {
                str = "";
            }
        }
        return str;
    }

    private static String getMacAddressFromWifiManager(Context paramContext) {
        try {
            WifiManager wifiManager = (WifiManager) paramContext.getSystemService("wifi");
            if (a(paramContext, "android.permission.ACCESS_WIFI_STATE")) {
                WifiInfo wifiInfo = wifiManager.getConnectionInfo();
                return wifiInfo.getMacAddress();
            }
            return "";
        } catch (Throwable throwable) {
            return "";
        }
    }
}

3. 项目里目前采用的方案(唯一id+本地存储)

因为app首次安装启动就要上报唯一id,判断是否是新用户等等,这些操作很可能是在获取权限之前,所以参考友盟和搜来的方案,对唯一id的生成方案做了优化,去掉了mac地址的获取,新增了伪id的生成,加上了存储唯一id到本地。

方案:首次启动就去生成唯一id(优先级:imei>serial number>android_id>伪imei),并存储到SharePreference中。

伪imei可参考

String m_szDevIDShort = "35" + //we make this look like a valid IMEI
            Build.BOARD.length()%10+ Build.BRAND.length()%10 + 
            Build.CPU_ABI.length()%10 + Build.DEVICE.length()%10 + 
            Build.DISPLAY.length()%10 + Build.HOST.length()%10 + 
            Build.ID.length()%10 + Build.MANUFACTURER.length()%10 + 
            Build.MODEL.length()%10 + Build.PRODUCT.length()%10 + 
            Build.TAGS.length()%10 + Build.TYPE.length()%10 + 
            Build.USER.length()%10 ; //13 digits

优点:无需关心权限,绝大部分手机都能生成的唯一id,存储到sp中保证了无论是否授予权限,唯一id都不会变化(下面这种情况例外)

缺点:当用户首次安装,启动,卸载后重装,手动到权限管理赋予android.permission.READ_PHONE_STATE权限,再启动,此时生成的唯一id可能会发生变化

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容