我的应用程序有一个特定的功能,只能在根可用的设备上工作。与其让这个特性在使用时失败(然后向用户显示适当的错误消息),我更希望能够先静默地检查根目录是否可用,如果不可用,则首先隐藏相应的选项。

有办法做到这一点吗?


当前回答

截至2021年(今天),似乎没有任何可靠的方法或方法来检测根目录,特别是在启用了MagiskHide等强大的隐藏工具的情况下。这里的大多数答案都不再相关,所以不要在生产中使用它。依靠可靠的检查,如SafetyNet,而不是额外的英里检测根,我建议在两个运行时保护你的应用程序,如防止调试器/检测,并确保使用混淆。

其他回答

我建议使用本机代码进行根检测。 下面是一个完整的工作示例。

JAVA包装程序:

package com.kozhevin.rootchecks.util;


import android.support.annotation.NonNull;

import com.kozhevin.rootchecks.BuildConfig;

public class MeatGrinder {
    private final static String LIB_NAME = "native-lib";
    private static boolean isLoaded;
    private static boolean isUnderTest = false;

    private MeatGrinder() {

    }

    public boolean isLibraryLoaded() {
        if (isLoaded) {
            return true;
        }
        try {
            if(isUnderTest) {
                throw new UnsatisfiedLinkError("under test");
            }
            System.loadLibrary(LIB_NAME);
            isLoaded = true;
        } catch (UnsatisfiedLinkError e) {
            if (BuildConfig.DEBUG) {
                e.printStackTrace();
            }
        }
        return isLoaded;
    }

    public native boolean isDetectedDevKeys();

    public native boolean isDetectedTestKeys();

    public native boolean isNotFoundReleaseKeys();

    public native boolean isFoundDangerousProps();

    public native boolean isPermissiveSelinux();

    public native boolean isSuExists();

    public native boolean isAccessedSuperuserApk();

    public native boolean isFoundSuBinary();

    public native boolean isFoundBusyboxBinary();

    public native boolean isFoundXposed();

    public native boolean isFoundResetprop();

    public native boolean isFoundWrongPathPermission();

    public native boolean isFoundHooks();

    @NonNull
    public static MeatGrinder getInstance() {
        return InstanceHolder.INSTANCE;
    }

    private static class InstanceHolder {
        private static final MeatGrinder INSTANCE = new MeatGrinder();
    }
}

JNI包装器(native-lib.c):

JNIEXPORT jboolean JNICALL
Java_com_kozhevin_rootchecks_util_MeatGrinder_isDetectedTestKeys(
        JNIEnv *env,
        jobject  this ) {

    return (jboolean) isDetectedTestKeys();
}

JNIEXPORT jboolean JNICALL
Java_com_kozhevin_rootchecks_util_MeatGrinder_isDetectedDevKeys(
        JNIEnv *env,
        jobject  this ) {

    return (jboolean) isDetectedDevKeys();
}

JNIEXPORT jboolean JNICALL
Java_com_kozhevin_rootchecks_util_MeatGrinder_isNotFoundReleaseKeys(
        JNIEnv *env,
        jobject  this ) {

    return (jboolean) isNotFoundReleaseKeys();
}

JNIEXPORT jboolean JNICALL
Java_com_kozhevin_rootchecks_util_MeatGrinder_isFoundDangerousProps(
        JNIEnv *env,
        jobject  this ) {

    return (jboolean) isFoundDangerousProps();
}

JNIEXPORT jboolean JNICALL
Java_com_kozhevin_rootchecks_util_MeatGrinder_isPermissiveSelinux(
        JNIEnv *env,
        jobject  this ) {

    return (jboolean) isPermissiveSelinux();
}

JNIEXPORT jboolean JNICALL
Java_com_kozhevin_rootchecks_util_MeatGrinder_isSuExists(
        JNIEnv *env,
        jobject  this ) {

    return (jboolean) isSuExists();
}

JNIEXPORT jboolean JNICALL
Java_com_kozhevin_rootchecks_util_MeatGrinder_isAccessedSuperuserApk(
        JNIEnv *env,
        jobject  this ) {

    return (jboolean) isAccessedSuperuserApk();
}

JNIEXPORT jboolean JNICALL
Java_com_kozhevin_rootchecks_util_MeatGrinder_isFoundSuBinary(
        JNIEnv *env,
        jobject  this ) {

    return (jboolean) isFoundSuBinary();
}

