Android 设备唯一标识(适配Android版本)

简介: Android 设备唯一标识(适配Android版本)

设备唯一标识


前言


  相信在看这篇文章之前你已经看过一些类似的文章了,那么你肯定知道自己想要的是什么。


正文


首先要知道设备唯一标识的重要性,它可以做什么?


① 大数据统计,比如采集这个APP的安装量,那么一个唯一标识就代表一个Android设备

② 放置多设备重复登录,比如QQ、微信,你在A手机登录了,如果又到B手机上登录,这时候A手机就会下线。

③ 有一些APP的资源是每天限量免费的,它不需要你登录,但是你只能看几个,而且卸载重装也是一样的,次数不会刷新,这就是因为再后台添加了你的设备唯一标识。

④ 网络安全,比如银行类APP,第一次登录会麻烦一些,后面就比较的容易了。


而在实际开发中用的最多的就是防止重复登录了。


1. 唯一标识的含义


唯一标识简单来说就是一串符号(或者数字),映射现实中硬件设备。这些符号和设备是一一对应的,可称之为“唯一设备ID(Unique Device Identifier)”。这就是概念,也就是说你要拿到的唯一标识是独一无二的才行。


 可惜的是Android平台并没有提供稳定的API来让我们获取到唯一设备ID。你可能要说IMEI和Mac地址可以获取到,但是它并不会适配Android的所有版本。在高版本中这个已经被弃用了,比如Android9.0、Android10.0、Android11.0。虽然现在Android11.0还没有正式投产,但是已经有Beta版本可以提供给开发者进行开发了,因此我们的应用如果要适配高版本就要另谋出路。


 由于Android的碎片化很严重,而版本又很多,导致你要在获取设备唯一标识的同时还是兼容Android的各个版本,这一点就比较难受了,而我看网络上的一些文章,好像都是类似的内容,重复的排版,有的甚至是标题都不换,就跟粘贴复制的一样,故此自己写一篇,起码以后我在获取唯一标识的时候可以看看,就当是做个笔记了。


2. 新建项目


熟悉我写博客思路的读者会明白,通常我会重新建一个项目来演示文章的内容和细节,而不是简单的丢几行代码随便解释一下就完事,那样是不负责任的。那么下面新建一个项目,命名为OnlyPhoneID。如下图所示


20201210102850899.png

3. 项目配置


 这里需要对Android的以往版本进行适配,可以选取几个有代表性的版本,那就是Android5.0、Android6.0、Android8.0、Android10.0。为了掩饰方便我会下载对应版本的模拟器来测试。


下面先配置这个项目,在上面我说过IMEI在Android9.0时就被弃用了,说是弃用实际上是禁止第三方应用获取IMEI,这么一说,那它在Android9.0以下就是可以用的,那么在Android的1.0至8.0都是可以通过获取IMEI来作为唯一标识的。


而IMEI要获取需要在AndroidManifest.xml中注册静态权限。下面进行添加


    <!--获取手机状态-->
    <uses-permission android:name="android.permission.READ_PHONE_STATE"/>
    <!--获取特权手机状态  高版本编译时需要-->
    <uses-permission android:name="android.permission.READ_PRIVILEGED_PHONE_STATE"
        tools:ignore="ProtectedPermissions" />


我习惯了图文并茂。

20201210111956509.png



因为我现在的项目编译版本比较高,我当前的目标版本是Android11.0,最低适配到Android5.0。Android的高版本会自动适配低版本。


20201210111730768.png

4. Android 5.0


那么首先在Android5.0中来尝试获取IMEI。


修改一下activity_main.xml的布局代码:


<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
    <TextView
        android:id="@+id/tv_device_id"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:text="Hello World!"
        android:textColor="#000"
        android:textSize="16sp" />
</RelativeLayout>


很简单的相对布局中放了一个用于显示设备id的文本控件。

然后进入到MainActivity,修改代码之后如下:


package com.llw.onlyphoneid;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.telecom.TelecomManager;
import android.telephony.TelephonyManager;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
    private TextView tvDeviceId;
    private TelephonyManager telephonyManager;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        tvDeviceId = findViewById(R.id.tv_device_id);
        //获取系统电话服务
        telephonyManager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE);
        //显示设备Id
        tvDeviceId.setText(telephonyManager.getDeviceId());
    }
}

