Article / 文章中心

检测App是否在Android模拟器中运行

发布时间:2023-05-15 点击数:4732

引言

在Android开发中,经常会使用到Android模拟器,普通用户也可能由于游戏等其他需求而使用Android模拟器。

但是,由于模拟器往往与实际真机有差异,会存在使用模拟器刷单、频繁大量请求等恶意行为,于是就产生了区分模拟器与真机的需求。

在此,提供了区分模拟器与真机的方法,并提供了Java工具类 EmulatorHelper,方便各位开发同事在业务需求中调用以区分当前程序是否运行在模拟器当中。

以下是该工具类实现思路的说明,如有不妥当之处,欢迎指出并与我交流。
实现思路

区分模拟器的基本思路就是根据Android的Build类中一些与硬件相关联的常量及系统参数,在真机与模拟器中值的不同,从而区分模拟器。

但是实际使用中,现在的模拟器往往都会支持修改这当中的一些值,导致只是用静态变量及系统参数的方法准确率不高,因此还需要辅助其他方法(例如:用户的行为、检查传感器、基带信息、进程信息等),再综合判断:
硬件名称检查

硬件名称(ro.hardware),是安卓系统变量文件build.prop中的一个参数,用于描述该硬件的名称,而部分模拟器的某些版本(早期版本),该值的内容是有特定值的。

因此该因素可以作为一个判断模拟器的因素,具体值在下方代码中已经列出,但是由于这个值是可以在模拟器中进行修改的,因此该因素判断的准确率一般。
/**
 * 特征参数-硬件名称
 */
private EmulatorCheckResult checkFeaturesByHardware() {
    String hardware = getProperty("ro.hardware");
 if (null == hardware) {
        return new EmulatorCheckResult(Result.RESULT_MAYBE_EMULATOR, null);
 }
    Result result;
 String tempValue = hardware.toLowerCase();
 switch (tempValue) {
        case "ttvm":
            //天天模拟器
 case "nox":
            //夜神模拟器
 case "cancro":
            //网易MUMU模拟器
 case "intel":
            //逍遥模拟器
 case "vbox":
        case "vbox86":
            //腾讯手游助手
 case "android_x86":
            //雷电模拟器
 result = Result.RESULT_CONFIRM_EMULATOR;
 break;
 default:
            result = Result.RESULT_UNKNOWN;
 break;
 }
    return new EmulatorCheckResult(result, hardware);
}
发布渠道检查

设备发布渠道信息(ro.build.flavor),是安卓系统变量文件build.prop中的一个参数,用于描述该设备ISO发布时的渠道,而在部分模拟器中,该值的内容是有特定值的。

因此该因素可以作为一个判断模拟器的因素,具体值在下方代码中已经列出,但是由于这个值在某些模拟器中是不固定或可以根据系统镜像进行修改的,因此该因素判断的准确率一般。
/**
 * 特征参数-渠道
 */
private EmulatorCheckResult checkFeaturesByFlavor() {
    String flavor = getProperty("ro.build.flavor");
 if (null == flavor) {
        return new EmulatorCheckResult(Result.RESULT_MAYBE_EMULATOR, null);
 }
    Result result;
 String tempValue = flavor.toLowerCase();
 if (tempValue.contains("vbox")) {
        result = Result.RESULT_CONFIRM_EMULATOR;
 } else if (tempValue.contains("sdk_gphone")) {
        result = Result.RESULT_CONFIRM_EMULATOR;
 } else {
        result = Result.RESULT_UNKNOWN;
 }
    return new EmulatorCheckResult(result, flavor);
}
设备型号检查

设备型号(ro.product.model),是安卓系统变量文件build.prop中的一个参数,用于描述该设备型号,部分模拟器中该值是存在特定值的。

因此该因素可以作为一个判断模拟器的因素,具体值在下方代码中已经列出,但是由于这个值在某些模拟器中是不固定或可以根据系统镜像进行修改的,因此该因素判断的准确率一般。
/**
 * 特征参数-设备型号
 */
private EmulatorCheckResult checkFeaturesByModel() {
    String model = getProperty("ro.product.model");
 if (null == model) {
        return new EmulatorCheckResult(Result.RESULT_MAYBE_EMULATOR, null);
 }
    Result result;
 String tempValue = model.toLowerCase();
 if (tempValue.contains("google_sdk")) {
        result = Result.RESULT_CONFIRM_EMULATOR;
 } else if (tempValue.contains("emulator")) {
        result = Result.RESULT_CONFIRM_EMULATOR;
 } else if (tempValue.contains("android sdk built for x86")) {
        result = Result.RESULT_CONFIRM_EMULATOR;
 } else {
        result = Result.RESULT_UNKNOWN;
 }
    return new EmulatorCheckResult(result, model);
}
硬件制造商检查

