檢測App是否在Android模擬器中運(yùn)行
引言
在Android開發(fā)中,經(jīng)常會(huì)使用到Android模擬器,普通用戶也可能由于游戲等其他需求而使用Android模擬器。
但是,由于模擬器往往與實(shí)際真機(jī)有差異,會(huì)存在使用模擬器刷單、頻繁大量請(qǐng)求等惡意行為,于是就產(chǎn)生了區(qū)分模擬器與真機(jī)的需求。
在此,提供了區(qū)分模擬器與真機(jī)的方法,并提供了Java工具類 EmulatorHelper,方便各位開發(fā)同事在業(yè)務(wù)需求中調(diào)用以區(qū)分當(dāng)前程序是否運(yùn)行在模擬器當(dāng)中。
以下是該工具類實(shí)現(xiàn)思路的說明,如有不妥當(dāng)之處,歡迎指出并與我交流。
實(shí)現(xiàn)思路
區(qū)分模擬器的基本思路就是根據(jù)Android的Build類中一些與硬件相關(guān)聯(lián)的常量及系統(tǒng)參數(shù),在真機(jī)與模擬器中值的不同,從而區(qū)分模擬器。
但是實(shí)際使用中,現(xiàn)在的模擬器往往都會(huì)支持修改這當(dāng)中的一些值,導(dǎo)致只是用靜態(tài)變量及系統(tǒng)參數(shù)的方法準(zhǔn)確率不高,因此還需要輔助其他方法(例如:用戶的行為、檢查傳感器、基帶信息、進(jìn)程信息等),再綜合判斷:
硬件名稱檢查
硬件名稱(ro.hardware),是安卓系統(tǒng)變量文件build.prop中的一個(gè)參數(shù),用于描述該硬件的名稱,而部分模擬器的某些版本(早期版本),該值的內(nèi)容是有特定值的。
因此該因素可以作為一個(gè)判斷模擬器的因素,具體值在下方代碼中已經(jīng)列出,但是由于這個(gè)值是可以在模擬器中進(jìn)行修改的,因此該因素判斷的準(zhǔn)確率一般。
/**
* 特征參數(shù)-硬件名稱
*/
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":
//網(wǎng)易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);
}
發(fā)布渠道檢查
設(shè)備發(fā)布渠道信息(ro.build.flavor),是安卓系統(tǒng)變量文件build.prop中的一個(gè)參數(shù),用于描述該設(shè)備ISO發(fā)布時(shí)的渠道,而在部分模擬器中,該值的內(nèi)容是有特定值的。
因此該因素可以作為一個(gè)判斷模擬器的因素,具體值在下方代碼中已經(jīng)列出,但是由于這個(gè)值在某些模擬器中是不固定或可以根據(jù)系統(tǒng)鏡像進(jìn)行修改的,因此該因素判斷的準(zhǔn)確率一般。
/**
* 特征參數(shù)-渠道
*/
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);
}
設(shè)備型號(hào)檢查
設(shè)備型號(hào)(ro.product.model),是安卓系統(tǒng)變量文件build.prop中的一個(gè)參數(shù),用于描述該設(shè)備型號(hào),部分模擬器中該值是存在特定值的。
因此該因素可以作為一個(gè)判斷模擬器的因素,具體值在下方代碼中已經(jīng)列出,但是由于這個(gè)值在某些模擬器中是不固定或可以根據(jù)系統(tǒng)鏡像進(jìn)行修改的,因此該因素判斷的準(zhǔn)確率一般。
/**
* 特征參數(shù)-設(shè)備型號(hào)
*/
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),是安卓系統(tǒng)變量文件build.prop中的一個(gè)參數(shù),用于描述該設(shè)備的制造商,部分模擬器中該值是存在特定值的。
因此該因素可以作為一個(gè)判斷模擬器的因素,具體值在下方代碼中已經(jīng)列出,但是由于這個(gè)值在某些模擬器中是不固定或可以根據(jù)系統(tǒng)鏡像進(jìn)行修改的,因此該因素判斷的準(zhǔn)確率一般。
/**
* 特征參數(shù)-硬件制造商
*/
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")) {
//網(wǎng)易MUMU模擬器
result = Result.RESULT_CONFIRM_EMULATOR;
} else {
result = Result.RESULT_UNKNOWN;
}
return new EmulatorCheckResult(result, manufacturer);
}
主板名稱檢查
主板名稱(ro.product.board),是安卓系統(tǒng)變量文件build.prop中的一個(gè)參數(shù),用于描述該設(shè)備的主板名稱信息,部分模擬器中該值是存在特定值的。
因此該因素可以作為一個(gè)判斷模擬器的因素,具體值在下方代碼中已經(jīng)列出,但是由于這個(gè)值在某些模擬器中是不固定或可以根據(jù)系統(tǒng)鏡像進(jìn)行修改的,因此該因素判斷的準(zhǔn)確率一般。
/**
* 特征參數(shù)-主板名稱
*/
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);
}
主板平臺(tái)檢查
主板平臺(tái)(ro.product.platform),是安卓系統(tǒng)變量文件build.prop中的一個(gè)參數(shù),用于描述該設(shè)備的主板平臺(tái)信息,部分模擬器中該值是存在特定值的。
因此該因素可以作為一個(gè)判斷模擬器的因素,具體值在下方代碼中已經(jīng)列出,但是由于這個(gè)值在某些模擬器中是不固定或可以根據(jù)系統(tǒng)鏡像進(jìn)行修改的,因此該因素判斷的準(zhǔn)確率一般。
/**
* 特征參數(shù)-主板平臺(tái)
*/
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),是安卓系統(tǒng)變量文件build.prop中的一個(gè)參數(shù),用于描述該設(shè)備的基帶信息,部分模擬器中該值是存在特定值的(AS自帶模擬器),由于該值是在基帶芯片中寫入的,因而大部門市面主流的模擬器,該值都是無法獲取的。
因此該因素可以作為一個(gè)判斷模擬器的因素,且該值無法獲取時(shí),大概率是模擬器,具體值在下方代碼中已經(jīng)列出,該值獲取失敗時(shí),是模擬器的可能性非常大。
/**
* 特征參數(shù)-基帶信息
*/
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);
}
傳感器數(shù)量檢查
當(dāng)前(2020年初),大部分市面上銷售的手機(jī)都具有很多傳感器(例如陀螺儀、溫度傳感器、光線傳感器等),我個(gè)人的小米9 Pro 5G手機(jī),傳感器檢測出的數(shù)量多達(dá)42個(gè),而模擬器中,該數(shù)量僅為個(gè)位數(shù)。因此該因素可以作為一個(gè)判斷模擬器的因素,且該值數(shù)量較少(<7)時(shí),大概率是模擬器,判斷邏輯在下方已經(jīng)列出。
/**
* 獲取傳感器數(shù)量
*/
private int getSensorNumber(Context context) {
SensorManager sm = (SensorManager) context.getSystemService(SENSOR_SERVICE);
return sm.getSensorList(Sensor.TYPE_ALL).size();
}
第三方應(yīng)用數(shù)量檢查
根據(jù)我們?nèi)粘5氖謾C(jī)使用經(jīng)驗(yàn)可以得知,正常的用戶一般都會(huì)下載安裝多個(gè)應(yīng)用軟件(微信、支付寶、微博、抖音、視頻軟件、游戲軟件等),而模擬器中的軟件數(shù)量一般較少。因此該因素可以作為一個(gè)判斷模擬器的因素,且該值數(shù)量較少(<5)時(shí),大概率是模擬器,但該因素不絕對(duì)正確,可能一個(gè)用戶手機(jī)上一個(gè)軟件都沒有,也可能一個(gè)模擬器上應(yīng)用軟件非常多,因此該因素?zé)o法絕對(duì)確定,判斷邏輯在下方已經(jīng)列出。
/**
* 獲取已安裝第三方應(yīng)用數(shù)量
*/
private int getUserAppNumber() {
String userApps = exec("pm list package -3");
return getUserAppNum(userApps);
}
相機(jī)支持檢查
部分模擬器的某些版本,是不支持相機(jī)的,因此該因素也可以作為一個(gè)判斷模擬器的因素,但是由于一些非常低端的、非常早期的手機(jī)也是不支持相機(jī)的,因此該因素也無法絕對(duì)確定,獲取是否支持相機(jī)的代碼邏輯在下方已經(jīng)列出。
/**
* 是否支持相機(jī)
*/
private boolean supportCamera(Context context) {
return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY);
}
閃光燈支持檢查
與相機(jī)不同,我調(diào)研的目前市面上的主流模擬器(包含支持相機(jī)的模擬器)基本都不支持閃光燈,因此該因素也可以作為一個(gè)判斷模擬器的因素,但是由于一些非常低端的、非常早期的手機(jī)也是不支持閃光燈的,因此該因素也無法絕對(duì)確定,獲取是否支持閃光燈的代碼邏輯在下方已經(jīng)列出。
/**
* 是否支持閃光燈
*/
private boolean supportCameraFlash(Context context) {
return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_FLASH);
}
藍(lán)牙支持檢查
與閃光燈類似,我調(diào)研的目前市面上的主流模擬器(包含支持相機(jī)的模擬器)基本都不支持藍(lán)牙,因此該因素也可以作為一個(gè)判斷模擬器的因素,但是由于一些非常低端的、非常早期的手機(jī)也是不支持藍(lán)牙的,因此該因素也無法絕對(duì)確定,獲取是否支持藍(lán)牙的代碼邏輯在下方已經(jīng)列出。
/**
* 是否支持藍(lán)牙
*/
private boolean supportBluetooth(Context context) {
return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH);
}
光傳感器支持檢查
與閃光燈類似,我調(diào)研的目前市面上的主流模擬器(包含支持相機(jī)的模擬器)基本都不支持光傳感器,因此該因素也可以作為一個(gè)判斷模擬器的因素,但是由于一些非常低端的、非常早期的手機(jī)也是不支持光傳感器的,因此該因素也無法絕對(duì)確定,獲取是否支持光傳感器的代碼邏輯在下方已經(jīng)列出。
/**
* 判斷是否存在光傳感器來判斷是否為模擬器
* 部分真機(jī)也不存在溫度和壓力傳感器。其余傳感器模擬器也存在。
*/
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;
}
}
進(jìn)程組信息檢查
在真機(jī)中,進(jìn)程組信息都會(huì)存儲(chǔ)在/proc/self/cgroup目錄中,使用cat命令可以查看到該信息,但某些模擬器是不會(huì)寫入該信息的,因此該檢查也可以作為一個(gè)模擬器檢查的因素,但是因?yàn)椴唤^對(duì),因此無法100%確定是模擬器。
/**
* 特征參數(shù)-進(jìn)程組信息
*/
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);
}
核心判斷邏輯
到這里,可以發(fā)現(xiàn),上述的各個(gè)因素,都無法確定程序的運(yùn)行環(huán)境一定是模擬器,而是只存在一定可能性。
因此,該工具類綜合使用了上述所有的因素來綜合確定當(dāng)前是否是模擬器
(例如,當(dāng)前有一個(gè)設(shè)備同時(shí)滿足了好幾個(gè)條件:用戶安裝的第三方軟件不到5個(gè)、檢查不到基帶信息、沒有相機(jī)、也沒有光線傳感器,我們就可以認(rèn)為這個(gè)設(shè)備是模擬器了)。
代碼中引入了一個(gè)變量
currentDoubt
懷疑值,來代表對(duì)當(dāng)前設(shè)備是模擬器的懷疑程度,滿足一個(gè)條件,該值就+1,這樣就可以根據(jù)這個(gè)值來確定當(dāng)前是不是模擬器。
設(shè)置這個(gè)值的閾值,就可以調(diào)整這個(gè)工具類對(duì)于模擬器探測的敏感程度(比如:設(shè)置為1,表示非常敏感,只要有一個(gè)因素滿足了就認(rèn)為是模擬器;
設(shè)置為10,表示比較不敏感,必須有10個(gè)因素都不滿足,才認(rèn)為是模擬器)。
當(dāng)前代碼中,這個(gè)值設(shè)置為3,即三個(gè)因素同時(shí)達(dá)到了,就認(rèn)為當(dāng)前設(shè)備是模擬器。
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;
}
//檢測設(shè)備型號(hào)
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;
}
//檢測主板平臺(tái)
EmulatorCheckResult platformResult = checkFeaturesByPlatform();
if (handleCheckResult(platformResult, CheckType.PLATFORM)) {
return true;
}
//檢測基帶信息
EmulatorCheckResult baseBandResult = checkFeaturesByBaseBand();
if (handleCheckResult(baseBandResult, CheckType.BASE_BAND)) {
return true;
}
//檢測傳感器數(shù)量
int sensorNumber = getSensorNumber(context);
if (sensorNumber <= 7) {
++currentDoubt;
}
//檢測已安裝第三方應(yīng)用數(shù)量
int userAppNumber = getUserAppNumber();
if (userAppNumber <= 5) {
++currentDoubt;
}
//檢測是否支持閃光燈
boolean supportCameraFlash = supportCameraFlash(context);
if (!supportCameraFlash) {
++currentDoubt;
}
//檢測是否支持相機(jī)
boolean supportCamera = supportCamera(context);
if (!supportCamera) {
++currentDoubt;
}
//檢測是否支持藍(lán)牙
boolean supportBluetooth = supportBluetooth(context);
if (!supportBluetooth) {
++currentDoubt;
}
//檢測光線傳感器
boolean hasLightSensor = hasLightSensor(context);
if (!hasLightSensor) {
++currentDoubt;
}
//檢測進(jìn)程組信息
EmulatorCheckResult cGroupResult = checkFeaturesByCgroup();
if (cGroupResult.result == Result.RESULT_CONFIRM_EMULATOR) {
++currentDoubt;
}
//可疑值>3,就認(rèn)為是模擬器,可調(diào)整這個(gè)值,調(diào)節(jié)認(rèn)為是模擬器的靈敏度
return currentDoubt > 3;
}
測試結(jié)果
經(jīng)過本人、本人所在團(tuán)隊(duì)同事的測試,該工具類在如下環(huán)境中驗(yàn)證通過:
Android Studio 自帶模擬器、網(wǎng)易MUMU模擬器、夜神模擬器、Xiaomi 9 Pro 5G、Xiaomi 6X、Google Pixel 2、Huawei Mate20 Pro、Huawei 暢享9、Huawei Mate30 Pro等機(jī)型上驗(yàn)證通過。
由于條件有限,無法驗(yàn)證全部機(jī)型與模擬器,如果在使用中遇到了檢測錯(cuò)誤的情況,請(qǐng)?jiān)诖颂幜粞裕覀兛梢砸黄饋砀倪M(jìn)該工具類。
真正在生產(chǎn)環(huán)境使用該工具類時(shí),還需要在灰度升級(jí)過程中,根據(jù)用戶規(guī)模、日志回傳情況,來調(diào)節(jié)懷疑值的閾值,以達(dá)到一個(gè)檢測效果與設(shè)備規(guī)模的平衡。