20201210112622551.png


看到图中画横线这个方法,你把鼠标放上去,它会说已经过时了,也就是弃用的意思,因为在build.gradle中当前的版本是Android11.0,而我之前说过,在Android9.0时就已经弃用了,使用过时的方法会很容易出问题,当然这个问题,你在可以使用的Android版本设备中运行是不会出现的。


下面运行一下:


20201210112552440.png


可以看到在Android5.0上是可以正常获取到IMEI的。


刚才我是通过获取IMEI号,下面来试试获取序列号、设备序列号以及WIFI 模块的MAC地址。


下面修改一下activity_main.xml。


<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
    <!--获取IMEI-->
    <Button
        android:id="@+id/btn_get_imei"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="获取IMEI" />
    <!--获取序列号-->
    <Button
        android:id="@+id/btn_get_sn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@+id/btn_get_imei"
        android:text="获取序列号" />
    <!--获取设备序列号-->
    <Button
        android:id="@+id/btn_get_device_sn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@+id/btn_get_sn"
        android:text="获取设备序列号" />
    <!--最终获取结果显示-->
    <TextView
        android:id="@+id/tv_device_id"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:text="Hello World!"
        android:textColor="#000"
        android:textSize="16sp" />
    <!--Android版本-->
    <TextView
        android:id="@+id/tv_android_version"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_centerHorizontal="true"
        android:layout_marginBottom="20dp"
        android:textColor="#000"
        android:textSize="16sp" />
</RelativeLayout>


MainActivity


package com.llw.onlyphoneid;
import androidx.appcompat.app.AppCompatActivity;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.os.Build;
import android.os.Bundle;
import android.telecom.TelecomManager;
import android.telephony.TelephonyManager;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
/**
 * @author llw
 */
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    public static final String TAG = "MainActivity";
    private TextView tvDeviceId;
    private TextView tvAndroidVersion;
    private TelephonyManager telephonyManager;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initView();//初始化
    }
    /**
     * 初始化
     */
    private void initView() {
        tvDeviceId = findViewById(R.id.tv_device_id);
        tvAndroidVersion = findViewById(R.id.tv_android_version);
        Button btnGetIMEI = findViewById(R.id.btn_get_imei);
        Button btnGetSN = findViewById(R.id.btn_get_sn);
        Button btnGetDeviceSN = findViewById(R.id.btn_get_device_sn);
        btnGetIMEI.setOnClickListener(this);
        btnGetSN.setOnClickListener(this);
        btnGetDeviceSN.setOnClickListener(this);
        //获取系统电话服务
        telephonyManager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE);
        Log.d(TAG,"Android " + android.os.Build.VERSION.RELEASE);
        tvAndroidVersion.setText("Android " + android.os.Build.VERSION.RELEASE);
    }
    /**
     * 页面控件点击事件
     *
     * @param v
     */
    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.btn_get_imei://获取IMEI
                //显示设备Id
                Log.d(TAG, "IMEI: " + telephonyManager.getDeviceId());
                tvDeviceId.setText(telephonyManager.getDeviceId());
                break;
            case R.id.btn_get_sn://获取序列号
                Log.d(TAG, "序列号: " + telephonyManager.getSimSerialNumber());
                tvDeviceId.setText(telephonyManager.getSimSerialNumber());
                break;
            case R.id.btn_get_device_sn://获取设备序列号
                Log.d(TAG, "设备序列号: " + Build.SERIAL);
                tvDeviceId.setText(Build.SERIAL);
                break;
            default:
                break;
        }
    }
}



运行之后,三个按钮分别点击一下。


20201210151204307.png


OK,下面在6.0中运行试一下。


5. Android 6.0


  Android6.0推出了动态权限,规定危险权限需要动态申请,而用户需要通过才可以使用。


下面修改一下app的build.gradle。


android闭包下

  compileOptions {//指定使用的JDK1.8
        sourceCompatibility = 1.8
        targetCompatibility = 1.8
    }


dependencies闭包下


  //权限
    implementation 'com.tbruyelle.rxpermissions2:rxpermissions:0.9.4@aar'
    implementation 'io.reactivex.rxjava2:rxandroid:2.0.2'
    implementation "io.reactivex.rxjava2:rxjava:2.0.0"

