Android自定义全局捕获异常并上传,实现实时收集APP崩溃crash信息

1,551 阅读5分钟

在APP上线后,可能会出现一些BUG导致了APP的闪退,用户体验就非常致命,我们一定要第一时间找到问题的所在

1、我们需要自定义一个异常收集类(创建一个Thread.UncaughtExceptionHandler的继承类); 2、替换掉APP本身的异常处理(在Thread.UncaughtExceptionHandler实现类中使用Thread.setDefaultUncaughtExceptionHandler(this)方法替换);
3、在类中收集信息,这个信息最好包括手机的一些信息,比如:厂商、型号、cup型号、内存大小等...,因为安卓手机的多样性导致我们在适配的时候非常麻烦,也是因为有些问题的出现是因为个别的硬件差异造成的,所以这些信息最好收集;

整体思路就是,自定义一个异常收集类替换到本来的异常处理类,在这个类中去收集一些必要的信息回传到我们的后台,我们可以在崩溃信息发生的第一时间去处理

以下是异常收集类代码,可以用作参考

public class CrashHandlerUtil implements Thread.UncaughtExceptionHandler {
   private static final String TAG = "CrashHandlerUtil";
   //创建CrashHandlerUtil实列
   private static CrashHandlerUtil crashHandlerUtil = new CrashHandlerUtil();
   //系統默认UncaughtException处理类
   private Thread.UncaughtExceptionHandler mDefaultHandler;
   //程序Context对象
   private Context mContext;
   //用来存储设备信息和异常信息
   private Map<String, String> infos = new HashMap<>();
  //用于格式化日期,作为日志名的一部分
  private DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss",
        Locale.CHINA);
   private static String error = "程序崩溃!!!";
   // 外置存储卡默认预警临界值
   private static final long THRESHOLD_WARNING_SPACE = 100 * M;
   // 保存文件时所需的最小空间的默认值
   public static final long THRESHOLD_MIN_SPCAE = 60 * M;
   public static CrashHandlerUtil getInstance() {
    return crashHandlerUtil;
}
  public void init(Context context){
    mContext=context;
    //获取系统默认uncaughtException处理器
    mDefaultHandler=Thread.getDefaultUncaughtExceptionHandler();
    //设置CrashHandler为程序默认处理器
    Thread.setDefaultUncaughtExceptionHandler(this);
}
/**
 * 程序异常调用该方法
 *@param thread 线程
 * @param  throwable 异常
 */
@Override
public void uncaughtException(Thread thread, Throwable throwable) {
    StringBuilder sb = new StringBuilder();
    // 1.获取当前应用程序的版本号.
    PackageManager pm = mContext.getPackageManager();
    try {
        PackageInfo packinfo = pm.getPackageInfo(mContext.getPackageName(),
                0);
        sb.append("程序的版本号为" + packinfo.versionName);
        sb.append("\n");

        // 2.获取手机的硬件信息.
        Field[] fields = Build.class.getDeclaredFields();
        for (int i = 0; i < fields.length; i++) {
            // 暴力反射,获取私有的字段信息
            fields[i].setAccessible(true);
            String name = fields[i].getName();
            sb.append(name + " = ");
            String value = fields[i].get(null).toString();
            sb.append(value);
            sb.append("\n");
        }
        // 3.获取程序错误的堆栈信息 .
        StringWriter writer = new StringWriter();
        PrintWriter printWriter = new PrintWriter(writer);
        throwable.printStackTrace(printWriter);

        String result = writer.toString();
        sb.append(result);
        // 4将错误信息转成文件上传到服务器
        uploadFile(sb.toString());
    } catch (Exception e) {
        e.printStackTrace();
    }
}
/**
 * url       完整的接口地址
 * file       需要上传的文件
 *  fileUploadObserver 上传回调
 */
private void uploadFile(String s) {
    try {
        File file = string2file(s);
        RetrofitHelp.getIns().upLoadFile("", file, new
                FileUploadObserver<ResponseBody>() {
                    @Override
                    public void onUpLoadSuccess(ResponseBody responseBody) {
                        //上传成功
                    }

                    @Override
                    public void onUpLoadFail(Throwable e) {
                        //上传失败
                    }

                    @Override
                    public void onProgress(int progress) {
                        //上传进度
                    }
                });
    } catch (Exception e) {
        e.printStackTrace();
    }

}

/**
 * 自定义错误处理,收集错误信息,发送错误报告等操作均在此完成
 *
 * @param ex
 * @return true:如果处理了该异常信息;否则返回 false
 */
private boolean handleException(Throwable ex) {
    if (ex == null) {
        return false;
    }
    // 收集设备参数信息
    collectDeviceInfo(mContext);
    // 保存日志文件
    saveCrashInfo2File(ex);
    // 使用 Toast 来显示异常信息
    new Thread() {
        @Override
        public void run() {
            Looper.prepare();
            Toast.makeText(mContext, error, Toast.LENGTH_LONG).show();
            Looper.loop();
        }
    }.start();
    return true;
}

/**
 * 收集设备参数信息
 *
 * @param context 上下文
 */
public void collectDeviceInfo(Context context){
    try {
        PackageManager packageManager = context.getPackageManager();
        PackageInfo packageInfo = packageManager.getPackageInfo(context.getPackageName(), PackageManager.GET_ACTIVITIES);
        if(packageInfo!=null){
            String versionName= packageInfo.versionName == null ? "null" : packageInfo.versionName;
            String versionCode = packageInfo.versionCode + "";
            infos.put("versionName",versionName);
            infos.put("versionCode",versionCode);
        }
    } catch (PackageManager.NameNotFoundException e) {
        Log.e(TAG, "an error occured when collect package info", e);
    }
    Field[] fields = Build.class.getDeclaredFields();
    for (Field field:fields){
        try {
            field.setAccessible(true);
            infos.put(field.getName(),field.get(null).toString());
            Log.d(TAG, field.getName() + " : " + field.get(null));
        } catch (IllegalAccessException e) {
            Log.e(TAG, "an error occured when collect crash info", e);
        }
    }
}
/**
 * 保存错误信息到文件中 *
 *
 * @param ex
 * @return 返回文件名称,便于将文件传送到服务器
 */
