基于android编写本地日志工具类
android 写本地日志工具类,项目中应该会比较常用,在此记录一下
功能:
- 支持多级别日志(info / warn / error / debug)
- 异步写入,不阻塞主线程
- 自动按日期分文件
- 线程安全
- 支持异常堆栈记录
- 自动清理过期日志
完整代码
package com.eternal.base.utils;
import android.content.context;
import android.os.environment;
import android.util.log;
import java.io.bufferedwriter;
import java.io.file;
import java.io.filewriter;
import java.io.ioexception;
import java.io.printwriter;
import java.io.stringwriter;
import java.text.simpledateformat;
import java.util.date;
import java.util.locale;
import java.util.concurrent.executorservice;
import java.util.concurrent.executors;
/**
* 本地日志写入工具类
* <p>
* 功能:
* 1. 支持多级别日志(info / warn / error / debug)
* 2. 异步写入,不阻塞主线程
* 3. 自动按日期分文件
* 4. 线程安全
* 5. 支持异常堆栈记录
* 6. 自动清理过期日志
* </p>
* <p>
* 使用示例:
* <pre>
* // 初始化(application.oncreate 中调用一次)
* writefilelog.init(context);
*
* // 写日志
* writefilelog.i("tag", "普通信息");
* writefilelog.e("tag", "错误信息", throwable);
* </pre>
* </p>
*/
public class writefilelog {
private static final string tag = "writefilelog";
/** 日志级别常量 */
private static final string level_info = "i";
private static final string level_warn = "w";
private static final string level_error = "e";
private static final string level_debug = "d";
/** 日志文件后缀 */
private static final string log_suffix = ".log";
/** 日志文件夹名称(位于应用外部存储 files 目录下) */
private static final string log_dir_name = "logs";
/** 单个日志文件保留天数,超过自动删除 */
private static final int retain_days = 7;
/** 单个日志文件最大大小(字节),超过则自动滚动生成新文件 */
private static final long max_file_size = 10 * 1024 * 1024l; // 10mb
private static volatile boolean sinitialized = false;
private static file slogdir;
/** 单线程写入器,保证线程安全且按顺序落盘 */
private static final executorservice sexecutor = executors.newsinglethreadexecutor(r -> {
thread t = new thread(r, "writefilelog-writer");
t.setdaemon(true);
t.setpriority(thread.min_priority);
return t;
});
private static final simpledateformat file_date_format =
new simpledateformat("yyyy-mm-dd", locale.us);
private static final simpledateformat log_date_format =
new simpledateformat("yyyy-mm-dd hh:mm:ss.sss", locale.us);
private writefilelog() {
throw new unsupportedoperationexception("utility class");
}
/**
* 初始化日志工具类,建议在 {@code application.oncreate()} 中调用一次。
*
* @param context 应用上下文
*/
public static void init(context context) {
if (context == null) {
log.e(tag, "init: context is null");
return;
}
context app = context.getapplicationcontext();
// 使用应用专属外部存储目录,无需申请 write_external_storage 权限(android 10+)
file externalfilesdir = app.getexternalfilesdir(null);
if (externalfilesdir == null) {
// 回退到内部 files 目录
externalfilesdir = app.getfilesdir();
}
slogdir = new file(externalfilesdir, log_dir_name);
if (!slogdir.exists() && !slogdir.mkdirs()) {
log.e(tag, "init: failed to create log dir: " + slogdir.getabsolutepath());
}
sinitialized = true;
// 启动时清理过期日志
cleanexpiredlogs();
log.i(tag, "writefilelog initialized, log dir: " + slogdir.getabsolutepath());
}
/** 返回日志文件所在目录,未初始化时返回 null */
public static file getlogdir() {
return slogdir;
}
/** ---------------------- 对外 api ---------------------- */
public static void i(string tag, string msg) {
write(level_info, tag, msg, null);
}
public static void w(string tag, string msg) {
write(level_warn, tag, msg, null);
}
public static void w(string tag, string msg, throwable tr) {
write(level_warn, tag, msg, tr);
}
public static void e(string tag, string msg) {
write(level_error, tag, msg, null);
}
public static void e(string tag, string msg, throwable tr) {
write(level_error, tag, msg, tr);
}
public static void d(string tag, string msg) {
write(level_debug, tag, msg, null);
}
/**
* 通用写入方法。
*
* @param level 日志级别
* @param tag 标签
* @param msg 消息内容
* @param tr 可选的异常堆栈
*/
public static void write(final string level, final string tag,
final string msg, final throwable tr) {
if (!sinitialized) {
// 未初始化时仅打印到 logcat,避免丢失关键信息
log.w(tag, "writefilelog not initialized, fallback to logcat");
return;
}
final long timestamp = system.currenttimemillis();
final string threadname = thread.currentthread().getname();
// 在主调线程同步获取调用位置,异步后拿不到真实调用栈
final string caller = getcallerinfo();
sexecutor.execute(() -> dowrite(level, tag, msg, tr, timestamp, threadname, caller));
}
/** ---------------------- 内部实现 ---------------------- */
private static void dowrite(string level, string tag, string msg, throwable tr,
long timestamp, string threadname, string caller) {
file targetfile = resolvelogfile(timestamp);
if (targetfile == null) return;
string time = formattime(log_date_format, new date(timestamp));
stringbuilder sb = new stringbuilder(256);
sb.append(time).append(' ')
.append(level).append('/').append(tag).append(' ')
.append('[').append(threadname).append("] ")
.append(caller).append(" - ")
.append(msg != null ? msg : "null")
.append('\n');
if (tr != null) {
sb.append(getstacktracestring(tr)).append('\n');
}
bufferedwriter writer = null;
try {
// append = true,追加写入
writer = new bufferedwriter(new filewriter(targetfile, true), 8192);
writer.write(sb.tostring());
writer.flush();
} catch (ioexception e) {
log.e(tag, "dowrite failed", e);
} finally {
if (writer != null) {
try { writer.close(); } catch (ioexception ignored) { }
}
}
}
/**
* 根据时间戳解析当前要写入的日志文件。
* 超过 {@link #max_file_size} 时自动滚动(文件名附加序号)。
*/
private static file resolvelogfile(long timestamp) {
string date = formattime(file_date_format, new date(timestamp));
// 先尝试基础文件名
file base = new file(slogdir, date + log_suffix);
if (!base.exists() || base.length() < max_file_size) {
return base;
}
// 超过大小上限,滚动生成带序号的文件
int index = 1;
file rolled;
while (true) {
rolled = new file(slogdir, date + "_" + index + log_suffix);
if (!rolled.exists() || rolled.length() < max_file_size) {
return rolled;
}
index++;
// 安全兜底,避免死循环
if (index > 1000) return base;
}
}
private static string getstacktracestring(throwable tr) {
if (tr == null) return "";
stringwriter sw = new stringwriter(256);
printwriter pw = new printwriter(sw);
tr.printstacktrace(pw);
pw.flush();
return sw.tostring();
}
/**
* 解析调用者的类名、方法名和行号。
* <p>
* 调用链示例:caller -> i() -> write() -> getcallerinfo() -> throwable
* 栈帧索引:
* [0] java.lang.thread.getstacktrace
* [1] writefilelog.getcallerinfo
* [2] writefilelog.write
* [3] writefilelog.i / w / e / d
* [4] 真正的调用者
* </p>
* 通过按类名过滤跳过本类的所有帧,第一个非本类帧即为真实调用者。
*/
private static string getcallerinfo() {
stacktraceelement[] stack = thread.currentthread().getstacktrace();
string selfclass = writefilelog.class.getname();
boolean foundself = false;
for (stacktraceelement element : stack) {
string classname = element.getclassname();
if (classname.equals(selfclass)) {
foundself = true;
continue;
}
if (foundself) {
// 形如:mainactivity.init(mainactivity.java:58)
return element.getclassname()
+ "." + element.getmethodname()
+ "(" + element.getfilename()
+ ":" + element.getlinenumber() + ")";
}
}
return "unknown";
}
private static string formattime(simpledateformat sdf, date date) {
// simpledateformat 非线程安全,每次加锁调用
synchronized (sdf) {
return sdf.format(date);
}
}
/** 删除超过 {@link #retain_days} 天的旧日志文件 */
public static void cleanexpiredlogs() {
if (slogdir == null || !slogdir.exists()) return;
sexecutor.execute(() -> {
long cutoff = system.currenttimemillis() - retain_days * 24l * 60 * 60 * 1000;
file[] files = slogdir.listfiles();
if (files == null) return;
int deleted = 0;
for (file f : files) {
if (f.isfile() && f.lastmodified() < cutoff) {
if (f.delete()) deleted++;
}
}
if (deleted > 0) {
log.i(tag, "cleanexpiredlogs: deleted " + deleted + " expired log files");
}
});
}
/** 清空全部日志文件(谨慎使用) */
public static void clearalllogs() {
if (slogdir == null || !slogdir.exists()) return;
sexecutor.execute(() -> {
file[] files = slogdir.listfiles();
if (files == null) return;
for (file f : files) {
if (f.isfile()) f.delete();
}
log.i(tag, "clearalllogs: done");
});
}
/**
* 判断 sd 卡是否可用。
* 用于外部写入前预检。
*/
public static boolean isexternalstoragewritable() {
string state = environment.getexternalstoragestate();
return environment.media_mounted.equals(state);
}
}使用前在 application 里初始化一下
@override
public void oncreate() {
super.oncreate();
try {
// 关闭位置权限会重新启动app,而不启动mainactivity,通过反射重新初始化mainmodel
this.mviewmodel = createviewmodel("com.eternal.main.model.mainmodel");
} catch (exception e) {
klog.e(e);
}
if (buildconfig.debug) {
writefilelog.init(this);
}
}使用示例:
// 初始化(application.oncreate 中调用一次)
writefilelog.init(context);
// 写日志
writefilelog.i("tag", "普通信息");
writefilelog.e("tag", "错误信息", throwable);到此这篇关于基于android编写简单的本地日志工具类的文章就介绍到这了,更多相关android本地日志工具类内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论