Android全面檢測設(shè)備是否模擬器

前言

前段時(shí)間工作有個(gè)需求,要求檢測App是否在模擬器環(huán)境下運(yùn)行,就像在有些手機(jī)游戲上可以看到這個(gè)功能


乍一看蠻簡單的,后來我查了一下資料,然后頭都大了······

這多虧了國內(nèi)pc端模擬器的發(fā)展,現(xiàn)在市面上的模擬器越來越多,也越來越“逼真”了,模擬器和真機(jī)的區(qū)別在逐步縮小,這就使得模擬器的檢測存在偏差,不管有多小,偏差總是會(huì)存在的,如何降低這種偏差值,就是這篇文章像討論的內(nèi)容。

先來看一下我是怎么頭大的

1.撥號(hào)檢測法

首先一開始想到的就是能否撥號(hào),真機(jī)肯定可以的,不然手機(jī)的根基就沒了,模擬器肯定不能撥號(hào),所以我很快寫下代碼:

public boolean isSimulator1() {
        String url = "tel:" + "123456";
        Intent intent = new Intent();
        intent.setData(Uri.parse(url));
        intent.setAction(Intent.ACTION_DIAL);
        // 是否可以處理跳轉(zhuǎn)到撥號(hào)的 Intent
        boolean canResolveIntent = intent.resolveActivity(mContext.getPackageManager()) != null;
        return !canResolveIntent;
}

完事收工!... ... 等會(huì),夜神模擬器怎么可以返回個(gè)false?也就是夜神模擬器是可以跳轉(zhuǎn)撥號(hào)的??。


2.設(shè)備標(biāo)識(shí)符檢測法

不行我就換一種方式,我記得Android有個(gè)設(shè)備標(biāo)識(shí)符Build.MANUFACTURER,它是用來標(biāo)注手機(jī)廠商的,例如小米手機(jī)的MANUFACTURER的值為:Xiaomi,三星手機(jī)則為:Samsung……而模擬器的值一般是跟他們的品牌有關(guān),例如Genymotion的Build.MANUFACTURER為Genymotion,Mumu模擬器的值為netease等,可以根據(jù)比較此值來較為方便的區(qū)分模擬器和真實(shí)設(shè)備。
但是!又是夜神模擬器,他有個(gè)很騷的地方,就是這個(gè)值你可以通過系統(tǒng)設(shè)置修改,比如我把他改成小米的:


結(jié)果輸出的Build.MANUFACTURER的值正是Xiaomi,所以這種方式也不可行
查了下網(wǎng)上很多也用到的類似這種比較設(shè)備標(biāo)識(shí)符的方法,但是效果也不是很好,幾乎都會(huì)卡在夜神模擬器這關(guān),例如將篩選條件變得更加多樣:(方法實(shí)現(xiàn)可以查看這篇博客

public boolean isSimulator2() {
    String url = "tel:" + "123456";
    Intent intent = new Intent();
    intent.setData(Uri.parse(url));
    intent.setAction(Intent.ACTION_DIAL);
    // 是否可以處理跳轉(zhuǎn)到撥號(hào)的 Intent
    boolean canResolveIntent = intent.resolveActivity(MainActivity.this.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) MainActivity.this.getSystemService(Context.TELEPHONY_SERVICE))
            .getNetworkOperatorName().toLowerCase().equals("android")
            || !canResolveIntent;
}

依舊返回的還是false,這種方法的博主也主注意到了這一點(diǎn),他發(fā)現(xiàn)夜神的SERIAL為16位,比真機(jī)的多了8位,所以Build.SERIAL這里加了個(gè)判斷Build.SERIAL.length() > 8,問題似乎可以得到解決了。
但是,Android10.0以后,Build.SERIAL的獲取變得麻煩起來,甚至有些手機(jī),比如我的小米9,得到了一個(gè)"unknown",也就是說我的手機(jī)會(huì)被識(shí)別為模擬器!所以我們又回到了原點(diǎn) ??


3.包名檢測法

找啊找,終于,我看到一種有特點(diǎn)的檢測方式了,包名檢測法:
原理是通過獲取設(shè)備和模擬器中的包名來區(qū)分是否模擬器,每個(gè)品牌的模擬器都有應(yīng)用商店和一些系統(tǒng)應(yīng)用,這些都是不可卸載的,這些應(yīng)用對應(yīng)著唯一的包名,那么包名就反過來可以鑒定模擬器的品牌。
舉個(gè)例子??網(wǎng)易Mumu模擬器:”com.mumu.launcher“這個(gè)包名就是網(wǎng)易Mumu啟動(dòng)時(shí)的系統(tǒng)應(yīng)用,我們就可以用他這一點(diǎn)來作為鑒定的依據(jù)之一。