硬件制造商(ro.product.manufacturer),是安卓系统变量文件build.prop中的一个参数,用于描述该设备的制造商,部分模拟器中该值是存在特定值的。

因此该因素可以作为一个判断模拟器的因素,具体值在下方代码中已经列出,但是由于这个值在某些模拟器中是不固定或可以根据系统镜像进行修改的,因此该因素判断的准确率一般。
/**
 * 特征参数-硬件制造商
 */
private EmulatorCheckResult checkFeaturesByManufacturer() {
    String manufacturer = getProperty("ro.product.manufacturer");
 if (null == manufacturer) {
        return new EmulatorCheckResult(Result.RESULT_MAYBE_EMULATOR, null);
 }
    Result result;
 String tempValue = manufacturer.toLowerCase();
 if (tempValue.contains("genymotion")) {
        result = Result.RESULT_CONFIRM_EMULATOR;
 } else if (tempValue.contains("netease")) {
        //网易MUMU模拟器
 result = Result.RESULT_CONFIRM_EMULATOR;
 } else {
        result = Result.RESULT_UNKNOWN;
 }
    return new EmulatorCheckResult(result, manufacturer);
}
主板名称检查

主板名称(ro.product.board),是安卓系统变量文件build.prop中的一个参数,用于描述该设备的主板名称信息,部分模拟器中该值是存在特定值的。

因此该因素可以作为一个判断模拟器的因素,具体值在下方代码中已经列出,但是由于这个值在某些模拟器中是不固定或可以根据系统镜像进行修改的,因此该因素判断的准确率一般。
/**
 * 特征参数-主板名称
 */
private EmulatorCheckResult checkFeaturesByBoard() {
    String board = getProperty("ro.product.board");
 if (null == board) {
        return new EmulatorCheckResult(Result.RESULT_MAYBE_EMULATOR, null);
 }
    Result result;
 String tempValue = board.toLowerCase();
 if (tempValue.contains("android")) {
        result = Result.RESULT_CONFIRM_EMULATOR;
 } else if (tempValue.contains("goldfish")) {
        result = Result.RESULT_CONFIRM_EMULATOR;
 } else {
        result = Result.RESULT_UNKNOWN;
 }
    return new EmulatorCheckResult(result, board);
}
主板平台检查

主板平台(ro.product.platform),是安卓系统变量文件build.prop中的一个参数,用于描述该设备的主板平台信息,部分模拟器中该值是存在特定值的。

因此该因素可以作为一个判断模拟器的因素,具体值在下方代码中已经列出,但是由于这个值在某些模拟器中是不固定或可以根据系统镜像进行修改的,因此该因素判断的准确率一般。
/**
 * 特征参数-主板平台
 */
private EmulatorCheckResult checkFeaturesByPlatform() {
    String platform = getProperty("ro.board.platform");
 if (null == platform) {
        return new EmulatorCheckResult(Result.RESULT_MAYBE_EMULATOR, null);
 }
    Result result;
 String tempValue = platform.toLowerCase();
 if (tempValue.contains("android")) {
        result = Result.RESULT_CONFIRM_EMULATOR;
 } else {
        result = Result.RESULT_UNKNOWN;
 }
    return new EmulatorCheckResult(result, platform);
}
基带信息检查

基带信息(gsm.version.baseband),是安卓系统变量文件build.prop中的一个参数,用于描述该设备的基带信息,部分模拟器中该值是存在特定值的(AS自带模拟器),由于该值是在基带芯片中写入的,因而大部门市面主流的模拟器,该值都是无法获取的。

因此该因素可以作为一个判断模拟器的因素,且该值无法获取时,大概率是模拟器,具体值在下方代码中已经列出,该值获取失败时,是模拟器的可能性非常大。
/**
 * 特征参数-基带信息
 */
private EmulatorCheckResult checkFeaturesByBaseBand() {
    String baseBandVersion = getProperty("gsm.version.baseband");
 if (null == baseBandVersion) {
        return new EmulatorCheckResult(Result.RESULT_MAYBE_EMULATOR, null);
 }
    Result result;
 if (baseBandVersion.contains("1.0.0.0")) {
        result = Result.RESULT_CONFIRM_EMULATOR;
 } else {
        result = Result.RESULT_UNKNOWN;
 }
    return new EmulatorCheckResult(result, baseBandVersion);
}
传感器数量检查

