StrictMode 机制以及性能调优

作为Android开发,日常的开发工作中或多或少要接触到性能问题,比如我的Android程序运行缓慢卡顿,并且常常出现ANR对话框等等问题。既然有性能问题,就需要进行性能优化。正所谓工欲善其事,必先利其器。一个好的工具,可以帮助我们发现并定位问题,进而有的放矢进行解决。本文主要介绍StrictMode 在Android 应用开发中的应用和一些问题。

Google 官方的StrictMode文档: https://developer.android.com/reference/android/os/StrictMode


概述

StrictMode,严苛模式,是Android提供的一种运行时检测机制,用于检测代码运行时的一些不规范的操作,最常见的场景是用于发现主线程的IO操作和网络读写等耗时的操作。

StrictMode包含两个维度的概念:
Policy(策略): 是指StrictMode对一些违规操作的发现策略,分为两类:
一类是针对一个具体的线程(ThreadPolicy)。
线程策略检测的内容有

  • 自定义的耗时调用 使用detectCustomSlowCalls()开启
  • 磁盘读取操作 使用detectDiskReads()开启
  • 磁盘写入操作 使用detectDiskWrites()开启
  • 网络操作 使用detectNetwork()开启

    另一类是针对虚拟机的所有对象(VMPolicy)。
    虚拟机策略检测的内容有

  • Activity泄露 使用detectActivityLeaks()开启
  • 未关闭的Closable对象泄露 使用detectLeakedClosableObjects()开启
  • 泄露的Sqlite对象 使用detectLeakedSqlLiteObjects()开启
  • 检测实例数量 使用setClassInstanceLimit()开启

Penalty(惩罚):是指StrictMode发现违规操作后进行惩罚的方式,譬如绘制红框、打印日志、显示对话框、杀掉进程等。

Android在很多关键的代码路径上都植入了StrictMode,譬如磁盘读写、网络访问、系统进程启动等。StrictMode会根据设置的策略进行检查,如果某个进程在代码运行时出现了违规操作,那么就会受到”惩罚”。

应用程序可以利用StrictMode尽可能的发现一些编码的疏漏, Android在 packages/experimental/StrictModeTest 这个APK中提供了常见违规操作的样例, 谨作为大家的反面教材。

本文深入分析StrictMode背后的实现原理以及使用场景。

StrictMode机制

StrictMode的实现涉及到以下源码:
libcore/dalvik/src/main/java/dalvik/system/BlockGuard.java
libcore/dalvik/src/main/java/dalvik/system/CloseGuard.java
frameworks/base/core/java/android/os/StrictMode.java

总体而言,StrictMode机制所涉及到的代码量并不大,但Android中植入StrictMode的地方都是一些重要的关口,StrictMode所体现的面向接口编程的思想以及设计模式的应用,值得我们好好学习。 下面,我们就深入源码,分析一下StrictMode机制的内部实现。

BlockGuard和CloseGuard

StrictMode针对单个线程和虚拟机的所有对象都定义了检查策略,用来发现一些违规操作,譬如:主线程中的磁盘读/写、网络访问、未关闭cursor,这些操作都能够被StrictMode检查出来。 怎么做到的呢?在做这些操作时,植入StrictMode的检查代码就可以了。有一部分植入代码是建立在BlockGuard和CloseGuard之上的,可以说,StrictMode是建立在BlockGuard和CloseGuard之上的机制。

Guard有“守卫”的意思,Block是阻塞的意思,在进行一些耗时操作时,譬如磁盘读写、网络操作,有一个守卫在监测着,它就是BlockGuard,如果这些耗时的操作导致主线程阻塞,BlockGuard就会发出通知; Close对应到可打开的文件,在文件被打开后,也有一个守卫在监测着,它就是CloseGuard,如果没有关闭文件,则CloseGuard就会发出通知。

对应的来看一下 BlockGuard.java的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
package dalvik.system;

import java.io.FileDescriptor;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.net.SocketException;

