“我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第2篇文章,点击查看活动详情”
前言
惯例,我们先来看看官方对它的定义
StrictMode 是一个开发人员工具,它可以检测您可能不小心做的事情,并将它们提请您注意,以便您修复它们。 StrictMode 最常用于捕获应用程序主线程上的意外磁盘或网络访问,其中接收 UI 操作并发生动画。使磁盘和网络操作远离主线程可以使应用程序更加流畅、响应速度更快。通过保持应用程序的主线程响应,您还可以防止向用户显示 ANR 对话框
可以看到,StrictMode
主要是一个检测工具
,帮助我们检测主线程上做得一些IO访问等危险操作
使用
我们看下使用方法,可以在Activity、Application的onCreate中启用
public void onCreate() {
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();
}
其实主要分两个方法,一个是当前线程的检测,一个是虚拟机进程中所有线程的检测
1、ThreadPolicy.Builder
方法名 | 意义 |
---|---|
detectCustomSlowCalls() | 慢方法检测 |
detectDiskReads() | 磁盘读检测 |
detectDiskWrites() | 磁盘写检测 |
detectNetwork() | 网络检测 |
detectResourceMismatches() | 资源不匹配检测 |
detectUnbufferedIo() | 没有buffer检测 |
2、VmPolicy.Builder
方法名 | 意义 |
---|---|
detectActivityLeaks() | 检测Activity泄露 |
detectCleartextNetwork() | 检测没有SSL/TLS包裹的流量 |
detectContentUriWithoutPermission() | 检测是否有权限到第三方 |
detectCredentialProtectedWhileLocked() | 检测对用户锁定文件的访问 |
detectFileUriExposure() | 检测File Uri暴露 |
detectIncorrectContextUse() | 检测为正确使用的context |
detectLeakedClosableObjects() | 检测close对象的泄漏 |
detectLeakedRegistrationObjects() | 检测广播的泄露 |
detectLeakedSqlLiteObjects() | 检测sqllite泄露 |
detectNonSdkApiUsage() | 检测不是sdk的反射使用 |
detectUnsafeIntentLaunch() | 检测不安全的intent |
detectUntaggedSockets() | 检测socket |
源码解读
我们先从ThreadPolicy
开始
1、onReadFromDisk
在主线程读取文件,我们这里主要是调用了System.loadLibrary()
,我们先看下告警信息
2022-09-05 23:33:00.738 15172-15172/com.army.crypto D/StrictMode: StrictMode policy violation; ~duration=15 ms: android.os.strictmode.DiskReadViolation
at android.os.StrictMode$AndroidBlockGuardPolicy.onReadFromDisk(StrictMode.java:1596)
at libcore.io.BlockGuardOs.open(BlockGuardOs.java:249)
at libcore.io.ForwardingOs.open(ForwardingOs.java:166)
at android.app.ActivityThread$AndroidOs.open(ActivityThread.java:7542)
at libcore.io.IoUtils.canOpenReadOnly(IoUtils.java:275)
at dalvik.system.DexPathList$NativeLibraryElement.findNativeLibrary(DexPathList.java:884)
at dalvik.system.DexPathList.findLibrary(DexPathList.java:595)
at dalvik.system.BaseDexClassLoader.findLibrary(BaseDexClassLoader.java:281)
at java.lang.Runtime.loadLibrary0(Runtime.java:1061)
at java.lang.Runtime.loadLibrary0(Runtime.java:1008)
at java.lang.System.loadLibrary(System.java:1664)
at com.army.cryptolib.Crypto.<clinit>(Crypto.java:6)
at com.army.cryptolib.Crypto.encode(Native Method)
at com.army.crypto.ui.main.MainFragment.initView$lambda-2$lambda-1(MainFragment.kt:64)
at com.army.crypto.ui.main.MainFragment.$r8$lambda$fpt5zWKHDPomBcQX5YIo9wNFnSg(Unknown Source:0)
at com.army.crypto.ui.main.MainFragment$$ExternalSyntheticLambda1.onClick(Unknown Source:4)
at android.view.View.performClick(View.java:7448)
at com.google.android.material.button.MaterialButton.performClick(MaterialButton.java:1119)
at android.view.View.performClickInternal(View.java:7425)
at android.view.View.access$3600(View.java:810)
at android.view.View$PerformClick.run(View.java:28305)
at android.os.Handler.handleCallback(Handler.java:938)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:223)
at android.app.ActivityThread.main(ActivityThread.java:7656)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)
从堆栈调用顺序来看是,下面列出主要的几个节点
System.loadLibrary->IoUtils.canOpenReadOnly->BlockGuardOs.open
我们来看下BlockGuardOs.open()
,这里主要是打开文件的作用,发现里面间接调用了3个api。2个是
ThreadPolicy中的onReadFromDisk(),onWriteToDisk()
,还有一个则是直接回传给了VmPolicy
@Override public FileDescriptor open(String path, int flags, int mode) throws ErrnoException {
BlockGuard.getThreadPolicy().onReadFromDisk();
BlockGuard.getVmPolicy().onPathAccess(path);
if ((flags & O_ACCMODE) != O_RDONLY) {
BlockGuard.getThreadPolicy().onWriteToDisk();
}
return super.open(path, flags, mode);
}
这里的VmPolicy我们先忽略
那我们先看看ThreadPolicy
是什么
public interface Policy {
void onWriteToDisk();
void onReadFromDisk();
void onNetwork();
void onUnbufferedIO();
void onExplicitGc();
int getPolicyMask();
}
发现是一个接口
,这里值得注意的是,我们前面介绍ThreadPolicy.Builder()
,刚好和这里对应上,都是6个api
,那这里的实现是什么呢?得先从StrictMode.setThreadPolicy()
说起
public static void setThreadPolicy(final ThreadPolicy policy) {
setThreadPolicyMask(policy.mask);
sThreadViolationListener.set(policy.mListener);
sThreadViolationExecutor.set(policy.mCallbackExecutor);
}
这里可以看到,我们之前通过ThreadPolicy.Builder
设置的一大堆,其实最后都会归纳到一个状态上,这个状态类型是int,这个实现原理是巧妙运用掩码和位运算,可以发现Android源码里随处可见这种设计,大家可以学起来,尤其是针对那种一个场景可能映射多个状态时,特别有用
public static final int PENALTY_LOG = 1 << 30;
public static final int PENALTY_DIALOG = 1 << 29;
public static final int PENALTY_DEATH = 1 << 28;
public static final int PENALTY_FLASH = 1 << 27;
public static final int PENALTY_DROPBOX = 1 << 26;
public static final int PENALTY_DEATH_ON_NETWORK = 1 << 25;
public static final int PENALTY_DEATH_ON_CLEARTEXT_NETWORK = 1 << 24;
public static final int PENALTY_DEATH_ON_FILE_URI_EXPOSURE = 1 << 23;
我们继续看setThreadPolicyMask()
public static void setThreadPolicyMask(@ThreadPolicyMask int threadPolicyMask) {
setBlockGuardPolicy(threadPolicyMask);
Binder.setThreadStrictModePolicy(threadPolicyMask);
}
2、setBlockGuardPolicy
继续setBlockGuardPolicy
,这里还将我们的设置同步给了Binder
private static void setBlockGuardPolicy(@ThreadPolicyMask int threadPolicyMask) {
if (threadPolicyMask == 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 = THREAD_ANDROID_POLICY.get();
BlockGuard.setThreadPolicy(androidPolicy);
}
androidPolicy.setThreadPolicyMask(threadPolicyMask);
}
这里通过BlockGuard.getThreadPolicy()
获取了一个BlockGuard.Policy
看下BlockGuard.getThreadPolicy()
的实现
public static @NonNull Policy getThreadPolicy() {
return threadPolicy.get();
}
然后继续看threadPolicy
的实现
private static ThreadLocal<Policy> threadPolicy = new ThreadLocal<Policy>() {
@Override protected Policy initialValue() {
return LAX_POLICY;
}
};
然后看LAX_POLICY
的实现
public static final Policy LAX_POLICY = new Policy() {
@Override public String toString() { return "LAX_POLICY"; }
@Override public void onWriteToDisk() {}
@Override public void onReadFromDisk() {}
@Override public void onNetwork() {}
@Override public void onUnbufferedIO() {}
@Override public void onExplicitGc() {}
@Override
public int getPolicyMask() {
return 0;
}
};
空实现?扯淡吧?其实我们前面忽略了一个小点。就是下面代码
final BlockGuard.Policy policy = BlockGuard.getThreadPolicy();
final AndroidBlockGuardPolicy androidPolicy;
if (policy instanceof AndroidBlockGuardPolicy) {
androidPolicy = (AndroidBlockGuardPolicy) policy;
} else {
androidPolicy = THREAD_ANDROID_POLICY.get();
BlockGuard.setThreadPolicy(androidPolicy);
}
androidPolicy.setThreadPolicyMask(threadPolicyMask);
第一次取,取得是空实现,那自然就不满足instanceof AndroidBlockGuardPolicy
,然后从THREAD_ANDROID_POLICY
里去取,再将取到的值更新到了BlockGuard.threadPolicy
中, 我们看看THREAD_ANDROID_POLICY
是什么
private static final ThreadLocal<AndroidBlockGuardPolicy> THREAD_ANDROID_POLICY =
new ThreadLocal<AndroidBlockGuardPolicy>() {
@Override
protected AndroidBlockGuardPolicy initialValue() {
return new AndroidBlockGuardPolicy(0);
}
};
真像水落石出,在这里直接new AndroidBlockGuardPolicy()
,其实前面堆栈日志就打印了这里,不过还是不得不佩服这个代码写的是真6,学到了。
那既然我们已经知道了ThreadPolicy的实现是AndroidBlockGuardPolicy
,那回到最开始的地方,我们看看AndroidBlockGuardPolicy
的onReadFromDisk()
实现
public void onReadFromDisk() {
if ((mThreadPolicyMask & DETECT_THREAD_DISK_READ) == 0) {
return;
}
if (tooManyViolationsThisLoop()) {
return;
}
startHandlingViolationException(new DiskReadViolation());
}
这里首先判断了我们配置里有没有把检测磁盘读取打开,然后去检测是否有太多的检测在执行,默认最大是10个
,然后则调用了startHandlingViolationException()
void startHandlingViolationException(Violation e) {
final int penaltyMask = (mThreadPolicyMask & PENALTY_ALL);
final ViolationInfo info = new ViolationInfo(e, penaltyMask);
info.violationUptimeMillis = SystemClock.uptimeMillis();
handleViolationWithTimingAttempt(info);
}
3、handleViolationWithTimingAttempt
接着来看 handleViolationWithTimingAttempt
,我们先把这个方法分成三段来讲解
先来第一段
void handleViolationWithTimingAttempt(final ViolationInfo info) {
Looper looper = Looper.myLooper();
if (looper == null || (info.mPenaltyMask == PENALTY_DEATH)) {
info.durationMillis = -1; // unknown (redundant, already set)
onThreadPolicyViolation(info);
return;
}
...
}
这里判断当前线程是否有创建Looper
,或者是否开启了PENALTY_DEATH
策略(即触发严格模式,则让将整个App进程杀掉),
为什么有这个逻辑呢?是因为主线程肯定Looper!=null
,那么只有子线程
或者开启了PENALTY_DEATH
的才会走到这里,联系后面的逻辑会在窗口显示一个红色的框
,也就是说在非主线程做的高危动作是不会亮红框的,同时开启了PENALTY_DEATH策略,触发了高危应用进程会被马上kill
掉,也就没必要显示红框。
值得注意的是,如果我们自己在Thread里创建了Looper,那其实还是会去显示红框
同时触发了if的条件,就不再执行后面的逻辑,就直接调用了onThreadPolicyViolation()
,这是一个打印日志的方法,就是我们前面触发严格模式时,在控制台看到的log日志
,这里官方有一大段的注释,大概意思是用户比较关心的是这个危险操作具体消耗了多少时间,但是这里应该是拿不到的,只有线程持有Looper才能统计得到
接着看第二段
部分代码我用省略号代替
void handleViolationWithTimingAttempt(final ViolationInfo info) {
...
final ArrayList<ViolationInfo> records = violationsBeingTimed.get();
if (records.size() >= MAX_OFFENSES_PER_LOOP) {
// Not worth measuring. Too many offenses in one loop.
return;
}
records.add(info);
if (records.size() > 1) {
return;
}
final IWindowManager windowManager =
info.penaltyEnabled(PENALTY_FLASH) ? sWindowManager.get() : null;
if (windowManager != null) {
try {
windowManager.showStrictModeViolation(true);
} catch (RemoteException unused) {
}
}
...
}
这里通过violationsBeingTimed,拿到当前线程的一个List数组
,里面存储的是ViolationInfo
,然后下面直接判断了数组的容量,不能超过最大值MAX_OFFENSES_PER_LOOP=10
条,这里为什么这样处理,是因为到这里是默认线程里都有Looper的,过多的任务会对当前线程造成压力。这里如果容量超过的时候是直接丢弃的
然后下面又判断了records.size() > 1
,这表示我们之前post到looper
里的任务还没来得及处理,就直接往数组add了,直接等待handler任务被调用,数组里的信息也会被输出。
然后接下来就是重点了,根据PENALTY_FLASH是否开启
,直接去调windowManager.showStrictModeViolation(true)
,那这个是什么呢?其实就是显示一个红框,提醒我们在主线程做了一些危险操作(理论上来说是主线程,不排除有人创建了一个线程,然后也创建了looper的情况)
接着来看第三段
void handleViolationWithTimingAttempt(final ViolationInfo info) {
...
THREAD_HANDLER
.get()
.postAtFrontOfQueue(
() -> {
long loopFinishTime = SystemClock.uptimeMillis();
if (windowManager != null) {
try {
windowManager.showStrictModeViolation(false);
} catch (RemoteException unused) {
}
}
for (int n = 0; n < records.size(); ++n) {
ViolationInfo v = records.get(n);
v.violationNumThisLoop = n + 1;
v.durationMillis =
(int) (loopFinishTime - v.violationUptimeMillis);
onThreadPolicyViolation(v);
}
records.clear();
});
}
这里通过当前线程拿到Handler然后调用postAtFrontOfQueue将任务放到最前面
,这样下一次消息迭代就会被取出然后执行,然后我们看看回调Runnable里做了什么逻辑
先调用了windowManager.showStrictModeViolation(false)
,关闭了窗口上的红框显示,这个之前是开启的。
然后去除records里的记录,循环调用onThreadPolicyViolation()
去打印日志,
值得注意的是这里v.durationMillis
的赋值,记录了这个危险执行大概消耗了多少时间
,这也就解释了之前为啥去判断Looper的值情况,因为那样是不支持统计耗时的
4、showStrictModeViolation
为什么说这是一个显示红框的逻辑,我们到源码里看看
final IWindowManager windowManager =
info.penaltyEnabled(PENALTY_FLASH) ? sWindowManager.get() : null;
if (windowManager != null) {
try {
windowManager.showStrictModeViolation(true);
} catch (RemoteException unused) {
}
}
继续看WindowManagerService
的实现
public void showStrictModeViolation(boolean on) {
final int pid = Binder.getCallingPid();
if (on) {
// Show the visualization, and enqueue a second message to tear it
// down if we don't hear back from the app.
mH.sendMessage(mH.obtainMessage(H.SHOW_STRICT_MODE_VIOLATION, 1, pid));
mH.sendMessageDelayed(mH.obtainMessage(H.SHOW_STRICT_MODE_VIOLATION, 0, pid),
DateUtils.SECOND_IN_MILLIS);
} else {
mH.sendMessage(mH.obtainMessage(H.SHOW_STRICT_MODE_VIOLATION, 0, pid));
}
}
发现没有,在调用打开on=true
的情况下,会接连发送一个关闭的操作,这里延时是1s
,也就意味着一次告警会显示1次红框1s,我们再来看看这个红框是怎么实现的
最终会调用到showStrictModeViolation
private void showStrictModeViolation(int arg, int pid) {
final boolean on = arg != 0;
synchronized (mGlobalLock) {
if (on && !mRoot.canShowStrictModeViolation(pid)) {
return;
}
if (SHOW_VERBOSE_TRANSACTIONS) Slog.i(TAG_WM, ">>> showStrictModeViolation");
if (mStrictModeFlash == null) {
mStrictModeFlash = new StrictModeFlash(getDefaultDisplayContentLocked(),
mTransaction);
}
mStrictModeFlash.setVisibility(on, mTransaction);
mTransaction.apply();
}
}
判断了一下当前是否能显示红框,然后具体的实现交给了StrictModeFlash
,通过传入窗口容器RootWindowContainer拿到了DisplayContent
我们看看它的setVisibility()
public void setVisibility(boolean on, SurfaceControl.Transaction t) {
if (mSurfaceControl == null) {
return;
}
drawIfNeeded();
if (on) {
t.show(mSurfaceControl);
} else {
t.hide(mSurfaceControl);
}
}
继续看drawIfNeeded()
private void drawIfNeeded() {
if (!mDrawNeeded) {
return;
}
mDrawNeeded = false;
final int dw = mLastDW;
final int dh = mLastDH;
mBlastBufferQueue.update(mSurfaceControl, dw, dh, PixelFormat.RGBA_8888);
Canvas c = null;
try {
c = mSurface.lockCanvas(null);
} catch (IllegalArgumentException | OutOfResourcesException e) {
}
if (c == null) {
return;
}
// Top
c.save();
c.clipRect(new Rect(0, 0, dw, mThickness));
c.drawColor(Color.RED);
c.restore();
// Left
c.save();
c.clipRect(new Rect(0, 0, mThickness, dh));
c.drawColor(Color.RED);
c.restore();
// Right
c.save();
c.clipRect(new Rect(dw - mThickness, 0, dw, dh));
c.drawColor(Color.RED);
c.restore();
// Bottom
c.save();
c.clipRect(new Rect(0, dh - mThickness, dw, dh));
c.drawColor(Color.RED);
c.restore();
mSurface.unlockCanvasAndPost(c);
}
mThickness=20,这不就画了一个宽度20像素的红色矩形吗,依次将上、左、右、下
四条边通过canvas画了出来
5、onThreadPolicyViolation
接下来我们再来看看 onThreadPolicyViolation
,我们也分成几部分来看
首先是第一部分
void onThreadPolicyViolation(final ViolationInfo info) {
if (LOG_V) Log.d(TAG, "onThreadPolicyViolation; penalty=" + info.mPenaltyMask);
if (info.penaltyEnabled(PENALTY_GATHER)) {
ArrayList<ViolationInfo> violations = gatheredViolations.get();
if (violations == null) {
violations = new ArrayList<>(1);
gatheredViolations.set(violations);
}
for (ViolationInfo previous : violations) {
if (info.getStackTrace().equals(previous.getStackTrace())) {
// Duplicate. Don't log.
return;
}
}
violations.add(info);
return;
}
}
这里会根据PENALTY_GATHER
的开关情况执行下面逻辑,会去收集其它进程传递过来的异常信息,即进程A跨进程通过AIDL调用B后,B中的告警信息也会回传回来,实现逻辑主要在Parcel中的readExceptionCode()
中
public final int readExceptionCode() {
int code = readInt();
if (code == EX_HAS_NOTED_APPOPS_REPLY_HEADER) {
AppOpsManager.readAndLogNotedAppops(this);
code = readInt();
}
if (code == EX_HAS_STRICTMODE_REPLY_HEADER) {
int headerSize = readInt();
if (headerSize == 0) {
Log.e(TAG, "Unexpected zero-sized Parcel reply header.");
} else {
StrictMode.readAndHandleBinderCallViolations(this);
}
return 0;
}
return code;
}
接着看第二段
Integer crashFingerprint = info.hashCode();
long lastViolationTime = 0;
long now = SystemClock.uptimeMillis();
if (sLogger == LOGCAT_LOGGER) { // Don't throttle it if there is a non-default logger
if (mRealLastViolationTime != null) {
Long vtime = mRealLastViolationTime.get(crashFingerprint);
if (vtime != null) {
lastViolationTime = vtime;
}
clampViolationTimeMap(mRealLastViolationTime, Math.max(MIN_LOG_INTERVAL_MS,
Math.max(MIN_DIALOG_INTERVAL_MS, MIN_DROPBOX_INTERVAL_MS)));
} else {
mRealLastViolationTime = new SparseLongArray(1);
}
mRealLastViolationTime.put(crashFingerprint, now);
}
long timeSinceLastViolationMillis =
lastViolationTime == 0 ? Long.MAX_VALUE : (now - lastViolationTime);
if (info.penaltyEnabled(PENALTY_LOG)
&& timeSinceLastViolationMillis > MIN_LOG_INTERVAL_MS) {
sLogger.log(info);
}
这里主要是打印log,然后做了防抖的设计
,防止同一个类型的告警重复去打印,这里具体可以看看info.hashCode()
,有自定义的逻辑
接着看第三段
if (penaltyMask != 0) {
final boolean justDropBox = (info.mPenaltyMask == PENALTY_DROPBOX);
if (justDropBox) {
// If all we're going to ask the activity manager
// to do is dropbox it (the common case during
// platform development), we can avoid doing this
// call synchronously which Binder data suggests
// isn't always super fast, despite the implementation
// in the ActivityManager trying to be mostly async.
dropboxViolationAsync(penaltyMask, info);
} else {
handleApplicationStrictModeViolation(penaltyMask, info);
}
}
if (info.penaltyEnabled(PENALTY_DEATH)) {
throw new RuntimeException("StrictMode ThreadPolicy violation", violation);
}
这里主要就是根据 PENALTY_DROPBOX
标志即我们初始化调用penaltyDropBox()
设置的, 去把数据同步给DropBox
,一般会在/data/system/dropbox/
下生成日志文件,不过这里有一个地方要注意的是同步和异步的处理
private static void dropboxViolationAsync(
final int penaltyMask, final ViolationInfo info) {
...
BackgroundThread.getHandler().post(() -> {
handleApplicationStrictModeViolation(penaltyMask, info);
int outstandingInner = sDropboxCallsInFlight.decrementAndGet();
if (LOG_V) Log.d(TAG, "Dropbox complete; in-flight=" + outstandingInner);
});
}
为什么要异步处理呢?我们看下handleApplicationStrictModeViolation()
private static void handleApplicationStrictModeViolation(int penaltyMask,
ViolationInfo info) {
final int oldMask = getThreadPolicyMask();
try {
setThreadPolicyMask(0);
IActivityManager am = ActivityManager.getService();
if (am == null) {
Log.w(TAG, "No activity manager; failed to Dropbox violation.");
} else {
am.handleApplicationStrictModeViolation(
RuntimeInit.getApplicationObject(), penaltyMask, info);
}
} catch (RemoteException e) {
if (e instanceof DeadObjectException) {
// System process is dead; ignore
} else {
Log.e(TAG, "RemoteException handling StrictMode violation", e);
}
} finally {
setThreadPolicyMask(oldMask);
}
}
学到了没有,这种我们不需要同步调用的跨进程方法,最好把它放到子线程
里面去,比如我们应用层很多的getService().xx
操作,最好是封装异步回调有木有
总结
我们从onReadFromDisk出发,到窗口显示红框,最后到打印日志,跟了这一整条调用链下来
我们发现StrictMode
的实现机制其实很简单,就是在系统很多api进行预埋,然后被调用到则进行处理提示的过程。
在开发测试阶段,我们接入StrictMode,并指定一系列我们自己的策略,能很有效的帮助我们将某些问题在开发环境中就得到解决,而不必要带到线上去验证,同时也能促进我们一个很好的编码规范