当前(2020年初),大部分市面上销售的手机都具有很多传感器(例如陀螺仪、温度传感器、光线传感器等),我个人的小米9 Pro 5G手机,传感器检测出的数量多达42个,而模拟器中,该数量仅为个位数。因此该因素可以作为一个判断模拟器的因素,且该值数量较少(<7)时,大概率是模拟器,判断逻辑在下方已经列出。
/**
 * 获取传感器数量
 */
private int getSensorNumber(Context context) {
    SensorManager sm = (SensorManager) context.getSystemService(SENSOR_SERVICE);
 return sm.getSensorList(Sensor.TYPE_ALL).size();
}
第三方应用数量检查

根据我们日常的手机使用经验可以得知,正常的用户一般都会下载安装多个应用软件(微信、支付宝、微博、抖音、视频软件、游戏软件等),而模拟器中的软件数量一般较少。因此该因素可以作为一个判断模拟器的因素,且该值数量较少(<5)时,大概率是模拟器,但该因素不绝对正确,可能一个用户手机上一个软件都没有,也可能一个模拟器上应用软件非常多,因此该因素无法绝对确定,判断逻辑在下方已经列出。
/**
 * 获取已安装第三方应用数量
 */
private int getUserAppNumber() {
    String userApps = exec("pm list package -3");
 return getUserAppNum(userApps);
}
相机支持检查

部分模拟器的某些版本,是不支持相机的,因此该因素也可以作为一个判断模拟器的因素,但是由于一些非常低端的、非常早期的手机也是不支持相机的,因此该因素也无法绝对确定,获取是否支持相机的代码逻辑在下方已经列出。
/**
 * 是否支持相机
 */
private boolean supportCamera(Context context) {
    return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY);
}
闪光灯支持检查

与相机不同,我调研的目前市面上的主流模拟器(包含支持相机的模拟器)基本都不支持闪光灯,因此该因素也可以作为一个判断模拟器的因素,但是由于一些非常低端的、非常早期的手机也是不支持闪光灯的,因此该因素也无法绝对确定,获取是否支持闪光灯的代码逻辑在下方已经列出。
/**
 * 是否支持闪光灯
 */
private boolean supportCameraFlash(Context context) {
    return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_FLASH);
}
蓝牙支持检查

与闪光灯类似,我调研的目前市面上的主流模拟器(包含支持相机的模拟器)基本都不支持蓝牙,因此该因素也可以作为一个判断模拟器的因素,但是由于一些非常低端的、非常早期的手机也是不支持蓝牙的,因此该因素也无法绝对确定,获取是否支持蓝牙的代码逻辑在下方已经列出。
/**
 * 是否支持蓝牙
 */
private boolean supportBluetooth(Context context) {
    return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH);
}
光传感器支持检查

与闪光灯类似,我调研的目前市面上的主流模拟器(包含支持相机的模拟器)基本都不支持光传感器,因此该因素也可以作为一个判断模拟器的因素,但是由于一些非常低端的、非常早期的手机也是不支持光传感器的,因此该因素也无法绝对确定,获取是否支持光传感器的代码逻辑在下方已经列出。
/**
 * 判断是否存在光传感器来判断是否为模拟器
 * 部分真机也不存在温度和压力传感器。其余传感器模拟器也存在。
 */
private boolean hasLightSensor(Context context) {
    SensorManager sensorManager = (SensorManager) context.getSystemService(SENSOR_SERVICE);
 Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_LIGHT);
 if (null == sensor) {
        return false;
 } else {
        return true;
 }
}
进程组信息检查

在真机中,进程组信息都会存储在/proc/self/cgroup目录中,使用cat命令可以查看到该信息,但某些模拟器是不会写入该信息的,因此该检查也可以作为一个模拟器检查的因素,但是因为不绝对,因此无法100%确定是模拟器。
/**
 * 特征参数-进程组信息
 */
private EmulatorCheckResult checkFeaturesByCgroup() {
    String filter = exec("cat /proc/self/cgroup");
 if (null == filter) {
        return new EmulatorCheckResult(Result.RESULT_MAYBE_EMULATOR, null);
 }
    return new EmulatorCheckResult(Result.RESULT_UNKNOWN, filter);
}
核心判断逻辑

到这里,可以发现,上述的各个因素,都无法确定程序的运行环境一定是模拟器,而是只存在一定可能性。