20201210161837933.png


然后点击Sync同步一下。


同步好了之后回到MainActiivty,修改一下代码。


  /**
     * 初始化
     */
    private void initView() {
        tvDeviceId = findViewById(R.id.tv_device_id);
        tvAndroidVersion = findViewById(R.id.tv_android_version);
        Button btnGetIMEI = findViewById(R.id.btn_get_imei);
        Button btnGetSN = findViewById(R.id.btn_get_sn);
        Button btnGetDeviceSN = findViewById(R.id.btn_get_device_sn);
        btnGetIMEI.setOnClickListener(this);
        btnGetSN.setOnClickListener(this);
        btnGetDeviceSN.setOnClickListener(this);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            //Android6.0以上,请求动态权限
            RxPermissions rxPermissions = new RxPermissions(this);
            rxPermissions.request(Manifest.permission.READ_PHONE_STATE)
                    .subscribe(granted -> {
                        if (granted) {
                            //获取系统电话服务
                            telephonyManager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE);
                        } else {
                            Toast.makeText(this,"权限未通过",Toast.LENGTH_SHORT).show();
                        }
                    });
        } else {
            //获取系统电话服务
            telephonyManager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE);
        }
        Log.d(TAG, "Android " + android.os.Build.VERSION.RELEASE);
        tvAndroidVersion.setText("Android " + android.os.Build.VERSION.RELEASE);
    }


实际上只要修改一下initView中对于Android版本的判断即可。当用户通过权限之后你点击获取IMEI就可以获取到。否则程序ANR。


下面运行在Android6.0的模拟器上面,

20201210162226426.png


点击ALLOW,然后三个按钮都点一下:


20201210162310693.png


然后你会发现一个问题,那就是Android5.0和6.0打印的内容,除了版本不一样,其他的都一样,这是为什么?这是因为虚拟机是不存在的,所以Google就给你重复的数据,你想要真正获取到不一样的标识,还是要通过真机来操作,如果你不信的话,可以用自己电脑上的虚拟机试试,说不定你得到的数据和我这里也是一模一样的。不过我已经采购了两台低版本的Android手机,分别是5.0和6.0的,到时候我还是要用真机来试试。


下面用Android8.0来进行运行


6. Android 8.0


其实Android8.0的在获取唯一标识这个方面的变化不大,所以你都不需要做什么改动,你可以直接运行刚才的代码到8.0的虚拟机上面。

20201210163311771.png


各个按钮都点一下,你会发现和Android5.0、6.0是一样的。


20201210163504959.png


不过不用担心,这是在虚拟机上面,真机上不会这样的。


7. Android 10.0


在上面我就说过在Android9.0及以后版本中第三方应用是无法获取到IMEI的,那么现在你依然不用改代码,直接运行在Android10.0的虚拟机上。


2020121016402319.png


你会发现系统默认的弹窗都变得好看了一些。


然后你点击第一个按钮获取IMEI,直接闪退到桌面了。


20201210164239497.png


报错的意思就是当前应用不满足访问设备标识符的要求。因为你不是系统级应用,所以你获取不到这个IMEI。那么重新运行一次,点击第二个按钮试试。你会发现依然会闪退,而且报错的内容和上面的图片一模一样。然后再运行一次,点击第三个按钮。


2020121016453516.png


这个倒是没有报错了,但是是一个unknown,也就是未知,说明这三个方式在Android9.0之后全军覆没,而现在的常用手机版本都是Android9.0、10.0了。基本上都会去升级手机的版本。没有升级的,慢慢的用户也就自己淘汰了。看到这里你就会问了,那现在Android9.0之后要怎么获取设备的唯一标识呢?


8. 解决方案


  可以通过硬件标识来制作唯一设备id。


通过一个工具类来获取,这个工具类我也是通过视频学到的,挺牛逼的。

新建一个DeviceIdUtil 类。


package com.llw.onlyphoneid;
import android.content.Context;
import android.os.Build;
import android.provider.Settings;
import android.telephony.TelephonyManager;
import android.util.Log;
import java.security.MessageDigest;
import java.util.Locale;
import java.util.UUID;
/**
 * 获取手机的唯一标识ID
 */
