Android 检测设备是否为模拟器

最近有一个新的需求,检测设备是否为模拟器,如果是模拟器就禁用某些功能。

你还在为开发中频繁切换环境打包而烦恼吗?快来试试 Environment Switcher 吧!使用它可以在app运行时一键切换环境,而且还支持其他贴心小功能,有了它妈妈再也不用担心频繁环境切换了。https://github.com/CodeXiaoMai/EnvironmentSwitcher

市面上的模拟器

打开 Google 搜索 “模拟器”,各种模拟器映入眼帘。“逍遥安卓-超强安卓模拟器”、“天天模拟器”、“网易MuMu”、“BlueStacks蓝叠安卓模拟器”、“夜神安卓模拟器”、“海马玩模拟器”、“51模拟器”当然还有功能强大的“Genymotion”……

搜索解决办法

经过上网查找,发现类似的帖子并不是太多,其中经过筛选,发现下面几个通用的解决方案。

方案一:

public boolean isEmulator() {
    return ((TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE))
            .getNetworkOperatorName().toLowerCase().equals("android");
}

方案二:

public boolean isEmulator() {
    return Build.FINGERPRINT.startsWith("generic")
        || Build.FINGERPRINT.toLowerCase().contains("vbox")
        || Build.FINGERPRINT.toLowerCase().contains("test-keys")
        || Build.MODEL.contains("google_sdk")
        || Build.MODEL.contains("Emulator")
        || Build.SERIAL.equalsIgnoreCase("unknown")
        || Build.SERIAL.equalsIgnoreCase("android")
        || Build.MODEL.contains("Android SDK built for x86")
        || Build.MANUFACTURER.contains("Genymotion")
        || (Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic"))
        || "google_sdk".equals(Build.PRODUCT);
}

于是把上面两种方案结合起来,就是:

public boolean isEmulator() {
        return Build.FINGERPRINT.startsWith("generic")
            || Build.FINGERPRINT.toLowerCase().contains("vbox")
            || Build.FINGERPRINT.toLowerCase().contains("test-keys")
            || Build.MODEL.contains("google_sdk")
            || Build.MODEL.contains("Emulator")
            || Build.SERIAL.equalsIgnoreCase("unknown")
            || Build.SERIAL.equalsIgnoreCase("android")
            || Build.MODEL.contains("Android SDK built for x86")
            || Build.MANUFACTURER.contains("Genymotion")
            || (Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic"))
            || "google_sdk".equals(Build.PRODUCT)
            || ((TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE))
                .getNetworkOperatorName().toLowerCase().equals("android");
}

测试结果

经过在各个模拟器上测试,发现大多数都是可以检测出来的,只有各别模拟器不可以检测出来,其中包括“夜神安卓模拟器”。经过观察与对比发现,夜神安卓模拟器有一个和其他模拟器以及手机(手头的)不同的地方,就是“Build.SERIAL”是一个16位的字符串,而其他模拟器都是“unknow"或者"android",真机是 8 位的字符串,哈哈小样被我抓住了吧,于是修改了检测方法。

public boolean isEmulator() {
        return Build.FINGERPRINT.startsWith("generic")
            || Build.FINGERPRINT.toLowerCase().contains("vbox")
            || Build.FINGERPRINT.toLowerCase().contains("test-keys")
            || Build.MODEL.contains("google_sdk")
            || Build.MODEL.contains("Emulator")
            || Build.SERIAL.equalsIgnoreCase("unknown")
            || Build.SERIAL.equalsIgnoreCase("android")
            || Build.SERIAL.length() > 8
            || Build.MODEL.contains("Android SDK built for x86")
            || Build.MANUFACTURER.contains("Genymotion")
            || (Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic"))
            || "google_sdk".equals(Build.PRODUCT)
            || ((TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE))
                .getNetworkOperatorName().toLowerCase().equals("android");
}

再次检测,成功识别!!

问题再现

由于手头的手机有限,担心将手机识别错误,于是在 weTest 平台抽样对各品牌手机进行测试,果然不出所料,问题出现了。当测试到华为畅享5s的时候,竟然也被识别为模拟器。这下悲剧了,毕竟手机用户还是主要的,可不能错杀好人啊!!!经过观察,发现问题出现在上面自作聪明加的一个判断中 Build.SERIAL.length() > 8 ,这个手机的 Build.SERIAL 也是 16 位,这可如何是好???

一个 Crash 让我灵光乍现

App 中有一个跳转到拨号盘的功能,当然在模拟器中无意点到这个按钮的时候,App 居然 Crash 了,这引起了我的注意,加为之前在真机上从来没有出现过问题,于是再次尝试点击这个按钮,它再次如我所料的 Crash 掉了。我实然灵机一动,对啊这是模拟器,不能拨打电话,所以 Crash 了,这不正是解决方案吗?(一不小心一个 Crash 竟然救了我)于是我在其他几个模拟器中也尝试点击这个按钮,结果是大部分都不支持这个操作,而且都是简单粗暴的直接 Crash 。虽然不能 100% 的识别,但大多数还是可以以此来做识别凭证的。

接下来再修改方法,慢着!大多数平板也是不支持拨打电话的,由于手头也是只有一台华为的平板,测试了一下,发现是跳转到保存联系页面,这个至少也不是 Crash,所以算通过了。

最终结果

最终将几种方案整合修改后如下:

public boolean isEmulator() {
        String url = "tel:" + "123456";
        Intent intent = new Intent();
        intent.setData(Uri.parse(url));
        intent.setAction(Intent.ACTION_DIAL);
        // 是否可以处理跳转到拨号的 Intent
        boolean canResolveIntent = intent.resolveActivity(mContext.getPackageManager()) != null;

        return Build.FINGERPRINT.startsWith("generic")
            || Build.FINGERPRINT.toLowerCase().contains("vbox")
            || Build.FINGERPRINT.toLowerCase().contains("test-keys")
            || Build.MODEL.contains("google_sdk")
            || Build.MODEL.contains("Emulator")
            || Build.SERIAL.equalsIgnoreCase("unknown")
            || Build.SERIAL.equalsIgnoreCase("android")
            || Build.MODEL.contains("Android SDK built for x86")
            || Build.MANUFACTURER.contains("Genymotion")
            || (Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic"))
            || "google_sdk".equals(Build.PRODUCT)
            || ((TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE))
                .getNetworkOperatorName().toLowerCase().equals("android")
            || !canResolverIntent;
}

后记

其实,我相信还有更好的方法去检测,比如通过一些硬件特性,或者模拟器不能模拟的其他特性,但目前还没有找到,如果你有好的办法,欢迎分享!!!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,544评论 25 707
  • 凌晨1点多,儿子和母亲在津津有味地为着个肉圆在那里互相抢着吃。 昨天凌晨1点多,儿子和母亲在那为一个谁去下饺子猜拳...
    小轶阅读 347评论 0 2
  • 雪.慈然堂欢迎你的到来 澳美慧鸸鹋油抹是以鸸鹋油为主要原料,再加以多种天然植物精华,采用冷冻萃取、雾化、纳米化等最...
    左一脚阅读 14,884评论 0 0
  • 1 财富自由的核心其实就是时间的自主权: 曾经在《超体》里看过,真正的计量单位不是1,2,3这样的数字,而是时间。...
    多谋善断阅读 294评论 0 0
  • 灯火伴着萤火 枝叶碎润月光 现在融化 我很慌张 看不清了你的面庞 月桂晶 雪片腥 风向稳定 目光和(huo)情...
    殷殷囷囷阅读 255评论 10 7