因此,该工具类综合使用了上述所有的因素来综合确定当前是否是模拟器

(例如,当前有一个设备同时满足了好几个条件:用户安装的第三方软件不到5个、检查不到基带信息、没有相机、也没有光线传感器,我们就可以认为这个设备是模拟器了)。

代码中引入了一个变量

currentDoubt
怀疑值,来代表对当前设备是模拟器的怀疑程度,满足一个条件,该值就+1,这样就可以根据这个值来确定当前是不是模拟器。

设置这个值的阈值,就可以调整这个工具类对于模拟器探测的敏感程度(比如:设置为1,表示非常敏感,只要有一个因素满足了就认为是模拟器;

设置为10,表示比较不敏感,必须有10个因素都不满足,才认为是模拟器)。

当前代码中,这个值设置为3,即三个因素同时达到了,就认为当前设备是模拟器。
public boolean check(Context context) {
    if (context == null) {
        Log.e(TAG, "check(), context is null!");
 return false;
 }
    currentDoubt = 0;

 //检测硬件名称
 EmulatorCheckResult hardwareResult = checkFeaturesByHardware();
 if (handleCheckResult(hardwareResult, CheckType.HARDWARE_NAME)) {
        return true;
 }

    //检测渠道
 EmulatorCheckResult flavorResult = checkFeaturesByFlavor();
 if (handleCheckResult(flavorResult, CheckType.FLAVOR)) {
        return true;
 }

    //检测设备型号
 EmulatorCheckResult modelResult = checkFeaturesByModel();
 if (handleCheckResult(modelResult, CheckType.DEVICE_MODULE)) {
        return true;
 }

    //检测硬件制造商
 EmulatorCheckResult manufacturerResult = checkFeaturesByManufacturer();
 if (handleCheckResult(manufacturerResult, CheckType.MANUFACTURER)) {
        return true;
 }

    //检测主板名称
 EmulatorCheckResult boardResult = checkFeaturesByBoard();
 if (handleCheckResult(boardResult, CheckType.BOARD)) {
        return true;
 }

    //检测主板平台
 EmulatorCheckResult platformResult = checkFeaturesByPlatform();
 if (handleCheckResult(platformResult, CheckType.PLATFORM)) {
        return true;
 }

    //检测基带信息
 EmulatorCheckResult baseBandResult = checkFeaturesByBaseBand();
 if (handleCheckResult(baseBandResult, CheckType.BASE_BAND)) {
        return true;
 }

    //检测传感器数量
 int sensorNumber = getSensorNumber(context);
 if (sensorNumber <= 7) {
        ++currentDoubt;
 }

    //检测已安装第三方应用数量
 int userAppNumber = getUserAppNumber();
 if (userAppNumber <= 5) {
        ++currentDoubt;
 }

    //检测是否支持闪光灯
 boolean supportCameraFlash = supportCameraFlash(context);
 if (!supportCameraFlash) {
        ++currentDoubt;
 }
    //检测是否支持相机
 boolean supportCamera = supportCamera(context);
 if (!supportCamera) {
        ++currentDoubt;
 }
    //检测是否支持蓝牙
 boolean supportBluetooth = supportBluetooth(context);
 if (!supportBluetooth) {
        ++currentDoubt;
 }

    //检测光线传感器
 boolean hasLightSensor = hasLightSensor(context);
 if (!hasLightSensor) {
        ++currentDoubt;
 }

    //检测进程组信息
 EmulatorCheckResult cGroupResult = checkFeaturesByCgroup();
 if (cGroupResult.result == Result.RESULT_CONFIRM_EMULATOR) {
        ++currentDoubt;
 }

    //可疑值>3,就认为是模拟器,可调整这个值,调节认为是模拟器的灵敏度
 return currentDoubt > 3;
}
测试结果

经过本人、本人所在团队同事的测试,该工具类在如下环境中验证通过:

Android Studio 自带模拟器、网易MUMU模拟器、夜神模拟器、Xiaomi 9 Pro 5G、Xiaomi 6X、Google Pixel 2、Huawei Mate20 Pro、Huawei 畅享9、Huawei Mate30 Pro等机型上验证通过。

由于条件有限,无法验证全部机型与模拟器,如果在使用中遇到了检测错误的情况,请在此处留言,我们可以一起来改进该工具类。

真正在生产环境使用该工具类时,还需要在灰度升级过程中,根据用户规模、日志回传情况,来调节怀疑值的阈值,以达到一个检测效果与设备规模的平衡。