Acra详细分析

概述

Acra是老牌的bug自动采集系统。接入sdk后,可以实现程序崩溃自动发送崩溃日志。
发送自定义的错误日志等功能。具体详细介绍可以参见 acra官网地址
整体来看,Acra就是通过sdk收集进程的崩溃日志,然后以http或mail(默认的两类Sender)的方式将数据发送出去。服务器则是一套基于json的restful的接口。
服务端方面不是本次分析重点,暂不进行分析。
本系列文章将基于Acra 4.9.0 RC2源码进行分析。

Backend

服务端方面我们需要先搭建一个server,才能更好的看到我们的崩溃信息,
更直观的看到acra给我们提供了哪些针对崩溃的采集内容。
官方提供了acralyzer以及一些针对acra的第三方开源实现。
关于世面上常用的server端的,该文章做了明确分析, 针对不同backend的比较
官方 backend acralyzer的搭建非常简单,具体可以参见该文章的 server配置部分
项目搭建完成后可以使用通过如下的url对server端进行访问。

查看app崩溃的表结构
http://ip:port/_utils/


查看崩溃日志
http://ip:port/acralyzer/_design/acralyzer/index.html#/dashboard/


关于server端的介绍结束。不是重点。

Client
项目构建


最新版本项目基于Gradle构建,了解Acra历史的肯定知道该项目是存在了很久了.
Android世界中项目最早是基于ant构建,后来是maven,现在是Gradle。
在没有Gradle的编译环境之前,基本上大部分是基于maven构建。
查看最新版本的代码可以看到仍然包含了之前maven的配置文件。
并且使用Gradle编译编译中使用到的version name等配置参数也都是从pom.xml中读取的。

具体可以参看 build.gradle中关于版本号的相关配置。
需要注意的是,从github clone下来的项目是无法直接使用Gradle进行编译的。
熟悉Gradle android 编译流程的人应该从build.gradle文件中可以找出错误的原因。
具体的编译文件需要修改的地方为,在build.gradle中开头位置添加编译android项目使用到的plugin。
如下所示:
//此部分添加到build.gradle开头
buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.1.0'
    }
}
allprojects {
    repositories {
        jcenter()
    }
}
task clean(type: Delete) {
    delete rootProject.buildDir
}
//此部分添加到build.gradle开头

添加之后,就可以执行gradle build命令打出需要使用的aar包。

项目配置及使用

首先需要注意一点,Acra使用独立进程:acra,进行采集数据的发送,保证当app崩溃时,采集仍然能发送出去。
由于使用独立的进程,所以会导致application被实例化多次,这样就需要注意app自身的某些业务逻辑,不要在application类中执行多次,从而导致app产生bug。
对Acra的相关配置一般在application中进行初始化。

初始化配置

在application中进行初始化配置。

使用注解初始化
import org.acra.*;
 import org.acra.annotation.*;

 @ReportsCrashes(
     formUri = "http://www.backendofyourchoice.com/reportpath"
 )
 public class MyApplication extends Application {
     @Override
     protected void attachBaseContext(Context base) {
         super.attachBaseContext(base);
         // 调用init方法,对acra进行初始化.
         ACRA.init(this);
     }
 }

动态初始化配置
import org.acra.ACRA;
 import org.acra.configuration.*;

 public class MyApplication extends Application {
     @Override
     protected void attachBaseContext(Context base) {
         super.attachBaseContext(base);
         //使用ConfigurationBuilder构建ACRAConfirueation
         final ACRAConfiguration config = new ConfigurationBuilder(this)
             .setFoo(foo)
             .setBar(bar)
             .build();
         // 传参的方式初始化acra
         ACRA.init(this, config);
     }
 }

一般使用acra我们的目的是采集崩溃,所以需要在manifest中申请网络权限,以保证crash的正常发送。
<uses-permission android:name="android.permission.INTERNET"/>


目标服务器配置

acra中发送crash数据是通过Sender实现的,Sender是通过ReportSenderFactory实例化出来的。
而ReportSenderFactory是可以在初始化时进行配置的。
acra默认提供了email及http 两种sender。
如果自定义Sender则需要两个步骤,

  • 实现ReportSender接口,用来执行发送报告操作。
  • 实现ReportSenderFactory接口,用来创建自定义sender。

public class YourOwnSender implements ReportSender {
    @Override
    public void send(Context context, CrashReportData report) throws ReportSenderException {
        // 遍历 CrashReportData 并做发送操作
    }
}

public class YourOwnSenderfactory implements ReportSenderFactory {
    // 由于在SenderService中通过Class.newInstance()来实例化对象
    // 所以需要保证实例化的类的构造函数有一个默认无参的构造函数
    // 自定义的ReportSenderFactory必须包含一个不含参数的构造函数
    public ReportSender create(Context context, ACRAConfiguration config) {
        ...
        return new YourOwnSender(someConfigPerhaps);
    }
}

针对Sender的配置有两种形式,一种为注解,一种为通过代码进行设置。
//注解的方式设置Sender
@ReportCrashes{
   reportSenderFactoryClasses = {
        your.funky.ReportSenderFactory.class, 
        other.funky.ReportSenderFactory.class
   } 
}
public class YourApplication extends Application {
   ...
}

//代码的方式设置Sender
@ReportCrashes{
   ...
}
public class YourApplication extends Application {
    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);

        final Class<? extends ReportSenderFactory>[] myReportSenderFactoryClasses = ...

        // 初始化一个ConfigurationBuilder,并设置ReportSenderFactoryClasses.
        final ACRAConfiguration config = new ConfigurationBuilder(this)
            .setReportSenderFactoryClasses(myReportSenderFactoryClasses)
            .build();
        ACRA.init(this, config);
    }
}

Acra中默认提供两个Sender

HttpSender

提供了Post及Put两种提交crash到服务器的方式。
提交的类型可以为JSON或Form表单两种方式。
建议使用Put方式进行提交。
Put可以理解成已经知道了某个资源的位置.代表直接更新或创建该资源。
POST为不知道某个资源的位置,由server端来决定对该资源进行何种方式的存储。
所以在此场景下使用Put操作更合适,因为每一条bug实际上就应该对应与数据库中的一条,
只是该条记录还没有上传到服务器。
关于post与put的差别,具体可以查看该文档when should use PUT and when should use POST


EmailIntentSender
组拼crash Report 通过intent调用系统提供的发送email的app。

流程分析及重点类分析
初始化设置流程


Acra的初始化函数为init,所以使用入口函数ACRA.init()对acra进行初始化。
一般入口函数在application初始化时进行调用。

ACRA.init()

使用ReportsCrashes来初始化Acra。
ACRA提供多个init方法,经过内部调用,最终都会调用参数最多的init方法完成初始化相关逻辑。
下面对重要的init方法进行说明

class ACRA {
    //使用Application的注解进行初始化
    public static void init(Application app){
        //获取application上的注解
        final ReportsCrashes reportsCrashes = 
            app.getClass().getAnnotation(ReportsCrashes.class);
        //ConfigurationBuilder中通过注解获取application上配置的注解信息
        init(app, new ConfigurationBuilder(app).build());
    } 
    //参数 checkReportsOnApplicationStart 表示
    //是否立即执行ErrorReporter.checkReportsOnApplicationStart()方法
    public static void init(Application app, ACRAConfiguration config, boolean checkReportsOnApplicationStart){
        //根据process的名字判断执行当前方法执行时所在的进程是否是发送crash的进程
        final boolean senderServiceProcess = isACRASenderServiceProcess(app);
        //ACRA只支持2.3以上的系统版本,所以预先做判断
        final boolean supportedAndroidVersion = Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO;
        //保存config
        configProxy = config;
        //获取ACRA保存配置的SharedPreferences
        final SharedPreferences prefs = new SharedPreferencesFactory(mApplication, configProxy).create();
        if (!prefs.getBoolean(PREF__LEGACY_ALREADY_CONVERTED_TO_4_8_0, false)) {
            //处理之前的版本的日志文件
        }
        errorReporterSingleton = new ErrorReporter(mApplication, configProxy, prefs, enableAcra, supportedAndroidVersion, !senderServiceProcess);   
        //当在非Sender进程,并设置app启动时发送report的情况下进行检测。
        //当在Sender进程中,不需要进行检测,因为Sender进程中的逻辑自己会进行判断处理
        if (checkReportsOnApplicationStart && !senderServiceProcess) {
        //执行发送的相关业务逻辑
            final ApplicationStartupProcessor startupProcessor = 
                new ApplicationStartupProcessor(mApplication,  config);
                if (config.deleteOldUnsentReportsOnApplicationStart()) {
                    startupProcessor.deleteUnsentReportsFromOldAppVersion();
                }
                if (config.deleteUnapprovedReportsOnApplicationStart()) {
                    startupProcessor.deleteAllUnapprovedReportsBarOne();
                }
                if (enableAcra) {
                    startupProcessor.sendApprovedReports();
                }
         }
    }
}

