严格模式也能干大事

StrictMode按字面翻译,也称作严格模式,可以用于检测UI线程上的磁盘操作和网络请求等。

严格模式检查项

检测两大类型,分别是线程相关setThreadPolicy和虚拟机相关setVmPolicy。

  • setThreadPolicy
  1. 设置线程需要检测的策略(当前线程实际就是main线程);
  2. 设置检测命中后的警告方式,如日志输出,弹框,杀死进程,屏幕闪烁

具体来说,目前支持如下几种检测:

  1. detectDiskReads 磁盘读操作
  2. detectDiskWrites 磁盘写操作
  3. detectNetwork 网络连接
  4. detectCustomSlowCalls 自定义的耗时操作
  5. detectResourceMismatches >=API 23 资源类型不匹配
  6. detectUnbufferedIo >= API 26 未缓冲的IO
  • setVmPolicy
  1. 设置虚拟机进程检测的策略(包括进程内的任意线程)
  2. 设置检测命中后的警告方式,如日志输出,弹框,杀死进程,屏幕闪烁

具体来说,目前支持如下几种检测:

  1. detectActivityLeaks Activity泄漏
  2. detectLeakedClosableObjects 未调用关闭方法,比如Closeable的close
  3. detectLeakedRegistrationObjects 为注销监听/注册,比如BroadcastReceiver
  4. detectLeakedSqlLiteObjects sql游标未关闭
  5. detectFileUriExposure >= API 18 对外暴露file://
  6. detectCleartextNetwork >= API 23 网路操作明文检测,未使用SSL/TLS加密
  7. detectUntaggedSockets >= API 26 为标记Socket连接,TrafficStats
  8. detectContentUriWithoutPermission >= API 26 对外暴露content://时,未做权限要求

已知限制

前面介绍了很多种检测项,那么作为开发者是不是都需要记住?

笔者建议简单了解即可,不需要记住每一项检测,因为检测项很多,而且存在不确定性,要记住是比较麻烦的事情。 在实际工作中,只需要通过代码提示即可,同时我们也可以通过detectAll(),直接开启所有检测项:

StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder().detectAll().penaltyLog().build());
StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder().detectAll().penaltyLog().build());

StrictMode虽然可以很好地检测UI线程上的磁盘读写和网络请求,但是需要注意:

  1. 不保证能检测出所有问题;
  2. 目前不支持检测JNI调用中的问题;
  3. 确保在发布版本中关闭StrictMode, 未来严格模式所支持的内容存在不确定性,可能增加,也可能减少;

正确设置检测项

根据官网的介绍,设置检测项可以通过StrictMode配置,写在Application或者Activity的onCreate方法中(或者其他组件的onCreate方法)。网上很多接扫文章也会说直接写在Application的onCreate中,如此只需要配置一次就可以实现对整个App的检测。

那么是不是这样呢?

Example code to enable from early in your Application, Activity, or other application component's onCreate() method:

应该说不完全正确,下面我们通过案例来解释一下。

public class CustomApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        if (BuildConfig.DEVELOP_MODE) {
            StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
                    .detectDiskReads()
                    .detectDiskWrites()
                    .detectNetwork()
                    .detectCustomSlowCalls()
                    // API 23
                    .detectResourceMismatches()
                    // API 26
                    //.detectUnbufferedIo()
                    .penaltyLog()
                    .build());
            StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
                    .detectActivityLeaks()
                    .detectLeakedClosableObjects()
                    .detectLeakedRegistrationObjects()
                    .detectLeakedSqlLiteObjects()
                    // API 18
                    .detectFileUriExposure()
                    // API 23
                    .detectCleartextNetwork()
                    // API 26
                    //.detectUntaggedSockets()
                    //.detectContentUriWithoutPermission()
                    .penaltyLog()
                    .build());
        }
    }
}

在这段配置中,我们配置了磁盘IO读写,和自定义的慢检测。如果此时你在Activity中进行主线程的IO读写,慢操作执行,都是不会被检测出来的。

读者可以花几分钟思考一下,这是为什么?

... ...

现在如果你还没有找到原因,继续往下看。通过阅读StrictMode的源码我们知道,配置属性通过Builder模式,记录在一个整形变量中mask, 经由 ThreadPolicy 最后设置给 AndroidBlockGuardPolicyBinder:

private static void setThreadPolicyMask(final int policyMask) {
    // In addition to the Java-level thread-local in Dalvik's
    // BlockGuard, we also need to keep a native thread-local in
    // Binder in order to propagate the value across Binder calls,
    // even across native-only processes.  The two are kept in
    // sync via the callback to onStrictModePolicyChange, below.
    setBlockGuardPolicy(policyMask);

    // And set the Android native version...
    Binder.setThreadStrictModePolicy(policyMask);
}

// Sets the policy in Dalvik/libcore (BlockGuard)
private static void setBlockGuardPolicy(final int policyMask) {
    if (policyMask == 0) {
        BlockGuard.setThreadPolicy(BlockGuard.LAX_POLICY);
        return;
    }
    final BlockGuard.Policy policy = BlockGuard.getThreadPolicy();
    final AndroidBlockGuardPolicy androidPolicy;
    if (policy instanceof AndroidBlockGuardPolicy) {
        androidPolicy = (AndroidBlockGuardPolicy) policy;
    } else {
        androidPolicy = threadAndroidPolicy.get();
        BlockGuard.setThreadPolicy(androidPolicy);
    }
    androidPolicy.setPolicyMask(policyMask);
}

最后通过按位与操作,判断是否需要处理,比如:

// Not part of BlockGuard.Policy; just part of StrictMode:
void onCustomSlowCall(String name) {
    if ((mPolicyMask & DETECT_CUSTOM) == 0) {
        return;
    }
    if (tooManyViolationsThisLoop()) {
        return;
    }
    BlockGuard.BlockGuardPolicyException e = new StrictModeCustomViolation(mPolicyMask, name);
    e.fillInStackTrace();
    startHandlingViolationException(e);
}

通过跟踪设置逻辑,我们知道,检测项最终能否给开发者警告提示需要经过很多层判断,比如设置项判断,当前以及触发的警告数量是否超过阈值: MAX_OFFENSES_PER_LOOP = 10

在上诉示例中,我们的情况是(mPolicyMask & DETECT_CUSTOM) == 0检查未通过,直接return,因此是设置失效。

为什么在Application中的设置会失效? 要理解这个问题,需要继续查阅源代码ActivityThread#handleBindApplication

我们知道Application被创建之后需要经过ActivityThread来显示调用onCreate方法, 下面这段代码很明显,在调用onCreate之前记录了ThreadPolicy为运行磁盘读写,并且在onCreate完成之后被设置到了StrictMode中,所以在onCreate中我们显示设置的检查项配置,最后会被系统预设的配置给覆盖掉。

// Allow disk access during application and provider setup. This could
// block processing ordered broadcasts, but later processing would
// probably end up doing the same disk access.
final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskWrites();
try {
    // If the app is being launched for full backup or restore, bring it up in
    // a restricted environment with the base application class.
    Application app = data.info.makeApplication(data.restrictedBackupMode, null);
    mInitialApplication = app;

    // don't bring up providers in restricted mode; they may depend on the
    // app's custom Application class
    if (!data.restrictedBackupMode) {
        if (!ArrayUtils.isEmpty(data.providers)) {
            installContentProviders(app, data.providers);
            // For process that contains content providers, we want to
            // ensure that the JIT is enabled "at some point".
            mH.sendEmptyMessageDelayed(H.ENABLE_JIT, 10*1000);
        }
    }

    // Do this after providers, since instrumentation tests generally start their
    // test thread at this point, and we don't want that racing.
    try {
        mInstrumentation.onCreate(data.instrumentationArgs);
    }
    catch (Exception e) {
        throw new RuntimeException(
            "Exception thrown in onCreate() of "
            + data.instrumentationName + ": " + e.toString(), e);
    }

    try {
        mInstrumentation.callApplicationOnCreate(app);
    } catch (Exception e) {
        if (!mInstrumentation.onException(app, e)) {
            throw new RuntimeException(
                "Unable to create application " + app.getClass().getName()
                + ": " + e.toString(), e);
        }
    }
} finally {
    StrictMode.setThreadPolicy(savedPolicy);
}

要解决这个问题,也很简单,可以将配置延迟到onCreate之后。比如放到第一个Activity中,或者通过Handler延迟处理。

new Handler().postAtFrontOfQueue(new Runnable() {
    @Override
    public void run() {
        StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder().detectAll().penaltyLog().build());
        StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder().detectAll().penaltyLog().build());
    }
});

检测示例

开启检测项既可以根据需要开启每一项,也可以直接调用detectAll检测所有支持项。下面我们举几个例子来看看检测效果。

UI线程磁盘读写操作

public class DiskIOSuspicious implements ISuspicious {
    private WeakReference<Context> reference;

    public DiskIOSuspicious(Context context) {
        this.reference = new WeakReference<Context>(context.getApplicationContext());
    }