/**
* Mechanism to let threads set restrictions on what code is allowed
* to do in their thread.
*
* <p>This is meant for applications to prevent certain blocking
* operations from running on their main event loop (or "UI") threads.
*
* <p>Note that this is all best-effort to catch most accidental mistakes
* and isn't intended to be a perfect mechanism, nor provide any sort of
* security.
*
* @hide
*/
public final class BlockGuard {

// TODO: refactor class name to something more generic, since its scope is
// growing beyond just blocking/logging.

public static final int DISALLOW_DISK_WRITE = 0x01;//不允许磁盘盘写
public static final int DISALLOW_DISK_READ = 0x02;//不允许从磁盘读
public static final int DISALLOW_NETWORK = 0x04;//不允许有网络操作
public static final int PASS_RESTRICTIONS_VIA_RPC = 0x08;
public static final int PENALTY_LOG = 0x10;//惩罚的log
public static final int PENALTY_DIALOG = 0x20;//惩罚的dialog
public static final int PENALTY_DEATH = 0x40;//惩罚kill

public interface Policy {
/**
* Called on disk writes.
*/
void onWriteToDisk();

/**
* Called on disk reads.
*/
void onReadFromDisk();

/**
* Called on network operations.
*/
void onNetwork();

/**
* Called on unbuffered input/ouput operations.
*/
void onUnbufferedIO();

/**
* Returns the policy bitmask, for shipping over Binder calls
* to remote threads/processes and reinstantiating the policy
* there. The bits in the mask are from the DISALLOW_* and
* PENALTY_* constants.
*/
int getPolicyMask();
}

public static class BlockGuardPolicyException extends RuntimeException {
// bitmask of DISALLOW_*, PENALTY_*, etc flags
private final int mPolicyState;
private final int mPolicyViolated;
private final String mMessage; // may be null

public BlockGuardPolicyException(int policyState, int policyViolated) {
this(policyState, policyViolated, null);
}

public BlockGuardPolicyException(int policyState, int policyViolated, String message) {
mPolicyState = policyState;
mPolicyViolated = policyViolated;
mMessage = message;
fillInStackTrace();
}

public int getPolicy() {
return mPolicyState;
}

public int getPolicyViolation() {
return mPolicyViolated;
}

public String getMessage() {
// Note: do not change this format casually. It's
// somewhat unfortunately Parceled and passed around
// Binder calls and parsed back into an Exception by
// Android's StrictMode. This was the least invasive
// option and avoided a gross mix of Java Serialization
// combined with Parcels.
return "policy=" + mPolicyState + " violation=" + mPolicyViolated +
(mMessage == null ? "" : (" msg=" + mMessage));
}
}

/**
* The default, permissive policy that doesn't prevent any operations.
*/
public static final Policy LAX_POLICY = new Policy() {
public void onWriteToDisk() {}
public void onReadFromDisk() {}
public void onNetwork() {}
public void onUnbufferedIO() {}
public int getPolicyMask() {
return 0;
}
};

private static ThreadLocal<Policy> threadPolicy = new ThreadLocal<Policy>() {
@Override protected Policy initialValue() {
return LAX_POLICY;
}
};

/**
* Get the current thread's policy.
*
* @return the current thread's policy. Never returns null.
* Will return the LAX_POLICY instance if nothing else is set.
*/
public static Policy getThreadPolicy() {
return threadPolicy.get();
}

/**
* Sets the current thread's block guard policy.
*
* @param policy policy to set. May not be null. Use the public LAX_POLICY
* if you want to unset the active policy.
*/
public static void setThreadPolicy(Policy policy) {
if (policy == null) {
throw new NullPointerException("policy == null");
}
threadPolicy.set(policy);
}

private BlockGuard() {}
}

从BlockGuard的代码来看,很简单,就定义了一些类型和方法接口。

同样的看看CloseGuard.java方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
package dalvik.system;

/**
* CloseGuard is a mechanism for flagging implicit finalizer cleanup of
* resources that should have been cleaned up by explicit close
* methods (aka "explicit termination methods" in Effective Java).
* <p>
* A simple example: <pre> {@code
* class Foo {
*
* private final CloseGuard guard = CloseGuard.get();
*
* ...
*
* public Foo() {
* ...;
* guard.open("cleanup");
* }
*
* public void cleanup() {
* guard.close();
* ...;
* }
*
* protected void finalize() throws Throwable {
* try {
* // Note that guard could be null if the constructor threw.
* if (guard != null) {
* guard.warnIfOpen();
* }
* cleanup();
* } finally {
* super.finalize();
* }
* }
* }
* }</pre>
*
* In usage where the resource to be explicitly cleaned up are
* allocated after object construction, CloseGuard protection can
* be deferred. For example: <pre> {@code
* class Bar {
*
* private final CloseGuard guard = CloseGuard.get();
*
* ...
*
* public Bar() {
* ...;
* }
*
* public void connect() {
* ...;
* guard.open("cleanup");
* }
*
* public void cleanup() {
* guard.close();
* ...;
* }
*
* protected void finalize() throws Throwable {
* try {
* // Note that guard could be null if the constructor threw.
* if (guard != null) {
* guard.warnIfOpen();
* }
* cleanup();
* } finally {
* super.finalize();
* }
* }
* }
* }</pre>
*
* When used in a constructor calls to {@code open} should occur at
* the end of the constructor since an exception that would cause
* abrupt termination of the constructor will mean that the user will
* not have a reference to the object to cleanup explicitly. When used
* in a method, the call to {@code open} should occur just after
* resource acquisition.
*
* @hide
*/
public final class CloseGuard {

/**
* Instance used when CloseGuard is disabled to avoid allocation.
*/
private static final CloseGuard NOOP = new CloseGuard();

/**
* Enabled by default so we can catch issues early in VM startup.
* Note, however, that Android disables this early in its startup,
* but enables it with DropBoxing for system apps on debug builds.
*/
private static volatile boolean ENABLED = true;

/**
* Hook for customizing how CloseGuard issues are reported.
*/
private static volatile Reporter REPORTER = new DefaultReporter();

/**
* The default {@link Tracker}.
*/
private static final DefaultTracker DEFAULT_TRACKER = new DefaultTracker();

/**
* Hook for customizing how CloseGuard issues are tracked.
*/
private static volatile Tracker currentTracker = DEFAULT_TRACKER;

/**
* Returns a CloseGuard instance. If CloseGuard is enabled, {@code
* #open(String)} can be used to set up the instance to warn on
* failure to close. If CloseGuard is disabled, a non-null no-op
* instance is returned.
*/
public static CloseGuard get() {
if (!ENABLED) {
return NOOP;
}
return new CloseGuard();
}

/**
* Used to enable or disable CloseGuard. Note that CloseGuard only
* warns if it is enabled for both allocation and finalization.
*/
public static void setEnabled(boolean enabled) {
ENABLED = enabled;
}

/**
* True if CloseGuard mechanism is enabled.
*/
public static boolean isEnabled() {
return ENABLED;
}

/**
* Used to replace default Reporter used to warn of CloseGuard
* violations. Must be non-null.
*/
public static void setReporter(Reporter reporter) {
if (reporter == null) {
throw new NullPointerException("reporter == null");
}
REPORTER = reporter;
}

/**
* Returns non-null CloseGuard.Reporter.
*/
public static Reporter getReporter() {
return REPORTER;
}

/**
* Sets the {@link Tracker} that is notified when resources are allocated and released.
*
* <p>This is only intended for use by {@code dalvik.system.CloseGuardSupport} class and so
* MUST NOT be used for any other purposes.
*
* @throws NullPointerException if tracker is null
*/
public static void setTracker(Tracker tracker) {
if (tracker == null) {
throw new NullPointerException("tracker == null");
}
currentTracker = tracker;
}

/**
* Returns {@link #setTracker(Tracker) last Tracker that was set}, or otherwise a default
* Tracker that does nothing.
*
* <p>This is only intended for use by {@code dalvik.system.CloseGuardSupport} class and so
* MUST NOT be used for any other purposes.
*/
public static Tracker getTracker() {
return currentTracker;
}

private CloseGuard() {}

/**
* If CloseGuard is enabled, {@code open} initializes the instance
* with a warning that the caller should have explicitly called the
* {@code closer} method instead of relying on finalization.
*
* @param closer non-null name of explicit termination method
* @throws NullPointerException if closer is null, regardless of
* whether or not CloseGuard is enabled
*/
public void open(String closer) {
// always perform the check for valid API usage...
if (closer == null) {
throw new NullPointerException("closer == null");
}
// ...but avoid allocating an allocationSite if disabled
if (this == NOOP || !ENABLED) {
return;
}
String message = "Explicit termination method '" + closer + "' not called";
allocationSite = new Throwable(message);
currentTracker.open(allocationSite);
}

private Throwable allocationSite;

/**
* Marks this CloseGuard instance as closed to avoid warnings on
* finalization.
*/
public void close() {
currentTracker.close(allocationSite);
allocationSite = null;
}

/**
* If CloseGuard is enabled, logs a warning if the caller did not
* properly cleanup by calling an explicit close method
* before finalization. If CloseGuard is disabled, no action is
* performed.
*/
public void warnIfOpen() {
if (allocationSite == null || !ENABLED) {
return;
}

String message =
("A resource was acquired at attached stack trace but never released. "
+ "See java.io.Closeable for information on avoiding resource leaks.");

REPORTER.report(message, allocationSite);
}

/**
* Interface to allow customization of tracking behaviour.
*
* <p>This is only intended for use by {@code dalvik.system.CloseGuardSupport} class and so
* MUST NOT be used for any other purposes.
*/
public interface Tracker {
void open(Throwable allocationSite);
void close(Throwable allocationSite);
}

/**
* Default tracker which does nothing special and simply leaves it up to the GC to detect a
* leak.
*/
private static final class DefaultTracker implements Tracker {
@Override
public void open(Throwable allocationSite) {
}

@Override
public void close(Throwable allocationSite) {
}
}

/**
* Interface to allow customization of reporting behavior.
*/
public interface Reporter {
void report (String message, Throwable allocationSite);
}

/**
* Default Reporter which reports CloseGuard violations to the log.
*/
private static final class DefaultReporter implements Reporter {
@Override public void report (String message, Throwable allocationSite) {
System.logW(message, allocationSite);
}
}
}

Android在很多代码中植入了BlockGuard,以BlockGuardOs为例,这个类代理大部分POSIX系统调用接口,所谓代理,从代码角度,就是在一个类外层再做一层封装。 BlockGuardOs代理了Os类,并植入了BlockGuard,譬如BlockGuardOs.read()这个系统调用:

1
2
3
4
public int read(FileDescriptor fd, byte[] bytes, int byteOffset, int byteCount) throws ErrnoException, InterruptedIOException {
BlockGuard.getThreadPolicy().onReadFromDisk();
return os.read(fd, bytes, byteOffset, byteCount);
}

经过BlockGuard的一层封装,在每次进行read()系统调用时,都会通过BlockGuard通知发生了读磁盘的操作:BlockGuard.getThreadPolicy().onReadFromDisk()
这里用到了BlockGuard的getThreadPolicy()方法,其实BlockGuard内部有一个Policy,定义了可能导致阻塞的方法:

1
2
3
4
5
6
public interface Policy {
void onWriteToDisk();
void onReadFromDisk();
void onNetwork();
int getPolicyMask();
}

这个Policy只是一个接口定义,专门暴露给外部的 ,StrictMode就实现了BlockGuard.Policy:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
private static class AndroidBlockGuardPolicy implements BlockGuard.Policy {
private int mPolicyMask;

// Map from violation stacktrace hashcode -> uptimeMillis of
// last violation. No locking needed, as this is only
// accessed by the same thread.
private ArrayMap<Integer, Long> mLastViolationTime;

public AndroidBlockGuardPolicy(final int policyMask) {
mPolicyMask = policyMask;
}

@Override
public String toString() {
return "AndroidBlockGuardPolicy; mPolicyMask=" + mPolicyMask;
}

// Part of BlockGuard.Policy interface:
public int getPolicyMask() {
return mPolicyMask;
}

// Part of BlockGuard.Policy interface:
public void onWriteToDisk() {
if ((mPolicyMask & DETECT_DISK_WRITE) == 0) {
return;
}
if (tooManyViolationsThisLoop()) {
return;
}
BlockGuard.BlockGuardPolicyException e = new StrictModeDiskWriteViolation(mPolicyMask);
e.fillInStackTrace();
startHandlingViolationException(e);
}
....

StrictMode不仅针对BlockGuard.Policy实现了自身的处理逻辑,还扩展了一个方法onCustomSlowCall(),通过BlockGuard.setThreadPolicy()就能够将AndroidBlockGuardPolicy植入到BlockGuard中。

再来看CloseGuard,与BlockGuard一样,Android在很多代码中也植入了CloseGuard,以FileInputStream为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class FileInputStream extends InputStream {
// 1. 新建CloseGuard全局变量
private final CloseGuard guard = CloseGuard.get();

public FileInputStream(File file) throws FileNotFoundException {
...
// 2. 设置CloseGuard标志
guard.open("close");
}

public void close() throws IOException {
// 3. 清除CloseGuard标志
guard.close();
...
}

protected void finalize() throws IOException {
// 4. 判断Close标志是否被清除
if (guard != null) {
guard.warnIfOpen();
}
...
}
}

CloseGuard的植入逻辑很清晰,一共分为4部分:

  • 1)新建一个CloseGuard全局变量
  • 2)在对象初始化时,设置一个标志,表示需要调用close()方法关闭该对象
  • 3)在关闭方法中,调用CloseGuard.close()方法,清除标志
  • 4)在对象销毁时,调用CloaseGuard.warnIfOpen()方法,判断标志是否被清除:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public void warnIfOpen() {
    if (allocationSite == null || !ENABLED) {
    return;
    }

    String message =
    ("A resource was acquired at attached stack trace but never released. "
    + "See java.io.Closeable for information on avoiding resource leaks.");

    REPORTER.report(message, allocationSite);
    }

从CloseGuard.warnIfOpen()方法中,可以看到,设置的标志就是allocationSite变量,如果该变量已经置空了,表示已经被清除过了; 否则,就会通过REPORTER报告违规操作。

REPORTER是CloseGuard暴露一个接口,StrictMode就实现了这个接口:

1
2
3
4
5
private static class AndroidCloseGuardReporter implements CloseGuard.Reporter {
public void report (String message, Throwable allocationSite) {
onVmPolicyViolation(message, allocationSite);
}
}

当StrictMode启用时,REPORTER就被设置成了AndroidCloseGuardReporter对象,如此一来,StrictMode就能够收集到CloseGuard报告的未关闭文件。

至此,我们揭开了StrictMode的面纱:Android通过BlockGuard和CloseGuard在一些执行路径中埋入了一些切点,譬如磁盘读写时BlockGuard会收到通知,对象销毁时CloseGuard就会收到通知。 BlockGuard和CloseGuard都设计了一套接口:BlockGuard.Policy和CloseGuard.Reporter,其实就是切点的不同分类,StrictMode正是利用这两个接口所定义的一些切点,切入了自已的处理逻辑。

题外话: 得益于面向接口的设计,我们可以另起炉灶,完全独立于StrictMode再实现其他BlockGuard.Policy和CloseGuard.Reporter的处理逻辑; 也可以对BlockGuard.Policy和CloseGuard.Reporter进行扩展,StrictMode只需要实现新的处理逻辑即可,这都不会影响已有的架构。 接口定义和接口实现分离,两者可以独立变化,适应新的需求,这是桥接模式(Bridge Pattern)的精髓,它降低了Guard和StrictMode两者之间的耦合度。
从BlockGuardOs的设计中,我们也看到了代理模式(Proxy Pattern),BlockGuardOs对被代理的Os类进行了简单控制,植入了BlockGuard的逻辑,作为一个中间者,处于调用者和被调用实体中间,能够降低两者的耦合度。

StrictMode Policy

StrictMode利用了BlockGuard和CloseGuard,不仅实现了两者定义的一些策略(Policy),还进行了扩展。 这些策略,在StrictMode看来,就是一些违规操作,下面我们深入介绍StrictMode定义的每一项违规操作。

ThreadPolicy

ThreadPolicy细分为以下几种:
• Disk Write:实现了BlockGuard的策略,写磁盘操作
• Disk Read:实现了BlockGuard的策略,读磁盘操作
• Network Access:实现了BlockGuard的策略,网络访问操作
• Custom Slow Code:StrictMode扩展的策略,目前只有Webview中植入了这项检查
前三项的植入都是通过BlockGuard完成的,StrictMode只是实现了处理逻辑;最后一项是StrictMode扩展的, 如果一个方法执行的时间较长,可以调用StrictMode.noteSlowCall()方法来发出通知。 当这些操作发生后,最终都会调用StrictMode.handleViolation()方法进行处理,后文再展开讨论这个方法。