public class DeviceIdUtil {
    public static String getDeviceId(Context context) {
        StringBuilder sbDeviceId = new StringBuilder();
        String imei = getIMEI(context);
        String androidId = getAndroidId(context);
        String serial = getSerial();
        String uuid = getDeviceUUID();
        //附加imei
        if (imei != null && imei.length() > 0) {
            sbDeviceId.append(imei);
            sbDeviceId.append("|");
        }
        //附加androidId
        if (androidId != null && androidId.length() > 0) {
            sbDeviceId.append(androidId);
            sbDeviceId.append("|");
        }
        //附加serial
        if (serial != null && serial.length() > 0) {
            sbDeviceId.append(serial);
            sbDeviceId.append("|");
        }
        //附加uuid
        if (uuid != null && uuid.length() > 0) {
            sbDeviceId.append(uuid);
        }
        if (sbDeviceId.length() > 0) {
            try {
                byte[] hash = getHashByString(sbDeviceId.toString());
                String sha1 = bytesToHex(hash);
                if (sha1 != null && sha1.length() > 0) {
                    //返回最终的DeviceId
                    return sha1;
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return null;
    }
    /**
     * 转16进制字符串
     *
     * @param data 数据
     * @return 16进制字符串
     */
    private static String bytesToHex(byte[] data) {
        StringBuilder sb = new StringBuilder();
        String string;
        for (int i = 0; i < data.length; i++) {
            string = (Integer.toHexString(data[i] & 0xFF));
            if (string.length() == 1) {
                sb.append("0");
            }
            sb.append(string);
        }
        return sb.toString().toUpperCase(Locale.CHINA);
    }
    /**
     * 取 SHA1
     *
     * @param data 数据
     * @return 对应的Hash值
     */
    private static byte[] getHashByString(String data) {
        try {
            MessageDigest messageDigest = MessageDigest.getInstance("SHA1");
            messageDigest.reset();
            messageDigest.update(data.getBytes("UTF-8"));
            return messageDigest.digest();
        } catch (Exception e) {
            return "".getBytes();
        }
    }
    /**
     * 获取硬件的UUID
     *
     * @return
     */
    private static String getDeviceUUID() {
        String deviceId = "9527" + Build.ID +
                Build.DEVICE +
                Build.BOARD +
                Build.BRAND +
                Build.HARDWARE +
                Build.PRODUCT +
                Build.MODEL +
                Build.SERIAL;
        return new UUID(deviceId.hashCode(), Build.SERIAL.hashCode()).toString().replace("-", "");
    }
    private static String getSerial() {
        try {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                return Build.getSerial();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
    /**
     * 获取AndroidId
     *
     * @param context 上下文
     * @return AndroidId
     */
    private static String getAndroidId(Context context) {
        try {
            String androidId = Settings.Secure.getString(context.getContentResolver(),
                    Settings.Secure.ANDROID_ID);
            return androidId;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return "";
    }
    /**
     * 获取IMEI
     *
     * @param context 上下文
     * @return IMEI
     */
    private static String getIMEI(Context context) {
        try {
            TelephonyManager telephonyManager = (TelephonyManager)
                    context.getSystemService(Context.TELEPHONY_SERVICE);
            return telephonyManager.getDeviceId();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return "";
    }
}


然后回到MainActivity,在onCreate中。


  @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initView();//初始化
        //唯一标识ID,兼容Android版本
        Toast.makeText(this, DeviceIdUtil.getDeviceId(this), Toast.LENGTH_SHORT).show();
        Log.d(TAG, "Android " + android.os.Build.VERSION.RELEASE);
        Log.d(TAG, "deviceId--> " + DeviceIdUtil.getDeviceId(this));
    }


下面先运行在Android5.0上。


20201210180859161.png


运行在Android6.0上


20201210181144181.png


运行在Android8.0上


202012101812403.png


运行在Android10.0上


20201210181359316.png


都可以,而且都不一样,当然你也可以把模拟器上的应用卸载再安装,唯一标识码也不会变化。


而你需要的只是一个工具类而已。


总结


其实也没有啥好总结的,设备唯一标识码通过硬件的信息来获取,不会受到Android版本的影响,应用安装的影响,你甚至都不需要给权限。简单粗暴且有用。


源码就是上面的那个DeviceIdUtil工具类,复制到自己的项目中直接使用即可。


相关文章
|
29天前
|
人工智能 搜索推荐 物联网
Android系统版本演进与未来展望####
本文深入探讨了Android操作系统从诞生至今的发展历程,详细阐述了其关键版本迭代带来的创新特性、用户体验提升及对全球移动生态系统的影响。通过对Android历史版本的回顾与分析,本文旨在揭示其成功背后的驱动力,并展望未来Android可能的发展趋势与面临的挑战,为读者呈现一个既全面又具深度的技术视角。 ####
|
3月前
|
调度 Android开发 UED
Android经典实战之Android 14前台服务适配
本文介绍了在Android 14中适配前台服务的关键步骤与最佳实践,包括指定服务类型、请求权限、优化用户体验及使用WorkManager等。通过遵循这些指南,确保应用在新系统上顺畅运行并提升用户体验。
258 6
|
4月前
|
Android开发
基于Amlogic 安卓9.0, 驱动简说(四):Platform平台驱动,驱动与设备的分离
本文介绍了如何在基于Amlogic T972的Android 9.0系统上使用Platform平台驱动框架和设备树(DTS),实现设备与驱动的分离,并通过静态枚举在设备树中描述设备,自动触发驱动程序的加载和设备创建。
77 0
基于Amlogic 安卓9.0, 驱动简说(四):Platform平台驱动,驱动与设备的分离
|
4月前
|
Android开发 C语言
基于Amlogic 安卓9.0, 驱动简说(二):字符设备驱动,自动创建设备
这篇文章是关于如何在基于Amlogic T972的Android 9.0系统上,通过自动分配设备号和自动创建设备节点文件的方式,开发字符设备驱动程序的教程。
73 0
基于Amlogic 安卓9.0, 驱动简说(二):字符设备驱动,自动创建设备
|
4月前
|
自然语言处理 Shell Linux
基于Amlogic 安卓9.0, 驱动简说(一):字符设备驱动,手动创建设备
本文是关于在Amlogic安卓9.0平台上创建字符设备驱动的教程,详细介绍了驱动程序的编写、编译、部署和测试过程,并提供了完整的源码和应用层调用示例。
112 0
基于Amlogic 安卓9.0, 驱动简说(一):字符设备驱动,手动创建设备
|
4月前
|
传感器 Android开发 芯片
不写一行代码(三):实现安卓基于i2c bus的Slaver设备驱动
本文是系列文章的第三篇,展示了如何在Android系统中利用现有的i2c bus驱动,通过编写设备树节点和应用层的控制代码,实现对基于i2c bus的Slaver设备(如六轴陀螺仪模块QMI8658C)的控制,而无需编写设备驱动代码。
59 0
不写一行代码(三):实现安卓基于i2c bus的Slaver设备驱动
|
4月前
|
Android开发
不写一行代码(二):实现安卓基于PWM的LED设备驱动
本文介绍了在Android系统中不编写任何代码,通过设备树配置和内核支持的通用PWM LED驱动来实现基于PWM的LED设备驱动,并通过测试命令调整LED亮度级别。
63 0
不写一行代码(二):实现安卓基于PWM的LED设备驱动
|
存储 Android开发 开发者
Android适配全面总结(三)----ROM适配
版权声明:本文为博主原创文章(部分引用他人博文,已加上引用说明),未经博主允许不得转载。https://www.jianshu.com/p/f9c67a4b908e 转载请标明出处:https://www.jianshu.com/p/f9c67a4b908e 本文出自 AWeiLoveAndroid的博客 第一篇文章讲了 Android适配全面总结(一)----屏幕适配 上一篇文章讲了 Android适配全面总结(二)----版本适配 这一篇文章讲一下 ROM适配。
2227 0
|
存储 API Android开发
Android适配全面总结(二)----版本适配
版权声明:本文为博主原创文章(部分引用他人博文,已加上引用说明),未经博主允许不得转载。https://www.jianshu.com/p/49fa8ebc0105 转载请标明出处:https://www.
1419 0
|
编解码 API Android开发