JNIEXPORT jboolean JNICALL
Java_com_kozhevin_rootchecks_util_MeatGrinder_isFoundBusyboxBinary(
        JNIEnv *env,
        jobject  this ) {

    return (jboolean) isFoundBusyboxBinary();
}


JNIEXPORT jboolean JNICALL
Java_com_kozhevin_rootchecks_util_MeatGrinder_isFoundXposed(
        JNIEnv *env,
        jobject  this ) {

    return (jboolean) isFoundXposed();
}

JNIEXPORT jboolean JNICALL
Java_com_kozhevin_rootchecks_util_MeatGrinder_isFoundResetprop(
        JNIEnv *env,
        jobject  this ) {

    return (jboolean) isFoundResetprop();
}

JNIEXPORT jboolean JNICALL
Java_com_kozhevin_rootchecks_util_MeatGrinder_isFoundWrongPathPermission(
        JNIEnv *env,
        jobject  this ) {

    return (jboolean) isFoundWrongPathPermission();
}

JNIEXPORT jboolean JNICALL
Java_com_kozhevin_rootchecks_util_MeatGrinder_isFoundHooks(
        JNIEnv *env,
        jobject  this ) {

    return (jboolean) isFoundHooks();
}

常量:

// Comma-separated tags describing the build, like= "unsigned,debug".
const char *const ANDROID_OS_BUILD_TAGS = "ro.build.tags";

// A string that uniquely identifies this build. 'BRAND/PRODUCT/DEVICE:RELEASE/ID/VERSION.INCREMENTAL:TYPE/TAGS'.
const char *const ANDROID_OS_BUILD_FINGERPRINT = "ro.build.fingerprint";

const char *const ANDROID_OS_SECURE = "ro.secure";

const char *const ANDROID_OS_DEBUGGABLE = "ro.debuggable";
const char *const ANDROID_OS_SYS_INITD = "sys.initd";
const char *const ANDROID_OS_BUILD_SELINUX = "ro.build.selinux";
//see https://android.googlesource.com/platform/system/core/+/master/adb/services.cpp#86
const char *const SERVICE_ADB_ROOT = "service.adb.root";

const char * const MG_SU_PATH[] = {
        "/data/local/",
        "/data/local/bin/",
        "/data/local/xbin/",
        "/sbin/",
        "/system/bin/",
        "/system/bin/.ext/",
        "/system/bin/failsafe/",
        "/system/sd/xbin/",
        "/su/xbin/",
        "/su/bin/",
        "/magisk/.core/bin/",
        "/system/usr/we-need-root/",
        "/system/xbin/",
        0
};

const char * const MG_EXPOSED_FILES[] = {
        "/system/lib/libxposed_art.so",
        "/system/lib64/libxposed_art.so",
        "/system/xposed.prop",
        "/cache/recovery/xposed.zip",
        "/system/framework/XposedBridge.jar",
        "/system/bin/app_process64_xposed",
        "/system/bin/app_process32_xposed",
        "/magisk/xposed/system/lib/libsigchain.so",
        "/magisk/xposed/system/lib/libart.so",
        "/magisk/xposed/system/lib/libart-disassembler.so",
        "/magisk/xposed/system/lib/libart-compiler.so",
        "/system/bin/app_process32_orig",
        "/system/bin/app_process64_orig",
        0
};

const char * const MG_READ_ONLY_PATH[] = {
        "/system",
        "/system/bin",
        "/system/sbin",
        "/system/xbin",
        "/vendor/bin",
        "/sbin",
        "/etc",
        0
};

本地代码的根检测:

struct mntent *getMntent(FILE *fp, struct mntent *e, char *buf, int buf_len) {

    while (fgets(buf, buf_len, fp) != NULL) {
        // Entries look like "/dev/block/vda /system ext4 ro,seclabel,relatime,data=ordered 0 0".
        // That is: mnt_fsname mnt_dir mnt_type mnt_opts mnt_freq mnt_passno.
        int fsname0, fsname1, dir0, dir1, type0, type1, opts0, opts1;
        if (sscanf(buf, " %n%*s%n %n%*s%n %n%*s%n %n%*s%n %d %d",
                   &fsname0, &fsname1, &dir0, &dir1, &type0, &type1, &opts0, &opts1,
                   &e->mnt_freq, &e->mnt_passno) == 2) {
            e->mnt_fsname = &buf[fsname0];
            buf[fsname1] = '\0';
            e->mnt_dir = &buf[dir0];
            buf[dir1] = '\0';
            e->mnt_type = &buf[type0];
            buf[type1] = '\0';
            e->mnt_opts = &buf[opts0];
            buf[opts1] = '\0';
            return e;
        }
    }
    return NULL;
}


