再见友盟!Acra详细分析 -Part 1

作者: yftx_ | 来源:发表于2016-05-26 12:00 被阅读2331次

    概述

    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/

    utils.png

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

    analyze dashboard.png

    关于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中进行初始化配置。

    1. 使用注解初始化
        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);
            }
        }
    
    1. 动态初始化配置
        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则需要两个步骤,

    1. 实现ReportSender接口,用来执行发送报告操作。
    2. 实现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

    1. HttpSender
      • 提供了Post及Put两种提交crash到服务器的方式。
      • 提交的类型可以为JSON或Form表单两种方式。

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

    1. 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的相关属性。
    提供了两种方式来设置相关属性的值。

    1. 构造函数通过注解的方式,获取Application中定义注解的值,进行设置。
    2. 通过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值
        //服务端可以根据该值做崩溃分类
    }
    
    

    结语

    本部分内容主要包括

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

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

    相关文章

      网友评论

      • 飘逸_春秋:帮助很大,期待part2
      • Doctor_ZN:能联系下您吗,我现在在安装服务器端的时候遇到了问题,点击repulicator不能实现复制远程的数据库到本地,:cold_sweat: 自己网页访问确能返回json字符串,我按他的格式创建了同名的数据库,但是没有效果啊,acralyzer才显示8.1k,跟你们显示的200多kb不一样,用不了啊,求助~~ qq2790635600
        yftx_:@Doctor_ZN 你翻墙吧。翻过去就好了。
      • ditclear:赞,刚才看了一堆官方英文文档 :smile:

      本文标题:再见友盟!Acra详细分析 -Part 1

      本文链接:https://www.haomeiwen.com/subject/nsrjrttx.html