安卓错误日志捕捉

最近整理了一下错误日志捕捉的代码,这个虽然网上很多,但是加了点自己的想法吧!下面时代码:

import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Build;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.lang.Thread.UncaughtExceptionHandler;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Locale;

public class CrashHandler implements UncaughtExceptionHandler {

    //单例
    private volatile static CrashHandler instance;

    //异常日志路径
    private File mLogPath;

    //设备信息
    private String mDeviceInfo;

    //默认的主线程异常处理器
    private UncaughtExceptionHandler mDefaultHandler;

    //在application中初始化
    public static void init(Context context) {
        CrashHandler crashHandler = getInstance();
        crashHandler.mLogPath = getCrashCacheDir(context);
        crashHandler.mDeviceInfo = CrashHandler.getDeviceInfo(context);
        crashHandler.mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();
        //设置主线程处理器为自定义处理器
        Thread.setDefaultUncaughtExceptionHandler(crashHandler);
    }

    //获取部分设备信息
    private static String getDeviceInfo(Context context) {
        StringBuilder sb = new StringBuilder();
        try {
            PackageManager pkgMgr = context.getPackageManager();
            PackageInfo pkgInfo = pkgMgr.getPackageInfo(context.getPackageName(), 0);
            sb.append("packageName: ").append(pkgInfo.packageName).append("\n");
            sb.append("versionCode: ").append(pkgInfo.versionCode).append("\n");
            sb.append("versionName: ").append(pkgInfo.versionName).append("\n");
        } catch (Exception e) {
            e.printStackTrace();
        }

        //厂商,例如Xiaomi,Meizu,Huawei等。
        sb.append("brand: ").append(Build.BRAND).append("\n");
        //产品型号全称,例如 meizu_mx3
        sb.append("product: ").append(Build.PRODUCT).append("\n");
        //产品型号名,例如 mx3
        sb.append("device: ").append(Build.DEVICE).append("\n");
        //安卓版本名,例如 4.4.4
        sb.append("androidVersionName: ").append(Build.VERSION.RELEASE).append("\n");
        //安卓API版本,例如 19
        sb.append("androidApiVersion: ").append(Build.VERSION.SDK_INT).append("\n");

        //通过反射记录Build所有数据
//        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 (Exception e) {
//                Log.e(TAG, "an error occured when collect crash info", e);
//            }
//        }
        return sb.toString();
    }

    //获取日志缓存目录,不用权限
    private static File getCrashCacheDir(Context context) {
        String cachePath;
        if (context.getExternalCacheDir() != null) {
            cachePath = context.getExternalCacheDir().getPath();
        } else {
            cachePath = context.getFilesDir().getPath();
        }

        File dir = new File(cachePath + File.separator + "log");
        if (!dir.exists()) {
            //noinspection ResultOfMethodCallIgnored
            dir.mkdirs();
        }
        return dir;
    }

    //DCL双重校验
    public static CrashHandler getInstance() {
        if (instance == null) {
            synchronized (CrashHandler.class) {
                if (instance == null) {
                    instance = new CrashHandler();
                }
            }
        }
        return instance;
    }


    @Override
    public void uncaughtException(Thread thread, Throwable ex) {
        //自行处理异常
        handleException(ex);
        //自行处理完再交给默认处理器
        if (null != mDefaultHandler) {
            mDefaultHandler.uncaughtException(thread, ex);
        }
    }

    //处理异常
    private void handleException(final Throwable throwable) {
        StringBuilder sb = new StringBuilder();
        //获取异常信息
        getExceptionInfo(sb, throwable);
        //附加设备信息
        sb.append(mDeviceInfo);
        //保存到文件
        new Thread(()->saveCrash2File(sb.toString())).start();
    }