bool isPresentMntOpt(const struct mntent *pMnt, const char *pOpt) {
    char *token = pMnt->mnt_opts;
    const char *end = pMnt->mnt_opts + strlen(pMnt->mnt_opts);
    const size_t optLen = strlen(pOpt);
    while (token != NULL) {
        const char *tokenEnd = token + optLen;
        if (tokenEnd > end) break;
        if (memcmp(token, pOpt, optLen) == 0 &&
            (*tokenEnd == '\0' || *tokenEnd == ',' || *tokenEnd == '=')) {
            return true;
        }
        token = strchr(token, ',');
        if (token != NULL) {
            token++;
        }
    }
    return false;
}

static char *concat2str(const char *pString1, const char *pString2) {
    char *result;
    size_t lengthBuffer = 0;

    lengthBuffer = strlen(pString1) +
                   strlen(pString2) + 1;
    result = malloc(lengthBuffer);
    if (result == NULL) {
        GR_LOGW("malloc failed\n");
        return NULL;
    }
    memset(result, 0, lengthBuffer);
    strcpy(result, pString1);
    strcat(result, pString2);
    return result;
}

static bool
isBadPropertyState(const char *key, const char *badValue, bool isObligatoryProperty, bool isExact) {
    if (badValue == NULL) {
        GR_LOGE("badValue may not be NULL");
        return false;
    }
    if (key == NULL) {
        GR_LOGE("key may not be NULL");
        return false;
    }
    char value[PROP_VALUE_MAX + 1];
    int length = __system_property_get(key, value);
    bool result = false;
    /* A length 0 value indicates that the property is not defined */
    if (length > 0) {
        GR_LOGI("property:[%s]==[%s]", key, value);
        if (isExact) {
            if (strcmp(value, badValue) == 0) {
                GR_LOGW("bad value[%s] equals to [%s] in the property [%s]", value, badValue, key);
                result = true;
            }
        } else {
            if (strlen(value) >= strlen(badValue) && strstr(value, badValue) != NULL) {
                GR_LOGW("bad value[%s] found in [%s] in the property [%s]", value, badValue, key);
                result = true;
            }
        }
    } else {
        GR_LOGI("[%s] property not found", key);
        if (isObligatoryProperty) {
            result = true;
        }
    }
    return result;
}

bool isDetectedTestKeys() {
    const char *TEST_KEYS_VALUE = "test-keys";
    return isBadPropertyState(ANDROID_OS_BUILD_TAGS, TEST_KEYS_VALUE, true, false);
}

bool isDetectedDevKeys() {
    const char *DEV_KEYS_VALUE = "dev-keys";
    return isBadPropertyState(ANDROID_OS_BUILD_TAGS, DEV_KEYS_VALUE, true, false);
}

bool isNotFoundReleaseKeys() {
    const char *RELEASE_KEYS_VALUE = "release-keys";
    return !isBadPropertyState(ANDROID_OS_BUILD_TAGS, RELEASE_KEYS_VALUE, false, true);
}

bool isFoundWrongPathPermission() {

    bool result = false;
    FILE *file = fopen("/proc/mounts", "r");
    char mntent_strings[BUFSIZ];
    if (file == NULL) {
        GR_LOGE("setmntent");
        return result;
    }

    struct mntent ent = {0};
    while (NULL != getMntent(file, &ent, mntent_strings, sizeof(mntent_strings))) {
        for (size_t i = 0; MG_READ_ONLY_PATH[i]; i++) {
            if (strcmp((&ent)->mnt_dir, MG_READ_ONLY_PATH[i]) == 0 &&
                isPresentMntOpt(&ent, "rw")) {
                GR_LOGI("%s %s %s %s\n", (&ent)->mnt_fsname, (&ent)->mnt_dir, (&ent)->mnt_opts,
                        (&ent)->mnt_type);
                result = true;
                break;
            }
        }
        memset(&ent, 0, sizeof(ent));
    }
    fclose(file);
    return result;
}


