0%

崩溃收集 - ACRA原理

为什么选择ACRA做崩溃收集?

项目主页:https://github.com/ACRA/acra

  1. ACRA历史悠久,github显示最早的4.2.3的版本是2011年7月2日。
  2. Issues里不停的有人报问题,也在不停的发新的release,项目处于积极的持续维护的状态。
  3. 自定义性很强,框架所有功能都是可配置自定义的,也提供了服务端的标准实现。
  4. 使用广泛。ACRA is used in 1.57% (See AppBrain/stats) of all apps on Google Play as of June 2020. That’s over 13 thousand apps and over 5 billion downloads including ACRA.

Android上捕获崩溃的基本原理是什么?

通过Thread.setDefaultUncaughtExceptionHandler()设置一个UncaughtExceptionHandler,当有未捕获的异常出现时,会调用UncaughtExceptionHandler的uncaughtException(Thread,Throwable)

ACRA框架大致原理

  • 通过Thread.setDefaultUncaughtExceptionHandler()捕获崩溃异常对象。
  • 调用配置的多个Collector收集崩溃发生时的环境信息,可以通过SPI注入自定义Collector收集想要的数据。
  • 把异常堆栈和环境信息存储到文件。
  • 立即开启新的进程上传文件。

为什么采用文件存储?

这是跟崩溃的场景有关。

崩溃数据不会频繁产生,并且崩溃后立即上传,存储到文件直接上传文件是最方便的。

数据库存储主要针对批量查询或者复杂约束条件的场景,否则发挥不出来明显优势。
甚至有一定劣势,比如数据库如果损坏,所有信息都读取不到了,单个崩溃产生的文件如果损坏了不影响其他文件。

为什么上传错误要在单独的进程里执行?

因为崩溃要立刻上传,但是当前进程已经要停止了,网络请求耗时的话可能没有请求完就结束了,所以单独开一个进程做上传,不受崩溃进程的影响。

参考:https://github.com/ACRA/acra/issues/375

如何对崩溃做唯一标识以便统计崩溃次数?

后端要对上传的崩溃做唯一性识别以统计同一个崩溃的次数。

把异常对象传递给 StacktraceCollector.getStackTraceHash(Throwable) 返回一个字符串,作为这个异常对象的唯一标识。

源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private String getStackTraceHash(@Nullable Throwable th) {
final StringBuilder res = new StringBuilder();
Throwable cause = th;
while (cause != null) {
final StackTraceElement[] stackTraceElements = cause.getStackTrace();
for (final StackTraceElement e : stackTraceElements) {
res.append(e.getClassName());
res.append(e.getMethodName());
}
cause = cause.getCause();
}

return Integer.toHexString(res.toString().hashCode());
}

遍历StackTraceElement数组,把每一个StackTraceElement的类名和方法名拼接为字符串,以字符串的hashCode为唯一标识。

也就是异常里的方法调用栈中的所有类名和方法名作为异常唯一标识。

程序入口在哪?

在自定义的Application的attachBaseContext()中调用ACRA.init(application);

ACRA.init(application)做了什么?

主要是在非崩溃发送的进程中,创建ErrorReporterImpl对象,保存为单例

ErrorReporterImpl构造函数里做了什么?

主要执行的逻辑:

  1. 创建了CrashReportDataFactory,并调用其collectStartUp(),收集App启动时的信息,如App启动时间
  2. 调用了Thread.setDefaultUncaughtExceptionHandler()拦截崩溃处理
  3. 如果checkReportsOnApplicationStart为true,就创建一个StartupProcessorExecutor,调用其processReports(),进行崩溃信息上报

CrashReportDataFactory.collectStartUp()做了什么?

CrashReportDataFactory构造函数里通过CoreConfiguration.pluginLoader().loadEnabled(config,Collector.class)加载出一个List<Collector>

collectStartUp()在Collector列表中寻找ApplicationStartupCollector调用其collectApplicationStartUp()。