private static final String[] PKG_NAMES = {"com.mumu.launcher", "com.ami.duosupdater.ui", "com.ami.launchmetro", "com.ami.syncduosservices", "com.bluestacks.home",
        "com.bluestacks.windowsfilemanager", "com.bluestacks.settings", "com.bluestacks.bluestackslocationprovider", "com.bluestacks.appsettings", "com.bluestacks.bstfolder",
        "com.bluestacks.BstCommandProcessor", "com.bluestacks.s2p", "com.bluestacks.setup", "com.bluestacks.appmart", "com.kaopu001.tiantianserver", "com.kpzs.helpercenter",
        "com.kaopu001.tiantianime", "com.android.development_settings", "com.android.development", "com.android.customlocale2", "com.genymotion.superuser",
        "com.genymotion.clipboardproxy", "com.uc.xxzs.keyboard", "com.uc.xxzs", "com.blue.huang17.agent", "com.blue.huang17.launcher", "com.blue.huang17.ime",
        "com.microvirt.guide", "com.microvirt.market", "com.microvirt.memuime", "cn.itools.vm.launcher", "cn.itools.vm.proxy", "cn.itools.vm.softkeyboard",
        "cn.itools.avdmarket", "com.syd.IME", "com.bignox.app.store.hd", "com.bignox.launcher", "com.bignox.app.phone", "com.bignox.app.noxservice", "com.android.noxpush",
        "com.haimawan.push", "me.haima.helpcenter", "com.windroy.launcher", "com.windroy.superuser", "com.windroy.launcher", "com.windroy.ime", "com.android.flysilkworm",
        "com.android.emu.inputservice", "com.tiantian.ime", "com.microvirt.launcher", "me.le8.androidassist", "com.vphone.helper", "com.vphone.launcher", "com.duoyi.giftcenter.giftcenter"};
private static final String[] PATHS = {"/sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq", "/system/lib/libc_malloc_debug_qemu.so", "/sys/qemu_trace", "/system/bin/qemu-props",
        "/dev/socket/qemud", "/dev/qemu_pipe", "/dev/socket/baseband_genyd", "/dev/socket/genyd"};
private static final String[] FILES = {"/data/data/com.android.flysilkworm", "/data/data/com.bluestacks.filemanager"};

// 包名檢測
public static boolean isSimulator3(Context paramContext) {
    try {
        List pathList = new ArrayList();
        pathList = getInstalledSimulatorPackages(paramContext);
        if (pathList.size() == 0) {
            for (int i = 0; i < PATHS.length; i++)
                if (i == 0) {  檢測的特定路徑
                    if (new File(PATHS[i]).exists()) continue;
                    pathList.add(Integer.valueOf(i));
                } else {
                    if (!new File(PATHS[i]).exists()) continue;
                    pathList.add(Integer.valueOf(i));
                }
        }
        if (pathList.size() == 0) {
            pathList = loadApps(paramContext);
        }
        return (pathList.size() == 0 ? null : pathList.toString()) != null;
    } catch (Exception e) {
        e.printStackTrace();
    }
    return false;
}

@SuppressLint("WrongConstant")
private static List getInstalledSimulatorPackages(Context context) {
    ArrayList localArrayList = new ArrayList();
    try {
        for (int i = 0; i < PKG_NAMES.length; i++)
            try {
                context.getPackageManager().getPackageInfo(PKG_NAMES[i], PackageManager.COMPONENT_ENABLED_STATE_ENABLED);
                localArrayList.add(PKG_NAMES[i]);
            } catch (PackageManager.NameNotFoundException localNameNotFoundException) {
            }
        if (localArrayList.size() == 0) {
            for (int i = 0; i < FILES.length; i++) {  
                if (new File(FILES[i]).exists())  // 檢測的特定文件
                    localArrayList.add(FILES[i]);
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
    return localArrayList;
}

public static List loadApps(Context context) {
    Intent intent = new Intent(Intent.ACTION_MAIN, null);
    intent.addCategory(Intent.CATEGORY_LAUNCHER);
    List<String> list = new ArrayList<>();
    List<ResolveInfo> apps = context.getPackageManager().queryIntentActivities(intent, 0);
    //for循環(huán)遍歷ResolveInfo對象獲取包名和類名
    for (int i = 0; i < apps.size(); i++) {
        ResolveInfo info = apps.get(i);
        String packageName = info.activityInfo.packageName;
        CharSequence cls = info.activityInfo.name;
        CharSequence name = info.activityInfo.loadLabel(context.getPackageManager());
        if (!TextUtils.isEmpty(packageName)) {
            if (packageName.contains("bluestacks")) {
                list.add("藍(lán)疊");
                return list;
            }
        }
    }
    return list;
}

其中還用到了檢測的特定文件來加強(qiáng)檢測精度,這種方法算是比較靠譜的了。具體實(shí)現(xiàn),可以查看這篇博客,寫的很好。
這種方法的成功率高狠多了,當(dāng)然失敗的概率是很小的,除非遇到以下情況:

  1. A模擬器安裝了B模擬器的應(yīng)用,導(dǎo)致識(shí)別的模擬器類型出錯(cuò)
  2. A手機(jī)安裝了B模擬器的應(yīng)用,一般情況下,模擬器的系統(tǒng)應(yīng)用是不可被下載安裝的;如果你足夠皮??,你可以隨便弄個(gè)包,把包名改成"com.mumu.launcher",那么你的手機(jī)也就會(huì)被識(shí)別為Mumu模擬器了。

4.特征值檢測

這種可以說是集大成者了,這種方式的檢測成功率極高,甚至之前的手動(dòng)改包名的騷操作也可以被揪出來,實(shí)現(xiàn)方式可以看這兒:一行代碼幫你檢測Android模擬器
這種方法的實(shí)現(xiàn)思路是通過定義一個(gè)嫌疑值,當(dāng)嫌疑值達(dá)到閥值的時(shí)候,bang!就把你識(shí)別成模擬器了。
隨便貼一下代碼截圖大家體會(huì)一下:


很厲害了!

當(dāng)然如果你想嘗試一下,可以用我的demo,以上四種方式都有,你可以隨便測,隨便玩~??
代碼地址:MonitorDemo


題外話

yysy確實(shí)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容