ConfigurationBuilder

主要用来封装构造ACRAConfiguration的相关属性。
提供了两种方式来设置相关属性的值。

  • 构造函数通过注解的方式,获取Application中定义注解的值,进行设置。
  • 通过set方法,设置每个不同的配置项。


获取属性值之后,通过调用build()方法,创建ACRAConfiguration对象。

//通过app的注解所配置的值对builder对象本身进行初始化
public ConfigurationBuilder(@NonNull Application app) 
{
    //.....
}
//构建ACRAConfiguration对象
public ACRAConfiguration build() {
    return new ACRAConfiguration(this);
}

....
//对外提供的设置相关属性的方法
public ConfigurationBuilder setHttpHeaders(@NonNull Map<String, String> headers) {
    this.httpHeaders.clear();
    this.httpHeaders.putAll(headers);
    return this;
}


ACRAConfiguration

用来保存ACRA涉及到的所有配置。

SharedPreferencesFactory

用来获取ACRA所使用的SharedPreferences的文件。
通过这层封装可以对sp进行一些自定义的设置,比如sp的名字。

public class SharedPreferencesFactory {
    //获取默认sharedPreferences的流程为
    //1.如果通过builder或ReportsCrashes配置所构建的类生成的config文件,
    //  包含sp相关配置,则使用该配置项。
    //2.如果不满足1的条件,则通过android api PreferenceManager返回默认的sp文件
    public SharedPreferences create() {
        if (context == null) {
        //..
        } else if (!"".equals(config.sharedPreferencesName())) {
            return context.getSharedPreferences(
                config.sharedPreferencesName(), config.sharedPreferencesMode()
            );
        } else {
            return PreferenceManager.getDefaultSharedPreferences(context);
        }
    }
}


ErrorReporter

ACRA最核心的类,该类用来捕获crash相关的信息,以及发送crash信息。
Android平台如果想要捕获java层代码的crash需要设置application Thread的UncaughtExceptionHandler。
ACRA会将ErrorReporter设置为Application Thread的UncaughtExceptionHandler。
从而实现对异常的捕获。
这里有一点需要注意的,Thread中的defaultUncaughtHandler为一个对象,
所以多次设置该属性,则会使用最后一个作为异常捕获的类。
比如现在市面上比较火的umeng等相关包含崩溃采集功能sdk。
使用的时候,需要注意查看文档或反编译其源码,查看sdk是怎么实现该部分功能的。
否则容易造成先设置的异常捕获类,无法被执行。


public class ErrorReporter implements Thread.UncaughtExceptionHandler {
    ErrorReporter(
        @NonNull Application context, @NonNull ACRAConfiguration config, 
        @NonNull SharedPreferences prefs,boolean enabled, 
        boolean supportedAndroidVersion, boolean listenForUncaughtExceptions)
    {
        ...
        //通过ConfigurationCollector获取系统的相关环境信息
         if (config.getReportFields().contains(ReportField.INITIAL_CONFIGURATION)) {
            initialConfiguration = ConfigurationCollector.collectConfiguration(this.context);
        } else {
            initialConfiguration = null;
        }
        //获取系统时间,崩溃发生时上传
        final Calendar appStartDate = new GregorianCalendar();
        crashReportDataFactory = new CrashReportDataFactory(
            this.context, config, prefs, appStartDate, initialConfiguration);
        final Thread.UncaughtExceptionHandler defaultExceptionHandler;
        //listenForUncaughtExceptions为Acra初始化流程中传过来的。
        //如果当前运行的进程是Sender进程则不监听崩溃。
        //如果当前运行的进程是app主进程则对崩溃进行监听
        if (listenForUncaughtExceptions) {
            defaultExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
            Thread.setDefaultUncaughtExceptionHandler(this);
        } else {
            defaultExceptionHandler = null;
        }
        //记录最后的activity
        final LastActivityManager lastActivityManager = new LastActivityManager(this.context);
        //用来保存针对崩溃的一些用户自定义的信息
        final ReportPrimer reportPrimer = getReportPrimer(config);

        reportExecutor = new ReportExecutor(
            context, config, crashReportDataFactory, 
            lastActivityManager, defaultExceptionHandler, reportPrimer);
        reportExecutor.setEnabled(enabled);
    }

    //崩溃采集需要实现UncaughtExceptionHandler为接口。
    @Override
    public void uncaughtException(@Nullable Thread t, @NonNull Throwable e) {
        //未开启crash采集时,使用之前默认的ExceptionHandler处理
        if (!reportExecutor.isEnabled()) {
            reportExecutor.handReportToDefaultExceptionHandler(t, e);
            return;
        }
        try {
            ACRA.log.e(LOG_TAG, "ACRA caught a " + e.getClass().getSimpleName() +
                " for " + context.getPackageName(), e);
            if (ACRA.DEV_LOGGING) ACRA.log.d(LOG_TAG, "Building report");
            performDeprecatedReportPriming();
            // 生成并发送report
            new ReportBuilder()
                .uncaughtExceptionThread(t)
                .exception(e)
                .endApplication()
                .build(reportExecutor);
        } catch (Throwable fatality) {
            // ACRA failed. Prevent any recursive call to ACRA.uncaughtException(), let the native reporter do its job.
            ACRA.log.e(LOG_TAG, "ACRA failed to capture the error - handing off to native error reporter" , fatality);
            reportExecutor.handReportToDefaultExceptionHandler(t, e);
        }
    }

}

参见代码可以知道,acra通过设置默认ExceptionHandler来捕获异常。
并把自己设置为处理对象。

LastActivityManager

是用来记录最后展示的Activity的,通过application.registerActivityLifecycleCallbacks来实现记录功能的。ACRA可以在崩溃的时候弹出Dialog,所以需要记住最后的Activity。

ReportExecutor

主要业务逻辑关注execute()方法.
该类主要负责调用CrashReportDataFactory采集数据,
调用CrashReportPersister对崩溃数据进行持久化,
调用SenderServiceStarter运行Service发送的报告。

ApplicationStartupProcessor

封装一些App启动时可能执行的任务

class ApplicationStartupProcessor{
    void deleteUnsentReportsFromOldAppVersion(){
        //app版本更新后,一般会修掉老的崩溃等问题,
        //所以当老版本更新到新版本后,可以将老版本记录的日志全部删除掉
    }

    void deleteAllUnapprovedReportsBarOne(){
        //unapproved的文件夹内的文件,只保留最新创建的日志文件,其他的全部删除掉。
    }

    void sendApprovedReports(){
        //调用SenderServiceStarter开启Service进行崩溃日志的发送。
    }

}

ReportLocator

关于ACRA对日志文件位置的处理主要是ReportLocator来设置的。
acra内部使用文件对崩溃日志进行保存,该类用来获取文件夹的名字。
内部有两个文件夹acra-unapproved(未处理),acra-approved(处理过)分别用来保存未处理及处理过的崩溃文件。

采集内容

崩溃采集,必然需要采集崩溃及手机的相关信息。
ACRA中涉及到崩溃相关信息的主要有如下一些类。
ReportBuilder,ReportPrimer,CrashReportDataFactory,CrashReportData,
LogCatCollector,DropBoxCollector,ReportUtils,UUID,
Installation,ConfigurationCollector,DumpSysCollector,ReflectionCollector,
DisplayManagerCollector,DeviceFeaturesCollector,settingsCollector,
LogFileCollector,MediaCodecListCollector,ThreadCollector.
ACRA获取全部数据,涉及到的类比较多。下面逐个分析。

