严格模式也能干大事
StrictMode按字面翻译,也称作严格模式,可以用于检测UI线程上的磁盘操作和网络请求等。
严格模式检查项
检测两大类型,分别是线程相关setThreadPolicy和虚拟机相关setVmPolicy。
- setThreadPolicy
- 设置线程需要检测的策略(当前线程实际就是main线程);
- 设置检测命中后的警告方式,如日志输出,弹框,杀死进程,屏幕闪烁
具体来说,目前支持如下几种检测:
- detectDiskReads 磁盘读操作
- detectDiskWrites 磁盘写操作
- detectNetwork 网络连接
- detectCustomSlowCalls 自定义的耗时操作
- detectResourceMismatches >=API 23 资源类型不匹配
- detectUnbufferedIo >= API 26 未缓冲的IO
- setVmPolicy
- 设置虚拟机进程检测的策略(包括进程内的任意线程)
- 设置检测命中后的警告方式,如日志输出,弹框,杀死进程,屏幕闪烁
具体来说,目前支持如下几种检测:
- detectActivityLeaks Activity泄漏
- detectLeakedClosableObjects 未调用关闭方法,比如Closeable的close
- detectLeakedRegistrationObjects 为注销监听/注册,比如BroadcastReceiver
- detectLeakedSqlLiteObjects sql游标未关闭
- detectFileUriExposure >= API 18 对外暴露file://
- detectCleartextNetwork >= API 23 网路操作明文检测,未使用SSL/TLS加密
- detectUntaggedSockets >= API 26 为标记Socket连接,TrafficStats
- 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线程上的磁盘读写和网络请求,但是需要注意:
- 不保证能检测出所有问题;
- 目前不支持检测JNI调用中的问题;
- 确保在发布版本中关闭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
最后设置给 AndroidBlockGuardPolicy
与 Binder
:
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;
}