程序崩溃是应用迭代中不可避免的问题,即使有着5年或者10年经验的程序猿也无法完全保证自己的代码没有任何的bug导致崩溃,现在有一些第三方平台可以帮助我们搜集应用程序的崩溃,比如友盟,详情如下图

Android 抓取崩溃日志 app崩溃日志抓取工具_java

Android 抓取崩溃日志 app崩溃日志抓取工具_Android 抓取崩溃日志_02

虽然能够看到崩溃的日志以及机型等,但还是不是很方便,如果需要精确定位的话需要用户提供崩溃的时间点、机型等信息,所以最好的办法就是我们把崩溃的信息保存在用户的sd卡上,必要的时候发送到后台或者让用户手动提供一下文件,下面就来看如何实现这个功能。

Android 系统提供了处理这类问题的方法,Thread 类中提供了一个方法 setDefaultUncaughtExceptionHandler,设置了这个默认的异常处理器之后当程序发生异常之后就会回调uncaugthException()这个方法,然后可以在这个回调里面捕获异常信息,保存到文件。

话不多说,直接上代码:

package com.hxc.supreme.utils;

import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Environment;
import android.os.Looper;
import android.util.Log;
import android.widget.Toast;


import com.hxc.supreme.MainApplication;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.lang.reflect.Field;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import static com.hxc.supreme.BuildConfig.DEBUG;

/**
 * UncaughtException处理类,当程序发生Uncaught异常的时候,有该类来接管程序,并记录发送错误报告.
 * 在Application 中调用
 * CrashHandlerUtil.getInstance().init(this);
 */
public class XCrashHandlerUtils implements Thread.UncaughtExceptionHandler {
    private static final String TAG = "XCrashHandlerUtils";
    //系统默认的UncaughtException处理类
    private Thread.UncaughtExceptionHandler mDefaultHandler;
    //CrashHandler实例
    private static XCrashHandlerUtils INSTANCE = new XCrashHandlerUtils();
    //程序的Context对象
    private Context mContext;
    //用来存储设备信息和异常信息
    private String crashTip = "似乎遇到了一点小麻烦,程序即将重新启动";
    /**
     * 文件名
     */
    public static final String FILE_NAME = "crash";
    /**
     * 异常日志 存储位置为根目录下的 Crash文件夹
     */
    private static final String PATH = Environment.getExternalStorageDirectory().getPath() +
            "/Supreme_crash/";
    /**
     * 文件名后缀
     */
    private static final String FILE_NAME_SUFFIX = ".txt";

    public String getCrashTip() {
        return crashTip;
    }

    public void setCrashTip(String crashTip) {
        this.crashTip = crashTip;
    }

    /**
     * 保证只有一个CrashHandler实例
     */
    private XCrashHandlerUtils() {
    }

    /**
     * 获取CrashHandler实例 ,单例模式
     *
     * @return 单例
     */
    public static XCrashHandlerUtils getInstance() {
        return INSTANCE;
    }

    /**
     * 初始化
     *
     * @param context 上下文
     */
    public void init(Context context) {
        mContext = context;
        //获取系统默认的UncaughtException处理器
        mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();
        //设置该CrashHandler为程序的默认处理器
        Thread.setDefaultUncaughtExceptionHandler(this);
    }

    /**
     * 这个是最关键的函数,当系统中有未被捕获的异常,系统将会自动调用 uncaughtException 方法
     *
     * @param thread 为出现未捕获异常的线程
     * @param ex     为未捕获的异常 ,可以通过e 拿到异常信息
     */
    @Override
    public void uncaughtException(Thread thread, final Throwable ex) {
        //导入异常信息到SD卡中
        try {
            dumpExceptionToSDCard(ex);
        } catch (IOException e) {
            e.printStackTrace();
        }
        //这里可以上传异常信息到服务器,便于开发人员分析日志从而解决Bug
//        uploadExceptionToServer();
        ex.printStackTrace();
        //如果系统提供了默认的异常处理器,则交给系统去结束程序,否则就由自己结束自己
        if (mDefaultHandler != null) {
            mDefaultHandler.uncaughtException(thread, ex);
        } else {
            android.os.Process.killProcess(android.os.Process.myPid());
            System.exit(1);
        }

    }


    /**
     * 将异常信息写入SD卡
     *
     * @param e
     */
    private void dumpExceptionToSDCard(Throwable e) throws IOException {
        //如果SD卡不存在或无法使用,则无法将异常信息写入SD卡
        if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
            if (DEBUG) {
                Log.w(TAG, "sdcard unmounted,skip dump exception");
                return;
            }
        }
        File dir = new File(PATH);
        //如果目录下没有文件夹,就创建文件夹
        if (!dir.exists()) {
            dir.mkdirs();
        }
        //得到当前年月日时分秒
        long current = System.currentTimeMillis();
        String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(current));
        //在定义的Crash文件夹下创建文件
        File file = new File(PATH + FILE_NAME + time + FILE_NAME_SUFFIX);

        try {
            PrintWriter pw = new PrintWriter(new BufferedWriter(new FileWriter(file)));
            //写入时间
            pw.println(time);
            //写入手机信息
            dumpPhoneInfo(pw);
            pw.println();//换行
            e.printStackTrace(pw);
            pw.close();//关闭输入流
        } catch (Exception e1) {
            Log.e(TAG, "dump crash info failed");
        }

    }

    /**
     * 获取手机各项信息
     *
     * @param pw
     */
    private void dumpPhoneInfo(PrintWriter pw) throws PackageManager.NameNotFoundException {
        //得到包管理器
        PackageManager pm = mContext.getPackageManager();
        //得到包对象
        PackageInfo pi = pm.getPackageInfo(mContext.getPackageName(), PackageManager.GET_ACTIVITIES);
        //写入APP版本号
        pw.print("App Version: ");
        pw.print(pi.versionName);
        pw.print("_");
        pw.println(pi.versionCode);
        //写入 Android 版本号
        pw.print("OS Version: ");
        pw.print(Build.VERSION.RELEASE);
        pw.print("_");
        pw.println(Build.VERSION.SDK_INT);
        //手机制造商
        pw.print("Vendor: ");
        pw.println(Build.MANUFACTURER);
        //手机型号
        pw.print("Model: ");
        pw.println(Build.MODEL);
        //CPU架构
        pw.print("CPU ABI: ");
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            pw.println(Build.SUPPORTED_ABIS);
        } else {
            pw.println(Build.CPU_ABI);
        }
    }

}

然后再Mainapplication的onCreate中调用一下XCrashHandlerUtils.init()方法,接下来写一个bug看一下效果,代码很简单

1 package com.hxc.supreme.activity;
 2 
 3 import android.content.ComponentName;
 4 import android.content.Intent;
 5 import android.content.ServiceConnection;
 6 import android.graphics.Color;
 7 import android.os.Bundle;
 8 import android.os.IBinder;
 9 import android.support.annotation.Nullable;
10 import android.support.v7.app.AppCompatActivity;
11 import android.view.Gravity;
12 import android.view.LayoutInflater;
13 import android.view.View;
14 import android.view.ViewGroup;
15 import android.widget.Button;
16 import android.widget.LinearLayout;
17 import android.widget.TextView;
18 
19 import com.hxc.supreme.R;
20 import com.hxc.supreme.service.MainService;
21 
22 
23 /**
24  * created by huxc  on 2017/9/28.
25  * func:  ViewsTestActivity
26  * email: hxc242313@qq.com
27  */
28 
29 public class ViewsTestActivity extends AppCompatActivity implements View.OnClickListener {
30     private LinearLayout mainLayout;
31 
32     @Override
33     protected void onCreate(@Nullable Bundle savedInstanceState) {
34         super.onCreate(savedInstanceState);
35         setContentView(R.layout.activity_views_test);
36 //        mainLayout = findViewById(R.id.layout_main);
37 
38         View view = LayoutInflater.from(this).inflate(R.layout.button_view, null);
39         TextView textView = new TextView(this);
40         textView.setText("add View Dynamic");
41         textView.setGravity(Gravity.CENTER);
42         textView.setAllCaps(false);
43         textView.setTextColor(Color.RED);
44         mainLayout.addView(textView,new LinearLayout.LayoutParams(300, 200));
45     }
46 
47     @Override
48     protected void onResume() {
49         super.onResume();
50     }
51 
52     @Override
53     public void onClick(View view) {
54         switch (view.getId()) {
55         }
56     }
57 
58 }


第36行把findviewById屏蔽了,然后运行了一下程序,看一下logcat中的崩溃信息:

Android 抓取崩溃日志 app崩溃日志抓取工具_android_03

 

crash文件的保存路径是Supreme_crash,看一下文件中的内容:

Android 抓取崩溃日志 app崩溃日志抓取工具_Android 抓取崩溃日志_04

和控制台输出的一毛一样,而且还打印出了手机的型号,app的版本等相关信息,大功告成!