前言
近期想要了解一下獲取設備的唯一標識,然后我就頭疼了。然后。。。。。。
今天讓我們來歸納總結一下。
IMEI,MEID,ESN,IMSI,android_id 之間的區別
我們先來確定幾個概念,有利于下面的開發:
1.IMEI
- (國際移動設備識別碼(IMEI:International Mobile Equipment Identification Number)是區別移動設備的標志,儲存在移動設備中,可用于監控被竊或無效的移動設備。IMEI組成如下圖所示,移動終端設備通過鍵入“*#06#” 即可查得。
- 其總長為15位,每位數字僅使用0~9的數字。其中TAC代表型號裝配碼,由歐洲型號標準中心分配;FAC代表裝配廠家號碼;SNR為產品序號,用于區別同一個TAC和FAC中的每臺移動設備;SP是備用編碼。
IMEI由15位數字組成,其組成為:
1、前6位數(TAC)是”型號核準號碼”,一般代表機型。
2、接著的2位數(FAC)是”最后裝配號”,一般代表產地。
3、之后的6位數(SNR)是”串號”,一般代表生產順序號。
4、最后1位數(SP)通常是”0”,為檢驗碼,目前暫備用。
串碼的作用:
IMEI為TAC + FAC + SNR + SP。IMEI(International Mobile Equipment Identity)是國際 移動設備身份碼的縮寫,國際移動裝備辨識碼,是由15位數字組成的"電子串號",它與每臺手機一一對應,而且該碼是全世界唯一的。每一只手機在組裝完成后都將被賦予一個全球唯一的一組號碼,這個號碼從生產到交付使用都將被制造生產的廠商所記錄。
當手機被盜的時候,如知道IEMI碼,可以通過手機供應商進行手機鎖定,即:獲知被盜之后的手機號碼,中止手機的通話功能,獲知手機的方位。一般情況下,供應商不會對個人或單位提出的定位或鎖定手機的請求進行受理。在國內,有關的國家安全部門會對手機串號進行一定程度的管理。
2.MEID
- Mobile Equipment IDentifier(MEID)是全球唯一的56bit移動終端標識號。標識號會被燒入終端里,并且不能被修改。可用來對移動式設備進行身份識別和跟蹤。由于ESN號段是有限的資源,基本上耗盡,可能還有少量回收利用的號段,所以制定了56bits的MEID號段,用來取代32bits的ESN號段。MEID主要分配給CDMA制式的手機。
- 由于CDMA移動設備增多,導致原來8位的ESN不夠用,所以56bits=(56/4=14bytes)的MEID橫空出世。現在的CDMA手機一般ESN/MEID兩者都有。MEID也是用16進制來表示。
MEID由14個十六進制數字標識,第15位為校驗位,不參與空中傳輸。
RR:范圍A0-FF,由官方分配
XXXXXX:范圍 000000-FFFFFF,由官方分配
ZZZZZZ:范圍 000000-FFFFFF,廠商分配給每臺終端的流水號
C/CD:0-F,校驗碼
手機中的IMEI和MEID號碼就如同我們生活中的身份證一樣,它是識別手機身份的重要依據,如用虛假號碼的手機,網絡運營商可隨時通過技術手段關閉此手機在網絡中的運營,這將給手機的使用者帶來巨大的使用風險。手機中所使用的IMEI或MEID等號段均可透過摩爾實驗室等相關機構合法申請。
另外,MEID的申請,是需要付費的。目前的價格是每1M范圍的MEID的費用是8000美元,每增加1M范圍的MEID號碼需要額外付費8000美元。
MEID號碼是由Telecommunications Industry Association(TIA)進行分配管理的。
MEID號碼的查看,目前沒有一個通用的方法,由各手機制造商自己設置。可以通過查看手機說明書得到查看MEID號碼的方法。
3.ESN
- 電子序列號,在CDMA 系統中,是鑒別一個物理硬件設備唯一的標識。也就是說每個手機都用這個唯一的ID來鑒別自己, 就跟人的身份證一樣。CDMA中的ESN對應于GSM網絡中的IMEI。 一個ESN有32 bits,
- 也就是 32/4 = 8 bytes。隨著CDMA移動設別的增多,ESN已經不夠用了,所以推出了位數更多的MEID。ESN用16進制來表示。
4.IMSI
- 國際移動用戶識別碼(IMSI:International Mobile Subscriber Identification Number)是區別移動用戶的標志,儲存在SIM卡中,可用于區別移動用戶的有效信息。
- IMSI總長度不超過15位,同樣使用0~9 的數字。其中MCC是移動用戶所屬國家代號,占3位數字,中國的MCC規定為460;MNC是移動網號碼,最多由兩位數字組成,用于識別移動用戶所歸屬的移動通信網;
- MSIN是移動用戶識別碼,用以識別某一移動通信網中的移動用戶。
5.android_id
- android設備的唯一識別碼
根據上面的總結獲取到自己手機的相關的信息如下::
meid[99000939335539]
imei1[863254036690765]
imei2[863254036690773]
simSerialNumber [89860117801033225378]
subscriberId [460011691516816]
下面是華為手機:
MEID:A000007F2C4002
IMEI1:866957038217380
IMEI2:866957038364067
PESN:802B5D2A
SN:8BN02179015937
因為我的手機是雙卡的手機所以有兩個imei的號碼。
獲取Android 唯一標識的分析
1.DEVICE_ID,不同的手機設備返回IMEI,MEID或者ESN碼
這是Android系統為開發者提供的用于標識手機設備的串號,也是各種方法中普適性較高的,可以說幾乎所有的設備都可以返回這個串號,并且唯一性良好。
這個DEVICE_ID可以同通過下面的方法獲取:
TelephonyManager tm = (TelephonyManager)getSystemService(Context.TELEPHONY_SERVICE);`
String DEVICE_ID = tm.getDeviceId();`
上面的方法在api26已經放棄。官方建議使用getImei()和getMeid()這兩個方法得到相應的值。
非手機設備: 如果只帶有Wifi的設備或者音樂播放器沒有通話的硬件功能的話就沒有這個DEVICE_ID
權限: 獲取DEVICE_ID需要READ_PHONE_STATE權限,但如果我們只為了獲取它,沒有用到其他的通話功能,那這個權限有點大才小用
1.1 Sim Serial Number 手機SIM卡唯一標識
代碼如下:
TelephonyManager tm = (TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE);
String SimSerialNumber = tm.getSimSerialNumber();
注意:對于CDMA設備,返回的是一個空值。
1.2 TelephonyManager可以得到的其他的一些值
TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
String deviceId = tm.getDeviceId();//不同的手機設備返回IMEI,MEID或者ESN碼
String imei = tm.getImei();//返回IMEI
String meid = tm.getMeid();//返回MEID
String simSerialNumber = tm.getSimSerialNumber();//手機SIM卡唯一標識
String subscriberId = tm.getSubscriberId();//返回例如獨特的用戶ID,一個GSM手機的號碼。
String line1Number = tm.getLine1Number();//手機號碼
TelephonyManager的詳情可以參考這篇博客telephonymanager詳解
2.MAC 地址
MAC 地址具有全局唯一性,無法由用戶重置,在恢復出廠設置后也不會發生變化。一般不建議使用 MAC 地址進行任何形式的用戶標識。因此,從 Android M 開始,無法再通過第三方 API 獲得本地設備 MAC 地址(例如,WLAN 和藍牙)。[WifiInfo.getMacAddress()]
方法和 [BluetoothAdapter.getDefaultAdapter().getAddress()]
方法都會返回 02:00:00:00:00:00
。
//wifi mac地址
WifiManager wifi = (WifiManager)
context.getSystemService(Context.WIFI_SERVICE);
WifiInfo info = wifi.getConnectionInfo();
String wifiMac = info.getMacAddress();
上面的方法在安卓6.0以前是可以得到的mac地址的,但是在安卓6.0以后得到的都是02:00:00:00:00:00這個地址。
網上有方法獲取8.0wifi的mac地址:
public static String getMacAddressFromIp(Context context) {
String mac_s = "";
StringBuilder buf = new StringBuilder();
try {
byte[] mac;
NetworkInterface ne = NetworkInterface.getByInetAddress(InetAddress.getByName(getIpAddress(context)));
mac = ne.getHardwareAddress();
for (byte b : mac) {
buf.append(String.format("%02X:", b));
}
if (buf.length() > 0) {
buf.deleteCharAt(buf.length() - 1);
}
mac_s = buf.toString();
} catch (Exception e) {
e.printStackTrace();
}
return mac_s;
}
public static String getIpAddress(Context context) {
NetworkInfo info = ((ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE)).getActiveNetworkInfo();
if (info != null && info.isConnected()) {
// 3/4g網絡
if (info.getType() == ConnectivityManager.TYPE_MOBILE) {
try {
for (Enumeration<NetworkInterface> en = NetworkInterface.getNetworkInterfaces(); en.hasMoreElements(); ) {
NetworkInterface intf = en.nextElement();
for (Enumeration<InetAddress> enumIpAddr = intf.getInetAddresses(); enumIpAddr.hasMoreElements(); ) {
InetAddress inetAddress = enumIpAddr.nextElement();
if (!inetAddress.isLoopbackAddress() && inetAddress instanceof Inet4Address) {
return inetAddress.getHostAddress();
}
}
}
} catch (SocketException e) {
e.printStackTrace();
}
} else if (info.getType() == ConnectivityManager.TYPE_WIFI) {
// wifi網絡
WifiManager wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
WifiInfo wifiInfo = wifiManager.getConnectionInfo();
String ipAddress = intIP2StringIP(wifiInfo.getIpAddress());
return ipAddress;
} else if (info.getType() == ConnectivityManager.TYPE_ETHERNET) {
// 有限網絡
return getLocalIp();
}
}
return null;
}
private static String intIP2StringIP(int ip) {
return (ip & 0xFF) + "." +
((ip >> 8) & 0xFF) + "." +
((ip >> 16) & 0xFF) + "." +
(ip >> 24 & 0xFF);
}
// 獲取有限網IP
private static String getLocalIp() {
try {
for (Enumeration<NetworkInterface> en = NetworkInterface.getNetworkInterfaces(); en.hasMoreElements(); ) {
NetworkInterface intf = en.nextElement();
for (Enumeration<InetAddress> enumIpAddr = intf.getInetAddresses(); enumIpAddr.hasMoreElements(); ) {
InetAddress inetAddress = enumIpAddr.nextElement();
if (!inetAddress.isLoopbackAddress() && inetAddress instanceof Inet4Address) {
return inetAddress.getHostAddress();
}
}
}
} catch (SocketException ex) {
}
return "0.0.0.0";
}
上面的方式在小米安卓8.0系統的手機和華為榮耀8.0系統的手機都是可以的。其他的手機未測試。
注意:
要想獲取mac地址,是必須wifi聯網的,否則得不到相應的名稱。
3.ANDROID_ID
在設備首次啟動時,系統會隨機生成一個64位的數字,并把這個數字以16進制字符串的形式保存下來,這個16進制的字符串就是ANDROID_ID,當設備被wipe后該值會被重置。
可以通過下面的方法獲取:
String ANDROID_ID = Settings.System.getString(context.getContentResolver(), Settings.System.ANDROID_ID);
ANDROID_ID可以作為設備標識,但需要注意:
1. 廠商定制系統的Bug:不同的設備可能會產生相同的ANDROID_ID:9774d56d682e549c。
2. 廠商定制系統的Bug:有些設備返回的值為null。
3. 設備差異:對于CDMA設備,ANDROID_ID和TelephonyManager.getDeviceId() 返回相同的值。
4.Serial Number 串號
Android系統2.3版本以上可以通過下面的方法得到Serial Number,且非手機設備也可以通過該接口獲取。
String serial= android.os.Build.SERIAL;
4.1 android.os.Build里面能夠獲取的一些值
直接上代碼:
設備序列號(Serial Number, SN)
String serialNum = android.os.Build.SERIAL;
制造商 (Manufacturer)
String manufacturer = android.os.Build.MANUFACTURER;
型號(Model)
String model = android.os.Build.MODEL;
品牌(Brand)
String brand = android.os.Build.BRAND;
設備名 (Device)
String device = android.os.Build.DEVICE;
下面是華為榮耀9和小米得到的相關的信息:
Serial_Num: 8BN0217919015937
Manufacturer: HUAWEI
Model: STF-AL00
Brand: HONOR
Device: HWSTF
下面是小米6的相關信息
Sim_SN: 89860117801033225378
Manufacturer: Xiaomi
Model: MI 6
Brand: Xiaomi
Device: sagit
5. Installtion ID 安裝ID
這種方式的原理是在程序安裝后第一次運行時生成一個ID,該方式和設備唯一標識不一樣,不同的應用程序會產生不同的ID,同一個程序重新安裝也會不同。所以這不是設備的唯一ID,但是可以保證每個用戶的ID是不同的。可以說是用來標識每一份應用程序的唯一ID(即Installtion ID),可以用來跟蹤應用的安裝數量等。
直接上代碼:
/**
* @author yzzCool
* @desc 安裝ID 的生成和獲取,就是和UUID存取SP是一個道理
* create at 2018/8/28
*/
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();
}
}
上面的代碼也可以用SharedPreferences實現:
SharedPreferences和UUID的配合:
從SharedPreferences里面獲取uuid,如果有直接返回,如果沒有生成
String uuid = UUID.randomUUID().toString();
然后返回uuid和把這次生成的uuid存儲到SharedPreferences里面。
標識符使用實戰
首先,我們先要弄清自己跟蹤設備的具體需求。目前看來,需求無非兩種:
- 1.跟蹤用戶設備使用周期層次上的設備。
意思是將每次用戶的擦除設備、恢復出廠設置動作后將設備視為一臺新的設備。 - 2.跟蹤硬件層次上的設備。
意思是無論設備擦除數據或者恢復出廠設置后都需要將該設備視為同一臺設備。
1.跟蹤用戶使用層次上的設備
理論上,Android_ID 這一個值就已經足夠我們實現這樣的需求,不過正是因為 Android_ID 存在缺陷,所以我們無法直接拿來識別設備。這里我們使用多個值拼湊來規避這些缺點。
與用戶設備使用周期有關的標識符我推薦使用Android_ID和Sim Serial Number。另外可以加上Device_ID,通過 UUID 或者 MD5 等來計算生成設備的標識符。
- UUID 實現:
UUID deviceUuid = new UUID(androidId.hashCode(), ((long)deviceId.hashCode() << 32) | simSerialNum.hashCode());
String deviceId = deviceUuid.toString();
結果類似:00000000-15e4-6485-ca79-2ee02e8e8bdc
- MD5 實現(MD5 算法見下文):
String md5ID = md5(androidId + deviceId + simSerialNum);
結果類似:8d78296180f8c488de11837384350bce
2.跟蹤硬件層次上的設備
跟蹤硬件層次上的設備建議使用硬件的標識符,比如設備ID(DeviceId)、Mac 地址、設備序列號(SN)或者設備的品牌,型號名等,這些值在用戶擦除數據或者恢復出廠設置后也不會改變。同樣的,為了提升穩定性及排除單一標識符所存在的缺陷,我們使用多個標識符拼接,然后通過 UUID 或者 MD5 算法計算得出我們需要的設備標識符。
- UUID 實現:
UUID deviceUuid = new UUID(sn.hashCode(), ((long)deviceId.hashCode() << 32) | macAddress.hashCode());
String deviceId = deviceUuid.toString();
結果類似:00000000-15e4-6485-ca79-2ee02e8e8bdc
- MD5 實現(MD5 算法見下文):
String md5ID = md5(sn+ deviceId + macAddress);
結果類似:8d78296180f8c488de11837384350bce
補充上面所需的md5的方法:
// MD5加密,32位小寫
public static String md5(String str) {
MessageDigest md5 = null;
try {
md5 = MessageDigest.getInstance("MD5");
} catch (Exception e) {
e.printStackTrace();
return "";
}
md5.update(str.getBytes());
byte[] md5Bytes = md5.digest();
StringBuilder hexValue = new StringBuilder();
for (int i = 0; i < md5Bytes.length; i++) {
int val = ((int) md5Bytes[i]) & 0xff;
if (val < 16) {
hexValue.append("0");
}
hexValue.append(Integer.toHexString(val));
}
return hexValue.toString();
}
上面的實戰重要的是思想。
總結
上面的總結,總結的我都快瘋了。這一塊比較瑣碎,沒有一種系統的感覺。東一塊系一塊然后拼接。我的天啊!!!