    @Override
    public void onDoingSomething() {
        Context context = reference.get();
        if (context == null) {
            return;
        }
        File file = new File(context.getCacheDir(), "hello-world");
        if (!file.exists()) {
            try {
                file.createNewFile();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        try {
            FileOutputStream outputStream = new FileOutputStream(file);
            write2Stream(outputStream);
            outputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        try {
            FileInputStream inputStream = new FileInputStream(file);
            read2Log(inputStream);
            inputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

UI线程网络操作

public class NetworkSuspicious implements ISuspicious {
    @Override
    public void onDoingSomething() {
        try {
            URL url = new URL("https://www.baidu.com");
            URLConnection connection = url.openConnection();
            connection.connect();
            InputStream inputStream = connection.getInputStream();
            SimpleUtils.read2Log(inputStream);
            inputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

自定义耗时操作

public class SlowSuspicious implements ISuspicious {
    private static final int THRESHOLD = 16; // ms
    private static final int WIDTH = 1080;
    private static final int HEIGHT = 1920;

    @Override
    public void onDoingSomething() {
        long start = System.currentTimeMillis();
        Bitmap bitmap = Bitmap.createBitmap(WIDTH, HEIGHT, Bitmap.Config.ARGB_8888);
        for (int x = 0; x < WIDTH; x++) {
            for (int y = 0; y < HEIGHT; y++) {
                bitmap.setPixel(x, y, randomPixel(x, y));
            }
        }
        long end = System.currentTimeMillis();
        long diff = end - start;
        Log.i("Detect", "time diff=" + diff);
        if (diff > THRESHOLD) {
            Log.i("Detect", "notify slow method" );
            StrictMode.noteSlowCall("SlowSuspicious");
        }
    }

    private int randomPixel(int x, int y) {
        return (int) (Math.random() * 0xff);
    }
}

Activity泄漏检测

public class LeakActivitySuspicious implements ISuspicious {
    WeakReference<Context> mReference;

    public LeakActivitySuspicious(Context context) {
        this.mReference = new WeakReference<Context>(context);
    }

    @Override
    public void onDoingSomething() {
        Context context = mReference.get();
        if (context == null) {
            return;
        }
        context.startActivity(new Intent(context, LeakActivity.class));
        context.startActivity(new Intent(context, LeakActivity.class));
    }
}

Activity泄漏检测原理

ActivityThread#performLaunchActivity

显示调用StrictMode.incrementExpectedActivityCount(activity.getClass()); 触发引用计数

ContextImpl appContext = createBaseContextForActivity(r);
Activity activity = null;
try {
    java.lang.ClassLoader cl = appContext.getClassLoader();
    activity = mInstrumentation.newActivity(
            cl, component.getClassName(), r.intent);
    StrictMode.incrementExpectedActivityCount(activity.getClass());
    r.intent.setExtrasClassLoader(cl);
    r.intent.prepareToEnterProcess();
    if (r.state != null) {
        r.state.setClassLoader(cl);
    }
} catch (Exception e) {
    if (!mInstrumentation.onException(activity, e)) {
        throw new RuntimeException(
            "Unable to instantiate activity " + component
            + ": " + e.toString(), e);
    }
}

ActivityThread#performDestroyActivity 显示调用StrictMode.decrementExpectedActivityCount(activityClass);

private ActivityClientRecord performDestroyActivity(IBinder token, boolean finishing,
        int configChanges, boolean getNonConfigInstance) {
    ActivityClientRecord r = mActivities.get(token);
    Class<? extends Activity> activityClass = null;
    if (localLOGV) Slog.v(TAG, "Performing finish of " + r);
    if (r != null) {
        activityClass = r.activity.getClass();
        r.activity.mConfigChangeFlags |= configChanges;
        if (finishing) {
            r.activity.mFinished = true;
        }

        performPauseActivityIfNeeded(r, "destroy");

        if (!r.stopped) {
            try {
                r.activity.performStop(r.mPreserveWindow);
            } catch (SuperNotCalledException e) {
                throw e;
            } catch (Exception e) {
                if (!mInstrumentation.onException(r.activity, e)) {
                    throw new RuntimeException(
                            "Unable to stop activity "
                            + safeToComponentShortString(r.intent)
                            + ": " + e.toString(), e);
                }
            }
            r.stopped = true;
            EventLog.writeEvent(LOG_AM_ON_STOP_CALLED, UserHandle.myUserId(),
                    r.activity.getComponentName().getClassName(), "destroy");
        }
        if (getNonConfigInstance) {
            try {
                r.lastNonConfigurationInstances
                        = r.activity.retainNonConfigurationInstances();
            } catch (Exception e) {
                if (!mInstrumentation.onException(r.activity, e)) {
                    throw new RuntimeException(
                            "Unable to retain activity "
                            + r.intent.getComponent().toShortString()
                            + ": " + e.toString(), e);
                }
            }
        }
        try {
            r.activity.mCalled = false;
            mInstrumentation.callActivityOnDestroy(r.activity);
            if (!r.activity.mCalled) {
                throw new SuperNotCalledException(
                    "Activity " + safeToComponentShortString(r.intent) +
                    " did not call through to super.onDestroy()");
            }
            if (r.window != null) {
                r.window.closeAllPanels();
            }
        } catch (SuperNotCalledException e) {
            throw e;
        } catch (Exception e) {
            if (!mInstrumentation.onException(r.activity, e)) {
                throw new RuntimeException(
                        "Unable to destroy activity " + safeToComponentShortString(r.intent)
                        + ": " + e.toString(), e);
            }
        }
    }
    mActivities.remove(token);
    StrictMode.decrementExpectedActivityCount(activityClass);
    return r;
}

参考

powered by Gitbook更新: 2018-04-02

results matching ""

    No results matching ""