设备ID,简单来说就是一串符号(或者数字),映射现实中硬件设备。如果这些符号和设备是一一对应的,可称之为“唯一设备ID(Unique Device Identifier)”。不幸的是,对于Android平台而言,没有稳定的API可以让开发者获取到这样的设备ID。
开发者通常会遇到这样的困境:随着项目的演进, 越来越多的地方需要用到设备ID;然而随着Android版本的升级,获取设备ID却越来越难了。加上Android平台碎片化的问题,获取设备ID之路,可以说是步履维艰。
Android中的唯一识别码
IMEI
国际移动设备识别码(International Mobile Equipment Identity,IMEI),即通常所说的手机序列号、手机“串号”,用于在移动电话网络中识别每一部独立的手机等移动通信设备,相当于移动电话的身份证。
手机IMEI码由15-17位数字组成。
- 第一部分 TAC,Type Allocation Code,类型分配码,由8位数字组成(早期是6位),是区分手机品牌和型号的编码,该代码由GSMA及其授权机构分配。其中TAC码前两位又是分配机构标识(Reporting Body Identifier),是授权IMEI码分配机构的代码,如01为美国CTIA,35为英国BABT,86为中国TAF。
- 第二部分 FAC,Final Assembly Code,最终装配地代码,由2位数字构成,仅在早期TAC码为6位的手机中存在,所以TAC和FAC码合计一共8位数字。FAC码用于生产商内部区分生产地代码。
- 第三部分 SNR,Serial Number,序列号,由第9位开始的6位数字组成,区分每部手机的生产序列号。
- 第四部分 CD,Check Digit,验证码,由前14位数字通过Luhn算法计算得出。
- 第五部分 SVN,Software Version Number,软件版本号,区分同型号手机出厂时使用的不同软件版本,仅在部分品牌的部分机型中存在。
在大部分终端设备中都可以通过拨号输入*#06#来查询。
在Android 8.0(API Level 26)以下,可以通过TelephonyManager的getDeviceId()方法获取到设备的IMEI码(其实这里的说法不准确,该方法是会根据手机设备的制式(GSM或CDMA)返回相应的设备码(IMEI、MEID和ESN)),该方法在Android 8.0及之后的版本已经被废弃了,取而代之的是getImei()方法。获取设备IMEI码的示例代码如下:
private String getIMEI(Context context) { TelephonyManager tm = (TelephonyManager) context.getSystemService(Service.TELEPHONY_SERVICE); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { return tm.getImei(); } else { return tm.getDeviceId(); } }
IMEI码的获取方式很简单,也能保证唯一性和不变性,目前很多应用都使用IMEI码作为设备的唯一标识,但众所周知,在Android 6.0以上获取IMEI码是需要动态申请READ_PHONE_STATE权限的,一旦用户拒绝了该权限就获取不到了。这还不是最要命的,在Android 10中官方已经明确说明第三方应用无法获取到IMEI码,详细内容可以查看Android 10 中的隐私权变更,这里附上一张图。
下面我们分几种情况来验证一下IMEI码的获取情况:
- Android 6.0以下:无需申请权限,可以通过getDeviceId()方法获取到IMEI码
- Android 6.0-Android 8.0:需要申请READ_PHONE_STATE权限,可以通过getDeviceId()方法获取到IMEI码,如果用户拒绝了权限,会抛出lang.SecurityException异常
- Android 8.0-Android 10:需要申请READ_PHONE_STATE权限,可以通过getImei()方法获取到IMEI码,如果用户拒绝了权限,会抛出lang.SecurityException异常
- Android 10及以上:分为以下两种情况:
- targetSdkVersion<29:没有申请权限的情况,通过getImei()方法获取IMEI码时抛出lang.SecurityException异常;申请了权限,通过getImei()方法获取到IMEI码为null
- targetSdkVersion=29:无论是否申请了权限,通过getImei()方法获取IMEI码时都会直接抛出lang.SecurityException异常
不难看出,IMEI码在Android 10之后已经无法获取到了,而且甚至会直接抛出异常导致程序崩溃,在Android 10以下版本虽然可以获取到IMEI码,但是需要在应用获取到了READ_PHONE_STATE权限的前提下,我们依然无法保证这一点。
IEMI Luhn校验算法:
import random def luhn_residue(digits): return sum(sum(divmod(int(d)*(1 + i%2), 10)) for i, d in enumerate(digits[::-1])) % 10 def get_IMEI(N): part = ''.join(str(random.randrange(0,9)) for _ in range(N-1)) res = luhn_residue('{}{}'.format(part, 0)) return '{}{}'.format(part, -res%10) print(getImei(15))
IMSI
国际移动用户识别码(英语:IMSI,International Mobile Subscriber Identity),是用于区分蜂窝网络中不同用户的、在所有蜂窝网络中不重复的识别码。手机将IMSI存储于一个64比特的字段发送给网络。IMSI可以用来在归属位置寄存器(HLR,Home Location Register)或拜访位置寄存器(VLR,Visitor Location Register)中查询用户的信息。为了避免被监听者识别并追踪特定的用户,大部分情形下手机和网络之间的通信会使用随机产生的临时移动用户识别码(TMSI,Temporary Mobile Subscriber Identity)代替IMSI。
只要一个移动网络的用户需要与其他移动网络互通,就必须使用IMSI。在GSM、UMTS和LTE网络中,IMSI来自SIM卡,在CDMA2000网络中则是直接来自手机,或者RUIM。简单地理解就是,IMSI是SIM卡的id号码,可以区分每一张SIM卡。
IMSI由一串十进制数字组成,最大长度为15位。实际使用的IMSI的长度绝大部分都是15位,短于15位的例子少见,例如,南非MTN集团有一些仍在网络中使用的较旧的IMSI为14位数字。
MSI由以下三部分构成:
- MCC(Mobile Country Code,移动国家代码),长度为3位,唯一地识别移动客户所属的国家。我国为460。。
- MNC(Mobile Nation Code,移动网络代码),MNC长度由MCC的值决定,可以是2位(欧洲标准)或3位数字(北美标准),中国移动公司GSM PLMN网为00,中国联通公司GSM PLMN网为0l。
- MSIN(Mobile subscription identification number,移动订户识别代码),MSIN的值由运营商分配,采用等长11位数字构成。唯一地识别国内GSM移动通信网中移动客户。
在同一个国家内,如果有多个PLMN(Public Land Mobile Network,公共陆地移动网,一般某个国家的一个运营商对应一个PLMN),可以通过MNC来进行区别,即每一个PLMN都要分配唯一的MNC。中国移动系统使用00、02、04、07,中国联通GSM系统使用01、06、09,中国电信CDMA系统使用03、05、电信4G使用11,中国铁通系统使用20。
Android代码获取手机的IMSI码等相关信息:
# 方式1 TelephonyManager telManager = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE); /** 获取SIM卡的IMSI码 * SIM卡唯一标识:IMSI 国际移动用户识别码(IMSI:International Mobile Subscriber Identification Number)是区别移动用户的标志, * 储存在SIM卡中,可用于区别移动用户的有效信息。IMSI由MCC、MNC、MSIN组成,其中MCC为移动国家号码,由3位数字组成, * 唯一地识别移动客户所属的国家,我国为460;MNC为网络id,由2位数字组成, * 用于识别移动客户所归属的移动网络,中国移动为00,中国联通为01,中国电信为03;MSIN为移动客户识别码,采用等长11位数字构成。 * 唯一地识别国内GSM移动通信网中移动客户。所以要区分是移动还是联通,只需取得SIM卡中的MNC字段即可 */ String imsi = telManager.getSubscriberId(); if(imsi!=null){ if(imsi.startsWith("46000") || imsi.startsWith("46002")){//因为移动网络编号46000下的IMSI已经用完,所以虚拟了一个46002编号,134/159号段使用了此编号 //中国移动 }else if(imsi.startsWith("46001")){ //中国联通 }else if(imsi.startsWith("46003")){ //中国电信 } } # 方式2 TelephonyManager tm = (TelephonyManager) this.getSystemService(Context.TELEPHONY_SERVICE);// String str = ""; str += "DeviceId(IMEI) = " + tm.getDeviceId() + "\n"; str += "DeviceSoftwareVersion = " + tm.getDeviceSoftwareVersion() + "\n"; str += "Line1Number = " + tm.getLine1Number() + "\n"; str += "NetworkCountryIso = " + tm.getNetworkCountryIso() + "\n"; str += "NetworkOperator = " + tm.getNetworkOperator() + "\n"; str += "NetworkOperatorName = " + tm.getNetworkOperatorName() + "\n"; str += "NetworkType = " + tm.getNetworkType() + "\n"; str += "PhoneType = " + tm.getPhoneType() + "\n"; str += "SimCountryIso = " + tm.getSimCountryIso() + "\n"; str += "SimOperator = " + tm.getSimOperator() + "\n"; str += "SimOperatorName = " + tm.getSimOperatorName() + "\n"; str += "SimSerialNumber = " + tm.getSimSerialNumber() + "\n"; str += "SimState = " + tm.getSimState() + "\n"; str += "SubscriberId(IMSI) = " + tm.getSubscriberId() + "\n"; str += "VoiceMailNumber = " + tm.getVoiceMailNumber() + "\n"; mTv.setText(str);*/
MAC地址
MAC地址(Media Access Control Address),直译为媒体存取控制位址,也称为局域网地址、以太网地址或物理地址,由48位二进制数组成。与我们熟悉的IP地址不同,mac地址只由设备的网卡决定,每个网卡都会有一个唯一的mac地址,只要不更换设备的网卡,mac地址就不会变,因此mac地址符合我们对于设备标识的要求。
在Android 6.0以下版本可以通过下面的代码获取到设备的mac地址:
private String getMacAddress(Context context) { WifiManager wm = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); return wm.getConnectionInfo().getMacAddress(); }
通过该方法获取mac地址需要声明ACCESS_WIFI_STATE权限,并且设备需要开启wifi。但是从Android 6.0开始,使用该方法获取到的mac地址都为02:00:00:00:00:00。替代方案是通过读取系统文件/sys/class/net/wlan0/address来获取mac地址,示例代码如下:
private String getMacAddress() { return new BufferedReader(new FileReader(new File("/sys/class/net/wlan0/address"))).readLine(); }
不幸的是,该方法在Android 7.0开始也行不通了,执行上面的代码会抛出java.io.FileNotFoundException: /sys/class/net/wlan0/address (Permission denied)异常,也就是说我们没有权限读取该文件。但好在目前还是有获取mac地址的方法的,即通过扫描所有的网络接口,示例代码如下:
private String getMacAddress() { try { List<NetworkInterface> all = Collections.list(NetworkInterface.getNetworkInterfaces()); for (NetworkInterface nif : all) { if (!nif.getName().equalsIgnoreCase("wlan0")) { continue; } byte[] macBytes = nif.getHardwareAddress(); if (macBytes == null) { return ""; } StringBuilder res1 = new StringBuilder(); for (byte b : macBytes) { res1.append(String.format("%02X:", b)); } if (res1.length() > 0) { res1.deleteCharAt(res1.length() - 1); } return res1.toString(); } } catch (Exception e) { e.printStackTrace(); } return null; }
目前在Android 10的真机和模拟器上测试了该方法,都能获取到mac地址,甚至都不需要联网,而且每次获取到mac地址都是一样的。可以看出,mac地址的获取相对来说是最麻烦的一个,但好在目前还是能获取到的,因此我们可以考虑使用mac地址来作为设备标识。
mac地址随机化这个特性并不是所有Android 10的手机都支持的,目前大部分手机还不支持这个特性,因此获取到的mac地址就是固定的。如何判断手机是否支持mac地址随机化?可以打开手机的开发者选项,如果有看到“连接时随机选择MAC网址”这个选项,就说明手机是支持这个特性的,当开启了这个选项后,每次切换wifi网络获取到的mac地址就是随机的了。
随着各大厂商手机的更新换代,当市面上大部分手机都支持了这一特性后,这种方案就不太可行了。
ANDROID_ID
ANDROID_ID是设备的系统首次启动时随机生成的一串字符,由16个16进制数(64位)组成,基本上还是可以保证唯一性的,获取ANDROID_ID的示例代码如下:
String androidId = Settings.System.getString(getContentResolver(), Settings.Secure.ANDROID_ID);
相比于上面几种设备相关标识,ANDROID_ID的获取门槛是最低的,不需要任何权限,但哪里有十全十美的事,ANDROID_ID也存在一些缺点,就是无法保证稳定性,root、刷机或恢复出厂设置都会导致设备的ANDROID_ID发生改变。
此外,我看到部分文章中有提到某些厂商定制系统的Bug会导致不同的设备可能会产生相同的ANDROID_ID(9774d56d682e549c),而且某些设备获取到的ANDROID_ID为null。总体来说,相比于其他几种设备标识或多或少都有被官方“照顾”过,ANDROID_ID还是比较稳定的,如果应用对于设备标识的要求不是特别高的话还是一个值得考虑的方案。
Android 开发者文档和谷歌开发者中文博客对 Android 8.0 后的隐私性和 SSAID 变化做出了说明:
从图中不难看出,在 Android 8.0 以后,签名不同的 App 所获取的 Android ID(SSAID)是不一样的,但同一个开发者可以根据自己的数字签名,将所开发的不同 App 进行关联。
设备序列号
设备序列号是手机生产厂商提供的,如果拼接上厂商名称(Build.MANUFACTURER)基本上可以保证唯一性。在Android 8.0以下版本,可以通过android.os.Build.SERIAL获取到设备序列号,同样的,这种方式在Android 8.0及以上版本被废弃了,通过Build.SERIAL在Android 8.0及以上设备获取到设备的序列号始终为“unknown”,取而代之的是使用android.os.Build.getSerial()方法。获取设备序列号的示例代码如下:
private String getSerial() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { return Build.getSerial(); } else { return Build.SERIAL; } }
和getImei()方法的弊端相同,Build.getSerial()方法在Android 6.0及以上版本是需要动态申请READ_PHONE_STATE权限的,并且该方法在Android 10上同样无法获取到设备序列号。
我们同样来看一下几种情况下获取设备序列号的情况:
- Android 8.0以下:无需申请权限,可以通过SERIAL获取到设备序列号
- Android 8.0-Android 10:需要申请READ_PHONE_STATE权限,可以通过getSerial()获取到设备序列号,如果用户拒绝了权限,会抛出java.lang.SecurityException异常
- Android 10及以上:分为以下两种情况:
- targetSdkVersion<29:没有申请权限的情况,调用getSerial()方法时抛出java.lang.SecurityException异常;申请了权限,通过Build.getSerial()方法获取到的设备序列号为“unknown”
- targetSdkVersion=29:无论是否申请了权限,调用getSerial()方法时都会直接抛出java.lang.SecurityException异常
可以看出,和IMEI码一样,官方同样限制了设备序列号的获取。此外,由于序列号是手机生产厂商提供的,无法保证各个厂商的规范性,甚至有些厂商的手机获取不到设备序列号。
OAID
OAID全称是Open Anonymous Device Identifier,中文名是匿名设备标识符。OAID是一种非永久性设备标识符,最长64位,在系统首次启动的时候生成。
因传统的移动终端设备标识如国际移动设备识别码(IMEI)等已被部分国家认定为用户隐私的一部分,并存在被篡改和冒用的风险,所以在Android 10及后续版本中非厂商系统应用将无法获取IMEI、MAC等设备信息。无法获取IMEI会在用户行为统计过程中对设备识别产生一定影响。尤其是CPI广告(CPI广告是按照实际的安装数量结算,需要唯一标识来确保没有重复计算),所以移动安全联盟搞了这个OAID,其本质是一个设备唯一标识。
为什么要用OAID
为了加强对终端用户的隐私保护,Android Q(Android 10)操作系统禁止了非系统级应用对于设备识别码(IMEI、Device ID)的访问与获取,同时还默认配置WiFi Mac地址随机化,将导致开发者不能继续使用Device ID和WiFi Mac 地址作为设备唯一的标识符,强依赖于上述信息的数据业务,如广告追踪、归因、用户画像、数据统计等也将因此受到影响。简单说就是Android 10获取不到IMEI(International Mobile Equipment Identity,国际移动设备识别码)、MAC等设备信息,但是很多的业务情况是需要一个用户唯一标识的,所以我们自己就弄一个呗。
为了应对这个变化,MSA发布《移动智能终端补充设备标识体系规范》,为保护用户的隐私和标识设备的唯一性,推出了OAID用于逐步取代移动设备原有IMEI码。OAID是由中国信息通讯研究院号召,移动安全联盟(MSA)联合终端厂商(手机厂家)推出的团体标准,最具权威性。这里面涉及到三个角色,首先是国信息通讯研究院是官方牵头单位,其次是移动安全联盟,这是由中国信息通信研究院与主流终端生产企业组成的一个行业组织,最后是终端厂商就是华为,小米,oppo,vivo等终端厂商。
移动智能终端补充设备标识体系架构共涉及四类实体,包括开发者、开发者开发的应用软件、移动智能终端设备的操作系统、用户及用户使用的设备。为保护用户用户的隐私和标识设备的唯一性,根据不同使用对象和不同用途,基于移动智能终端设备,分别生成设备唯一标识符、匿名设备标识符、开发者匿名设备标识符和应用匿名设备标识符,将这四个设备标识符构成补充设备标识体系。
导入SDK后,通过isSupported()方法判断设备是否支持,支持后便可以通过相应方法获取对应设备标识:
UUID、GUID
UUID 也叫做实例 ID,这两个 ID 可以说是在计算机体系内的通用标识符。这也是官方推荐的生成的唯一标识码生成方式,有一点不同的时,官方方案将生成的UUID存在应用内部存储当中,APP的卸载重装会导致发生更改;在实际使用当中我们可以存储到外部存储,除非人为的删除、损坏,这样它的不变性也得到了保障,而它的唯一性则由UUID来保证。
保存UUID的方案:
- 使用SharedPreferemces存储(缺点会随着app卸载移除)
- 使用android自带sqlite数据库存储(也会随着app卸载移除)
- 以外部存储方式存储文件(不存在app私有目录下)(缺点文件删除,UUID随之移除)
新增文件方式存储(最新的Android版本删除应用会提示删除创建的文件)
直接将他赋值命名这个text文本,这就省去了读取流的操作,当然了需要读写权限:
思路首先我们新建一个文件夹在/storage/emulated/0/下面,免得App卸载也会删除对应文件夹,然后我们还要对这个文件夹/storage/emulated/0/netLog/下面进行是否存在文件的判断,有文件说明我们第一次运行程序的时候已经生成了唯一UUID的text文本,我们只需要取出来使用即可,没有那就新增一个UUID的文本即可,下面是详细步骤:
public class StaticMethodUtil { /** * 获取文件 * * @return 文件 */ public static String getLogFile(String uuid) { File file; //因为适用版本就是Android10+的,所以不必担心这里报错 file = new File(Environment.getExternalStorageDirectory()+"/netLog"); // 若目录不存在则创建目录 if (!file.exists()) { file.mkdir(); } if (!hasFile(file.getPath())){ File logFile = new File(file.getPath()+"/" +uuid+ ".txt"); if (!logFile.exists()) { try { logFile.createNewFile(); } catch (Exception e) { Log.e("文件帮助类!", "Create log file failure !!! " + e.toString()); } } return logFile.getName(); }else { return getFileName(file.getPath(), ".txt").get(0); } } //判断文件夹下是否含有文件 public static boolean hasFile(String fileAbsolutePaht) { File file = new File(fileAbsolutePaht); return file.list().length==0?false:true; } }
唯一识别码的使用方式
关于设备ID的作用,大概可以分为下面几点:
- 统计需求。统计需求是设备ID最常见的用途,包括DAU, MAU的统计,行为统计,广告激活的统计等。
- 业务需求。比如结合行为统计做用户画像,以为用户提供个性化的服务。
- 风控需求。设备ID还可用于防刷单,反作弊等。风控需求仅靠设备ID是无法完成的,通常需要建立一套反作弊系统。
Installtion ID
在程序安装后第一次运行时生成一个ID,该方式和设备唯一标识不一样,不同的应用程序会产生不同的ID,同一个程序重新安装也会不同。所以这不是设备的唯一ID,但是可以保证每个用户的ID是不同的。 可以说是用来标识每一份应用程序的唯一ID(即Installtion ID),可以用来跟踪应用的安装数量等(其实就是UUID)。
import android.content.Context; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.RandomAccessFile; import java.util.UUID; 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(); } }
UniquePsuedoID
通过读取设备的ROM版本号、厂商名、CPU型号和其他硬件信息来组合出一串15位的号码和设备硬件序列号作为种子生成UUID。一串15位的号码(批量生产的设备每项信息基本相同,所以这一段相同的可能性特别高);硬件序列,在一些没有电话功能的设备会提供,某些手机上也可能提供(Devices without telephony are required to report a unique device ID here; some phones may do so also.),所以就是经常会返回Unknown。
import android.os.Build; import java.util.UUID; public class UniquePsuedoID { public static String getUniquePsuedoID() { String m_szDevIDShort = "35" + // 主板 Build.BOARD.length() % 10 + // android系统定制商 Build.BRAND.length() % 10 + // cpu指令集 Build.CPU_ABI.length() % 10 + // 设备参数 Build.DEVICE.length() % 10 + // 显示屏参数 Build.DISPLAY.length() % 10 + Build.HOST.length() % 10 + // 修订版本列表 Build.ID.length() % 10 + // 硬件制造商 Build.MANUFACTURER.length() % 10 + // 版本 Build.MODEL.length() % 10 + // 手机制造商 Build.PRODUCT.length() % 10 + // 描述build的标签 Build.TAGS.length() % 10 + // builder类型 Build.TYPE.length() % 10 + Build.USER.length() % 10; //13 位 //A hardware serial number, if available. Alphanumeric only, case-insensitive. String serial = Build.SERIAL; //使用硬件信息拼凑出来的15位号码 return new UUID(m_szDevIDShort.hashCode(), serial.hashCode()).toString(); } }
Universal ID
首先通过读取Android_id,作为UUID的种子。若得到Android_Id等于9774d56d682e549c或者 发生错误则random一个UUID作为备用方案,最后把得到的UUID同时存入内部存储和外部存储。下次使用UUID的时候优先从外部存储读取,再从背部存储读取,最后在重新生成,尽可能的保证其不变性。
import android.content.Context; import android.os.Environment; import android.provider.Settings; import java.io.File; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.UUID; public class UniversalID { private static String filePath = File.separator + "UTips" + File.separator + "UUID"; public static String getUniversalID(Context context) { String androidId; String fileRootPath = getPath(context) + filePath; String uuid = FileUtils.readFile(fileRootPath); if (uuid == null || uuid.equals("")) { androidId = "" + Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID); try { if (!"9774d56d682e549c".equals(androidId)) { uuid = UUID.nameUUIDFromBytes(androidId.getBytes("utf8")).toString(); } else { uuid = UUID.randomUUID().toString(); } } catch (Exception e) { uuid = UUID.randomUUID().toString(); } if(!uuid.equals("")){ saveUUID(context,uuid); } } return uuid; } private static void saveUUID(Context context, String UUID) { String ExternalSdCardPath = getExternalSdCardPath() + filePath; FileUtils.writeFile(ExternalSdCardPath, UUID); String InnerPath = context.getFilesDir().getAbsolutePath() + filePath; FileUtils.writeFile(InnerPath,UUID); } public static String getPath(Context context) { //首先判断是否有外部存储卡,如没有判断是否有内部存储卡,如没有,继续读取应用程序所在存储 String phonePicsPath = getExternalSdCardPath(); if (phonePicsPath == null) { phonePicsPath = context.getFilesDir().getAbsolutePath(); } return phonePicsPath; } /** * 遍历 "system/etc/vold.fstab” 文件,获取全部的Android的挂载点信息 * * @return */ private static ArrayList<String> getDevMountList() { String[] toSearch = FileUtils.readFile("/system/etc/vold.fstab").split(" "); ArrayList<String> out = new ArrayList<>(); for (int i = 0; i < toSearch.length; i++) { if (toSearch[i].contains("dev_mount")) { if (new File(toSearch[i + 2]).exists()) { out.add(toSearch[i + 2]); } } } return out; } /** * 获取扩展SD卡存储目录 * <p/> * 如果有外接的SD卡,并且已挂载,则返回这个外置SD卡目录 * 否则:返回内置SD卡目录 * * @return */ public static String getExternalSdCardPath() { if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { File sdCardFile = new File(Environment.getExternalStorageDirectory().getAbsolutePath()); return sdCardFile.getAbsolutePath(); } String path = null; File sdCardFile = null; ArrayList<String> devMountList = getDevMountList(); for (String devMount : devMountList) { File file = new File(devMount); if (file.isDirectory() && file.canWrite()) { path = file.getAbsolutePath(); String timeStamp = new SimpleDateFormat("ddMMyyyy_HHmmss").format(new Date()); File testWritable = new File(path, "test_" + timeStamp); if (testWritable.mkdirs()) { testWritable.delete(); } else { path = null; } } } if (path != null) { sdCardFile = new File(path); return sdCardFile.getAbsolutePath(); } return null; } }
唯一识别码遇到的挑战
设备ID的两个概念:唯一性和稳定性。
唯一性
唯一性: 两台不同的设备获取到的设备ID不相同。解决方案:
按规则构造
比如自增ID(包括分步自增),分段构造的ID(如snowflake算法)等,此类ID能保证唯一性。设备ID中的IMEI,设备序列号,MAC等,都是按照规则构造的,理论上能保证唯一性。设备序列号是对厂商本身唯一,全局唯一需要在加上 Build.MANUFACTURER。不过,设备序列号和MAC的唯一要打个问号,因为要看厂商是否遵守规则。
随机生成
比如UUID和Android ID,这类ID有一定的概率会重复,关键是看ID的长度(有多少bit)。有人做了这样一张随机数的冲突概率表:
Android ID是长度为16的十六进制字符串,其实就是64bit。假如APP累计激活量达到50亿的APP,则每两个这样的APP就大约有一个会有重复的Android ID。不过这里的“重复”不是大量的重复,而是“至少有两个相同”,也就是,如果设备激活量有50亿,那么有可能会有少量的重复的Android ID。总体而言, Android ID的唯一性还是不错的。JDK的randomUUID,大致可以认为是128bit的随机数(其中有6bit是固定的),即使到达200亿的数量,有重复的概率也仅仅是10的负18次方,微乎其微。
稳定性
稳定性:同一台设备在不同的时间, 获取到设备ID相同。
稳定性有两个层面:
- ID的生命周期
- IMEI,序列号,MAC等都是硬件相关,即使刷机也不会改变;
- Android ID则稳定性较弱,恢复出厂设置和刷机都会改变Android ID。
- 受版本的变化的影响
- 随着Android版本的提升,Google对权限是越收越紧了。获取设备ID的API,要么收起不给用(IMEI), 要么获取变得困难(SERIAL ),要么不同签名的APP获取的值不一样(Android ID)。
- 同时,Android 10中存储权限也收缩了,之前的那种生成唯一ID写到SD卡的某个角落的,以求卸载重装后读之前的ID等方法也不奏效了。
加强隐私方面的权限,对用户而言是好事,但对开发者而言就比较难受了。尤其是有的API本来可以用,升级后就获取不到了,这种断崖式的变化,可能会对数据统计造成影响。
什么时候唯一识别码会改变?
- 恢复出厂设置
- root/恢复root
- 三清
- 刷机
- 系统更新
- 软件修改(一般是模拟器,xposed,root)
行业的唯一识别码解决方案
阿里云唯一设备id
val deviceId = PushServiceFactory.getCloudPushService().deviceId //Mob CloudPushService pushService = PushServiceFactory.getCloudPushService(); pushService.register(applicationContext, new CommonCallback() { @Override public void onSuccess(String response) { Log.e("TAG", "onSuccess: "+response); } @Override public void onFailed(String errorCode, String errorMessage) { } });
友盟唯一设备ID:
val pushAgent = PushAgent.getInstance(context) pushAgent.register(object : UPushRegisterCallback { override fun onSuccess(deviceToken: String) { //注册成功会返回deviceToken deviceToken是推送消息的唯一标志 Log.i(TAG, "注册成功:deviceToken:--> $deviceToken") } override fun onFailure(errCode: String, errDesc: String) { Log.e(TAG, "注册失败:--> code:$errCode, desc:$errDesc") } })
参考链接: