Android全面檢測設備是否模擬器

前言

前段時間工作有個需求,要求檢測App是否在模擬器環境下運行,就像在有些手機遊戲上可以看到這個功能

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

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

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

1.撥號檢測法

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

public boolean isSimulator1() {
        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 !canResolveIntent;
}

完事收工!… … 等會,夜神模擬器怎麼可以返回個false?也就是夜神模擬器是可以跳轉撥號的😓。


2.設備標識符檢測法

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

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

public boolean isSimulator2() {
	String url = "tel:" + "123456";
	Intent intent = new Intent();
	intent.setData(Uri.parse(url));
	intent.setAction(Intent.ACTION_DIAL);
	// 是否可以處理跳轉到撥號的 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,這種方法的博主也主注意到了這一點,他發現夜神的SERIAL爲16位,比真機的多了8位,所以Build.SERIAL這裏加了個判斷Build.SERIAL.length() > 8,問題似乎可以得到解決了。
但是,Android10.0以後,Build.SERIAL的獲取變得麻煩起來,甚至有些手機,比如我的小米9,得到了一個"unknown",也就是說我的手機會被識別爲模擬器!所以我們又回到了原點 😳


3.包名檢測法

找啊找,終於,我看到一種有特點的檢測方式了,包名檢測法:
原理是通過獲取設備和模擬器中的包名來區分是否模擬器,每個品牌的模擬器都有應用商店和一些系統應用,這些都是不可卸載的,這些應用對應着唯一的包名,那麼包名就反過來可以鑑定模擬器的品牌。
舉個例子👉網易Mumu模擬器:”com.mumu.launcher“這個包名就是網易Mumu啓動時的系統應用,我們就可以用他這一點來作爲鑑定的依據之一。

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循環遍歷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("藍疊");
				return list;
			}
		}
	}
	return list;
}

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

  1. A模擬器安裝了B模擬器的應用,導致識別的模擬器類型出錯
  2. A手機安裝了B模擬器的應用,一般情況下,模擬器的系統應用是不可被下載安裝的;如果你足夠皮👀,你可以隨便弄個包,把包名改成"com.mumu.launcher",那麼你的手機也就會被識別爲Mumu模擬器了。

4.特徵值檢測

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

很厲害了!

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


題外話

yysy確實

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章