DropBox系列-打造车载系统APM框架

时间:2022-12-18 09:56:53

前言:

作者本人负责公司的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));
            }
            ...
        }
        ...
    }

主要做了三件事:

  1. 生成错误信息;

  1. 通过binder通知到AMS的handlerApplicationCrash方法;

  1. 删掉当前进程,完成应用的彻底退出。

AMS的handlerApplicationCrash方法中收到崩溃信息后,会通过DroxBoxManagerSerivice服务完成最终崩溃信息的记录和持久化,最终存储到data/system/dropbox文件夹下,这一块我们就不详细赘述了,我们只要支持,所有APP的崩溃,都会通知到AMS即可。

所以,我们只要在AMS中加上适当的钩子,完成崩溃消息的转发,就可以记录所有APP的java层崩溃(native崩溃另讲,检测机制不一样)。

2.2 ANR监控原理

具体原理可以参考这一篇文章,本章仍是简介。ANR系列之一:ANR显示和日志生成原理讲解_失落夏天的博客-CSDN博客

导致ANR的场景有很多种,比如输入事件无响应,广播事件无响应,Service中超时等等,具体类型和超时时间如下(下图中的某些时间其实是有问题的)。

DropBox系列-打造车载系统APM框架

无论哪种类型的ANR,最终都会通知到AnrHelper.appNotResponding方法中,所以我们从这个流程开始往下看。

DropBox系列-打造车载系统APM框架

 经过种种转发,最终的执行逻辑在ProcessErrorSatetRecord方法中,主要会执行下面三大块逻辑:

1.收集ANR信息

2.保存ANR日志

3.弹出ANR显示框

2.3 APP启动速度监控原理

APP启动,主要分为四部分:

  1. Laucher或其它应用通知AMS,显式或者隐式的去启动相应的Activity。

  1. 如果Activity所对应的APP进程不存在,则会走进程创建流程,通知Zygote去fork产生对应的进程,APP进程产生后,回调通知AMS。

  1. AMS通知APP一侧去加载对应的APK文件,以及创建和初始化Application及其它必要元素。

  1. AMS通知APP一侧去创建,初始化,以及最终显示Activity。

至此,整个启动流程结束,整个流程的时间可以理解为通俗理解上的冷启动时间。

完整的APP启动流程图如下:

DropBox系列-打造车载系统APM框架

所以,针对开始和结束分别设置一个钩子,那么两者之间的时间差,就是我们所需要冷启动时间了。

开始的事件很容易设置,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即可,不做其他任何的逻辑处理。这样对系统侧的修改就会尽可能的小。整体架构图设计如下:

DropBox系列-打造车载系统APM框架

接下来,我们该尝试,系统侧发现了异常之后,如何通知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中的所有文件,和本地已经上传成功的做对比,找出来那些没有上传过的,进行上传。