是否有唯一的 Android 设备 ID?

Android 设备是否具有唯一的 ID,如果是,则使用 Java 访问它的简单方法是什么?

答案

Settings.Secure#ANDROID_ID 以每个用户 64 位十六进制字符串的唯一身份返回 Android ID。

import android.provider.Settings.Secure;

private String android_id = Secure.getString(getContext().getContentResolver(),
                                                        Secure.ANDROID_ID);

更新 :从 Android 的最新版本开始, ANDROID_ID许多问题已解决,我相信不再需要这种方法。请看一下安东尼的回答

全面披露:我的应用最初使用以下方法,但不再使用该方法,现在我们使用emmby 的答案链接到的Android 开发者博客条目中概述的方法(即,生成并保存UUID#randomUUID() )。


这个问题有很多答案,其中大多数只会在某些时候起作用,但是不幸的是,这还不够好。

根据我对设备的测试(所有电话,其中至少一个未激活):

  1. 所有测试的设备都返回TelephonyManager.getDeviceId()的值
  2. 所有 GSM 设备(均已通过 SIM 测试)均返回TelephonyManager.getSimSerialNumber()的值
  3. 所有 CDMA 设备的getSimSerialNumber()返回 null(按预期方式)
  4. 所有添加了 Google 帐户的设备都返回了ANDROID_ID的值
  5. 所有 CDMA 设备对于ANDROID_IDTelephonyManager.getDeviceId()都返回相同的值(或得出相同的值TelephonyManager.getDeviceId() - 只要在设置过程中添加了 Google 帐户即可。
  6. 我还没有机会测试没有 SIM 卡的 GSM 设备,没有添加 Google 帐户的 GSM 设备或任何处于飞行模式的设备。

因此,如果您想要设备本身特有的功能,则TM.getDeviceId() 应该足够了。显然,某些用户比其他用户更偏执,因此散列 1 个或多个这些标识符可能会很有用,因此该字符串实际上对于设备仍然是唯一的,但未明确标识用户的实际设备。例如,结合使用String.hashCode()和 UUID:

final TelephonyManager tm = (TelephonyManager) getBaseContext().getSystemService(Context.TELEPHONY_SERVICE);

final String tmDevice, tmSerial, androidId;
tmDevice = "" + tm.getDeviceId();
tmSerial = "" + tm.getSimSerialNumber();
androidId = "" + android.provider.Settings.Secure.getString(getContentResolver(), android.provider.Settings.Secure.ANDROID_ID);

UUID deviceUuid = new UUID(androidId.hashCode(), ((long)tmDevice.hashCode() << 32) | tmSerial.hashCode());
String deviceId = deviceUuid.toString();

可能会导致以下内容: 00000000-54b3-e7c7-0000-000046bffd97

对我来说效果很好。

正如 Richard 在下面提到的那样,不要忘记您需要阅读TelephonyManager属性的权限,因此请将其添加到清单中:

<uses-permission android:name="android.permission.READ_PHONE_STATE" />

导入库

import android.content.Context;
import android.telephony.TelephonyManager;
import android.view.View;

上次更新时间:6/2/15


阅读了有关创建唯一 ID 的所有 Stack Overflow 帖子,Google 开发者博客和 Android 文档之后,我觉得 “伪 ID” 似乎是最好的选择。

主要问题:硬件与软件

硬件

  • 用户可以更改其硬件,Android 平板电脑或手机,因此基于硬件的唯一 ID 并不是跟踪用户的好主意
  • 对于跟踪硬件 ,这是一个好主意

软件

  • 如果用户是 root 用户,则可以擦除 / 更改其 ROM
  • 您可以跨平台(iOS,Android,Windows 和 Web)跟踪用户
  • 征得他们的同意跟踪个人用户最好的方法就是简单地让他们登录(使用 OAuth 使其无缝连接)

Android 的整体故障

- 保证 API> = 9/10 的唯一性(包括根设备)(占 Android 设备的 99.5%)

- 没有额外的权限

伪代码:

if API >= 9/10: (99.5% of devices)

return unique ID containing serial id (rooted devices may be different)

else

return the unique ID of build information (may overlap data - API < 9)

感谢 @stansult 发布了我们所有的选项 (在此 Stack Overflow 问题中)。

选项列表 - 为什么 / 为什么不使用它们的原因:

  • 用户电子邮件 - 软件

    • 用户可以更改电子邮件 - 不太可能
    • API 5+ <uses-permission android:name="android.permission.GET_ACCOUNTS" />
    • API 14+ <uses-permission android:name="android.permission.READ_PROFILE" /> <uses-permission android:name="android.permission.READ_CONTACTS" />如何获取 Android 设备的主要电子邮件地址
  • 用户电话号码 - 软件

    • 用户可以更改电话号码 - 不太可能
    • <uses-permission android:name="android.permission.READ_PHONE_STATE" />
  • IMEI - 硬件 (仅手机,需要android.permission.READ_PHONE_STATE

    • 大多数用户讨厌在权限中显示 “电话” 的事实。一些用户的评分很差,因为他们认为当您真正想要做的只是跟踪设备安装时,您只是在窃取他们的个人信息。很明显,您正在收集数据。
    • <uses-permission android:name="android.permission.READ_PHONE_STATE" />
  • Android ID - 硬件 (可以为空,可以在恢复出厂设置时更改,可以在有根设备上更改)

    • 由于它可以为 “null”,因此我们可以检查 “null” 并更改其值,但这意味着它将不再是唯一的。
    • 如果您的用户具有恢复出厂设置的设备,则该值可能已在有根设备上更改或更改,因此如果您要跟踪用户安装,则可能会有重复的条目。
  • WLAN MAC 地址 - 硬件 (需要android.permission.ACCESS_WIFI_STATE

    • 这可能是第二好的选择,但是您仍在收集和存储直接来自用户的唯一标识符。很明显,您正在收集数据。
    • <uses-permission android:name="android.permission.ACCESS_WIFI_STATE "/>
  • 蓝牙 MAC 地址 - 硬件 (带有蓝牙的设备,需要android.permission.BLUETOOTH

    • 市场上的大多数应用程序都不使用蓝牙,因此如果您的应用程序不使用蓝牙而您将其包括在内,则用户可能会变得可疑。
    • <uses-permission android:name="android.permission.BLUETOOTH "/>
  • 伪唯一 ID - 软件 (适用于所有 Android 设备)

    • 很有可能,可能包含碰撞 - 请参阅下面的我的方法!
    • 这使您可以从用户那里获得一个 “几乎唯一的” ID,而无需获取任何私有的东西。您可以根据设备信息创建自己的匿名 ID。

我知道不使用权限就没有任何 “完美” 的方式来获得唯一 ID。但是,有时我们只需要真正跟踪设备安装即可。在创建唯一 ID 时,我们可以仅基于 Android API 提供给我们的信息来创建 “伪唯一 ID”,而无需使用额外的权限。这样,我们可以显示用户的尊敬,并尝试提供良好的用户体验。

使用伪唯一 ID,您实际上只会遇到这样的事实,即基于存在相似设备的事实,可能存在重复项。您可以调整组合方法以使其更加独特。但是,一些开发人员需要跟踪设备安装,这将根据相似的设备来解决问题或提高性能。

API> = 9:

如果他们的 Android 设备是 API 9 或更高版本,则由于 “Build.SERIAL” 字段,因此可以保证其唯一。

请记住 ,从技术上讲,您仅会错过大约 0.5%的API <9用户。因此,您可以专注于其余部分:这是 99.5%的用户!

API <9:

如果用户的 Android 设备低于 API 9;希望他们没有恢复出厂设置,并且他们的 “Secure.ANDROID_ID” 将保留或不为 “null”。 (请参阅http://developer.android.com/about/dashboards/index.html

如果其他所有方法均失败:

如果所有其他方法均失败,则如果用户确实低于 API 9(低于 Gingerbread),已重置其设备或 “Secure.ANDROID_ID” 返回 “null”,则返回的 ID 仅基于其 Android 设备信息。这是可能发生碰撞的地方。

变化:

  • 由于恢复出厂设置而删除了 “Android.SECURE_ID”,可能导致值更改
  • 编辑代码以更改 API
  • 更改了伪

请看下面的方法:

/**
 * Return pseudo unique ID
 * @return ID
 */
public static String getUniquePsuedoID() {
    // If all else fails, if the user does have lower than API 9 (lower
    // than Gingerbread), has reset their device or 'Secure.ANDROID_ID'
    // returns 'null', then simply the ID returned will be solely based
    // off their Android device information. This is where the collisions
    // can happen.
    // Thanks http://www.pocketmagic.net/?p=1662!
    // Try not to use DISPLAY, HOST or ID - these items could change.
    // If there are collisions, there will be overlapping data
    String m_szDevIDShort = "35" + (Build.BOARD.length() % 10) + (Build.BRAND.length() % 10) + (Build.CPU_ABI.length() % 10) + (Build.DEVICE.length() % 10) + (Build.MANUFACTURER.length() % 10) + (Build.MODEL.length() % 10) + (Build.PRODUCT.length() % 10);

    // Thanks to @Roman SL!
    // https://stackoverflow.com/a/4789483/950427
    // Only devices with API >= 9 have android.os.Build.SERIAL
    // http://developer.android.com/reference/android/os/Build.html#SERIAL
    // If a user upgrades software or roots their device, there will be a duplicate entry
    String serial = null;
    try {
        serial = android.os.Build.class.getField("SERIAL").get(null).toString();

        // Go ahead and return the serial for api => 9
        return new UUID(m_szDevIDShort.hashCode(), serial.hashCode()).toString();
    } catch (Exception exception) {
        // String needs to be initialized
        serial = "serial"; // some value
    }

    // Thanks @Joe!
    // https://stackoverflow.com/a/2853253/950427
    // Finally, combine the values we have found by using the UUID class to create a unique identifier
    return new UUID(m_szDevIDShort.hashCode(), serial.hashCode()).toString();
}

新功能(适用于带有广告和 Google Play 服务的应用):

在 Google Play 开发者控制台中:

从 2014 年 8 月 1 日开始,Google Play 开发者计划政策要求所有新的应用上载和更新都必须使用广告 ID 代替任何其他永久性标识符用于任何广告目的。学到更多

实现方式

允许:

<uses-permission android:name="android.permission.INTERNET" />

码:

import com.google.android.gms.ads.identifier.AdvertisingIdClient;
import com.google.android.gms.ads.identifier.AdvertisingIdClient.Info;
import com.google.android.gms.common.GooglePlayServicesAvailabilityException;
import com.google.android.gms.common.GooglePlayServicesNotAvailableException;
import java.io.IOException;
...

// Do not call this function from the main thread. Otherwise, 
// an IllegalStateException will be thrown.
public void getIdThread() {

  Info adInfo = null;
  try {
    adInfo = AdvertisingIdClient.getAdvertisingIdInfo(mContext);

  } catch (IOException exception) {
    // Unrecoverable error connecting to Google Play services (e.g.,
    // the old version of the service doesn't support getting AdvertisingId).

  } catch (GooglePlayServicesAvailabilityException exception) {
    // Encountered a recoverable error connecting to Google Play services. 

  } catch (GooglePlayServicesNotAvailableException exception) {
    // Google Play services is not available entirely.
  }
  final String id = adInfo.getId();
  final boolean isLAT = adInfo.isLimitAdTrackingEnabled();
}

来源 / 文件:

http://developer.android.com/google/play-services/id.html http://developer.android.com/reference/com/google/android/gms/ads/identifier/AdvertisingIdClient.html

重要:

当 Google Play 服务可用时,广告 ID 旨在完全替代出于广告目的而使用的其他其他标识符(例如,在 Settings.Secure 中使用 ANDROID_ID)。由 getAdvertisingIdInfo()引发的 GooglePlayServicesNotAvailableException 指示了 Google Play 服务不可用的情况。

警告,用户可以重置:

http://en.kioskea.net/faq/34732-android-reset-your-advertising-id

我试图参考我从中获取信息的每个链接。如果您不见了并且需要包括在内,请发表评论!

Google Player 服务 InstanceID

https://developers.google.com/instance-id/

正如 Dave Webb 所提到的, Android 开发者博客上有一篇文章对此进行了介绍。他们的首选解决方案是跟踪应用程序的安装而不是设备,并且在大多数情况下都能很好地工作。博客文章将向您显示完成该工作所需的代码,我建议您检查一下。

但是,如果您需要设备标识符而不是应用程序安装标识符,那么该博客文章将继续讨论解决方案。如果需要,我与 Google 的某人进行了交谈,以进一步澄清一些事项。这是我发现的有关设备标识符的内容,在上述博客文章中未提及:

  • ANDROID_ID 是首选的设备标识符。 ANDROID_ID 在 Android <= 2.1 或> = 2.3 的版本上完全可靠。只有 2.2 具有帖子中提到的问题。
  • 一些制造商的一些设备受 2.2 中 ANDROID_ID 错误的影响。
  • 据我所确定,所有受影响的设备都具有相同的 ANDROID_ID ,即9774d56d682e549c 。这也是仿真器 btw 报告的相同设备 ID。
  • Google 相信 OEM 已为其许多或大多数设备修复了该问题,但我能够验证,至少从 2011 年 4 月开始,找到具有 ANDROID_ID 损坏的设备还是很容易的。

根据 Google 的建议,我实现了一个类,该类将为每个设备生成一个唯一的 UUID,并在适当的情况下使用 ANDROID_ID 作为种子,并根据需要依赖 TelephonyManager.getDeviceId();如果失败,则采用随机生成的唯一 UUID 在重新启动应用程序后仍然存在(但不重新安装应用程序)。

请注意,对于必须回退到设备 ID 的设备,唯一 ID 在出厂重置之前保持不变。这是要注意的事情。如果您需要确保恢复出厂设置会重置您的唯一 ID,则可能需要考虑直接使用随机 UUID 代替设备 ID。

同样,此代码用于设备 ID,而不是应用程序安装 ID。在大多数情况下,您需要的是应用程序安装 ID。但是,如果您确实需要设备 ID,则以下代码可能对您有用。

import android.content.Context;
import android.content.SharedPreferences;
import android.provider.Settings.Secure;
import android.telephony.TelephonyManager;

import java.io.UnsupportedEncodingException;
import java.util.UUID;

public class DeviceUuidFactory {

    protected static final String PREFS_FILE = "device_id.xml";
    protected static final String PREFS_DEVICE_ID = "device_id";
    protected volatile static UUID uuid;

    public DeviceUuidFactory(Context context) {
        if (uuid == null) {
            synchronized (DeviceUuidFactory.class) {
                if (uuid == null) {
                    final SharedPreferences prefs = context
                            .getSharedPreferences(PREFS_FILE, 0);
                    final String id = prefs.getString(PREFS_DEVICE_ID, null);
                    if (id != null) {
                        // Use the ids previously computed and stored in the
                        // prefs file
                        uuid = UUID.fromString(id);
                    } else {
                        final String androidId = Secure.getString(
                            context.getContentResolver(), Secure.ANDROID_ID);
                        // Use the Android ID unless it's broken, in which case
                        // fallback on deviceId,
                        // unless it's not available, then fallback on a random
                        // number which we store to a prefs file
                        try {
                            if (!"9774d56d682e549c".equals(androidId)) {
                                uuid = UUID.nameUUIDFromBytes(androidId
                                        .getBytes("utf8"));
                            } else {
                                final String deviceId = (
                                    (TelephonyManager) context
                                    .getSystemService(Context.TELEPHONY_SERVICE))
                                    .getDeviceId();
                                uuid = deviceId != null ? UUID
                                    .nameUUIDFromBytes(deviceId
                                            .getBytes("utf8")) : UUID
                                    .randomUUID();
                            }
                        } catch (UnsupportedEncodingException e) {
                            throw new RuntimeException(e);
                        }
                        // Write the value out to the prefs file
                        prefs.edit()
                                .putString(PREFS_DEVICE_ID, uuid.toString())
                                .commit();
                    }
                }
            }
        }
    }

    /**
     * Returns a unique UUID for the current android device. As with all UUIDs,
     * this unique ID is "very highly likely" to be unique across all Android
     * devices. Much more so than ANDROID_ID is.
     * 
     * The UUID is generated by using ANDROID_ID as the base key if appropriate,
     * falling back on TelephonyManager.getDeviceID() if ANDROID_ID is known to
     * be incorrect, and finally falling back on a random UUID that's persisted
     * to SharedPreferences if getDeviceID() does not return a usable value.
     * 
     * In some rare circumstances, this ID may change. In particular, if the
     * device is factory reset a new device ID may be generated. In addition, if
     * a user upgrades their phone from certain buggy implementations of Android
     * 2.2 to a newer, non-buggy version of Android, the device ID may change.
     * Or, if a user uninstalls your app on a device that has neither a proper
     * Android ID nor a Device ID, this ID may change on reinstallation.
     * 
     * Note that if the code falls back on using TelephonyManager.getDeviceId(),
     * the resulting ID will NOT change after a factory reset. Something to be
     * aware of.
     * 
     * Works around a bug in Android 2.2 for many devices when using ANDROID_ID
     * directly.
     * 
     * @see http://code.google.com/p/android/issues/detail?id=10603
     * 
     * @return a UUID that may be used to uniquely identify your device for most
     *         purposes.
     */
    public UUID getDeviceUuid() {
        return uuid;
    }
}

这是 Reto Meier 今年在Google I / O演示中使用的代码,用于为用户获取唯一的 ID:

private static String uniqueID = null;
private static final String PREF_UNIQUE_ID = "PREF_UNIQUE_ID";

public synchronized static String id(Context context) {
    if (uniqueID == null) {
        SharedPreferences sharedPrefs = context.getSharedPreferences(
                PREF_UNIQUE_ID, Context.MODE_PRIVATE);
        uniqueID = sharedPrefs.getString(PREF_UNIQUE_ID, null);
        if (uniqueID == null) {
            uniqueID = UUID.randomUUID().toString();
            Editor editor = sharedPrefs.edit();
            editor.putString(PREF_UNIQUE_ID, uniqueID);
            editor.commit();
        }
    }
    return uniqueID;
}

如果将此方法与备份策略结合使用,以将首选项发送到云(在 Reto 的演讲中也有介绍),则应该有一个与用户相关联的 ID,并在擦除或更换设备后仍然存在。我计划使用此 ID。在未来的分析中(换句话说,我还没有做到这一点:)。

您也可以考虑 Wi-Fi 适配器的 MAC 地址。因此检索:

WifiManager wm = (WifiManager)Ctxt.getSystemService(Context.WIFI_SERVICE);
return wm.getConnectionInfo().getMacAddress();

清单中需要android.permission.ACCESS_WIFI_STATE权限。

报告为即使未连接 Wi-Fi 也可以使用。如果 Joe 从上面的答案中尝试使用多种设备,那将是不错的选择。

在某些设备上,关闭 Wi-Fi 时不可用。

注意:从 Android 6.x,它返回一致的假 mac 地址: 02:00:00:00:00:00

有相当有用的信息在这里

它涵盖了五种不同的 ID 类型:

  1. IMEI (仅适用于使用 Phone 的 Android 设备;需要android.permission.READ_PHONE_STATE
  2. 伪唯一 ID (适用于所有 Android 设备)
  3. Android ID (可以为 null,可以在恢复出厂设置时更改,可以在有根电话上更改)
  4. WLAN MAC 地址字符串(需要android.permission.ACCESS_WIFI_STATE
  5. BT MAC 地址字符串(具有蓝牙的设备,需要android.permission.BLUETOOTH

现在,Android 开发者官方博客正式版中有一篇完整的文章,即确定应用程序安装

Reto Meier 在Google I / O 上发布了一个强有力的答案,说明了如何解决此问题,这应满足大多数开发人员跟踪安装之间用户的需求。安东尼 · 诺兰(Anthony Nolan)在回答中指明了方向,但我想我会写出完整的方法,以便其他人可以轻松地看到如何做(我花了一段时间才弄清楚细节)。

这种方法将为您提供一个匿名的安全用户 ID,该 ID 对于不同设备(基于主要 Google 帐户)和安装之间的用户而言将是永久的。基本方法是生成随机用户 ID,并将其存储在应用程序的共享首选项中。然后,您使用 Google 的备份代理将链接到 Google 帐户的共享首选项存储在云中。

让我们通过完整的方法。首先,我们需要使用 Android 备份服务为 SharedPreferences 创建备份。首先通过http://developer.android.com/google/backup/signup.html注册您的应用。

Google 将为您提供备份服务密钥,您需要将其添加到清单中。您还需要告知应用程序使用 BackupAgent,如下所示:

<application android:label="MyApplication"
         android:backupAgent="MyBackupAgent">
    ...
    <meta-data android:name="com.google.android.backup.api_key"
        android:value="your_backup_service_key" />
</application>

然后,您需要创建备份代理,并告诉它对共享首选项使用助手代理:

public class MyBackupAgent extends BackupAgentHelper {
    // The name of the SharedPreferences file
    static final String PREFS = "user_preferences";

    // A key to uniquely identify the set of backup data
    static final String PREFS_BACKUP_KEY = "prefs";

    // Allocate a helper and add it to the backup agent
    @Override
    public void onCreate() {
        SharedPreferencesBackupHelper helper = new SharedPreferencesBackupHelper(this,          PREFS);
        addHelper(PREFS_BACKUP_KEY, helper);
    }
}

要完成备份,您需要在主活动中创建一个 BackupManager 实例:

BackupManager backupManager = new BackupManager(context);

最后,创建一个用户 ID(如果尚不存在),并将其存储在 SharedPreferences 中:

public static String getUserID(Context context) {
            private static String uniqueID = null;
        private static final String PREF_UNIQUE_ID = "PREF_UNIQUE_ID";
    if (uniqueID == null) {
        SharedPreferences sharedPrefs = context.getSharedPreferences(
                MyBackupAgent.PREFS, Context.MODE_PRIVATE);
        uniqueID = sharedPrefs.getString(PREF_UNIQUE_ID, null);
        if (uniqueID == null) {
            uniqueID = UUID.randomUUID().toString();
            Editor editor = sharedPrefs.edit();
            editor.putString(PREF_UNIQUE_ID, uniqueID);
            editor.commit();

            //backup the changes
            BackupManager mBackupManager = new BackupManager(context);
            mBackupManager.dataChanged();
        }
    }

    return uniqueID;
}

现在,即使用户移动设备,该 User_ID 也将在安装过程中保持不变。

有关此方法的更多信息,请参见Reto 的演讲

有关如何实施备份代理的完整详细信息,请参见数据备份 。我特别推荐测试的底部部分,因为备份不会立即发生,因此要测试您必须强制执行备份。

我认为这无疑是为唯一 ID 构建骨架的一种行之有效的方法。

在所有 Android 设备上均可使用的伪唯一 ID。某些设备没有电话(例如平板电脑),或者由于某些原因,您不希望包括 READ_PHONE_STATE 权限。您仍然可以阅读诸如 ROM 版本,制造商名称,CPU 类型以及其他硬件详细信息之类的详细信息,如果您想将 ID 用于序列密钥检查或其他常规用途,这些信息将非常适合。以这种方式计算出的 ID 不会是唯一的:可以找到两个具有相同 ID(基于相同的硬件和 ROM 映像)的设备,但实际应用程序中的更改可以忽略不计。为此,您可以使用 Build 类:

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

大多数 Build 成员都是字符串,我们在这里要做的是获取它们的长度,并通过以位为模的方式对其进行转换。我们有 13 个这样的数字,并且在前面增加了两个(35),以具有与 IMEI 相同的大小 ID(15 个数字)。这里还有其他可能性,只看这些字符串即可。返回类似355715565309247 。不需要特殊许可,这使此方法非常方便。


(额外信息:以上给出的技术是从Pocket Magic上的文章中复制的。)