StrictMode通过标志位来区别以上几项,为此还特意封装了一个内部类StrictMode.ThreadPlicy,目的是为了方便标志位的设定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static final class ThreadPolicy {
// ThreadPolicy标志位
final int mask;
private ThreadPolicy(int mask) {
this.mask = mask;
}

// 利用Builder完成标志位的初始化
public static final class Builder {
private int mMask = 0;

public Builder detectDiskReads() {
return enable(DETECT_DISK_READ);
}
}
}

ThreadPolicy的初始化采用了构建者模式(Builder Pattern),这样一来,调用者在使用起来就会更加自然一点,不用记住各个标志位的意义。 为了完成标志位的设定,StrictMode提供setThreadPolicy()方法,接收ThreadPolicy类型的对象作为参数,该方法的实现就是直接调用setThreadPolicyMask():

1
2
3
4
5
6
7
8
public static void setThreadPolicy(final ThreadPolicy policy) {
setThreadPolicyMask(policy.mask);
}

pivate static void setThreadPolicyMask(final int policyMask) {
setBlockGuardPolicy(policyMask);
Binder.setThreadStrictModePolicy(policyMask);
}

这里完成了两个层面的ThreadPolicy设定:
Java层,通过StrictMode.setBlockGuardPolicy()完成,最终会调用BlockGuard.setThreadPolicy()方法, 将AndroidBlockGuardPolicy对象设定为BlockGuard的Policy;

Native层,通过Binder.setThreadStrictModePolicy()完成,看到这里,想必各位读者心中有了疑问,为什么还会有Native层的ThreadPolicy设置? 其实,看到Binder,就很容易联想到这是用作跨进程调用的,当进程A发起跨进程调用进入到进程B后,那进程B中的违规操作怎么判定呢?当然也需要一个ThreadPolicy, Binder.setThreadStrictModePolicy()就是用来设置其他进程的ThreadPolicy。进程B中的违规异常也会通过Binder再传回进程A中,如此一来, 一个方法执行路径上的所有违规操作都会被StrictMode发现。

VMPolicy

ThreadPolicy主要用于发现一些容易导致主线程阻塞的操作,所以它针对的对象是单个线程; 而VMPolicy主要用于发现Java层的内存泄漏,所以它针对的是虚拟机的所有对象。 VMPolicy细分为以下几种:
Cursor Leak: 如果注册SQlite Cursor后没有调用close(),则发生了泄漏。
Closable Leak:这一项是CloseGuard的实现。如果存在未关闭的对象,则发生了泄漏。
Activity Leak: 如果Activity在销毁后,其对象引用还被持有,则发生了泄漏。
Instance Leak: StrictMode允许设置一个类的对象数量上限,在系统闲时,Strict会统计虚拟机中实际的对象数量,如果超出设定的上限,则判定为对象泄漏。
Registion Leak: 如果注册IntentReceiver后没有调用unregisterReceiver(),则发生了泄漏
File URI Exposure:这一项是安全性检查。通过file://的方式共享文件时,存在安全隐患。Android建议通过content://的方式共享文件。

如同ThreadPolicy一样,VMPolicy也采用了构建者模式(Builder Pattern)进行初始化,在Closable Leak这一项的使用上,与BlockGuard有异曲同工之妙, 但除了Closable Leak是利用CloseGuard以外,其他违规项都是StrictMode自身的逻辑,需要在一些关键路径上植入StrictMode的代码,我们举出两例:

例1:Cursor Leak

以下是SQLite Cursor植入了StrictMode机制的代码片段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public SQLiteCursor(SQLiteCursorDriver driver, String editTable, SQLiteQuery query) {
...
if (StrictMode.vmSqliteObjectLeaksEnabled()) {
mStackTrace = new DatabaseObjectNotClosedException().fillInStackTrace();
} else {
mStackTrace = null;
}
...
}

protected void finalize() {
try {
// if the cursor hasn't been closed yet, close it first
if (mWindow != null) {
if (mStackTrace != null) {
String sql = mQuery.getSql();
int len = sql.length();
StrictMode.onSqliteObjectLeaked(
"Finalizing a Cursor that has not been deactivated or closed. " +
"database = " + mQuery.getDatabase().getLabel() +
", table = " + mEditTable +
", query = " + sql.substring(0, (len > 1000) ? 1000 : len),
mStackTrace);
}
close();
}
} finally {
super.finalize();
}
}

在SQLiteCursor对象初始化时,设置一个变量mStackTrace,如果开启了DETECT_VM_CURSOR_LEAKS,则将其置为非空。 在SQLiteCursor对象销毁时,会对Cursor是否关闭进行判断(如果CursorWindow非空,则说明没有显示关闭Cursor)。此时,如果mStackTrace变量非空,则向StrictMode报告。

例2:Activity Leak

再来一例Activity植入StrictMode的逻辑:

1
2
3
4
5
ActivityThread.performLaunchActivity()
└── StrictMode.incrementExpectedActivityCount()

ActivityThread.performDestroyActivity()
└── StrictMode.decrementExpectedActivityCount()

