android獲取裝置唯一標識完美解決方案的思考以及實現方式

weixin_34320159發表於2018-01-21

關於Android裝置唯一識別符號號

前言

由於在開發中需要開發遊客模式,在使用者沒有登入的情況下必須確保裝置的唯一性,於是慣性思維想到的肯定是使用DevicesId 來作為裝置的唯一標識,用以代替使用者登入以後的唯一識別符號。

但是由於國內複雜的rom定製情況,以及使用者許可權禁止的情況。DevicesId 在使用中並不能百分百的貨到到。所以本篇文章就是描述一下,我在開發中如何處理裝置唯一識別符號的。

一、一些常用的獲取裝置唯一識別符號的方法

  • IMEI
  • Mac 地址
  • ANDROID_ID
  • Serial Number, SN(裝置序列號)
  • UniquePsuedoID

1.1 關於IMEI

IMEI 國際移動裝置身份碼 目前GSM/WCDMA/LTE手機終端需要使用IMEI號碼,在單卡工程中一個手機號對應一個IMEI號,雙卡手機則會對應兩個IMEI號,一張是手機卡對應一個。

1.1.1 關於獲取IMEI過程

需要的許可權

<uses-permission android:name="android.permission.READ_PHONE_STATE"/>

獲取IMEI 呼叫的方法

TelephonyManager telephonyManager = (TelephonyManager)context.getSystemService(context.TELEPHONY_SERVICE);  
String imei = telephonyManager.getDeviceId();
1.1.2 使用IMEI 存在的弊端

由以上可以看出使用IMEI來作為Android的裝置唯一識別符號存在一定的弊端, 如果使用者禁用掉相關許可權,那麼對於以上獲取引數的程式碼。則會直接報錯,不會得到我們想要的內容。

1.2Mac地址

Mac 指的就是我們裝置網路卡的唯一設別碼,該碼全球唯一,一般稱為實體地址,硬體地址用來定義裝置的位置

1.2.1 獲取裝置的Mac地址

需要的許可權

<!--訪問WIFI的許可權-->
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/> 

獲取mac地址的方法

