Android-实现记录“异常闪退“日志

1,引文

在开发进程中,因某些未知缘由,应用在运行时异常闪退,然而却无法看到错误日志,着实令人难受。

2,解决思路

借助 Process 来启动一个进程去执行 Logcat 命令,并把内容存储至指定文件内。如此一来,即便应用闪退,依然能够记录下关键信息(进程之间彼此独立)。

3,关键步骤

3.1,启动Logcat进程
java 复制代码
Process process = new ProcessBuilder("logcat").redirectErrorStream(true).start();
3.2,读取并保存Process输出内容
java 复制代码
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));) {
	FileWritter writter = ...;
	String content;
	while (process.isAlive() && (content = reader.readLine()) != null) {
		writer.write(content);
        writer.write("\n");
        writer.flush();
	}
} ...
3.3,关闭Logcat进程
java 复制代码
if (process != null && process.isAlive()) {
	process.destroyForcibly();
}

4,简单封装LoggerUtil工具

java 复制代码
import android.app.Activity;
import android.util.Log;

import androidx.annotation.NonNull;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Pattern;

public class LoggerUtil {

    @SuppressWarnings("unused")
    private static final String TAG = "LoggerUtil";

    /**
     * logcat进程对象
     */
    private static final AtomicReference<Process> gProcess = new AtomicReference<>(null);
    /**
     * logcat进程对象的创建器
     */
    private static final ProcessBuilder logcatBuilder = new ProcessBuilder("logcat");

    /**
     * 日志内容过滤器
     */
    private static Set<LogContentFilter> logContentFilters;
    /**
     * 日志目录名
     */
    private static String logDirName;
    /**
     *  日志文件名
     */
    private static String logFileName;
    /**
     * 单个日志文件最大大小
     */
    private static long logFileMaxSize;
    /**
     * 日志文件备份名获取类
     */
    private static BackupLogFileNameGetter backupLogFileNameGetter;

    /**
     * 日志内容过滤器基类
     */
    public interface LogContentFilter {
        /**
         * @param content 日志内容
         * @return true表示记录该内容,false不记录该内容
         */
        boolean isRecord(@NonNull String content);
    }

    /**
     * 日志等级
     */
    public enum Level {
        INFO("I"),
        WARN("W"),
        ERROR("E"),
        DEBUG("D"),
        VERBOSE("V"),
        ALL(".")
        ;
        final String value;
        Level(String value) {
            this.value = value;
        }
        public String getValue() {
            return value;
        }
    }

    /**
     * 默认通过日志等级进行过滤
     */
    public static class DefaultLogContentFilter implements LogContentFilter {
        private final Pattern p;

        public DefaultLogContentFilter(Level level) {
            p = Pattern.compile("^[0-9 -.:]* " + level.getValue() + " .*$");
        }

        @Override
        public boolean isRecord(@NonNull String content) {
            return p.matcher(content).matches();
        }
    }

    /**
     * 日志文件备份名获取基类
     */
    public interface BackupLogFileNameGetter {
        String get(String logFileName);
    }

    /**
     * 默认在当前文件名后面添加当前时间作为备份名
     */
    public static class DefaultBackupLogFileNameGetter implements BackupLogFileNameGetter {
        private final SimpleDateFormat fileDateFormat = new SimpleDateFormat("yyyyMMdd-HHmmss", Locale.ROOT);
        @Override
        public String get(String logFileName) {
            String bakLogFileFormat = "%s_%s.log";
            return String.format(bakLogFileFormat, logFileName, fileDateFormat.format(new Date()));
        }
    }

    /**
     * 参数构造器
     */
    public static class Builder {
        /**
         * 日志内容过滤器
         */
        private final Set<LogContentFilter> logContentFilters = new HashSet<>();
        /**
         * 日志文件夹名字
         */
        private String logDirName = "logs";
        /**
         * 日志文件名
         */
        private String logFileName = "app.log";
        /**
         * 单个日志文件最大大小
         */
        private long logFileMaxSize = 5 * 1024 * 1024; //MB
        /**
         * 日志文件备份名获取类
         */
        private BackupLogFileNameGetter backupLogFileNameGetter = new DefaultBackupLogFileNameGetter();

        public Builder() {
            logContentFilters.add(new DefaultLogContentFilter(Level.WARN));
            logContentFilters.add(new DefaultLogContentFilter(Level.ERROR));
            logContentFilters.add(new DefaultLogContentFilter(Level.DEBUG));
            logContentFilters.add(new DefaultLogContentFilter(Level.VERBOSE));
        }

        public Builder setBackupLogFileNameGetter(BackupLogFileNameGetter backupLogFileNameGetter) {
            if (backupLogFileNameGetter == null) return this;
            this.backupLogFileNameGetter = backupLogFileNameGetter;
            return this;
        }

        public Builder setLogFileName(String logFileName) {
            if (logFileName == null || logFileName.trim().isEmpty()) return this;
            this.logFileName = logFileName;
            return this;
        }

        public Builder setLogDirName(String logDirName) {
            if (logDirName == null || logDirName.trim().isEmpty()) return this;
            this.logDirName = logDirName;
            return this;
        }

        /**
         * 单位"B",最小为512KB <br/>
         * 1MB = 1024KB <br/>
         * 1KB = 1024B
         */
        public Builder setLogFileMaxSize(long logFileMaxSize) {
            if (logFileMaxSize < 512 * 1024) return this;
            this.logFileMaxSize = logFileMaxSize;
            return this;
        }