ReportBuilder

对throwable,message,自定义信息,以及exception的简单封装。
主要方法为build(),通过build方法调用ReportExecutor.execute()方法,
在ReportExcutor中进行真正的crash采集以及调用发送Service

ReportPrimer

用来设置崩溃时候,用户需要保存的一些用户自定义的信息。
比如崩溃时候在此类中设置一些用户账号等相关信息。
该类中设置的相关内容会一起发送到服务端,从而更好的定位一些崩溃信息。

CrashReportDataFactory,CrashReportData

CrashReportDataFactory类用来实例化CrashReportData。
其中最重要的方法为createCrashData()方法,使用该方法来组拼CrashReportData。
CrashReportData继承EnumMap,其中保存的数据的key为各种上传时候的key,
对应的值为崩溃的相关信息。后面的流程中该类中的值会通过CrashReportPersister类写入file文件。

LogCatCollector

用来获取logcat日志中的相关信息,执行Logcat命令,读取命令输出信息。

class LogCatCollector{
    public String collectLogCat(){
        //根据所传参数不同组拼不同的logcat命令
        //主要组拼出的命令为
        //1.logcat -t 100 -v time 
        //2.logcat -t 100 -v time -b radio
        //1.logcat -t 100 -v time -b events
    }
}


logcat -b events

05-18 19:45:46.158 31191 31191 I auditd  : type=1400 audit(0.0:505001): avc: denied { search } for comm="PerfFgMonitor" name="1711" dev="proc" ino=18618 scontext=u:r:untrusted_app:s0:c512,c768 tcontext=u:r:radio:s0 tclass=dir permissive=0

logcat -b radio

05-18 19:44:39.343  1711  1785 D RILJ    : [9679]< RIL_REQUEST_GET_CELL_INFO_LIST [CellInfoWcdma:{mRegistered=YES mTimeStampType=oem_ril mTimeStamp=1283159923921792ns CellIdentityWcdma:{ mMcc=460 mMnc=1 mLac=53529 mCid=101852154 mPsc=438} CellSignalStrengthWcdma: ss=8 ber=99}] [SUB0]
05-18 19:44:39.345  1711  1975 D GsmSST  : [GsmSST] SST.getAllCellInfo(): X size=1 list=[CellInfoWcdma:{mRegistered=YES mTimeStampType=oem_ril mTimeStamp=1283159923921792ns CellIdentityWcdma:{ mMcc=460 mMnc=1 mLac=53529 mCid=101852154 mPsc=438} CellSignalStrengthWcdma: ss=8 ber=99}]
05-18 19:44:39.346  1711  1975 D GsmSST  : [GsmSST] getCellLocation(): X ret WCDMA info=[53529,101852154,438]
05-18 19:44:43.068  1711  1927 D SubscriptionController: [getPhoneId]- no sims, returning default phoneId=2147483647

其实相信大部分人不太清楚logcat的相关命令。
针对以上的三条命令做如下解释
logcat -t 100 -v time
-t 限制打印100行内容
-v time 设置日志输出格式。打印日志的为:打印日期->触发时间->优先级(E,W,V)->tag->出问题进程的pid
关于日志输出格式的介绍参见此处日志输出格式。

logcat -b [options] 切换打印log的内容级别

radio radio/telephony相关log
events events-related相关log
main 默认的log


DropBoxCollector

通过DropBoxManager读取系统系统的日志信息
DropBoxManager,很多人应该也没接触过。
android系统实际上是有三种日志打印的。log EventLog DropBox,关于三种log的介绍参见此处。
三种log的介绍
关于DropBoxManager的相关内容可以参见此处。 dropboxManager介绍
class DropBoxCollector{
    public String read(){
        //通过DropBoxService获取系统的DropBoxManager
        //读取所有预先定义的不同tag对应的日志内容
    }
}

