前言:
作者本人负责公司的APM监控模块,因为工作的原因,对ANR,crash等流程研究的比较多,最近在打造APM监控平台的时候,顺带对DropBox的实现原理进行了一定的学习和研究,发现了一些妙用,因此写这篇文章来分享。
本系列分为两篇文章,
1.DropBox系列-安卓DropBox介绍:主要介绍DropBox的一些实现原理。
2.DropBox系列-利用DropBox打造车载系统APM框架:主要介绍利用DropBox打造一个面向所有APP的稳定性监控框架。
PS:本文的代码以安卓9(SDK=28)的源码来介绍,因为目前车载开发基本上都是以安卓9为基础来定制的,而且DropBox对于那些拥有系统权限的APP来讲意义更大一些。
一.车载系统APM框架介绍
1.1为什么要单独搞一套APM框架
为什么要做单独搞一套APM监控框架?直接用现成的bugly,友盟,KOOM不可以吗?为什么要重复的造*呢?
我的理解是这样的,如果有合适的*,自然是最完美的,但是目前的这些*,也许对我们来说并不是最适合的,原因有三,如下:
1.侵入性问题。直接使用现成的APM框架是可以的,但是并不是最好的选择。现有的互联网大厂的APM框架对于APP是有侵入性的,不但影响APP的包体积大小,而且有可能会影响APP性能的,甚至有可能接入监控框架导致出现性能问题,比如接入bugly造成IO异常。
2.完整性问题。单一的某一个框架,并不能完整的记录我们所需要的所有目标。比如bugly只能记录崩溃/ANR(6.0以上ANR功能失效)/错误数量,KOOM专注于内存状态的监控,没有一个大一统的框架可以帮我们监控到所有我们想要的数据。
3.接入成本问题。我们的车机上有一百多个应用,假设一个应用接入bugly需要半天时间(接入+测试 +验证),那么100个应用就需要50个工作日,成本可以见一般。
1.2 APM框架介绍
作为车载系统开发,我们有着定制framework这一得天独厚的优势,所以,我们可以针对freamwork层做一些不影响原有逻辑的注入,来实现对所有APP的APM监控。
车载APM就是基于原理去做的,它作为一个单独的APP存在,使用观察者模式,发现APP应用出现异常的时候,主动去补货异常日志进行上报,从而做一个观察者,对所有的APP应用进行稳定性监控。
二.安卓异常处理机制及指标监控
2.1 java崩溃流程简介
具体流程可以参考这一篇文章,本章只是简介。android源码学习-android异常处理机制_失落夏天的博客-CSDN博客_android 异常处理
安卓应用程序发生异常后,会一层一层的上报,最终上报到Thread的dispatchUncaughtException方法中:
public final void dispatchUncaughtException(Throwable e) {
Thread.UncaughtExceptionHandler initialUeh =
Thread.getUncaughtExceptionPreHandler();
if (initialUeh != null) {
try {
initialUeh.uncaughtException(this, e);
} catch (RuntimeException | Error ignored) {
// Throwables thrown by the initial handler are ignored
}
}
getUncaughtExceptionHandler().uncaughtException(this, e);
}
initialUeh用来记录崩溃信息,其实现类在安卓中是RuntimeInit.LoggingHandler,用来记录一些崩溃的信息。
而getUncaughtExceptionHandler()返回的是RuntimeInit.KillApplicationHandler,这个就是我们APP中真正去处理崩溃的实现类,我们看一下这个实现类:
private static class KillApplicationHandler implements Thread.UncaughtExceptionHandler {
private final LoggingHandler mLoggingHandler;
...
public KillApplicationHandler(LoggingHandler loggingHandler) {
this.mLoggingHandler = Objects.requireNonNull(loggingHandler);
}
@Override
public void uncaughtException(Thread t, Throwable e) {
try {
ensureLogging(t, e);
if (ActivityThread.currentActivityThread() != null) {
ActivityThread.currentActivityThread().stopProfiling();
}
// Bring up crash dialog, wait for it to be dismissed
ActivityManager.getService().handleApplicationCrash(
mApplicationObject, new ApplicationErrorReport.ParcelableCrashInfo(e));
}
...
}
...
}
主要做了三件事:
-
生成错误信息;
-
通过binder通知到AMS的handlerApplicationCrash方法;
-
删掉当前进程,完成应用的彻底退出。
AMS的handlerApplicationCrash方法中收到崩溃信息后,会通过DroxBoxManagerSerivice服务完成最终崩溃信息的记录和持久化,最终存储到data/system/dropbox文件夹下,这一块我们就不详细赘述了,我们只要支持,所有APP的崩溃,都会通知到AMS即可。
所以,我们只要在AMS中加上适当的钩子,完成崩溃消息的转发,就可以记录所有APP的java层崩溃(native崩溃另讲,检测机制不一样)。
2.2 ANR监控原理
具体原理可以参考这一篇文章,本章仍是简介。ANR系列之一:ANR显示和日志生成原理讲解_失落夏天的博客-CSDN博客
导致ANR的场景有很多种,比如输入事件无响应,广播事件无响应,Service中超时等等,具体类型和超时时间如下(下图中的某些时间其实是有问题的)。
无论哪种类型的ANR,最终都会通知到AnrHelper.appNotResponding方法中,所以我们从这个流程开始往下看。
经过种种转发,最终的执行逻辑在ProcessErrorSatetRecord方法中,主要会执行下面三大块逻辑:
1.收集ANR信息
2.保存ANR日志
3.弹出ANR显示框
2.3 APP启动速度监控原理
APP启动,主要分为四部分:
-
Laucher或其它应用通知AMS,显式或者隐式的去启动相应的Activity。
-
如果Activity所对应的APP进程不存在,则会走进程创建流程,通知Zygote去fork产生对应的进程,APP进程产生后,回调通知AMS。
-
AMS通知APP一侧去加载对应的APK文件,以及创建和初始化Application及其它必要元素。
-
AMS通知APP一侧去创建,初始化,以及最终显示Activity。
至此,整个启动流程结束,整个流程的时间可以理解为通俗理解上的冷启动时间。
完整的APP启动流程图如下:
所以,针对开始和结束分别设置一个钩子,那么两者之间的时间差,就是我们所需要冷启动时间了。
开始的事件很容易设置,AMS的中的startActivityWithFeature方法。
结束的事件相对会麻烦一些。因为最后的绘制流程,是APP进程和surfaceFlingger进程的通信,并不经过system_server进程。所以我们这里退而求其次,把APP进程把Window注册到WindowManager的事件作为结束的点,从而求出其中的差值。
这样,和标准意义上的冷启动流程,会有一些误差。这个误差就是Activity在onResume之后,首次measure,layout和draw的时间,这个误差理论上不会很大,除非是十分复杂的多层嵌套布局,或者ViewPager的不当使用。这一块的计算,我们放到后续再进行优化。
2.4 APP绘制卡顿监控
和上面说的监控首次绘制原理一样,APP绘制的流程除了注册Window,其余的渲染,以及vsync信息,都是surfaceFlingger进程来控制的。这一块需要想办法对surfaceFlingger进行一定的注入。这一项,我们一样放到后续进行相关的探索。
2.5 APP运行状态监控
APP运行状态包含很多指标,比如内存使用率,线程数,FD数量等等,这些数量的超标都会导致最终的OOM。KOOM和kensor两个框架都是对这些指标进行监控的。一样,我们放到后续在对这些指标进行相关的探索。
三.实现方案踩坑
3.1 车载系统APM架构设计
按照由简入难的原则,第一期的目标,我们先打算只实现崩溃和ANR两种异常指标的监控。
我们基于对系统代码改动量最小的原则,采用C/S的架构来设计。
首先我们通过一个独立的APP来实现异常信息的采集,分析和上报的工作,这个APP来实现绝大多数的逻辑。而系统侧只负责通知,即发生了异常之后,通知APP即可,不做其他任何的逻辑处理。这样对系统侧的修改就会尽可能的小。整体架构图设计如下:
接下来,我们该尝试,系统侧发现了异常之后,如何通知APP了。
3.2 和系统通讯方案尝试
方案1:信号量通讯
ANR系列之一:ANR显示和日志生成原理讲解_失落夏天的博客-CSDN博客
这篇文章中,我们有介绍,发现了ANR之后,系统会去主动采集最近使用进程的java层堆栈状态,而这种采集的通讯使用的是信号量,即系统发送一个信号量,APP侧接受了之后就开始采集自身的堆栈。APP侧并不止发生ANR的那一个,最近使用的APP也都会进行采集。
所以,我们只要在保证我们的APM_APP在最近使用的APP列表中,这样发生了ANR之后,我们只要监听这个信号量就知道发生ANR了。目前友盟和腾讯其实使用的就是这种方案。
优势:系统代码改动量很小,只需要添加一行代码,把APM_APP添加到最近使用列表中。
缺陷:ANR和Crash的实现方案就不统一了,因为Crash无法使用信号量。
因为设计初衷,还是希望crash和anr走一样的机制,这样方便后续的维护,所以信号量这个方案第一个被放弃。
方案2:binder通讯
由上面的流程我们可以知道,只要发生了crash,一定会通知AMS的handlerApplicationCrash方法,而ANR一定会通知AnrHelper的appNotResponding方法。
所以,可以通过APM_APP向系统注册一个binder引用。在上述的两个方法中,系统通过binder引用把异常信息传递给APM_APP。这样,就可以完成异常的监控了。
优势:监听是实时的,而且对于APP侧来说接口是一个,方法统一了。
缺陷:对系统的侵入性较大,而且一旦APM_APP被系统杀死,是完全无感知的。
这个方案需要修改系统源码,虽然作为车载系统开发修改源码是完全可以实现的,但是不利于后续的扩展,因为不同车型的系统源码都要改一遍。而且APM_APP是一个后台服务,一旦低内存被杀死,就不能继续进行异常的监听了。
所以这个方案作为一个备选,看是否有更好的方案再说。
方案3:文件夹监听
我们通过上面的原理流程可以得到,无论ANR还是crash,其实最终都会通知到DropBox去生成对应的异常日志文件。dropbox文件夹地址:data/system/dropbox,所以我们是否可以通过监听这个文件夹的变化,从而实现对异常信息的监听呢?
这样做有几个问题,首先是SELinux权限问题,普通APP是无法完成对这个文件夹的监听的。
但是对于车载系统开发来说问题不大,SELinux权限可以配置的。主动关掉SELinux后在进行尝试,最终发现这个方案不太可行。
首先监听crash文件是没有问题的,但是监听ANR文件的时候,由于ANR的生成文件是一个压缩包,所以通过FileObserver监听的时候,收到ANR类型的压缩文件会导致监听完全失效。而且这个方案和方案2存在一样的问题,一旦APM_APP进程挂掉,是无感知的,就无法监控后面异常信息了。
方案4:动态广播
通过上一篇文章的阅读
https://blog.csdn.net/rzleilei/article/details/128328967
我们知道dropbox完成异常信息的存储之后,会发送一个DropBoxManager.ACTION_DROPBOX_ENTRY_ADDED类型的广播,所以,我们只要注册一个广播接收器来接受这个广播,这样收到了这个广播之后,我们在去主动读取dropbox文件夹下对应的异常文件,就可以知道具体的异常信息了。
说干就干,主动尝试了一把,动态广播的方式是可以的。但是静态广播的方式,会提示:Background execution not allowed: receiving Intent错误。权限的问题,哪怕已经申请了对应的权限,还是不行。
动态广播的话,还是存在APM_APP进程被杀死的场景。但是这已经是一个很好的方向了。因为这个方案对系统侧是0侵入,所以完全不用考虑适配不同车机系统的场景。而且crash和ANR的异常类型上报统一是完全可以保证的。
方案5:静态广播
静态广播会在发送广播的时候创建对应的进程,所以是可以保活的,因为这无疑是最理想的方向。既然上面说到会提示权限的问题,那么我们就尝试去解决静态广播权限的问题。
具体的错误信息如下:
12-09 16:58:31.732 1251 1296 W BroadcastQueue: Background execution not allowed: receiving Intent { act=android.intent.action.DROPBOX_ENTRY_ADDED flg=0x10 (has extras) } to com.xxx.apm/.broadcast.DropBoxBroadcast
报错的代码如下:
if (!skip) {
final int allowed = mService.getAppStartModeLocked(
info.activityInfo.applicationInfo.uid, info.activityInfo.packageName,
info.activityInfo.applicationInfo.targetSdkVersion, -1, true, false, false);
if (allowed != ActivityManager.APP_START_MODE_NORMAL) {
// We won't allow this receiver to be launched if the app has been
// completely disabled from launches, or it was not explicitly sent
// to it and the app is in a state that should not receive it
// (depending on how getAppStartModeLocked has determined that).
if (allowed == ActivityManager.APP_START_MODE_DISABLED) {
Slog.w(TAG, "Background execution disabled: receiving "
+ r.intent + " to "
+ component.flattenToShortString());
skip = true;
} else if (((r.intent.getFlags()&Intent.FLAG_RECEIVER_EXCLUDE_BACKGROUND) != 0)
|| (r.intent.getComponent() == null
&& r.intent.getPackage() == null
&& ((r.intent.getFlags()
& Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND) == 0)
&& !isSignaturePerm(r.requiredPermissions))) {
mService.addBackgroundCheckViolationLocked(r.intent.getAction(),
component.getPackageName());
Slog.w(TAG, "Background execution not allowed: receiving "
+ r.intent + " to "
+ component.flattenToShortString());
skip = true;
}
}
}
flag=16,package=nulll,comonent=null。所以唯一的判断条件就是isSignaturePerm(r.requiredPermissions)。requiredPermissions其实应该是READ_LOGS的权限,但是APM_APP即使申请到了这个权限,仍然会提示这个错误。所以我们看一下isSignaturePerm这个方法:
final boolean isSignaturePerm(String[] perms) {
if (perms == null) {
return false;
}
IPackageManager pm = AppGlobals.getPackageManager();
for (int i = perms.length-1; i >= 0; i--) {
try {
PermissionInfo pi = pm.getPermissionInfo(perms[i], "android", 0);
if ((pi.protectionLevel & (PermissionInfo.PROTECTION_MASK_BASE
| PermissionInfo.PROTECTION_FLAG_PRIVILEGED))
!= PermissionInfo.PROTECTION_SIGNATURE) {
// If this a signature permission and NOT allowed for privileged apps, it
// is okay... otherwise, nope!
return false;
}
} catch (RemoteException e) {
return false;
}
}
return true;
}
系统竟然判断的是android包名是否具有这个权限,有点扯,不理解为什么判断的是系统包名,而不是APP包名。但是没有办法我们只能另外找办法。
继续往上照,发现allowed != ActivityManager.APP_START_MODE_NORMAL这里还有一个判断。也就是说allowed如果等于ActivityManager.APP_START_MODE_NORMAL,就不用走后面的判断了,我们朝着这个方向进行尝试。
最终发现在ActivityManagerService的appRestrictedInBackgroundLocked方法中有这样一个判断:
// Apps that target O+ are always subject to background check
if (packageTargetSdk >= Build.VERSION_CODES.O) {
if (DEBUG_BACKGROUND_CHECK) {
Slog.i(TAG, "App " + uid + "/" + packageName + " targets O+, restricted");
}
return ActivityManager.APP_START_MODE_DELAYED_RIGID;
}
targetSDK>=26,就直接返回APP_START_MODE_DELAYED_RIGID。所以,我们把APM_APP的targetSDK改为25在进行尝试,发现果然可行。
至此,和系统通讯方案的尝试终于有了结论,我们最终决定使用静态广播的方式来实现,它具有以下几大优势:
1.保活。哪怕APM_APP被杀死,下次广播来的时候系统也会帮忙重新启动对应进程。
2.对系统完全没有侵入性,0改动,移植到其它车型的系统也完全可用。
3.ANR和CRASH上报方案是一致的,不需要区别对应,方便后续维护管理。
四.车载系统APM框架设计和实现
前面探索出来了实现的原理,则最终实现起来就简单的多,
主要分为3个模块:
1.dropbox广播注册者:负责接收异常信息;
2.DB存储:负责记录已经上报过的日志信息;
3.上报模块:负责具体异常信息的上报。
4.1 异常信息接收
创建广播接收者
首先,创建一个广播接收者DropBoxBroadcast。
在onReceive方法中,会接受到系统传递过来的两个参数,分别为tag和time。
tag的形式为:data_app_crash或者data_app_anr,
time就是时间戳的形式。
两个参数一组合,就可以得到文件名,两者以tag@time的方式进行组合。
广播接收到,就意味着异常日志文件已经生成完成,我们直接去解析对应的异常文件,得到我们想要的参数进行上报即可。
需要注意的是,ANR类型的日志文件是gz格式的,需要进行解压缩后才能读取和使用。
广播注册
然后,通过静态注册的方式注册一个广播接收者,action为android.intent.action.DROPBOX_ENTRY_ADDED。
<receiver
android:name=".broadcast.DropBoxBroadcast"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.DROPBOX_ENTRY_ADDED" />
</intent-filter>
</receiver>
4.2 兜底方案处理
虽然我们的广播是可以保活的,但是为了考虑种种情况,我们还是做了一个兜底方案。
即APP启动之后,通过线程延时去读区dropbox中的所有文件,和本地已经上传成功的做对比,找出来那些没有上传过的,进行上传。