    //获取异常信息
    private void getExceptionInfo(StringBuilder sb, Throwable throwable) {
        Writer writer = new StringWriter();
        PrintWriter printWriter = new PrintWriter(writer);
        throwable.printStackTrace(printWriter);
        //循环获取包装异常的异常原因
        Throwable cause = throwable.getCause();
        while (cause != null) {
            cause.printStackTrace(printWriter);
            cause = cause.getCause();
        }
        printWriter.close();
        String exStr = writer.toString();
        sb.append(exStr).append("\n");
    }


    //保存到文件
    private void saveCrash2File(String crashInfo) {
        try {
            // 用于格式化日期,作为日志文件名的一部分
            DateFormat formatter = new SimpleDateFormat("yyMMddHHmmss", Locale.CHINA);
            String time = formatter.format(new Date());
            String fileName = "crash_" + time + ".log";
            String filePath = mLogPath.getAbsolutePath() + File.separator + fileName;
            FileOutputStream fos = new FileOutputStream(filePath);
            fos.write(crashInfo.getBytes());
            fos.flush();
            fos.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }


    //获取所有日志文件
    public static List<File> getCrashLogs() {
        CrashHandler crashHandler = getInstance();
        File crashDir = crashHandler.mLogPath;
        //避免直接使用asList方法,产生的视图无法修改
        return new ArrayList<>(Arrays.asList(crashDir.listFiles()));
    }

    //读取崩溃日志文件内容并返回
    public static List<String> getCrashLogsStrings() {
        List<String> logs = new ArrayList<>();
        List<File> files = getCrashLogs();

        FileInputStream inputStream;
        for (File file : files) {
            try {
                inputStream = new FileInputStream(file);
                int len = 0;
                byte[] temp = new byte[1024];
                StringBuilder sb = new StringBuilder("");
                while ((len = inputStream.read(temp)) > 0){
                    sb.append(new String(temp, 0, len));
                }
                inputStream.close();
                logs.add(sb.toString());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return logs;
    }

    //删除日志文件
    public static void removeCrashLogs(List<File> files) {
        for (File file : files) {
            //noinspection ResultOfMethodCallIgnored
            file.delete();
        }
    }
}

下面简单讲一下里面的一些东西。

错误日志捕捉

实际上程序崩溃就是主线程的崩溃,程序退出其实就是我们没有从主线程上捕获到异常,而 Java 的 Thread 类中含有一个 defaultUncaughtExceptionHandler 变量,该变量会处理线程发生的异常,并打印到默认输出上,也就是我们调试的时候出了问题可以在 log 中找到错误日志的原因。

// null unless explicitly set
private static volatile UncaughtExceptionHandler defaultUncaughtExceptionHandler;

...
    
public static void setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler eh) {
    defaultUncaughtExceptionHandler = eh;
}

所以要捕获错误日志,我们只要实现 UncaughtExceptionHandler 接口,设置主线程的处理器为我们自定义的即可,UncaughtExceptionHandler 也定义在 Thread 类中,如下:

    @FunctionalInterface
    public interface UncaughtExceptionHandler {
        /**
         * Method invoked when the given thread terminates due to the
         * given uncaught exception.
         * <p>Any exception thrown by this method will be ignored by the
         * Java Virtual Machine.
         * @param t the thread
         * @param e the exception
         */
        void uncaughtException(Thread t, Throwable e);
    }

获取设备信息

大部分时候我们调试好的程序都运行没问题,可是一到客户那就又这又那的,这里涉及到一些设备的数据,我们的测试也不太可能覆盖所有机型,当出现问题的时候,我们希望把日志和设备信息一起提交上来。

下面时一些设备信息,PackageManager 可以获取软件版本号和版本名称,当然填入其他 flag 可以获得更多其他信息,我们这不细说。Build 类里面包含了一些设备信息,下面我选取了一部分使用,也可以和注释里面一样,通过反射获取全部数据。

    //获取部分设备信息
    private static String getDeviceInfo(Context context) {
        StringBuilder sb = new StringBuilder();
        try {
            PackageManager pkgMgr = context.getPackageManager();
            PackageInfo pkgInfo = pkgMgr.getPackageInfo(context.getPackageName(), 0);
            sb.append("packageName: ").append(pkgInfo.packageName).append("\n");
            sb.append("versionCode: ").append(pkgInfo.versionCode).append("\n");
            sb.append("versionName: ").append(pkgInfo.versionName).append("\n");
        } catch (Exception e) {
            e.printStackTrace();
        }

        //厂商,例如Xiaomi,Meizu,Huawei等。
        sb.append("brand: ").append(Build.BRAND).append("\n");
        //产品型号全称,例如 meizu_mx3
        sb.append("product: ").append(Build.PRODUCT).append("\n");
        //产品型号名,例如 mx3
        sb.append("device: ").append(Build.DEVICE).append("\n");
        //安卓版本名,例如 4.4.4
        sb.append("androidVersionName: ").append(Build.VERSION.RELEASE).append("\n");
        //安卓API版本,例如 19
        sb.append("androidApiVersion: ").append(Build.VERSION.SDK_INT).append("\n");

        //通过反射记录Build所有数据
//        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 (Exception e) {
//                Log.e(TAG, "an error occured when collect crash info", e);
//            }
//        }
        return sb.toString();
    }

获取异常信息

    //获取异常信息
    private void getExceptionInfo(StringBuilder sb, Throwable throwable) {
        Writer writer = new StringWriter();
        PrintWriter printWriter = new PrintWriter(writer);
        throwable.printStackTrace(printWriter);
        //循环获取包装异常的异常原因
        Throwable cause = throwable.getCause();
        while (cause != null) {
            cause.printStackTrace(printWriter);
            cause = cause.getCause();
        }
        printWriter.close();
        String exStr = writer.toString();
        sb.append(exStr).append("\n");
    }

这里获取异常我们需要使用 Throwable 这个类,因为我们说的异常可能包括了 Error 和 Exception,都需要捕获。

由于异常可能包含包装的 case,所有我们需要循环一次拿到最原始的异常信息。

有关异常的详细信息,可以看我另一篇博客:

https://blog.csdn.net/lfq88/article/details/107173268

保存日志

    //获取日志缓存目录,不用权限
    private static File getCrashCacheDir(Context context) {
        String cachePath;
        if (context.getExternalCacheDir() != null) {
            cachePath = context.getExternalCacheDir().getPath();
        } else {
            cachePath = context.getFilesDir().getPath();
        }

        File dir = new File(cachePath + File.separator + "log");
        if (!dir.exists()) {
            //noinspection ResultOfMethodCallIgnored
            dir.mkdirs();
        }
        return dir;
    }

日志上传

关于日志的上传我并不想写在这里面,而是更希望在外部拿到日志数据后再处理,下面向外提供了获取异常数据的方法,另外提供了一个删除方法,目的是上传日志成功后,用来清除保存下拉的文件。

    //获取所有日志文件
    public static List<File> getCrashLogs() {
        CrashHandler crashHandler = getInstance();
        File crashDir = crashHandler.mLogPath;
        //避免直接使用asList方法,产生的视图无法修改
        return new ArrayList<>(Arrays.asList(crashDir.listFiles()));
    }

    //读取崩溃日志文件内容并返回
    public static List<String> getCrashLogsStrings() {
        List<String> logs = new ArrayList<>();
        List<File> files = getCrashLogs();

        FileInputStream inputStream;
        for (File file : files) {
            try {
                inputStream = new FileInputStream(file);
                int len = 0;
                byte[] temp = new byte[1024];
                StringBuilder sb = new StringBuilder("");
                while ((len = inputStream.read(temp)) > 0){
                    sb.append(new String(temp, 0, len));
                }
                inputStream.close();
                logs.add(sb.toString());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return logs;
    }

    //删除日志文件
    public static void removeCrashLogs(List<File> files) {
        for (File file : files) {
            //noinspection ResultOfMethodCallIgnored
            file.delete();
        }
    }

结语

东西写的不多,希望对读者有帮助,自己也记录一下。

end

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值