安卓设备是否有唯一的设备ID?

3079

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


48
如果你正在使用 ANDROID_ID,请务必阅读这个答案这个漏洞 - Dheeraj Vepakomma
54个回答

2236

521
有时候它被认为是无效的,文档中记录着“在出厂重置时可能会改变”。使用时需自负风险,在已获得 root 权限的手机上可轻松更改。 - Seva Alekseyev
33
http://groups.google.com/group/android-developers/browse_thread/thread/53898e508fab44f6/84e54feb28272384?lnk=raot - Erdal
23
关于第一个答案中使用ANDROID_ID哈希值的问题,我认为我们需要小心谨慎。因为该ID可能在应用程序首次运行时未被设置,可能稍后被设置,或者甚至在理论上发生更改,因此唯一标识可能会改变。请注意不要改变原来的意思,并尽量使表达更加通俗易懂。 - user604363
51
请注意,这个解决方案存在很大的局限性:http://android-developers.blogspot.com/2011/03/identifying-app-installations.html - emmby
41
自 Android 4.2 版本起,ANDROID_ID 不再唯一标识设备:https://dev59.com/jWYr5IYBdhLWcg3wxM4k#13465373 - Tom
显示剩余14条评论

1191

更新:在最近的 Android 版本中���许多关于 ANDROID_ID 的问题已得到解决,我认为这种方法不再必要。请看 Anthony的回答

完全公开透明地说,我的应用程序最初使用以下方法,但现在不再使用此方法,我们现在使用emmby的答案中链接到的Android Developer Blog条目中概述的方法(即生成并保存一个UUID#randomUUID())。


有很多关于此问题的答案,其中大多数只能“有时”起作用,不幸的是,那样还不够好。

根据我对设备的测试(所有手机,至少一个未激活):

  1. 所有测试过的设备都返回了 TelephonyManager.getDeviceId() 的值。
  2. 所有 GSM 设备(所有带 SIM 卡的设备)返回了 TelephonyManager.getSimSerialNumber() 的值。
  3. 所有 CDMA 设备返回了 getSimSerialNumber() 的 null 值(如预期所示)。
  4. 添加了 Google 帐户的所有设备都返回了 ANDROID_ID 的值。
  5. 所有 CDMA 设备在设置期间添加了 Google 帐户时,ANDROID_IDTelephonyManager.getDeviceId() 返回相同的值(或这些值的派生值)。
  6. 我还没有测试过没有 SIM 卡的 GSM 设备、没有添加 Google 帐户的 GSM 设备或任何飞行模式下的设备。

因此,如果您想要一些特定于设备本身的东西,则 TM.getDeviceId() 应该足够。显然,有些用户比其他用户更偏执,因此对一个或多个这些标识符进行哈希处理可能很有用,以便字符串仍然对设备几乎唯一,但不明确地标识用户的实际设备。例如,使用 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;

162
平板设备上不会有基于电话的身份验证系统,对吧? - Seva Alekseyev
24
所以我说大多数情况下不会一直有效 :) 我还没有看到对于所有设备、所有设备类型和所有硬件配置都可靠的答案。这就是为什么这个问题一开始就存在的原因。很明显,没有通用解决方案。个别设备制造商可能有设备序列号,但这些序列号不对我们公开使用,也不是必需的。因此,我们只能使用现有的东西。 - Joe
33
代码示例运行良好。请记得在清单文件中添加<uses-permission android:name="android.permission.READ_PHONE_STATE" />。如果将其存储在数据库中,返回的字符串长度为36个字符。 - Richard
12
注意,这个解决方案存在巨大的限制:http://android-developers.blogspot.com/2011/03/identifying-app-installations.html - emmby
22
我认为你所指的是emmby已经分享链接的Android开发者博客,它解释了你试图表达的内容,因此你应该只是在他的评论中点赞就好了。无论如何,正如emmby在他的回答中提到的,即使有了博客信息,仍然存在问题。问题要求唯一的设备标识符(而不是安装标识符),所以我不同意你的说法。该博客假设你想要的不一定是跟踪设备,而问题只是要求这样做。除此之外,我同意博客的观点。 - Joe
显示剩余9条评论

472

#最近更新:2015年6月2日