StrictMode对象中维护了Activity的计数器,统计着Activity对象的数量。在Activity对象新建和销毁的时候,会分别调用increment和decrement,对计数进行增减调整。 每一次有Activity对象销毁,都会调用VMDebug.countInstancesOfClass(),计算虚拟机中实际的Activity对象数量,如果实际Activity对象的数量超出了StrictMode的统计值, 则说明Activity对象虽然销毁了,但其对象引用还在,这就存在泄漏。

1
2
3
4
5
6
7
8
public static void decrementExpectedActivityCount(Class klass) {
...
long instances = VMDebug.countInstancesOfClass(klass, false);
if (instances > limit) {
Throwable tr = new InstanceCountViolation(klass, instances, limit);
onVmPolicyViolation(tr.getMessage(), tr);
}
}

从上述两例中,我们看到,虽然检测的形式各有不同,但本质都是在被检测的对象初始化时(constructor)设置一个标志,在对象销毁时(finalize)再对这个标志进行判断。其他的检测项的实现方式也都大同小异。

StrictMode Penalty

当StrictMode发现有违规操作后,提供一些惩罚的方式,使用者可以自行组合。
penaltyDialog: 弹出对话框
penaltyDeath: 杀掉进程
penaltyDeathOnNetwork
penaltyFlashScreen: 在屏幕的最外围绘制一个红框
penaltyLog:打印StrictMode日志
penaltyDropBox:将日志保存到Dropbox中

StrictMode内部是通过标志位来记录惩罚操作的类型的,并提供了上述的方法来设置不同的标志位。 StrictMode检测到违规操作后,最终都会调用StrictMode.handleViolation()方法,该方法中就会根据设置的标志位进行惩罚:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void handleViolation(final ViolationInfo info) {
final boolean justDropBox = (info.policy & THREAD_PENALTY_MASK) == PENALTY_DROPBOX;
if (justDropBox) {
dropboxViolationAsync(violationMaskSubset, info);
return;
}
...
ActivityManagerNative.getDefault().handleApplicationStrictModeViolation(
RuntimeInit.getApplicationObject(),
violationMaskSubset,
info);
...
if ((info.policy & PENALTY_DEATH) != 0) {
executeDeathPenalty(info);
}
}

方法的实现逻辑一目了然,最终通过Binder发起跨进程调用,走到ActivityManagerService.handleApplicationStrictModeViolation()中.

StrictMode使用

StrictMode机制只是用于发现一些违规操作,这些违规操作一般都是我们编码的疏漏,在运行时会被StrictMode暴露出来,但StrictMode并非真正意思上的“动态代码检查”。 各位读者有必要知道StrictMode的使用范围:

StrictMode只是用在开发调试阶段,在正式发布时,应该关掉StrictMode,

AOSP的源码中,USER版并没有打开StrictMode
由于Android还会对StrictMode的检查策略进行调整,所以Google Play建议上架的APK都关闭StrictMode; 从另一个角度,Google认为所有StrictMode的错误,在正式发布前,都应该解决。
StrictMode并不能发现Native层的违规操作,仅仅是用在Java层
StrictMode的使用场景可以分为三类,使用方式也都比较固定,可见StrictMode的对外接口还是封装得比较优美的。 下面,我们逐个介绍一下StrictMode的使用场景。

普通应用开启StrictMode

对于应用程序而言,Android提供了一个最佳使用实践:尽可能早的在android.app.Application或android.app.Activity的生命周期使能StrictMode, onCreate()方法就是一个最佳的时机,越早开启就能在更多的代码执行路径上发现违规操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void onCreate() {
if (DEVELOPER_MODE) {
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
.detectDiskReads()
.detectDiskWrites()
.detectNetwork() // or .detectAll() for all detectable problems
.penaltyLog()
.build());
StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
.detectLeakedSqlLiteObjects()
.detectLeakedClosableObjects()
.penaltyLog()
.penaltyDeath()
.build());
}
super.onCreate();
}

以上StrictMode的使能代码限定在DEVELOPER_MODE:
• 设定了Disk Read, Disk Write, Network Access三项ThreadPolicy,惩罚是打印日志;
• 设定了Cursor Leak, Closable Leak两项VMPolicy,惩罚是打印日志和杀掉进程。
当出现一些ThreadPolicy相关违规操作时,Android也提供了很多标准的解决方案,譬如Handler, AsyncTask, IntentService,能够将耗时的操作从主线程中分离出来。

系统应用开启StrictMode

对于Android系统应用和系统进程(system_server)而言,其实默认就会开启StrictMode。 StrictMode提供了conditionallyEnableDebugLogging()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public static boolean conditionallyEnableDebugLogging() {
boolean doFlashes = SystemProperties.getBoolean(VISUAL_PROPERTY, false)
&& !amTheSystemServerProcess();
final boolean suppress = SystemProperties.getBoolean(DISABLE_PROPERTY, false);
if (!doFlashes && (IS_USER_BUILD || suppress)) {
setCloseGuardEnabled(false);
return false;
}

int threadPolicyMask = StrictMode.DETECT_DISK_WRITE |
StrictMode.DETECT_DISK_READ |
StrictMode.DETECT_NETWORK;
...
if (!IS_USER_BUILD) {
threadPolicyMask |= StrictMode.PENALTY_DROPBOX;
}

StrictMode.setThreadPolicyMask(threadPolicyMask);
if (IS_USER_BUILD) {
setCloseGuardEnabled(false);
} else {
VmPolicy.Builder policyBuilder = new VmPolicy.Builder().detectAll().penaltyDropBox();
...
setVmPolicy(policyBuilder.build());
setCloseGuardEnabled(vmClosableObjectLeaksEnabled());
}
return true;
}