        public Builder addLogContentFilter(LogContentFilter logContentFilter) {
            if (logContentFilter == null) return this;
            logContentFilters.add(logContentFilter);
            return this;
        }

        public Builder addLogContentFilters(List<LogContentFilter> logContentFilters) {
            if (logContentFilters == null || logContentFilters.isEmpty()) return this;
            this.logContentFilters.addAll(logContentFilters);
            return this;
        }

        public Builder setLogContentFilter(LogContentFilter logContentFilter) {
            logContentFilters.clear();
            return addLogContentFilter(logContentFilter);
        }

        public Builder setLogContentFilters(List<LogContentFilter> logContentFilters) {
            this.logContentFilters.clear();
            return addLogContentFilters(logContentFilters);
        }
    }

    public static void start(Activity activity) {
        start(activity, null);
    }

    public static void start(Activity activity, Builder builder) {
        bind(builder);
        synchronized (gProcess) {
            stop();
            try {
                gProcess.set(logcatBuilder.redirectErrorStream(true).start());
            } catch (Exception e) {
                Log.e(TAG, "开启Logcat失败", e);
                return;
            }
        }

        final File logRootDir = activity.getExternalFilesDir(null);
        if (logRootDir == null) {
            stop();
            Log.e(TAG, "getExternalFilesDir获取失败");
            return;
        }
        CompletableFuture.runAsync(() -> {
            File logFile = getLogFile(logRootDir);
            if (logFile == null) {
                stop();
                return;
            }
            Process process = gProcess.get();
            FileWriter writer = null;
            try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));) {
                writer = new FileWriter(logFile, true);
                String content;
                while (process.isAlive() && (content = reader.readLine()) != null) {
                    if (content.trim().length() == 0 || logContentFilters.isEmpty()) continue;
                    boolean needRecord = true;
                    for (LogContentFilter logFilter : logContentFilters) {
                        if (!logFilter.isRecord(content)) {
                            needRecord = false;
                            break;
                        }
                    }
                    if (!needRecord) continue;

                    writer.write(content);
                    writer.write("\n");
                    writer.flush();

                    if (logFile.length() >= logFileMaxSize) {
                        logFile = getLogFile(logRootDir);
                        if (logFile == null) {
                            stop();
                            return;
                        }
                        writer.close();
                        writer = new FileWriter(getLogFile(logRootDir), true);
                    }
                }
            } catch (Exception ignored) {
            } finally {
                stop();
                if (writer != null) {
                    try {
                        writer.close();
                    } catch (IOException ignored) {
                    }
                }
            }
        }, Executors.newSingleThreadExecutor());
    }

    /**
     * 绑定Builder数据
     */
    private static void bind(Builder builder) {
        if (builder == null) {
            builder = new Builder();
        }
        logContentFilters = builder.logContentFilters;
        logDirName = builder.logDirName;
        logFileName = builder.logFileName;
        logFileMaxSize = builder.logFileMaxSize;
        backupLogFileNameGetter = builder.backupLogFileNameGetter;
    }

    private static File getLogFile(@NonNull File logRootDir) {
        File logDir = new File(logRootDir, logDirName);
        if (!logDir.exists() && !logDir.mkdirs()) {
            Log.e(TAG, "创建日志文件目录失败");
            return null;
        }
        File logFile = new File(logDir, logFileName);
        try {
            if (!logFile.exists() && !logFile.createNewFile()) {
                Log.e(TAG, "创建日志文件失败");
                return null;
            }
        } catch (IOException e) {
            Log.e(TAG, "创建日志文件失败", e);
            return null;
        }
        long fileSize = logFile.length();
        if (fileSize >= logFileMaxSize) {
            File bakFile = new File(logDir, backupLogFileNameGetter.get(logFileName));
            if (logFile.renameTo(bakFile)) {
                Log.e(TAG, "重命名日志文件失败");
                return null;
            }
            logFile = new File(logDir, logFileName);
        }

        return logFile;
    }

    public static void stop() {
        synchronized (gProcess) {
            Process process = gProcess.get();
            if (process != null && process.isAlive()) {
                process.destroyForcibly();
            }
            gProcess.set(null);
        }
    }
}
相关推荐
1024小神22 分钟前
tauri2.0版本开发苹果ios和安卓android应用,环境搭建和最后编译为apk
android·ios·tauri
兰琛30 分钟前
20241121 android中树结构列表(使用recyclerView实现)
android·gitee
TheITSea34 分钟前
云服务器宝塔安装静态网页 WordPress、VuePress流程记录
java·服务器·数据库
AuroraI'ncoding41 分钟前
SpringMVC接收请求参数
java
九圣残炎1 小时前
【从零开始的LeetCode-算法】3354. 使数组元素等于零
java·算法·leetcode
Y多了个想法1 小时前
RK3568 android11 适配敦泰触摸屏 FocalTech-ft5526
android·rk3568·触摸屏·tp·敦泰·focaltech·ft5526
天天扭码2 小时前
五天SpringCloud计划——DAY1之mybatis-plus的使用
java·spring cloud·mybatis
程序猿小柒2 小时前
leetcode hot100【LeetCode 4.寻找两个正序数组的中位数】java实现
java·算法·leetcode
NotesChapter2 小时前
Android吸顶效果,并有着ViewPager左右切换
android
不爱学习的YY酱2 小时前
【操作系统不挂科】<CPU调度(13)>选择题(带答案与解析)
java·linux·前端·算法·操作系统