android獲取設(shè)備唯一標(biāo)識完美解決方案的思考以及實現(xiàn)方式

關(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地址存在的弊端
  1. 如果使用Mac地址最重要的一點就是手機(jī)必須具有上網(wǎng)功能,
  2. 在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存在的弊端
  1. 手機(jī)恢復(fù)出廠設(shè)置以后該值會發(fā)生變化
  2. 在國內(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的弊端
  1. 由于是與設(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)識符的思考,如有不正確之處請忽略,

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容