该方法的目的就是要设置ThreadPolicy和VMPolicy,不过会有一些条件判断,具体的逻辑不表。我们来看一下调用这个方法的地方:

1
2
3
4
SystemServer.run()
ServiceThread.run()
ActivityThread.handleBindApplication()
└── StrictMode.conditionallyEnableDebugLogging()

这表示在system_server进程、一些全局的消息线程(IoThread, UiThread, FgThread, DisplayThread)、应用进程这些东西启动的时候开启StrictMode。 在ActivityThread.handleBindApplication()中有这么一段限制:

1
2
3
4
5
6
7
8
9
private void handleBindApplication(AppBindData data) {
...
if ((data.appInfo.flags &
(ApplicationInfo.FLAG_SYSTEM |
ApplicationInfo.FLAG_UPDATED_SYSTEM_APP)) != 0) {
StrictMode.conditionallyEnableDebugLogging();
}
...
}

表示只为系统应用(FLAG_SYSTEM, FLAG_UPDATED_SYSTEM_APP)开启了StrictMode,其他应用还是需要自行开启。

临时关闭StrictMode

对于某些操作而言,我们明确知道是StrictMode定义的违规操作,但实际上对性能并没有什么影响,那么,在执行这类操作的时候,可以临时关闭StrictMode。 譬如针对一些主线程快速写磁盘的操作:

1
2
3
4
5
6
StrictMode.ThreadPolicy old = StrictMode.getThreadPolicy();
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder(old)
.permitDiskWrites()
.build());
// 进行磁盘写操作...
StrictMode.setThreadPolicy(old);

首先,将旧的ThreadPolicy缓存一把; 然后,设置新的ThreadPolicy,并允许写磁盘操作; 最后,在进行完正常的写磁盘操作后,还原旧的ThreadPolicy。 这样就临时性的避开了StrictMode对写磁盘操作的检查。

查看开启StrictMode的结果

严格模式有很多种报告违例的形式,但是想要分析具体违例情况,还是需要查看日志,终端下过滤StrictMode就能得到违例的具体stacktrace信息。
adb logcat -b all | grep -rn StrictMode

解决违例

• 如果是主线程中出现文件读写违例,建议使用工作线程(必要时结合Handler)完成。
• 如果是对SharedPreferences写入操作,在API 9 以上 建议优先调用apply而非commit。
• 如果是存在未关闭的Closable对象,根据对应的stacktrace进行关闭。
• 如果是SQLite对象泄露,根据对应的stacktrace进行释放。