private String saveCrashInfo2File(Throwable ex) {
    StringBuffer sb = getTraceInfo(ex);
    for (Map.Entry<String, String> entry : infos.entrySet()) {
        String key = entry.getKey();
        String value = entry.getValue();
        sb.append(key + "=" + value + "\n");
    }
    Writer writer = new StringWriter();
    PrintWriter printWriter = new PrintWriter(writer);
    ex.printStackTrace(printWriter);
    Throwable cause = ex.getCause();
    while (cause != null) {
        cause.printStackTrace(printWriter);
        cause = cause.getCause();
    }
    printWriter.close();
    String result = writer.toString();
    sb.append(result);
    try {
        long timestamp = System.currentTimeMillis();
        String time = dateFormat.format(new Date());
        String fileName = "crash-" + time + "-" + timestamp + ".log";
        if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
            String path = Environment.getExternalStorageDirectory() + "/crash/";
            Log.i("TAG","路径======="+path);
            File dir = new File(path);
            if (!dir.exists()) {
                dir.mkdirs();
            }
            else if(!hasEnoughSpaceForWrite(path))
            {
                deleteDirectory(path);
            }
            FileOutputStream fos = new FileOutputStream(path + fileName);
            fos.write(sb.toString().getBytes());
            fos.close();
        }
        return fileName;
    } catch (Exception e) {
        Log.e(TAG, "an error occured while writing file...", e);
    }

    return null;
}
/**
 * 整理异常信息
 * @param e
 * @return
 */
public static StringBuffer getTraceInfo(Throwable e) {
    StringBuffer sb = new StringBuffer();
    Throwable ex = e.getCause() == null ? e : e.getCause();
    StackTraceElement[] stacks = ex.getStackTrace();
    for (int i = 0; i < stacks.length; i++) {
        sb.append("class: ").append(stacks[i].getClassName())
                .append("; method: ").append(stacks[i].getMethodName())
                .append("; line: ").append(stacks[i].getLineNumber())
                .append(";  Exception: ").append(ex.toString() + "\n");
    }
    return sb;
}
/**
 * 删除文件夹下的文件
 * @param   filePath 被删除目录下文件的路径
 * @return  目录删除成功返回true,否则返回false
 */
public boolean deleteDirectory(String filePath) {
    boolean flag = false;
    //如果filePath不以文件分隔符结尾,自动添加文件分隔符
    if (!filePath.endsWith(File.separator)) {
        filePath = filePath + File.separator;
    }
    File dirFile = new File(filePath);
    if (!dirFile.exists() || !dirFile.isDirectory()) {
        return false;
    }
    flag = true;
    File[] files = dirFile.listFiles();

    //遍历删除文件夹下的所有文件(包括子目录)
    for (int i = 0; i < files.length; i++) {
        if (files[i].isFile()) {
            //删除子文件
            flag = deleteFile(files[i].getAbsolutePath());
            if (!flag) break;
        }
    }
    if (!flag)
        return false;
    return true;
}
/**
 * 删除单个文件
 * @param   filePath    被删除文件的文件名
 * @return 文件删除成功返回true,否则返回false
 */
public boolean deleteFile(String filePath) {
    File file = new File(filePath);
    if (file.isFile() && file.exists()) {
        return file.delete();
    }
    return false;
}
/**
 * 判断外部存储是否存在,以及是否有足够空间保存指定类型的文件
 *
 * @param directoryPath
 * @return false: 无存储卡或无空间可写, true: 表示ok
 */
public boolean hasEnoughSpaceForWrite(String directoryPath) {
    long residual = getAvailableExternalSize(directoryPath);
    if (residual < THRESHOLD_MIN_SPCAE) {
        return false;
    } else if (residual < THRESHOLD_WARNING_SPACE) {

    }
    return true;
}
/**
 * 获取外置存储卡剩余空间
 * @return
 */
public long getAvailableExternalSize(String directoryPath) {
    try {
        StatFs sf = new StatFs(directoryPath);
        long blockSize = sf.getBlockSize();
        long availCount = sf.getAvailableBlocks();
        long availCountByte = availCount * blockSize;
        return availCountByte;
    } catch (Exception e) {
        e.printStackTrace();
    }
    return 0;
}
/**
 * 将String转成file
 * @param s
 * @return
 */
private File string2file (String s) throws Exception {
    File fileDir = new File(Environment.getExternalStorageDirectory() .getAbsolutePath() +"/log");
    if(!fileDir.exists()){
        fileDir.mkdirs();
    }
    Date date = new Date();
    File file = new File(fileDir, SPHelper.getInstence(mContext).getUid() + "-" + date.getYear()+date.getMonth()+date.getDay()
            +date.getHours()+date.getMinutes()+ ".txt");
    if(!file.exists()){
        file.createNewFile();
    }
    ByteArrayInputStream ins = new ByteArrayInputStream(s.getBytes());
    OutputStream os = new FileOutputStream(file);
    int bytesRead = 0;
    byte[] buffer = new byte[8192];
    while ((bytesRead = ins.read(buffer, 0, 8192)) != -1) {
        os.write(buffer, 0, bytesRead);
    }
    os.close();
    ins.close();
    return file;
   }
}