Collector是什么?

所有崩溃相关的信息都是通过Collector来收集,实现类都在org.acra.collector包下,收集如线程信息、内存信息、堆栈信息等。

ApplicationStartupCollector.collectApplicationStartUp()是做什么的?

在application启动时收集必要的信息,例如app启动时间

CoreConfiguration.pluginLoader().loadEnabled(config,Collector.class)底层原理是什么?

CoreConfiguration.pluginLoader()获取的是什么类?

ACRA.init(application)里会创建一个CoreConfigurationBuilder对象,CoreConfigurationBuilder构造函数里会创建一个BaseCoreConfigurationBuilder,BaseCoreConfigurationBuilder构造函数里会创建一个ServicePluginLoader,这是默认的pluginLoader,外部可以通过CoreConfigurationBuilder.setPluginLoader来设置其他的pluginLoader。

ServicePluginLoader.loadEnabled()做了什么?

通过ServiceLoader.load(clazz, getClass().getClassLoader())利用ServiceLoader的SPI机制加载所有的Collector的实现类,只保留Collector.enabled()返回true的Collector,汇总成一个Collector列表,并返回。

ServiceLoader机制是怎样的?

ServiceLoader.load(org.acra.collector.Collector.class)会去META-INF/services目录下寻找文件名为org.acra.collector.Collector的文件,文件里每一行都是org.acra.collector.Collector实现类的全限定名,然后ServiceLoader会把所有这个文件里指定的类都创建出来并返回。

用AutoService框架可以通过注解在实现类上标明接口,就可以避免自己手动创建META-INF/services下文件的繁琐过程,ACRA使用了AutoService,在所有Collector的实现类上都标明了@AutoService(Collector.class)。

StartupProcessorExecutor的processReports()做了什么?

  1. StartupProcessorExecutor构造函数会创建ReportLocator对象,通过reportLocator.getUnapprovedReports()和reportLocator.getApprovedReports()获取待上传的崩溃信息的文件。

  2. 通过config.pluginLoader().loadEnabled(config,StartupProcessor.class)获取一个List,遍历调用processReports(reports)

  3. StartupProcessor有两个子类,LimiterStartupProcessor、UnapprovedStartupProcessor

  4. approved文件夹下有文件,就依次调用SchedulerStarter.scheduleReports() -> DefaultSenderScheduler.scheduleReportSending() -> SendingConductor.sendReports() -> ReportDistributor.distribute() -> ReportDistributor.sendCrashReport(),遍历所有的ReportSender并调用其send()发送上报

根据ReportExecutor.execute(reportBuilder)的逻辑,approved目录下有文件,说明是要上报发送文件的,但是可能因为各种原因没有上报成功,所以这里重新发送。

reportLocator.getUnapprovedReports()和reportLocator.getApprovedReports()的区别?

关于ACRA对日志文件位置的处理主要是ReportLocator来设置的。

acra内部使用文件对崩溃日志进行保存,该类用来获取文件夹的名字。

内部有两个文件夹acra-unapproved(未处理),acra-approved(处理过)分别用来保存未处理及处理过的崩溃文件。

unapproved和approved的崩溃文件分别是什么情况会产生?什么状态算approved?什么状态算unapproved?

通过Thread.UncaughtExceptionHandler拦截到崩溃异常后,会收集崩溃相关所有信息,存储到一个文件,文件位于unapproved文件夹下;

当决定要发送上传崩溃信息文件了,就会把崩溃信息文件从unapproved文件夹移动到approved文件夹,再上传approved文件夹下的文件里的信息。

ErrorReporterImpl实现了Thread.UncaughtExceptionHandler,具体做了什么?

核心代码:

1
2
3
4
5
6
7
8
9
public void uncaughtException(@Nullable Thread t, @NonNull Throwable e) {
// Generate and send crash report
new ReportBuilder()
.uncaughtExceptionThread(t)
.exception(e)
.customData(customData)
.endApplication()
.build(reportExecutor);
}