bool isFoundDangerousProps() {
    const char *BAD_DEBUGGABLE_VALUE = "1";
    const char *BAD_SECURE_VALUE = "0";
    const char *BAD_SYS_INITD_VALUE = "1";
    const char *BAD_SERVICE_ADB_ROOT_VALUE = "1";

    bool result = isBadPropertyState(ANDROID_OS_DEBUGGABLE, BAD_DEBUGGABLE_VALUE, true, true) ||
                  isBadPropertyState(SERVICE_ADB_ROOT, BAD_SERVICE_ADB_ROOT_VALUE, false, true) ||
                  isBadPropertyState(ANDROID_OS_SECURE, BAD_SECURE_VALUE, true, true) ||
                  isBadPropertyState(ANDROID_OS_SYS_INITD, BAD_SYS_INITD_VALUE, false, true);

    return result;
}

bool isPermissiveSelinux() {
    const char *BAD_VALUE = "0";
    return isBadPropertyState(ANDROID_OS_BUILD_SELINUX, BAD_VALUE, false, false);
}

bool isSuExists() {
    char buf[BUFSIZ];
    char *str = NULL;
    char *temp = NULL;
    size_t size = 1;  // start with size of 1 to make room for null terminator
    size_t strlength;

    FILE *pipe = popen("which su", "r");
    if (pipe == NULL) {
        GR_LOGI("pipe is null");
        return false;
    }

    while (fgets(buf, sizeof(buf), pipe) != NULL) {
        strlength = strlen(buf);
        temp = realloc(str, size + strlength);  // allocate room for the buf that gets appended
        if (temp == NULL) {
            // allocation error
            GR_LOGE("Error (re)allocating memory");
            pclose(pipe);
            if (str != NULL) {
                free(str);
            }
            return false;
        } else {
            str = temp;
        }
        strcpy(str + size - 1, buf);
        size += strlength;
    }
    pclose(pipe);
    GR_LOGW("A size of the result from pipe is [%zu], result:\n [%s] ", size, str);
    if (str != NULL) {
        free(str);
    }
    return size > 1 ? true : false;
}

static bool isAccessedFile(const char *path) {
    int result = access(path, F_OK);
    GR_LOGV("[%s] has been accessed with result: [%d]", path, result);
    return result == 0 ? true : false;
}

static bool isFoundBinaryFromArray(const char *const *array, const char *binary) {
    for (size_t i = 0; array[i]; ++i) {
        char *checkedPath = concat2str(array[i], binary);
        if (checkedPath == NULL) { // malloc failed
            return false;
        }
        bool result = isAccessedFile(checkedPath);
        free(checkedPath);
        if (result) {
            return result;
        }
    }
    return false;
}

bool isAccessedSuperuserApk() {
    return isAccessedFile("/system/app/Superuser.apk");
}

bool isFoundResetprop() {
    return isAccessedFile("/data/magisk/resetprop");
}

bool isFoundSuBinary() {
    return isFoundBinaryFromArray(MG_SU_PATH, "su");
}

bool isFoundBusyboxBinary() {
    return isFoundBinaryFromArray(MG_SU_PATH, "busybox");
}

bool isFoundXposed() {
    for (size_t i = 0; MG_EXPOSED_FILES[i]; ++i) {
        bool result = isAccessedFile(MG_EXPOSED_FILES[i]);
        if (result) {
            return result;
        }
    }
    return false;
}

bool isFoundHooks() {
    bool result = false;
    pid_t pid = getpid();
    char maps_file_name[512];
    sprintf(maps_file_name, "/proc/%d/maps", pid);
    GR_LOGI("try to open [%s]", maps_file_name);
    const size_t line_size = BUFSIZ;
    char *line = malloc(line_size);
    if (line == NULL) {
        return result;
    }
    FILE *fp = fopen(maps_file_name, "r");
    if (fp == NULL) {
        free(line);
        return result;
    }
    memset(line, 0, line_size);
    const char *substrate = "com.saurik.substrate";
    const char *xposed = "XposedBridge.jar";
    while (fgets(line, line_size, fp) != NULL) {
        const size_t real_line_size = strlen(line);
        if ((real_line_size >= strlen(substrate) && strstr(line, substrate) != NULL) ||
            (real_line_size >= strlen(xposed) && strstr(line, xposed) != NULL)) {
            GR_LOGI("found in [%s]: [%s]", maps_file_name, line);
            result = true;
            break;
        }
    }
    free(line);
    fclose(fp);
    return result;
}

一些用于设置系统属性ro的修改版本。Modversion用于此目的。事情似乎已经发生了变化;我几个月前在TheDude上的构建是这样的:

cmb@apollo:~$ adb -d shell getprop |grep build
[ro.build.id]: [CUPCAKE]
[ro.build.display.id]: [htc_dream-eng 1.5 CUPCAKE eng.TheDudeAbides.20090427.235325 test-keys]
[ro.build.version.incremental]: [eng.TheDude.2009027.235325]
[ro.build.version.sdk]: [3]
[ro.build.version.release]: [1.5]
[ro.build.date]: [Mon Apr 20 01:42:32 CDT 2009]
[ro.build.date.utc]: [1240209752]
[ro.build.type]: [eng]
[ro.build.user]: [TheDude]
[ro.build.host]: [ender]
[ro.build.tags]: [test-keys]
[ro.build.product]: [dream]
[ro.build.description]: [kila-user 1.1 PLAT-RC33 126986 ota-rel-keys,release-keys]
[ro.build.fingerprint]: [tmobile/kila/dream/trout:1.1/PLAT-RC33/126986:user/ota-rel-keys,release-keys]
[ro.build.changelist]: [17615# end build properties]

另一方面,来自1.5 SDK的模拟器,运行1.5镜像,也有根,可能类似于Android Dev Phone 1(你可能想要允许),并有这个:

cmb@apollo:~$ adb -e shell getprop |grep build
[ro.build.id]: [CUPCAKE]
[ro.build.display.id]: [sdk-eng 1.5 CUPCAKE 148875 test-keys]
[ro.build.version.incremental]: [148875]
[ro.build.version.sdk]: [3]
[ro.build.version.release]: [1.5]
[ro.build.date]: [Thu May 14 18:09:10 PDT 2009]
[ro.build.date.utc]: [1242349750]
[ro.build.type]: [eng]
[ro.build.user]: [android-build]
[ro.build.host]: [undroid16.mtv.corp.google.com]
[ro.build.tags]: [test-keys]
[ro.build.product]: [generic]
[ro.build.description]: [sdk-eng 1.5 CUPCAKE 148875 test-keys]
[ro.build.fingerprint]: [generic/sdk/generic/:1.5/CUPCAKE/148875:eng/test-keys]

至于零售构建,我手头没有,但在site:xda developers.com下的各种搜索提供了大量信息。这是荷兰的G1, ro。build。tags没有test-key,我认为这可能是最可靠的属性。

使用谷歌SafetyNet认证API,您可以轻松检查您的设备是否已根:

Add dependency in build.gradle(:app) implementation 'com.google.android.gms:play-services-safetynet:17.0.0' Get Api key and enable Android Device Verification API using link public static void sendSafetyNetRequest(Activity context) { if(GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context, 13000000) == ConnectionResult.SUCCESS) { Log.e(TAG, "The SafetyNet Attestation API is available"); // TODO(developer): Change the nonce generation to include your own, used once value, // ideally from your remote server. String nonceData = "Safety Net Sample: " + System.currentTimeMillis(); ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); Random mRandom = new SecureRandom(); byte[] bytes = new byte[24]; mRandom.nextBytes(bytes); try { byteStream.write(bytes); byteStream.write(nonceData.getBytes()); } catch (IOException e) { e.printStackTrace(); } byte[] nonce = byteStream.toByteArray(); SafetyNetClient client = SafetyNet.getClient(context); Task<SafetyNetApi.AttestationResponse> task = client.attest(nonce, API_KEY_FROM_STEP_2_LINK); task.addOnSuccessListener(context, attestationResponse -> { /* TODO(developer): Forward this result to your server together with the nonce for verification. You can also parse the JwsResult locally to confirm that the API returned a response by checking for an 'error' field first and before retrying the request with an exponential backoff. NOTE: Do NOT rely on a local, client-side only check for security, you must verify the response on a remote server! */ String jwsResult = attestationResponse.getJwsResult(); Log.e(TAG, "Success! SafetyNet result:\n" + jwsResult + "\n"); if (jwsResult == null) { Log.e(TAG, "jwsResult Null"); } final String[] jwtParts = jwsResult.split("\\."); if (jwtParts.length == 3) { String decodedPayload = new String(Base64.decode(jwtParts[1], Base64.DEFAULT)); Log.e(TAG, "decodedPayload : " + decodedPayload); } }); task.addOnFailureListener(context, e -> { // An error occurred while communicating with the service. String mResult = null; if (e instanceof ApiException) { // An error with the Google Play Services API contains some additional details. ApiException apiException = (ApiException) e; Util.showLog(TAG, "Error: " + CommonStatusCodes.getStatusCodeString(apiException.getStatusCode()) + ": " + apiException.getStatusMessage()); } else { // A different, unknown type of error occurred. Log.e(TAG, "ERROR! " + e.getMessage()); } }); } else { Log.e(TAG, "Prompt user to update Google Play services."; } } ` Check your logs for decodedPayload if ctsProfileMatch and basicIntegrity both are false it means your device is rooted . The Attestation API returns a JWS response which looks like:

{"nonce": "6pLrr9zWyl6TNzj+kpbR4LZcfPY3U2FmZXR5IE5ldCBTYW1wbGU6IDE2MTQ2NzkwMTIzNjc=", " timestamms ": 9860437986543, "apkPackageName": "你的包名将显示在这里","ctsProfileMatch": true, "apkDigestSha256": ["base64编码,用于签名请求应用程序的证书的SHA-256哈希值"],"basicIntegrity": true, "evaluationType": "BASIC"}

欲了解更多信息,请查看此链接。

if [[ "`adb shell which su | grep -io "permission denied"`" != "permission denied" ]]; then
   echo "Yes. Rooted device."
 else
   echo "No. Device not rooted. Only limited tasks can be performed. Done."
    zenity --warning --title="Device Not Rooted" --text="The connected Android Device is <b>NOT ROOTED</b>. Only limited tasks can be performed." --no-wrap
fi

这里列出的许多答案都有内在的问题:

Checking for test-keys is correlated with root access but doesn't necessarily guarantee it "PATH" directories should be derived from the actual "PATH" environment variable instead of being hard coded The existence of the "su" executable doesn't necessarily mean the device has been rooted The "which" executable may or may not be installed, and you should let the system resolve its path if possible Just because the SuperUser app is installed on the device does not mean the device has root access yet

Stericson的RootTools库似乎可以更合理地检查root。它还有很多额外的工具和实用程序,所以我强烈推荐它。然而,并没有解释它是如何专门检查根目录的,而且它可能比大多数应用程序真正需要的要重一些。

我已经创建了几个基于RootTools库的实用程序方法。如果你只是想检查“su”可执行文件是否在设备上,你可以使用以下方法:

public static boolean isRootAvailable(){
    for(String pathDir : System.getenv("PATH").split(":")){
        if(new File(pathDir, "su").exists()) {
            return true;
        }
    }
    return false;
}

这个方法简单地遍历“PATH”环境变量中列出的目录,并检查其中是否存在“su”文件。

为了真正检查root访问权限,必须实际运行“su”命令。如果安装了一个像SuperUser这样的应用程序,那么在这一点上它可能会要求root访问,或者如果它已经被授予/拒绝,吐司可能会显示是否授予/拒绝访问。一个很好的命令是“id”,这样您可以验证用户id实际上是0(根)。

下面是一个示例方法来确定是否授予了根访问权:

public static boolean isRootGiven(){
    if (isRootAvailable()) {
        Process process = null;
        try {
            process = Runtime.getRuntime().exec(new String[]{"su", "-c", "id"});
            BufferedReader in = new BufferedReader(new InputStreamReader(process.getInputStream()));
            String output = in.readLine();
            if (output != null && output.toLowerCase().contains("uid=0"))
                return true;
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (process != null)
                process.destroy();
        }
    }

    return false;
}

实际测试运行"su"命令是很重要的,因为一些模拟器已经预安装了"su"可执行文件,但只允许某些用户像adb shell一样访问它。

在尝试运行“su”可执行文件之前检查它的存在也是很重要的,因为android已经知道不会正确地处理试图运行缺失命令的进程。随着时间的推移,这些幽灵进程会消耗大量内存。