獲取mac地址有一點需要注意的就是android 6.0版本後,以下注釋方法不再適用,不管任何手機都會返回"02:00:00:00:00:00"這個預設的mac地址,這是googel官方為了加強許可權管理而禁用了getSYstemService(Context.WIFI_SERVICE)方法來獲得mac地址。

        /*
        獲取mac地址有一點需要注意的就是android 6.0版本後,以下注釋方法不再適用,
 不管任何手機都會返回"02:00:00:00:00:00"這個預設的mac地址,
 這是googel官方為了加強許可權管理而禁用了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地址最重要的一點就是手機必須具有上網功能,
  2. 在Android6.0以後 google 為了執行時許可權對geMacAddress();作出修改通過該方法得到的mac地址永遠是一樣的, 但是可以其他途徑獲取。

1.3關於ANDROID_ID

在裝置首次執行的時候,系統會隨機生成一64位的數字,並把這個數值以16進位制儲存下來,這個16進位制的數字就是ANDROID_ID,但是如果手機恢復出廠設定這個值會發生改變。

1.3.1獲取ANDROID_ID
String ANDROID_ID = Settings.System.getString(getContentResolver(), Settings.System.ANDROID_ID); 
1.3.2使用ANDROID_ID存在的弊端
  1. 手機恢復出廠設定以後該值會發生變化
  2. 在國內Android定製的大環境下,有些裝置是不會返回ANDROID_ID的

1.4 Serial Number, SN(裝置序列號)

1.4.1 獲取序列號
String SerialNumber = android.os.Build.SERIAL;  

獲取序列號不需要許可權,但是有一定的侷限性,在有些手機上會出現垃圾資料,比如紅米手機返回的就是連續的非隨機數

1.5 UniquePsuedoID

具體稱呼不明, 但是從以下的情況看出是一些列硬體資訊拼裝獲取到的內容。

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 關於使用UniquePsuedoID的弊端
  1. 由於是與裝置資訊直接相關,如果是同一批次出廠的的裝置有可能出現生成的內容可能是一樣的。(通過模擬器實驗過,開啟兩個完全一樣的模擬器,生成的內容是完全一下),所以如果單獨使用該方法也是不能用於生成唯一識別符號的。

1.6 以上方法比較

比較以上5種方法。如果只是考慮單獨使用,那麼在不同程度上在使用者使用的情況下都會出現無法生成或者生成無效裝置唯一識別符號的情況。所以我在開發中採用混合使用的方式,同時結合SD卡 以及 sharepreference進行本地持久化處理。

二、我所使用的獲取裝置唯一標識實現方式

2.1 實現簡要描述

在開發中通過結合 device_id 、 MacAddress 以及 隨機生成的 UUID 進行生成裝置唯一識別符號(優先順序依次由高到低)。 然後通過把生成的唯一識別符號寫到SD卡中一個隱藏目錄中(為什麼是寫到影藏檔案中呢,主要是避避免使用者看見然後手動刪除。)。 同時會在相應App sharepreference 進行儲存一份,該內容只要內容只會在App 第一次啟動或者App清除資料以後會進行重寫操作,在其他時候不會進行寫操作,只會進行讀取操作。

2.2 如何生成唯一識別符號

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";
    //儲存的檔案 採用隱藏檔案的形式進行儲存
    private static final String DEVICES_FILE_NAME = ".DEVICES";

    /**
     * 獲取裝置唯一識別符號
     *
     * @param context
     * @return
     */
    public static String getDeviceId(Context context) {
        //讀取儲存的在sd卡中的唯一識別符號
        String deviceId = readDeviceID(context);
        //用於生成最終的唯一識別符號
        StringBuffer s = new StringBuffer();
        //判斷是否已經生成過,
        if (deviceId != null && !"".equals(deviceId)) {
            return deviceId;
        }
        try {
            //獲取IMES(也就是常說的DeviceId)
            deviceId = getIMIEStatus(context);
            s.append(deviceId);
        } catch (Exception e) {
            e.printStackTrace();
        }

        try {
            //獲取裝置的MACAddress地址 去掉中間相隔的冒號
            deviceId = getLocalMac(context).replace(":", "");
            s.append(deviceId);
        } catch (Exception e) {
            e.printStackTrace();
        }
//        }

        //如果以上搜沒有獲取相應的則自己生成相應的UUID作為相應裝置唯一識別符號
        if (s == null || s.length() <= 0) {
            UUID uuid = UUID.randomUUID();
            deviceId = uuid.toString().replace("-", "");
            s.append(deviceId);
        }
        //為了統一格式對裝置的唯一標識進行md5加密 最終生成32位字串
        String md5 = getMD5(s.toString(), false);
        if (s.length() > 0) {
            //持久化操作, 進行儲存到SD卡中
            saveDeviceID(md5, context);
        }
        return md5;
    }


    /**
     * 讀取固定的檔案中的內容,這裡就是讀取sd卡中儲存的裝置唯一識別符號
     *
     * @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;
        }
    }

    /**
     * 獲取裝置的DeviceId(IMES) 這裡需要相應的許可權<br/>
     * 需要 READ_PHONE_STATE 許可權
     *
     * @param context
     * @return
     */
    private static String getIMIEStatus(Context context) {
        TelephonyManager tm = (TelephonyManager) context
                .getSystemService(Context.TELEPHONY_SERVICE);
        String deviceId = tm.getDeviceId();
        return deviceId;
    }


    /**
     * 獲取裝置MAC 地址 由於 6.0 以後 WifiManager 得到的 MacAddress得到都是 相同的沒有意義的內容
     * 所以採用以下方法獲取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;


    }

    /**
     * 儲存 內容到 SD卡中,  這裡儲存的就是 裝置唯一識別符號
     * @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();
        }
    }

    /**
     * 對挺特定的 內容進行 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();
    }

    /**
     * 統一處理裝置唯一標識 儲存的檔案的地址
     * @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); // 用當前時間給取得的圖片命名
        } else {
            File cropdir = new File(context.getFilesDir(), CACHE_IMAGE_DIR);
            if (!cropdir.exists()) {
                cropdir.mkdirs();
            }
            mCropFile = new File(cropdir, DEVICES_FILE_NAME);
        }
        return mCropFile;
    }
}

以上程式碼就是生成 裝置唯一標識的具體實現方式。同時包括讀取裝置唯一標識的方法 。 關鍵點的說明已經在註釋中進行描述。這裡不再重複講述。

2.2.2具體的使用

在app 的啟動頁中增加以下程式碼

new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    //獲取儲存在sd中的 裝置唯一識別符號
                    String readDeviceID = Utils.readDeviceID(WelcomeActivity.this);
                    //獲取快取在  sharepreference 裡面的 裝置唯一標識
                    String string = SimplePreference.getPreference(WelcomeActivity.this).getString(SpConstant.SP_DEVICES_ID, readDeviceID);
                    //判斷 app 內部是否已經快取,  若已經快取則使用app 快取的 裝置id
                    if (string != null) {
                        //app 快取的和SD卡中儲存的不相同 以app 儲存的為準, 同時更新SD卡中儲存的 唯一識別符號
                        if (StringUtil.isBlank(readDeviceID) && !string.equals(readDeviceID)) {
                            // 取有效地 app快取 進行更新操作
                            if (StringUtil.isBlank(readDeviceID) && !StringUtil.isBlank(string)) {
                                readDeviceID = string;
                                Utils.saveDeviceID(readDeviceID, WelcomeActivity.this);
                            }
                        }
                    }
                    // app 沒有快取 (這種情況只會發生在第一次啟動的時候)
                    if (StringUtil.isBlank(readDeviceID)) {
                        //儲存裝置id
                        readDeviceID = Utils.getDeviceId(WelcomeActivity.this);
                    }
                    //左後再次更新app 的快取 
                    SimplePreference.getPreference(WelcomeActivity.this).saveString(SpConstant.SP_DEVICES_ID, readDeviceID);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();

以上程式碼只是生成裝置唯一識別符號,同時保證app 在一次啟動以後能夠保持使用是統一個裝置唯一識別符號。

2.2.3 注意

在 app 使用的時候只取 sharepreference 儲存的內容,不要取sd 卡中儲存的內容

2.3 使用總結

以上方法只是能夠最大限度的保持app 能夠使用同一個裝置唯一識別符號。 在特殊情況下裝置唯一識別符號還是會發生變化的。 例如 使用者沒有給SD卡的讀取許可權, 那麼app 在清楚資料以後再次生成的裝置唯一識別符號是有課能會發生變化的。

三、寫在最後

以上是如何最大限度生成一個唯一識別符號的思考,如有不正確之處請忽略,

相關文章