關(guān)于Android設(shè)備唯一標(biāo)識符號
前言
由于在開發(fā)中需要開發(fā)游客模式,在用戶沒有登錄的情況下必須確保設(shè)備的唯一性,于是慣性思維想到的肯定是使用DevicesId 來作為設(shè)備的唯一標(biāo)識,用以代替用戶登錄以后的唯一標(biāo)識符。
但是由于國內(nèi)復(fù)雜的rom定制情況,以及用戶權(quán)限禁止的情況。DevicesId 在使用中并不能百分百的貨到到。所以本篇文章就是描述一下,我在開發(fā)中如何處理設(shè)備唯一標(biāo)識符的。
一、一些常用的獲取設(shè)備唯一標(biāo)識符的方法
- IMEI
- Mac 地址
- ANDROID_ID
- Serial Number, SN(設(shè)備序列號)
- UniquePsuedoID
1.1 關(guān)于IMEI
IMEI 國際移動設(shè)備身份碼 目前GSM/WCDMA/LTE手機(jī)終端需要使用IMEI號碼,在單卡工程中一個手機(jī)號對應(yīng)一個IMEI號,雙卡手機(jī)則會對應(yīng)兩個IMEI號,一張是手機(jī)卡對應(yīng)一個。
1.1.1 關(guān)于獲取IMEI過程
需要的權(quán)限
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
獲取IMEI 調(diào)用的方法
TelephonyManager telephonyManager = (TelephonyManager)context.getSystemService(context.TELEPHONY_SERVICE);
String imei = telephonyManager.getDeviceId();
1.1.2 使用IMEI 存在的弊端
由以上可以看出使用IMEI來作為Android的設(shè)備唯一標(biāo)識符存在一定的弊端, 如果用戶禁用掉相關(guān)權(quán)限,那么對于以上獲取參數(shù)的代碼。則會直接報錯,不會得到我們想要的內(nèi)容。
1.2Mac地址
Mac 指的就是我們設(shè)備網(wǎng)卡的唯一設(shè)別碼,該碼全球唯一,一般稱為物理地址,硬件地址用來定義設(shè)備的位置
1.2.1 獲取設(shè)備的Mac地址
需要的權(quán)限
<!--訪問WIFI的權(quán)限-->
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
獲取mac地址的方法
獲取mac地址有一點需要注意的就是android 6.0版本后,以下注釋方法不再適用,不管任何手機(jī)都會返回"02:00:00:00:00:00"這個默認(rèn)的mac地址,這是googel官方為了加強(qiáng)權(quán)限管理而禁用了getSYstemService(Context.WIFI_SERVICE)方法來獲得mac地址。
/*
獲取mac地址有一點需要注意的就是android 6.0版本后,以下注釋方法不再適用,
不管任何手機(jī)都會返回"02:00:00:00:00:00"這個默認(rèn)的mac地址,
這是googel官方為了加強(qiáng)權(quán)限管理而禁用了getSYstemService(Context.WIFI_SERVICE)方法來獲得mac地址。
*/
// String macAddress= "";
// WifiManager wifiManager = (WifiManager) MyApp.getContext().getSystemService(Context.WIFI_SERVICE);
// WifiInfo wifiInfo = wifiManager.getConnectionInfo();
// macAddress = wifiInfo.getMacAddress();
// return macAddress;
String macAddress = null;
StringBuffer buf = new StringBuffer();
NetworkInterface networkInterface = null;
try {
networkInterface = NetworkInterface.getByName("eth1");
if (networkInterface == null) {
networkInterface = NetworkInterface.getByName("wlan0");
}
if (networkInterface == null) {
return "02:00:00:00:00:02";
}
byte[] addr = networkInterface.getHardwareAddress();
for (byte b : addr) {
buf.append(String.format("%02X:", b));
}
if (buf.length() > 0) {
buf.deleteCharAt(buf.length() - 1);
}
macAddress = buf.toString();
} catch (SocketException e) {
e.printStackTrace();
return "02:00:00:00:00:02";
}
return macAddress;
}
1.2.2 使用Mac地址存在的弊端
- 如果使用Mac地址最重要的一點就是手機(jī)必須具有上網(wǎng)功能,
- 在Android6.0以后 google 為了運(yùn)行時權(quán)限對geMacAddress();作出修改通過該方法得到的mac地址永遠(yuǎn)是一樣的, 但是可以其他途徑獲取。
1.3關(guān)于ANDROID_ID
在設(shè)備首次運(yùn)行的時候,系統(tǒng)會隨機(jī)生成一64位的數(shù)字,并把這個數(shù)值以16進(jìn)制保存下來,這個16進(jìn)制的數(shù)字就是ANDROID_ID,但是如果手機(jī)恢復(fù)出廠設(shè)置這個值會發(fā)生改變。
1.3.1獲取ANDROID_ID
String ANDROID_ID = Settings.System.getString(getContentResolver(), Settings.System.ANDROID_ID);
1.3.2使用ANDROID_ID存在的弊端
- 手機(jī)恢復(fù)出廠設(shè)置以后該值會發(fā)生變化
- 在國內(nèi)Android定制的大環(huán)境下,有些設(shè)備是不會返回ANDROID_ID的
1.4 Serial Number, SN(設(shè)備序列號)
1.4.1 獲取序列號
String SerialNumber = android.os.Build.SERIAL;
獲取序列號不需要權(quán)限,但是有一定的局限性,在有些手機(jī)上會出現(xiàn)垃圾數(shù)據(jù),比如紅米手機(jī)返回的就是連續(xù)的非隨機(jī)數(shù)
1.5 UniquePsuedoID
具體稱呼不明, 但是從以下的情況看出是一些列硬件信息拼裝獲取到的內(nèi)容。
1.5.1 具體的獲取方式
public static String getUniquePsuedoID()
{
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!
// http://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 phone, 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 e)
{
// String needs to be initialized
serial = "serial"; // some value
}
// Thanks @Joe!
// http://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();
}
1.5.2 關(guān)于使用UniquePsuedoID的弊端
- 由于是與設(shè)備信息直接相關(guān),如果是同一批次出廠的的設(shè)備有可能出現(xiàn)生成的內(nèi)容可能是一樣的。(通過模擬器實驗過,打開兩個完全一樣的模擬器,生成的內(nèi)容是完全一下),所以如果單獨(dú)使用該方法也是不能用于生成唯一標(biāo)識符的。
1.6 以上方法比較
比較以上5種方法。如果只是考慮單獨(dú)使用,那么在不同程度上在用戶使用的情況下都會出現(xiàn)無法生成或者生成無效設(shè)備唯一標(biāo)識符的情況。所以我在開發(fā)中采用混合使用的方式,同時結(jié)合SD卡 以及 sharepreference進(jìn)行本地持久化處理。
二、我所使用的獲取設(shè)備唯一標(biāo)識實現(xiàn)方式
2.1 實現(xiàn)簡要描述
在開發(fā)中通過結(jié)合 device_id 、 MacAddress 以及 隨機(jī)生成的 UUID 進(jìn)行生成設(shè)備唯一標(biāo)識符(優(yōu)先級依次由高到低)。 然后通過把生成的唯一標(biāo)識符寫到SD卡中一個隱藏目錄中(為什么是寫到影藏文件中呢,主要是避避免用戶看見然后手動刪除。)。 同時會在相應(yīng)App sharepreference 進(jìn)行保存一份,該內(nèi)容只要內(nèi)容只會在App 第一次啟動或者App清除數(shù)據(jù)以后會進(jìn)行重寫操作,在其他時候不會進(jìn)行寫操作,只會進(jìn)行讀取操作。
2.2 如何生成唯一標(biāo)識符
2.2.1 先上代碼
package com.wdsunday.utils;
import android.content.Context;
import android.os.Environment;
import android.telephony.TelephonyManager;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.security.MessageDigest;
import java.util.UUID;
/**
* @author liangjun on 2018/1/21.
*/
public class GetDeviceId {
//保存文件的路徑
private static final String CACHE_IMAGE_DIR = "aray/cache/devices";
//保存的文件 采用隱藏文件的形式進(jìn)行保存
private static final String DEVICES_FILE_NAME = ".DEVICES";
/**
* 獲取設(shè)備唯一標(biāo)識符
*
* @param context
* @return
*/
public static String getDeviceId(Context context) {
//讀取保存的在sd卡中的唯一標(biāo)識符
String deviceId = readDeviceID(context);
//用于生成最終的唯一標(biāo)識符
StringBuffer s = new StringBuffer();
//判斷是否已經(jīng)生成過,
if (deviceId != null && !"".equals(deviceId)) {
return deviceId;
}
try {
//獲取IMES(也就是常說的DeviceId)
deviceId = getIMIEStatus(context);
s.append(deviceId);
} catch (Exception e) {
e.printStackTrace();
}
try {
//獲取設(shè)備的MACAddress地址 去掉中間相隔的冒號
deviceId = getLocalMac(context).replace(":", "");
s.append(deviceId);
} catch (Exception e) {
e.printStackTrace();
}
// }
//如果以上搜沒有獲取相應(yīng)的則自己生成相應(yīng)的UUID作為相應(yīng)設(shè)備唯一標(biāo)識符
if (s == null || s.length() <= 0) {
UUID uuid = UUID.randomUUID();
deviceId = uuid.toString().replace("-", "");
s.append(deviceId);
}
//為了統(tǒng)一格式對設(shè)備的唯一標(biāo)識進(jìn)行md5加密 最終生成32位字符串
String md5 = getMD5(s.toString(), false);
if (s.length() > 0) {
//持久化操作, 進(jìn)行保存到SD卡中
saveDeviceID(md5, context);
}
return md5;
}
/**
* 讀取固定的文件中的內(nèi)容,這里就是讀取sd卡中保存的設(shè)備唯一標(biāo)識符
*
* @param context
* @return
*/
public static String readDeviceID(Context context) {
File file = getDevicesDir(context);
StringBuffer buffer = new StringBuffer();
try {
FileInputStream fis = new FileInputStream(file);
InputStreamReader isr = new InputStreamReader(fis, "UTF-8");
Reader in = new BufferedReader(isr);
int i;
while ((i = in.read()) > -1) {
buffer.append((char) i);
}
in.close();
return buffer.toString();
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
/**
* 獲取設(shè)備的DeviceId(IMES) 這里需要相應(yīng)的權(quán)限<br/>
* 需要 READ_PHONE_STATE 權(quán)限
*
* @param context
* @return
*/
private static String getIMIEStatus(Context context) {
TelephonyManager tm = (TelephonyManager) context
.getSystemService(Context.TELEPHONY_SERVICE);
String deviceId = tm.getDeviceId();
return deviceId;
}
/**
* 獲取設(shè)備MAC 地址 由于 6.0 以后 WifiManager 得到的 MacAddress得到都是 相同的沒有意義的內(nèi)容
* 所以采用以下方法獲取Mac地址
* @param context
* @return
*/
private static String getLocalMac(Context context) {
// WifiManager wifi = (WifiManager) context
// .getSystemService(Context.WIFI_SERVICE);
// WifiInfo info = wifi.getConnectionInfo();
// return info.getMacAddress();
String macAddress = null;
StringBuffer buf = new StringBuffer();
NetworkInterface networkInterface = null;
try {
networkInterface = NetworkInterface.getByName("eth1");
if (networkInterface == null) {
networkInterface = NetworkInterface.getByName("wlan0");
}
if (networkInterface == null) {
return "";
}
byte[] addr = networkInterface.getHardwareAddress();
for (byte b : addr) {
buf.append(String.format("%02X:", b));
}
if (buf.length() > 0) {
buf.deleteCharAt(buf.length() - 1);
}
macAddress = buf.toString();
} catch (SocketException e) {
e.printStackTrace();
return "";
}
return macAddress;
}
/**
* 保存 內(nèi)容到 SD卡中, 這里保存的就是 設(shè)備唯一標(biāo)識符
* @param str
* @param context
*/
public static void saveDeviceID(String str, Context context) {
File file = getDevicesDir(context);
try {
FileOutputStream fos = new FileOutputStream(file);
Writer out = new OutputStreamWriter(fos, "UTF-8");
out.write(str);
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 對挺特定的 內(nèi)容進(jìn)行 md5 加密
* @param message 加密明文
* @param upperCase 加密以后的字符串是是大寫還是小寫 true 大寫 false 小寫
* @return
*/
public static String getMD5(String message, boolean upperCase) {
String md5str = "";
try {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] input = message.getBytes();
byte[] buff = md.digest(input);
md5str = bytesToHex(buff, upperCase);
} catch (Exception e) {
e.printStackTrace();
}
return md5str;
}
public static String bytesToHex(byte[] bytes, boolean upperCase) {
StringBuffer md5str = new StringBuffer();
int digital;
for (int i = 0; i < bytes.length; i++) {
digital = bytes[i];
if (digital < 0) {
digital += 256;
}
if (digital < 16) {
md5str.append("0");
}
md5str.append(Integer.toHexString(digital));
}
if (upperCase) {
return md5str.toString().toUpperCase();
}
return md5str.toString().toLowerCase();
}
/**
* 統(tǒng)一處理設(shè)備唯一標(biāo)識 保存的文件的地址
* @param context
* @return
*/
private static File getDevicesDir(Context context) {
File mCropFile = null;
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
File cropdir = new File(Environment.getExternalStorageDirectory(), CACHE_IMAGE_DIR);
if (!cropdir.exists()) {
cropdir.mkdirs();
}
mCropFile = new File(cropdir, DEVICES_FILE_NAME); // 用當(dāng)前時間給取得的圖片命名
} else {
File cropdir = new File(context.getFilesDir(), CACHE_IMAGE_DIR);
if (!cropdir.exists()) {
cropdir.mkdirs();
}
mCropFile = new File(cropdir, DEVICES_FILE_NAME);
}
return mCropFile;
}
}
以上代碼就是生成 設(shè)備唯一標(biāo)識的具體實現(xiàn)方式。同時包括讀取設(shè)備唯一標(biāo)識的方法 。 關(guān)鍵點的說明已經(jīng)在注釋中進(jìn)行描述。這里不再重復(fù)講述。
2.2.2具體的使用
在app 的啟動頁中增加以下代碼
new Thread(new Runnable() {
@Override
public void run() {
try {
//獲取保存在sd中的 設(shè)備唯一標(biāo)識符
String readDeviceID = Utils.readDeviceID(WelcomeActivity.this);
//獲取緩存在 sharepreference 里面的 設(shè)備唯一標(biāo)識
String string = SimplePreference.getPreference(WelcomeActivity.this).getString(SpConstant.SP_DEVICES_ID, readDeviceID);
//判斷 app 內(nèi)部是否已經(jīng)緩存, 若已經(jīng)緩存則使用app 緩存的 設(shè)備id
if (string != null) {
//app 緩存的和SD卡中保存的不相同 以app 保存的為準(zhǔn), 同時更新SD卡中保存的 唯一標(biāo)識符
if (StringUtil.isBlank(readDeviceID) && !string.equals(readDeviceID)) {
// 取有效地 app緩存 進(jìn)行更新操作
if (StringUtil.isBlank(readDeviceID) && !StringUtil.isBlank(string)) {
readDeviceID = string;
Utils.saveDeviceID(readDeviceID, WelcomeActivity.this);
}
}
}
// app 沒有緩存 (這種情況只會發(fā)生在第一次啟動的時候)
if (StringUtil.isBlank(readDeviceID)) {
//保存設(shè)備id
readDeviceID = Utils.getDeviceId(WelcomeActivity.this);
}
//左后再次更新app 的緩存
SimplePreference.getPreference(WelcomeActivity.this).saveString(SpConstant.SP_DEVICES_ID, readDeviceID);
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
以上代碼只是生成設(shè)備唯一標(biāo)識符,同時保證app 在一次啟動以后能夠保持使用是統(tǒng)一個設(shè)備唯一標(biāo)識符。
2.2.3 注意
在 app 使用的時候只取 sharepreference 保存的內(nèi)容,不要取sd 卡中保存的內(nèi)容
2.3 使用總結(jié)
以上方法只是能夠最大限度的保持app 能夠使用同一個設(shè)備唯一標(biāo)識符。 在特殊情況下設(shè)備唯一標(biāo)識符還是會發(fā)生變化的。 例如 用戶沒有給SD卡的讀取權(quán)限, 那么app 在清楚數(shù)據(jù)以后再次生成的設(shè)備唯一標(biāo)識符是有課能會發(fā)生變化的。
三、寫在最后
以上是如何最大限度生成一個唯一標(biāo)識符的思考,如有不正確之處請忽略,