ReportUtils
封装的各种工具类,用来获取系统相关的信息
public getAvailableInternalMemorySize(){
    //通过StatFs类获取可用内存block数量及每个block的size
    //block_size * free_block_count = 可用内存数
}

public getTotalInternalMemorySize(){
    //通过StatFs类获取所有内存block数量及每个block的size
    //block_size * total_block_count = 总内存数
}

public getDeviceId(){
    //通过TelephonyManager获取deviceId
    //GSM手机对应与IMEI
    //CDMA手机对应与ESN或MEID
}

public getApplicationFilePath(){
    //通过context.getFilesDir()获取当前app的绝对路径
    //'/data/user/0/yftx.net.oschina.git.gradlesample/files'
}

public getLocalIpAddress(){
    //通过NetworkInterface 获取当前设备的ip
}

public getTimeString(){
    //通过Calendar类获取当前时间
}

UUID
java.util包中提供的类,用来生成唯一字符串的类。

Installation
用来生成唯一身份串的类。

class Installation{
    void id(){
        //获取的id用来标记用户的身份。
        //具体算法可以参见android blog中的解释。
        //http://android-developers.blogspot.com/2011/03/identifying-app-installations.html
    }
}

ConfigurationCollector
通过反射系统的Configuration类,获取系统相关参数。

class ConfigurationCollector{
    void collectConfiguration(Context context){
        //通过 context.getResources().getConfiguration()获取configration对象,
        //并用反射获取该类中的相关信息
    }
}


DumpSysCollector
通过执行dumpsys meminfo xxxpid 来分析内存
关于dumpsys的介绍参见此: dumsys相关介绍
class DumpSysCollector{
    void collectMemInfo(){
    //执行dumsys 相关命令
    }
}

ReflectionCollector
相当于Util类,主要通过反射获取传过来的类的一些信息。
class ReflectionCollector{
    void collectConstants(){
        //通过反射获取系统的相关信息
        //acra中主要获取Build,Build.Version中的相关数据
    }
}

DisplayManagerCollector
主要用来获取手机显示相关的数据
class DisplayManagerCollector{
    void collectDisplays(){
        //通过Display类获取屏幕宽,高,方向等显示相关的参数
    }
}

DeviceFeaturesCollector
通过PackageManager获取系统相关特性。比如glEsVersion等
class DeviceFeaturesCollector{
    void getFeatures(){
        //通过PackageManager获取系统相关特性。比如glEsVersion等
    }
}

SettingsCollector
使用反射获取android.provider.Settings.x中的相关内容。
class SettingsCollector{
    void collectSystemSettings(){
        //获取系统Settings类中的相关信息
    }

    void collectSecureSettings(){
        //获取Settings.Secure中的相关信息
    }

    void collectGlobalSettings(){
        //获取Settings.Global中的相关信息
    }
}

LogFileCollector
获取用户自己保存的相关的log文件,使用该接口可以让acra结合 logback-android这类类库相结合。
很多做android的同学都没有做过java web开发,并且android的Log接口也还算好用,再加上客户端编程和服务端编程系统的不同,所以可能理解不了logback-android这样库的意义。
实际上logback-android这类库主要就是可以指定log输出的位置,以及log的打印级别。
关于java开发中log的重要性可以参见此文章, java log的意义

MediaCodecListCollector
主要用来获取系统支持哪些音视频类型等媒体相关的。

ThreadCollector
获取崩溃线程的相关信息。
class ThreadCollector{
    void collect(Thread t){
        //获取线程t的相关信息,id,name,priority,groupName
    }
}

ACRA中用到的其他一些获取异常的方法
getStackTracehash(Throwable th){
    //通过组拼Error的className及MethodName生成的字符串
    //获取该字符串的hash值
    //服务端可以根据该值做崩溃分类
}

结语
本部分内容主要包括

  • ACRA如何配置(服务端,客户端的配置)
  • 崩溃信息相关内容如何采集,涉及到的关键类。

后面的部分会继续分析如何将生成的file发送到服务端。

猜你喜欢

转载自iaiai.iteye.com/blog/2308091