Leo's Blog

Android 设备获取唯一标识

Android设备有很多的”标识”号,比如常见的IMEI,SerizalNumber,UUID等概念。既然这样,那我们就把这些概念拉出来溜溜,对这些标识做一个总结,看看Android为毛整这么多标识。

DEVICE_ID

  • IMEI(International Mobile EquipmentIdentity)
    是国际移动设备身份码的缩写,由15为数字组成,它与每台手机一一对应,该码是全世界唯一的。

  • MEID(Mobile Equipment Identifier)
    全球唯一的56bit CDMA制式移动终端标识号。标示号会被烧入到终端里,不可修改。可用来对CDMA制式移动设备进行身份识别和跟踪,由14位数字组成。

1
2
3
4
public static String getDeviceID(Context context){
TelephonyManager tm = (TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE);
return tm.getDeviceID().toString();
}

以上代码会根据不同的手机设备返回IMEI,MEID或者ESN码,作为Android手机设备的唯一标识。是不是感觉很简单? 好吧,让我们看看官方开发者的是怎么说的。

  • Non-phones: Wifi-only devices or music players that don’t have telephony hardware just don’t have this kind of unique identifier.

  • Persistence: On devices which do have this, it persists across device data wipes and factory resets. It’s not clear at all if, in this situation, your app should regard this as the same device.

  • Privilege:It requires READ_PHONE_STATE permission, which is irritating if you don’t otherwise use or need telephony.

  • Bugs: We have seen a few instances of production phones for which the implementation is buggy and returns garbage, for example zeros or asterisks.

看完上面的介绍,感觉自己又被谷歌以及某些厂商们耍弄了一番,但是胳膊拧不过大腿,谁让人家强势呢,找替代方法吧,先去看看物理mac地址,地球人都知道这个玩意是唯一的。

MAC_ADDRESS

1
2
3
4
public static String getMacAddress(Context context){
WifiManager wm = (WifiManager)context.getSystemService(Context.WIFI_SERVICE);
return wm.getConnectionInfo().getMacAddress();
}
1
2
3
4
5
6
7
8
9
10
11
12
public static String getBluetoothAddress(){
BluetoothAdapter adapter = BluetoothAdapter.getDefauleAdapter();
if(adapter == null){
// 不支持蓝牙
return ""
}
if(!adapter.isEnabled()){
// 蓝牙未打开
return ""
}
return adapter.getAddress();
}

哦了,获取到了wifi或者蓝牙的mac地址,还没来得及庆幸一番,一盆凉水又浇了下来。

  • 硬件限制:不是所有的设备都有wifi或者蓝牙,硬件不支持,编一个mac地址出来?
  • 获取限制:如果wifi没有打开过,是无法获取mac地址的,蓝牙还要可恨一点,只有在打开的时候才能获取到。

Pass,宝宝心里苦,宝宝不说,继续找其他方法。。。

Serial Number

硬件序列,在Android 2.2 以上的设备可以通过android.os.Build.SERIAL获得序列号。没有电话功能的设备上被要求必须提供,手机就看厂商心情了。。。所以经常返回unKnown,大写的PASS!!!

ANDROID_ID

ANDROID_ID是设备第一次启动时产生和存储的64bit的一个数

1
2
3
public static String getAndroidId(Context context){
return Settings.Secure.getString(context.getContentResolver),Setting.Secure.ANDROID_ID);
}

这哥们看起来挺靠谱,但是android的各大厂商们又来刷存在感了。。。

  • 官方介绍:它在Android <=2.1 or Android>=2.3的版本是可靠的,但是2.2。。。,当然此缺陷可以忽略不计。。
  • 在某些厂商的设备,有一个bug,每个设备都会产生相同的ANDROID_ID:9774d56d682e549c。别问我为什么,我也不知道。。。
  • 某些厂商会返回null。。。
  • 设备差异:CDMA设备,ANDROID_ID和DEVICE_ID是同一个值。

Installtion: UUID

UUID是官方推荐的一个标识方法,该方法无需访问设备的资源,跟设备类型无关。它是在程序第一次运行后生成一个ID实现的(自己生成,非系统生成!),它不是标识设备,而是某一个应用的唯一ID。

Google Developer Blog 给出了这样一个框架:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class Installation {
private static String sID = null;
private static final String INSTALLATION = "INSTALLATION";
public synchronized static String id(Context context) {
if (sID == null) {
File installation = new File(context.getFilesDir(), INSTALLATION);
try {
if (!installation.exists())
writeInstallationFile(installation);
sID = readInstallationFile(installation);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
return sID;
}
private static String readInstallationFile(File installation) throws IOException {
RandomAccessFile f = new RandomAccessFile(installation, "r");
byte[] bytes = new byte[(int) f.length()];
f.readFully(bytes);
f.close();
return new String(bytes);
}
private static void writeInstallationFile(File installation) throws IOException {
FileOutputStream out = new FileOutputStream(installation);
String id = UUID.randomUUID().toString();
out.write(id.getBytes());
out.close();
}
}

前面说过,Installtion ID 不是设备的唯一标识,它会为不同的应用程序生成不同的ID,并且在程序重新安装后也会生成不同的ID。

到这里我的耐心基本被消磨殆尽。。。

##总结
既然通过一种方式,不能保证ID的可靠性,那就采用多个,对比以上的几种方案,决定通过ANDROID_ID,DEVICE_ID,INSTALLTION_ID来生成一个ID,先来看下此方案的注意事项:

  • 目前的手机基本上都在4.0以上,所以ANDROID_ID一般可用,可以通过其生成一个UUID
  • 厂商的定制导致ANDROID_ID返回9774d56d682e549c和Null,用DeviceID替代ANDROID_ID,作为种子生成一个UUID
  • 以上都无法获取,生成一个InstalltionID
  • 当需要生成一个deviceid时,即绑定设备时,采用该方案,不需要绑定设备,InstalltionID就可满足需求
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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{
private static final String PREFS_FILE = "device_id.xml";
private static final String PREFFS_DEVICE_ID = "device_id";
private 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(PREFFS_DEVICE_ID,null);
if(id != null){
uuid = UUID.fromString(id);
}else{
final String androidID = Secure.getString(context.getContentResolver(),Secure.ANDROID_ID);
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);
}
prefs.edit().putString(PREFFS_DEVICE_ID,uuid.toString()).commit();
}
}
}
}
}
}