在阅读了有关创建唯一ID的每篇Stack Overflow文章、Google开发者博客和Android文档后,我觉得“Pseudo ID”是最好的选择。

主要问题:硬件与软件

硬件

  • 用户可以更换他们的硬件,例如Android平板电脑或手机,因此基于硬件的唯一ID不适合用于跟踪用户
  • 对于跟踪硬件,这是一个很好的想法

软件

  • 如果用户已经获取了root权限,他们可以擦除/更改ROM
  • 您可以跨平台(iOS、Android、Windows和Web)跟踪用户
  • 用户同意为前提,跟踪单个用户的最佳方法就是让他们登录(使用OAuth使其无缝)

#Android的总体分析

###- 对于API >= 9/10(99.5%的Android设备),保证唯一性(包括已获取root权限的设备) ###- 不需要额外的权限

伪代码:

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 - 硬件 (可能为空,可能在出厂重置时更改,可能在root设备上被更改)

  • 由于它可能为'null',因此我们可以检查其是否为'null'并更改其值,但这意味着它将不再是唯一的。

  • 如果您有使用出厂重置设备的用户,则该值可能已更改或在root设备上更改,因此如果要跟踪用户安装,则可能会出现重复条目。

  • 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时,我们可以仅基于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更改
  • 更改伪唯一ID

请查看下面的方法:

/**
 * 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://dev59.com/V3E85IYBdhLWcg3wbS1h#4789483
    // 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://dev59.com/V3E85IYBdhLWcg3wbS1h#2853253
    // 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/


7
我在我的应用程序中使用了你的方法来发送评论。我有一个坏消息,很不幸,伪ID并不是完全唯一的。我的服务器记录了超过100个5个ID和将近30个ID的记录。最重复的ID是“ffffffff-fc8f-6093-ffff-ffffd8”(159条记录)和“ffffffff-fe99-b334-ffff-ffffef”(154次)。同时根据时间和评论,很明显有不同的人。到目前为止总共记录了10,000条。请告诉我这是为什么。谢谢。 - hojjat reyhane
3
我在1.5年前写了这个。我不确定为什么对你来说不是独特的。你可以尝试广告ID。如果不行,你可以想出自己的解决方案。 - Jared Burrows
3
有点儿..如果您能仔细阅读问题并发表您的想法,我会非常感激。 - Durai Amuthan.H
1
@user1587329 谢谢。我正在努力为大家保持最新状态。当涉及到硬件与软件以及跨平台时,这个问题就有些棘手了。 - Jared Burrows
为什么获取一个简单的不重复的唯一ID这么难呢?其实只需要使用伪唯一ID - 软件名称 + 日期(hhmmss),就可以避免冲突。组合越长,难度系数就越高。 - gumuruh

355

正如 Dave Webb 所提到的,在 Android Developer Blog 的一篇文章中介绍了相关内容。他们首选跟踪应用程序的安装情况而不是设备,这对大多数用例都有效。该博客文章将向您展示使其工作所需的必要代码,建议您查看。

但是,该博客文章继续讨论了解决方案,如果您需要设备标识符而不是应用程序安装标识符。我与 Google 的某位人员交谈,以获取有关需要这样做时几个项目的其他澄清。以下是我在上述博客文章中没有提到的有关设备标识符的发现:

  • ANDROID_ID 是首选的设备标识符。ANDROID_ID 在 Android 版本 <=2.1 或>=2.3 上非常可靠。只有 2.2 存在博客文章中提到的问题。
  • 几家制造商的几款设备受到 2.2 中 ANDROID_ID 错误的影响。
  • 据我所知,所有受影响的设备具有 相同的 ANDROID_ID,即9774d56d682e549c。顺便说一句,这也是模拟器报告的相同设备 ID。
  • Google 认为,OEM 已经修补了许多或大多数设备的问题,但我能够验证,至少在 2011 年 4 月初,仍然很容易找到具有错误 ANDROID_ID 的设备。

根据 Google 的建议,我实现了一个类,将使用适当的 ANDROID_ID 作为种子生成每个设备的唯一 UUID,必要时回退到 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;
    }
}

7
你应该对各种ID进行哈希处理,以使它们的大小都相同。此外,你应该对设备ID进行哈希处理,以避免意外地暴露私人信息。 - Steve Pomeroy
3
好的,Steve提出了很好的观点。我更新了代码,始终返回UUID。这确保了生成的ID始终具有相同的大小,并且在返回之前对Android和设备ID进行了哈希处理,以避免意外暴露个人信息。我还更新了描述,指出设备ID将在恢复出厂设置后保留,这可能不适合某些用户。 - emmby
9
好的,我已经更新了评论,强烈建议用户使用应用程序安装ID而不是设备ID。然而,我认为对于那些确实需要设备而不是安装ID的人来说,这个解决方案仍然很有价值。 - emmby
10
ANDROID_ID在重置出厂设置时可能会改变,因此不能很好地识别设备。 - Samuel
2
正如我在你的同一答案这里中所指出的,不能保证在不同进程中获得相同的生成UUID - SharedPrefs副本是本地的! - Mr_and_Mrs_D
显示剩余2条评论

188

以下是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 的演讲中介绍),您应该拥有一个标识,它与用户相关联,并在设备被擦除甚至替换后仍然存在。我计划在以后的分析中使用它(换句话说,我还没有完成这部分的工作:)。


4
如果您不需要在卸载和重新安装后保留唯一ID(例如促销活动/游戏,在此期间您有三次赢取机会),则这是一个很好的选择。 - Kyle Clegg
3
Meier的演示依赖于使用Android备份管理器,并且又依赖于用户选择打开此功能。对于应用程序用户首选项(Meier的用途)来说,这很好,因为如果用户没有选择该选项,她就不会得到那些备份。然而,最初的问题是关于为设备生成一个唯一的ID,而这个ID是按应用程序生成的,甚至不是按安装或设备生成的,并且由于它依赖于用户选择备份选项,所以它在用户首选项之外的用途(例如,用于限时试用)是有限的。 - Carl
17
卸载或清除数据将无法解决此问题。 - John Shelley
非常糟糕的解决方案。 - famfamfam
我认为这是一个不错的解决方案,设备和服务器上都有唯一的ID,以防卸载。您可以确保将其与客户端电子邮件一起保存,以便它保持不变 ;-p - vin shaba

140
这是一个简单的问题,但答案并不简单。此外,所有现有的答案要么过时,要么不可靠。因此,如果您在2020年之后寻找解决方案,请记住以下几点:所有基于硬件的标识符(IMEI、MAC、序列号等)对于非谷歌设备(除了像素和Nexus之类的设备)都是不可靠的,而这些设备在全球范围内统计大多数活跃的Android设备。因此,官方Android标识符最佳实践明确指出:避免使用硬件标识符,如IMEI、MAC地址等...这使得这里的大多数答案无效。另外,由于不同的Android安全更新,其中一些需要更新且更严格的运行时权限,用户可以轻松地拒绝这些权限。例如,CVE-2018-9489影响上述所有基于WIFI的技术。这不仅使这些标识符不可靠,而且在许多情况下也无法访问。因此,简单地说:不要使用这些技术。这里的许多其他答案建议使用AdvertisingIdClient,但这也是不兼容的,因为它的设计仅用于广告配置文件。这也在官方参考中说明。 仅在用户画像或广告使用情况下使用广告ID 它不仅对设备识别不可靠,而且您还必须遵循有关广告跟踪的用户隐私政策,该政策明确规定用户可以随时重置或阻止它。
因此,也不要使用它
由于您无法拥有所需的静态全局唯一和可靠设备标识符。 Android的官方参考建议: 尽可能使用Firebase安装ID(FID)或私下存储的GUID用于除付款欺诈预防和电话之外的所有其他用例。
它是设备上应用程序安装的唯一标识,因此当用户卸载应用程序时-它被清除,因此它不是100%可靠,但这是接近完美的解决方案。 注意截至今天,FirebaseInstanceId 已弃用,您应改用FirebaseInstallations
要使用FirebaseInstallations ,请将最新的firebase-messaging依赖项添加到gradle中。
implementation 'com.google.firebase:firebase-messaging:23.0.0'

使用以下代码获取Firebase ID:

FirebaseInstallations.getInstance().getId().addOnCompleteListener(task -> {
     if (task.isSuccessful()) {
        String firebaseIdentifier = task.getResult();
        // Do what you need with firebaseIdentifier
     }
});

如果您需要在远程服务器上存储设备标识符,则不要将其存储为明文,而是使用带有盐的哈希值
如今,根据GDPR-标识符和类似法规,这不仅是最佳实践,实际上必须这样做。

28
目前而言,这是最好的答案,而第一句话也是最好的概括:“这是一个简单的问题,没有简单的答案——就是热爱它。” - b2mob
4
@M.UsmanKhan,答案紧随其后:“今天不仅是最佳实践,根据GDPR和身份识别等法规,您实际上必须这样做。” - Nikita Kurtin
3
如果重新安装应用程序,Firebase 的唯一 ID 将会更改。 - famfamfam
1
@MichaelPaccione 不多,但特别是当应用程序被卸载时。请注意,我已经在答案中写了它。 - Nikita Kurtin
3
@famfamfam 因为跟踪设备对用户构成隐私和安全威胁。没有合法的情况需要使用它,因此没有理由让其成为可能。硬件 ID 被证明是错误的,Google 正在慢慢修复它们。 - Agent_L
显示剩余12条评论

106

另外,您可能需要考虑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


9
需要使用 android.permission.ACCESS_WIFI_STATE 权限。 - ohhorob
6
我认为你会发现,在几乎所有的安卓设备上,当WiFi关闭时,它是不可用的。关闭WiFi会在内核级别删除设备。 - chrisdowney
13
@Sanandrea - 让我们面对现实,在一个已经取得 root 权限的设备上,所有东西都可以被篡改。 - ocodo
6
在Android M上已禁止访问WiFi MAC地址:https://dev59.com/1l0Z5IYBdhLWcg3wiw0v - brz
8
从 Android 6.x 开始,它将返回一致的虚假 MAC 地址:02:00:00:00:00:00 - Behrouz.M
显示剩余6条评论

92

这里有相当有用的信息(点击此处).

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

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

3
重要的一点被省略了(在此和文章中):除非 WLAN 和 BT 开启,否则无法获取它们的 MAC 地址!否则我认为 WLAN MAC 将是完美的标识符。您不能保证用户会打开他们的 Wi-Fi,而且我不认为主动打开 Wi-Fi 是“恰当的”。 - Tom
2
@Tom,你错了。即使 WLAN 或 BT 被关闭,仍然可以读取它们的 MAC 地址。但是,不能保证设备是否有 WLAN 或 BT 模块可用。 - Marqs
4
值得注意的是,本地WiFi和蓝牙MAC地址不再可用。现在,aWifiInfo对象的getMacAddress()方法和BluetoothAdapter.getDefaultAdapter().getAddress()方法都将返回02:00:00:00:00:00。 - sarika kate
5
只有在Android 6.0 Marshmallow及以上版本中才是真实的......在6.0 Marshmallow以下的版本中,它仍然按预期工作。 - Smeet

55

6
这个论点的关键在于,如果你试图从硬件中获取唯一的ID,那么很可能会犯错。 - Tim Bray
4
如果您允许通过恢复出厂设置重置设备锁定,那么您的试用模式就几乎无法使用了。 - Seva Alekseyev
博客文章已经链接到此网站:https://developer.android.com/training/articles/user-data-ids - Bruno Bieri

53
Google I/O 上,Reto Meier 提出了一个坚实的方案来追踪用户跨不同安装设备的访问,这可以满足大多数开发人员的需求。Anthony Nolan 在他的回答中展示了方向,但我想写出完整的方法,以便其他人可以轻松地看到如何做到这一点(我花了一段时间来弄清楚细节)。
这种方法将为您提供一个匿名、安全的用户 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>

接下来您需要创建备份代理,并告诉它使用助手代理进行SharedPreferences的共享:

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);
    }
}

要完成备份,您需要在主 Activity 中创建一个 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's talk

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


9
当用户拥有多个设备时,比如一台平板电脑和一部手机,这样做是否会导致多个设备具有相同的ID? - Tosa
如果用户在手机上禁用了备份,那该怎么办呢?在这种情况下,没有任何应用程序数据会被备份! - mohammad reza sarsarabi

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接