背景
前段时间正在做单点登录相关的需求,提到单点登录不得不提到android设备的唯一标识,因为只有获取到设备的唯一标识并且传给服务器,服务器才能知道是不是有两台手机同时登录了一个账号。
#### 获取唯一标识的几种方法
1 IMEI
public static String getIMEI(Context context) {
TelephonyManager telephonyManager = (TelephonyManager) context.getSystemService(context.TELEPHONY_SERVICE);
String imei = telephonyManager.getDeviceId();
return imei;
}
缺点:
- 6.0以上需要android.permission.READ_PHONE_STATE权限
- android 碎片化严重,获取不到deviceid
- 只能获取到具备打电话功能的设备,如果是pad 则不起作用
- 如果用户手机root,则可以通过第三方软件修改imei
2 AndroidId
String ANDROID_ID = Settings.System.getString(getContentResolver(), Settings.System.ANDROID_ID);
优点:获取方式简单,不需要获取权限
缺点:手机恢复出厂设置该值就会发生变化,碎片化严重有的设备获取不到Android_id
3 Serial Number
String SerialNumber = android.os.Build.SERIAL;
Android系统2.3版本以上可以通过下面的方法得到Serial Number,且非手机设备也可以通过该接口获取。不需要权限,通用性也较高,但我测试发现红米手机返回的是 0123456789ABCDEF 明显是一个顺序的非随机字符串。也不一定靠谱。
4 Mac地址 – 属于Android系统的保护信息,高版本系统获取的话容易失败,返回0000000;
5 SimSerialNum
看名字也知道需要插入sim的设备,而且他也需要获取手机的android.permission.READ_PHONE_STATE权限
结论
由于历史原因,我公司用的是IMEI+MAC+uuid的方式获取唯一id
代码如下:
public static String getImei(final Context context) {
if (!TextUtils.isEmpty(mDeviceImei)) {
return mDeviceImei;
}
TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
String imei;
try {
imei = tm.getDeviceId();// imei
} catch (Exception e) {
LOGGER.e(TAG, "imei obtained exception", e);
imei = null;
}
if (StringUtils.isEmpty(imei) || "0".equals(imei)) {
// 如果imei号为空或0,取mac地址作为imei号传递
try {
WifiManager wifi = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
WifiInfo info = wifi.getConnectionInfo();
imei = info.getMacAddress();
} catch (Exception e) {
imei = null;
}
// 如果mac地址为空或0,则通过uuid生成的imei号来传递
if (StringUtils.isEmpty(imei) || "0".equals(imei)) {
imei = SharePersistentUtils.getString(context, LibConstant.IMEI);
LOGGER.d(TAG, "imei = " + imei);
if (StringUtils.isEmpty(imei)) {
imei = UUIDUtils.getUUID(15);
if (StringUtils.isEmpty(imei)) {
return "0";
}
LOGGER.d(TAG, "imei new = " + imei);
SharePersistentUtils.saveString(context, LibConstant.IMEI, imei);
/*
* SharePersistentUtils.savePerference(context,
* UtilLibConstant.IMEI, imei);
*/
}
}
}
// TODO (ly) 转为小写
if (imei != null) {
imei = imei.toLowerCase(Locale.US);
}
setmDeviceImei(imei);
return imei;
}
代码逻辑:
- 先获取imei(这里并没有判断版本,后面会说会引起的问题)
- 如果imei为空,再获取mac地址
- 如果mac 地址也为空,则利用uuid随机生成一串编码,并且保存到文件中。
存在问题:
- imei在6.0之前不需要权限可以直接获取,6.0以后需要read_phone_state
权限。此处并没有申请权限,会导致6.0以上手机一直获取的是mac地址或者是uuid 生成的编码 - 考虑这样一种情况,即使此处申请了 权限,但是用户拒绝了(此时唯一编码肯定是mac地址或者是uuid),用户操作app后续界面又弹出了相同权限(read_phone_state)此时用户同意了,下次进入app后获取的一定是imei,这将会导致第二次进入app会被登出(两次打开app设备唯一id上传不一致)
- 由于我们公司需要在所有接口上传基础信息(包括设备唯一id),所以此段代码的调用放在了application中,这也就是为什么此处没有申请权限的原因(application没法调用 requestPermission,这个会考虑将代码调用后移,最好放在一个activity中,例如WelcomeActivity)
如果你的需求是在activity中,那么你只需要将上面代码imei获取处增加版本判断申请权限,那么一般来说上面的逻辑还是比较完善的。
下面说说我开发中遇到的坑爹问题,以及解决思路(都是泪T.T)
先说问题,每次安装好发现首次打开app登录成功后杀掉进程,第二次再打开的时候就会提示我重新登录,后面就不会出现这个问题了,这个问题困扰了我好几个小时。
解决:
- 由于手机本身问题跳过了前两步,每次获取的都是uuid生成的编码,但是很奇怪的是第二次打开app会发现存储的uuid和上一次不一致,
- 我首先联想到的是是不是多线程调用。然后我习惯性的看了方法的调用处,果然有很多处。我一一排查并没有多线程的问题
- 就在我要崩溃的时候,我突然发现项目中有个pushlib的进程也在打印 getImei()方法的信息,我立即联想到多进程导致的问题。
- 我找到了pushlib进程的一个service,在里面并没有发现有调用getImei()的地方
- 就在我又快崩溃的时候,我想到既然pushlib进程并没有主动调用getImei()方法,会不会是“被调用”,最后我想到了多进程会导致Application被初始化多次,果然在Application中发现了调用getImei()的痕迹,此时问题迎刃而解,app启动的时候getImei()会被两个进程调用两次,导致uuid被获取两次(类似于线程并发问题),所以第一次和第二次启动app会不一致
找到问题根源,说下我的解决方案:
// 避免其他进程重复初始化application,导致此方法被调用多次。
public String getImei(){
String processName = getProcessName(this, Process.myPid());
if (null == processName || processName.endsWith(":pushservice")) {
return;
}
....
//否则执行后续代码
}