举个例子
以主线程中的文件写入为例,引起违例警告的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void writeToExternalStorage() {
File externalStorage = Environment.getExternalStorageDirectory();
File destFile = new File(externalStorage, "dest.txt");
try {
OutputStream output = new FileOutputStream(destFile, true);
output.write("droidyue.com".getBytes());
output.flush();
output.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}

引起的警告为:

1
2
3
4
5
6
7
8
D/StrictMode( 9730): StrictMode policy violation; ~duration=20 ms: android.os.StrictMode$StrictModeDiskReadViolation: policy=31 violation=2
D/StrictMode( 9730): at android.os.StrictMode$AndroidBlockGuardPolicy.onReadFromDisk(StrictMode.java:1176)
D/StrictMode( 9730): at libcore.io.BlockGuardOs.open(BlockGuardOs.java:106)
D/StrictMode( 9730): at libcore.io.IoBridge.open(IoBridge.java:390)
D/StrictMode( 9730): at java.io.FileOutputStream.<init>(FileOutputStream.java:88)
D/StrictMode( 9730): at com.example.strictmodedemo.MainActivity.writeToExternalStorage(MainActivity.java:56)
D/StrictMode( 9730): at com.example.strictmodedemo.MainActivity.onCreate(MainActivity.java:30)
D/StrictMode( 9730): at android.app.Activity.performCreate(Activity.java:4543)

因为上述属于主线程中的IO违例,解决方法就是讲写入操作放入工作线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void writeToExternalStorage() {
new Thread() {
@Override
public void run() {
super.run();
File externalStorage = Environment.getExternalStorageDirectory();
File destFile = new File(externalStorage, "dest.txt");
try {
OutputStream output = new FileOutputStream(destFile, true);
output.write("droidyue.com".getBytes());
output.flush();
output.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}.start();
}

然而这并非完善,因为OutputStream.write方法可能抛出IOException,导致存在OutputStream对象未关闭的情况,仍然需要改进避免出现Closable对象未关闭的违例。改进如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public void writeToExternalStorage() {
new Thread() {
@Override
public void run() {
super.run();
File externalStorage = Environment.getExternalStorageDirectory();
File destFile = new File(externalStorage, "dest.txt");
OutputStream output = null;
try {
output = new FileOutputStream(destFile, true);
output.write("droidyue.com".getBytes());
output.flush();
output.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (null != output) {
try {
output.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}.start();
}

使用StrictMode检测内存泄露

通常情况下,检测内存泄露,我们需要使用MAT对heap dump 文件进行分析,这种操作不困难,但也不容易。使用严格模式,只需要过滤日志就能发现内存泄露。

这里以Activity为例说明,首先我们需要开启对检测Activity泄露的违例检测。使用上面的detectAll或者detectActivityLeaks()均可。其次写一段能够产生Activity泄露的代码。

1
2
3
4
5
6
7
public class LeakyActivity extends Activity{
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
MyApplication.sLeakyActivities.add(this);
}
}

MyApplication中关于sLeakyActivities的部分实现

1
2
3
4
5
public class MyApplication extends Application {
public static final boolean IS_DEBUG = true;
public static ArrayList<Activity> sLeakyActivities = new ArrayList<Activity>();

}

当我们反复进入LeakyActivity再退出,过滤StrictMode就会得到这样的日志:

1
2
3
E/StrictMode( 2622): class com.example.strictmodedemo.LeakyActivity; instances=2; limit=1
E/StrictMode( 2622): android.os.StrictMode$InstanceCountViolation: class com.example.strictmodedemo.LeakyActivity; instances=2; limit=1
E/StrictMode( 2622): at android.os.StrictMode.setClassInstanceLimit(StrictMode.java:1)

分析日志,LeakyActivity本应该是只存在一份实例,但现在出现了2个,说明LeakyActivity发生了内存泄露。

严格模式除了可以检测Activity的内存泄露之外,还能自定义检测类的实例泄露。从API 11 开始,系统提供的这个方法可以实现我们的需求。

1
public StrictMode.VmPolicy.Builder setClassInstanceLimit (Class klass, int instanceLimit)

举个栗子,比如一个浏览器中只允许存在一个SearchBox实例,我们就可以这样设置已检测SearchBox实例的泄露

1
StrictMode.setVmPolicy(new VmPolicy.Builder().setClassInstanceLimit(SearchBox.class, 1).penaltyLog().build());

自定义 noteSlowCall

StrictMode从 API 11开始允许开发者自定义一些耗时调用违例,这种自定义适用于自定义的任务执行类中,比如我们有一个进行任务处理的类,为TaskExecutor。

1
2
3
4
5
public class TaskExecutor {
public void execute(Runnable task) {
task.run();
}
}

先需要跟踪每个任务的耗时情况,如果大于500毫秒需要提示给开发者,noteSlowCall就可以实现这个功能,如下修改代码:

1
2
3
4
5
6
7
8
9
10
11
12
public class TaskExecutor {

private static long SLOW_CALL_THRESHOLD = 500;
public void executeTask(Runnable task) {
long startTime = SystemClock.uptimeMillis();
task.run();
long cost = SystemClock.uptimeMillis() - startTime;
if (cost > SLOW_CALL_THRESHOLD) {
StrictMode.noteSlowCall("slowCall cost=" + cost);
}
}
}

执行一个耗时2000毫秒的任务

1
2
3
4
5
6
7
8
9
10
11
TaskExecutor executor = new TaskExecutor();
executor.executeTask(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});

得到的违例日志,注意其中~duration=20 ms并非耗时任务的执行时间,而我们的自定义信息msg=slowCall cost=2000才包含了真正的耗时。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
D/StrictMode(23890): StrictMode policy violation; ~duration=20 ms: android.os.StrictMode$StrictModeCustomViolation: policy=31 violation=8 msg=slowCall cost=2000
D/StrictMode(23890): at android.os.StrictMode$AndroidBlockGuardPolicy.onCustomSlowCall(StrictMode.java:1163)
D/StrictMode(23890): at android.os.StrictMode.noteSlowCall(StrictMode.java:1974)
D/StrictMode(23890): at com.example.strictmodedemo.TaskExecutor.executeTask(TaskExecutor.java:17)
D/StrictMode(23890): at com.example.strictmodedemo.MainActivity.onCreate(MainActivity.java:36)
D/StrictMode(23890): at android.app.Activity.performCreate(Activity.java:4543)
D/StrictMode(23890): at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1071)
D/StrictMode(23890): at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2158)
D/StrictMode(23890): at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2237)
D/StrictMode(23890): at android.app.ActivityThread.access$600(ActivityThread.java:139)
D/StrictMode(23890): at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1262)
D/StrictMode(23890): at android.os.Handler.dispatchMessage(Handler.java:99)
D/StrictMode(23890): at android.os.Looper.loop(Looper.java:156)
D/StrictMode(23890): at android.app.ActivityThread.main(ActivityThread.java:5005)
D/StrictMode(23890): at java.lang.reflect.Method.invokeNative(Native Method)
D/StrictMode(23890): at java.lang.reflect.Method.invoke(Method.java:511)
D/StrictMode(23890): at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:784)
D/StrictMode(23890): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:551)
D/StrictMode(23890): at dalvik.system.NativeStart.main(Native Method)

其他技巧

除了通过日志查看之外,我们也可以在开发者选项中开启严格模式,开启之后,如果主线程中有执行时间长的操作,屏幕则会闪烁,这是一个更加直接的方法。

注意

在线上环境即Release版本不建议开启严格模式。
严格模式无法监控JNI中的磁盘IO和网络请求。
应用中并非需要解决全部的违例情况,比如有些IO操作必须在主线程中进行。

参考资料

  1. StrictMode机制以及使用场景
  2. Android严苛模式StrictMode使用详解
  3. Android性能调优利器StrictMode