Android 写本地日志工具类

Android 写本地日志工具类,项目中应该会比较常用,在此记录一下

java 复制代码
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 里初始化一下

java 复制代码
@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);
    }
}
相关推荐
阿维的博客日记1 小时前
spring里面的@RequestParam什么意思
java·后端·spring
laowangpython1 小时前
昆仑通态 Mcgs Pro软件安装步骤(附安装包)Mcgs Pro 3.3.6 下载安装教程(保姆级)
java·服务器·数据库·其他
罗超驿2 小时前
6.Java LinkedList深度解析:从链表原理到源码实践
java·开发语言·链表
2501_948106912 小时前
计算机毕业设计之jsp物业管理系统
java·大数据·开发语言·汽车·课程设计
非典型Android程序员2 小时前
Launcher学习总结
android
黑taoA2 小时前
深度解析:Maven核心命令(clean、compile、package、install)与IDEA多模块项目避坑指南
java·maven·intellij-idea
TDengine (老段)2 小时前
TDengine 索引使用指南 — 何时建、怎么建、怎么用
java·大数据·数据库·物联网·时序数据库·tdengine·涛思数据
xlxxy_2 小时前
sap获取批次特性报表
java·linux·开发语言·前端·数据库·abap·mm
栋***t2 小时前
从“工具”到“基建”,麦塔在线考试系统经历的时代变局与定位逻辑
java·大数据·数据库·开源软件·无纸化