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);
}
}
}