Android开发之奔溃处理,知道你的App为啥崩溃了吗?

【版权申明】非商业目的可自由转载
博文地址:https://blog.csdn.net/ShuSheng0007/article/details/101061653
出自:shusheng007

概述

在Android开发中任何App都存在crash的可能性,所以当奔溃后如何获得有用信息进而修复这个问题,防止再次发生成了我们必须面对的问题。Android发展到现在,开发工具与最初时期已经不可同日而语了,就是关于这个关于崩溃的报告工具也是多如牛毛,我自己用过的就包括腾讯的 buggly,Google的fabric,百度的一个奔溃工具,那我们今天就稍微理解一下他们的工作原理。

Android (其实是Android 的JVM的功能)本身提供了一套处理由于未捕获的异常引起的奔溃机制,那就是使用下面这个定义在Thread类内部的接口。

@FunctionalInterface
public interface UncaughtExceptionHandler{
        void uncaughtException(Thread t, Throwable e);
}

这个接口的作用是当一个线程由于未捕获的异常而突然中止时,会回调其方法uncaughtException(). 市面上比较流行的崩溃监控工具都是基于这个原理开发的,当捕获了奔溃后将异常信息上传至其服务器,例如腾讯的 buggly,Google的fabric。

今天我们就详细了解一下这个接口,并写一个自己的异常处理器,这个异常处理器也是存在实际意义的,可以协助日常的debug工作。

原理

假设我们有一个线程Thread1, 其属于ThreadGroup1(java中每个线程都必须隶属于一个ThreadGroup),其由于未捕获的NullPointerException而崩溃了,虚拟机执行的步骤如下:

  1. 先查看Thread1 有没有设置UncaughtExceptionHandler,有的话就调用其uncaughtException()方法处理异常
  2. 否则就调用ThreadGroup1uncaughtException()方法处理异常,这个方法的执行逻辑如下
    a 先查看ThreadGroup1 有没有父 ThreadGroup,有则调用其父ThreadGroupuncaughtException()
    b 否则查看是否存在线程的默认处理器DefaultUncaughtExceptionHandler,这个处理器是对当前进程的所有线程起作用的。存在则调用其方法uncaughtException()方法处理异常。
    c否则检查异常是否是ThreadDeath类型,如果则不做任何处理,如果不是则打印异常到输出窗口

原理清楚了,我开始设计一个自己的异常处理器。我们要求当发生奔溃时,可以生成奔溃报告并

自定义uncaughtExceptionHandler

/**
 * Created by shusheng007
   */
public class UncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
    private static final String TAG = "UncaughtExceptionHandler ";
    private Thread.UncaughtExceptionHandler mDefaultHandler;
    private Context mContext;
    private String reportLocation;

    private ExecutorService mService = Executors.newSingleThreadExecutor();

    private UncaughtExceptionHandler () {
    }

    public static UncaughtExceptionHandler getInstance() {
        return InstanceMaker.instance;
    }

    public String getReportDefaultLocation(@NonNull Context context) {
        return context.getExternalFilesDir(null).getPath() + "/crashReports/";
    }

    public void init(Context context) {
        init(context, "");
    }

    public void init(Context context, String reportLocation) {
        mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();
        Thread.setDefaultUncaughtExceptionHandler(this);
        mContext = context.getApplicationContext();
        if (TextUtils.isEmpty(reportLocation)) {
            this.reportLocation = getReportDefaultLocation(context);
        } else if (reportLocation.endsWith("/")) {
            this.reportLocation = reportLocation + "crashReports/";
        } else {
            this.reportLocation = reportLocation + "/crashReports/";
        }
    }

    public void setReportLocation(String reportLocation) {
        this.reportLocation = reportLocation;
    }

    @Override
    public void uncaughtException(@NonNull Thread thread, @NonNull Throwable throwable) {
        Future<Boolean> future = mService.submit(() -> {
            save2File(reportLocation, generateReport(throwable).toString() + "\n\n");
            return true;
        });
        try {
            if (future.get().booleanValue()) {
               if (mDefaultHandler != null) {
                    mDefaultHandler.uncaughtException(thread, throwable);
                } else {
                    android.os.Process.killProcess(android.os.Process.myPid());
                }
            }
        } catch (ExecutionException | InterruptedException e) {
            Log.e(TAG, e.getMessage(), e);
        }
    }

    private void save2File(String reportLocation, String crashReport) {
        if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
            return;
        }
        File dir = new File(reportLocation);
        if (!dir.exists()) {
            dir.mkdir();
        }
        DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
        Calendar calendar = Calendar.getInstance();
        calendar.add(Calendar.DAY_OF_YEAR, -30);
        for (File file : dir.listFiles()) {
            if (!file.isFile()) {
                continue;
            }
            try {
                if (dateFormat.parse(file.getName().replace(".txt", "")).before(calendar.getTime())) {
                    file.delete();
                }
            } catch (ParseException e) {
                Log.e(TAG, e.getMessage(), e);
            }
        }
        String fileName = dateFormat.format(Calendar.getInstance().getTime()) + ".txt";
        File file = new File(dir, fileName);
        try (FileOutputStream fos = new FileOutputStream(file, true)) {
            fos.write(crashReport.getBytes());
        } catch (IOException e) {
            Log.e(TAG, e.getMessage(), e);
        }
    }

    private PackageInfo getPackageInfo(Context context) {
        PackageInfo info = null;
        try {
            info = context.getPackageManager().getPackageInfo(
                    context.getPackageName(), 0);
        } catch (PackageManager.NameNotFoundException e) {
            info = new PackageInfo();
        }
        return info;
    }

    private Report generateReport(Throwable e) {
        PackageInfo packageInfo = getPackageInfo(mContext);
        StackTraceElement[] elements = e.getStackTrace();
        StringBuilder sb = new StringBuilder();
        for (StackTraceElement element : elements) {
            sb.append(element.toString() + "\n");
        }
        final Runtime runtime = Runtime.getRuntime();
        final long usedMemory = (runtime.totalMemory() - runtime.freeMemory()) / (1024 * 1024);
        final long maxHeapSize = runtime.maxMemory() / (1024 * 1024);
        final long availableHeapSize = maxHeapSize - usedMemory;
        return new Report.Builder(Calendar.getInstance().getTime())
                .setVersionName(packageInfo.versionName)
                .setOsVersion(Build.VERSION.RELEASE)
                .setDeviceBrand(Build.MANUFACTURER)
                .setUsedMemory(usedMemory)
                .setAvailableHeepSize(availableHeapSize)
                .setErrorMessage(e.getMessage())
                .setInvokeStackInfo(sb.toString())
                .build();
    }

    private static class InstanceMaker {
        private static UncaughtExceptionHandler instance = new UncaughtExceptionHandler ();
    }
}

上面的代码其实已经比较清楚了,我们在此稍作解释
1:通过init()方法将当前注册handler注册到所有线程的上,并将线程默认的处理器保存到mDefaultHandler
2:在uncaughtException()中构建错误报告并保存到本地,然后调用mDefaultHandleruncaughtException()方法

当奔溃发生时,我们就会收集奔溃设备的各种信息,写入按天组织的文件中,即同一天的崩溃均会在一个文件中,设置日志最长保留时间。

需要注意的是在异常处理过程中是存在一些技巧的,不然有可能造成其他的异常分析库工作异常。我要先将线程默认的handler保存下来,待 我们这捕获了异常并且处理完成(写入文件)后,调用保存下来的处理器,这就给了其他处理器执行的机会。

例如我们要同时集成buggly和我们自己的这个处理器,怎么办呢?先注册buggly,后注册我们自己的处理器即可。
代码执行逻辑如下:先把buggly的处理器保存到mDefaultHandler中,等我们自己的处理器执行完成后,再调用mDefaultHandleruncaughtException()方法,执行buggly的逻辑。

Report 类

/**
 * Created by shusheng007
   */
public class Report {
    private Date time;
    private String versionName;
    private String osVersion;
    private String deviceBrand;
    private long usedMemory;
    private long availableHeepSize;
    private String errorMessage;
    private String invokeStackInfo;

    private DateFormat mDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    private Report(Builder builder) {
        this.time = builder.time;
        this.versionName = builder.versionName;
        this.osVersion = builder.osVersion;
        this.deviceBrand = builder.deviceBrand;
        this.usedMemory = builder.usedMemory;
        this.availableHeepSize = builder.availableHeepSize;
        this.errorMessage = builder.errorMessage;
        this.invokeStackInfo = builder.invokeStackInfo;
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append("\n\n------------------------------crash begin---------------------------------\n\n");
        sb.append("time: " + mDateFormat.format(time));
        sb.append("\n");
        sb.append("versionName: " + versionName);
        sb.append("\n");
        sb.append("osVersion: " + osVersion);
        sb.append("\n");
        sb.append("deviceBrand: " + deviceBrand);
        sb.append("\n");
        sb.append("usedMemory: " + usedMemory + "MB");
        sb.append("\n");
        sb.append("availableHeepSize: " + availableHeepSize + "MB");
        sb.append("\n");
        sb.append("errorMessage: " + errorMessage);
        sb.append("\n");
        sb.append("invokeStackInfo:\n" + invokeStackInfo);
        sb.append("\n\n-------------------------------crash end-----------------------------------\n\n");
        return sb.toString();
    }

    public static class Builder {
        private Date time;
        private String versionName;
        private String osVersion;
        private String deviceBrand;
        private long usedMemory;
        private long availableHeepSize;
        private String errorMessage;
        private String invokeStackInfo;

        public Builder(Date time) {
            this.time = time;
        }

        public Builder setVersionName(String versionName) {
            this.versionName = versionName;
            return this;
        }

        public Builder setOsVersion(String osVersion) {
            this.osVersion = osVersion;
            return this;
        }

        public Builder setDeviceBrand(String deviceBrand) {
            this.deviceBrand = deviceBrand;
            return this;
        }

        public Builder setErrorMessage(String errorMessage) {
            this.errorMessage = errorMessage;
            return this;
        }

        public Builder setInvokeStackInfo(String invokeStackInfo) {
            this.invokeStackInfo = invokeStackInfo;
            return this;
        }

        public Builder setUsedMemory(long usedMemory) {
            this.usedMemory = usedMemory;
            return this;
        }

        public Builder setAvailableHeepSize(long availableHeepSize) {
            this.availableHeepSize = availableHeepSize;
            return this;
        }

        public Report build() {
            return new Report(this);
        }
    }
}

    public void uncaughtException(@NonNull Thread thread, @NonNull Throwable throwable) {
        Future<Boolean> future = mService.submit(() -> {
            save2File(reportLocation, generateReport(throwable).toString() + "\n\n");
            return true;
        });
        try {
            if (future.get().booleanValue()) {
                 if (mDefaultHandler != null) {
                    mDefaultHandler.uncaughtException(thread, throwable);
                } else {
                    android.os.Process.killProcess(android.os.Process.myPid());
                }
            }
        } catch (ExecutionException | InterruptedException e) {
            Log.e(TAG, e.getMessage(), e);
        }

使用

在你App 的application 类里面初始化即可

UncaughtExceptionHandler .getInstance().init(this);

然后到你指定的目录里面去查看崩溃报告。

总结

明天就是国庆节了,是中华人民共和国成立70周年纪念日,祝伟大的祖国繁荣昌盛,人民安居乐业。

发布了88 篇原创文章 · 获赞 279 · 访问量 18万+

猜你喜欢

转载自blog.csdn.net/ShuSheng0007/article/details/101061653
今日推荐