ReportBuilder的build()里其实是执行了reportExecutor.execute(reportBuilder),在这个方法里实现了崩溃信息上报。

ReportExecutor.execute(reportBuilder)做了什么?

常规逻辑主要如下:
1. crashReportDataFactory.createCrashData(reportBuilder) 创建崩溃数据项
2. saveCrashReportFile(reportFile, crashReportData) 保存崩溃数据到本地文件中
3. sendReport(reportFile, executor.hasInteractions()) 根据初始化的配置是否立刻上报发送崩溃信息

crashReportDataFactory.createCrashData(reportBuilder)做了什么?

遍历所有的启用的Collector实现类,在单独的线程池里并发调用Collector的collect()方法收集信息,结果存到CrashReportData类中,CrashReportData以json存储信息。

saveCrashReportFile(reportFile, crashReportData) 做了什么?

把crashReportData转为json字符串,写入ACRA-unapproved目录下的文件中

sendReport(reportFile, executor.hasInteractions()) 做了什么?

把崩溃信息文件从ACRA-unapproved目录移动到ACRA-approved下

通过CoreConfiguration.reportSenderFactoryClasses()或CoreConfiguration.pluginLoader().loadEnabled(ReportSenderFactory.class)去加载ReportSenderFactory实现类,再创建ReportSender实例。

调用SchedulerStarter.scheduleReports() -> DefaultSenderScheduler.scheduleReportSending() 。

如果ReportSender是要后台发送的就用JobSenderService执行,JobSenderService是运行在单独的:acra进程中,JobSenderService做的就是创建一个SendingConductor对象,并调用SendingConductor.sendReports() 。

如果是前台发送的就再依次调用SendingConductor.sendReports() -> ReportDistributor.distribute() -> ReportDistributor.sendCrashReport(),遍历所有的ReportSender并调用其send()发送上报。

ReportSender默认的实现有
1. EmailIntentSender,通过Intent调起邮件App发送,前台发送
2. HttpSender,Http网络请求发送上传,后台发送

次要的逻辑有什么?

先获取所有ReportingAdministrator的实现类,ReportingAdministrator是控制是否应该发送崩溃报告,这里起作用的是LimitingReportAdministrator。

先调用ReportingAdministrator.shouldStartCollecting(context,config,reportBuilder)决定是否允许收集信息,也就是记录通过Thead.setDefaultUncaughtExceptionHandler()拦截到的崩溃异常,返回true表示允许,返回false表示不允许。允许的话就会调用crashReportDataFactory.createCrashData(reportBuilder)。

允许收集信息后,再调用ReportingAdministrator.shouldSendReport(crashReportData)决定是否应该上报,这里会把前面收集到的信息封装为crashReportData,传给shouldSendReport()决定是否应该上报这个崩溃信息。

如果ReportingAdministrator.shouldSendReport(crashReportData)返回false,即不允许发送崩溃报告,则调用ReportingAdministrator.notifyReportDropped()通知外部崩溃报告被丢弃,没有上报。

ReportingAdministrator总体来说起到了发送崩溃报告时的拦截器的作用。

对特定功能处理过程交给拦截器处理,拦截器可以动态注入

通过PluginLoader动态注入加载创建各种用户可扩展类,例如Collector、ConfigurationBuilderFactory、ReportingAdministrator、ReportInteraction、SenderSchedulerFactory、ReportSenderFactory、StartupProcessor。

PluginLoader可以在运行时手动load class(SimplePluginLoader),也可以通过ServiceLoad机制去动态加载(ServicePluginLoader)。

Log、EventLog、DropboxLog之间的区别?

在Logcat输出的日志主要是给App开发者看的。

EventLog和DropboxLog输出的日志主要是给平台开发者看的,区别在于EventLog不会保存到文件,DropBoxLog会保存到文件。

参考: