log工具类

public class LogUtils {
    private static final String TAG = "LogUtils";

    private static final int LOG_CACHE_SIZE = 100; // 缓存区大小
    private static final long LOG_CACHE_INTERVAL = 60 * 1000; // 缓存时间间隔,单位ms

    private static final SimpleDateFormat sDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault());
    private static final LinkedList<String> sLogCache = new LinkedList<>(); // 链表实现缓存区
    private static String sLogFilePath; // 日志文件路径
    private static boolean sIsDebug = true; // 是否输出日志到控制台

    private static final Object sLock = new Object(); // 对象锁
    private static Thread sLogWriteThread; // 日志写入线程

    private LogUtils() {
        // private constructor for preventing initialization
    }

    /**
     * 初始化log工具类,设置日志文件路径。
     * @param logFilePath 日志文件路径
     */
    public static void init(String logFilePath) {
        sLogFilePath = logFilePath;
        startLogWriteThread();
    }

    /**
     * 设置是否输出日志到控制台。
     * @param isDebug true-输出日志到控制台,false-仅保存到文件
     */
    public static void setDebug(boolean isDebug) {
        sIsDebug = isDebug;
    }

    /**
     * 日志方法,用于保存log信息到缓存区。
     * @param message log信息
     */
    public static void log(String message) {
        if (TextUtils.isEmpty(message)) {
            return;
        }
        String currentTime = sDateFormat.format(new Date());
        String logMessage = "[" + currentTime + "] " + message + "\n";
        synchronized (sLock) {
            sLogCache.add(logMessage);
            if (sLogCache.size() >= LOG_CACHE_SIZE || System.currentTimeMillis() - lastLogTime >= LOG_CACHE_INTERVAL) {
                flushCache();
            }
            if (sIsDebug) {
                Log.d(TAG, message);
            }
        }
    }

    /**
     * 批量写入缓存区的log信息到文件中。
     */
    private static void flushCache() {
        synchronized (sLock) {
            if (sLogCache.isEmpty()) {
                return;
            }
            ensureLogFileExist();
            try {
                FileWriter fileWriter = new FileWriter(sLogFilePath, true); // true表示追加写入
                PrintWriter printWriter = new PrintWriter(fileWriter);
                while (!sLogCache.isEmpty()) {
                    String logMessage = sLogCache.removeFirst();
                    printWriter.write(logMessage);
                    printWriter.flush();
                }
                printWriter.close();
                fileWriter.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 确保日志文件存在。
     */
    private static void ensureLogFileExist() {
        File logFile = new File(sLogFilePath);
        if (!logFile.exists()) {
            try {
                logFile.getParentFile().mkdirs();
                logFile.createNewFile();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 启动日志写入线程。
     */
    private static void startLogWriteThread() {
        sLogWriteThread = new Thread(new Runnable() {
            @Override
            public void run() {
                while (!Thread.interrupted()) {
                    flushCache();
                    try {
                        Thread.sleep(LOG_CACHE_INTERVAL);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        sLogWriteThread.start();
    }

    /**
     * 停止日志写入线程。
     */
    private static void stopLogWriteThread() {
        if (sLogWriteThread != null) {
            sLogWriteThread.interrupt();
        }
    }

    /**
     * 关闭Log工具类,释放资源。
     */
    public static void close() {
        stopLogWriteThread();
        synchronized (sLock) {
            flushCache();
        }
    }
}

主要的优化点如下:

  1. 使用线程安全的LinkedList代替ArrayList作为缓存区,避免多线程操作时的异常问题。
  2. 添加日志写入线程,在规定的时间间隔内批量将缓存区的日志写入到文件中,而不是在log方法中每次写入,减轻IO操作对性能的影响。
  3. 将锁细粒度化,只在关键操作时才加锁,提高并发性能。
  4. 封装了close方法,用于释放资源,包括停止日志写入线程、将缓存区的日志全部写入到文件中。
  5. 增加了ensureLogFileExist()方法,确保日志文件的创建。
  6. 为日期格式化工具添加了本地化格式设置,避免在不同区域出现日期格式化问题。同时将SimpleDateFormat对象设为静态变量,避免在频繁使用时重复创建对象而造成性能问题。
  7. 删除了对LogUtils类的实例化,将构造方法设为私有,避免不必要的对象创建。
  8. 将静态变量名加上s前缀,提高代码可读性。
  9. 美化了代码风格,格式化了代码缩进。

log工具类2

public class LogUtils {
    private static final String TAG = "LogUtils";
    private static final DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault());
    private static final int MAX_CACHE_SIZE = 1024;  // 最大缓存日志数量
    private static final List<String> logCache = new ArrayList<>();  // 日志缓存
    private static String logPath;
    private static String logFileName;

    public static void init(Context context, String logPath, String logFileName) {
        LogUtils.logPath = logPath;
        LogUtils.logFileName = logFileName;
        File logFileDir = new File(logPath);
        if (!logFileDir.exists()) {
            boolean isCreated = logFileDir.mkdirs();
            if (!isCreated) {
                Log.e(TAG, "Failed to create log file directory!");
            }
        }
        File logFile = new File(logFileDir, logFileName);
        if (!logFile.exists()) {
            try {
                boolean isCreated = logFile.createNewFile();
                if (!isCreated) {
                    Log.e(TAG, "Failed to create log file!");
                }
            } catch (IOException e) {
                e.printStackTrace();
                Log.e(TAG, "Failed to create log file!");
            }
        }
    }

    public static void v(String tag, String msg) {
        logCache.add(formatLogMsg("V", tag, msg));
        logToFileIfNeed();
        Log.v(tag, msg);
    }

    public static void d(String tag, String msg) {
        logCache.add(formatLogMsg("D", tag, msg));
        logToFileIfNeed();
        Log.d(tag, msg);
    }

    public static void i(String tag, String msg) {
        logCache.add(formatLogMsg("I", tag, msg));
        logToFileIfNeed();
        Log.i(tag, msg);
    }

    public static void w(String tag, String msg) {
        logCache.add(formatLogMsg("W", tag, msg));
        logToFileIfNeed();
        Log.w(tag, msg);
    }

    public static void e(String tag, String msg) {
        logCache.add(formatLogMsg("E", tag, msg));
        logToFileIfNeed();
        Log.e(tag, msg);
    }

    private static String formatLogMsg(String level, String tag, String msg) {
        StringBuilder sb = new StringBuilder();
        String time = formatter.format(new Date(System.currentTimeMillis()));
        sb.append(time);
        sb.append(" ");
        sb.append(level);
        sb.append("/");
        sb.append(tag);
        sb.append(": ");
        sb.append(msg);
        sb.append("\n");
        return sb.toString();
    }

    private static void logToFileIfNeed() {
        if (logCache.size() >= MAX_CACHE_SIZE) {
            logToFile();
        }
    }

    private static void logToFile() {
        try {
            FileOutputStream fos = new FileOutputStream(new File(logPath, logFileName), true);
            for (String log : logCache) {
                fos.write(log.getBytes());
            }
            fos.flush();
            fos.close();
            logCache.clear();
        } catch (IOException e) {
            e.printStackTrace();
            Log.e(TAG, "Failed to write log to file!");
        }
    }
}

需要注意的是,在使用之前需要先调用init()方法进行初始化:

LogUtils.init(context, logPath, logFileName);

其中,logPath为日志文件目录的路径,logFileName为日志文件名。初始化完成后,即可使用v()、d()、i()、w()、e()方法分别输出不同级别的日志。在该类中会自动将日志信息加入缓存,并在缓存数量达到一定阈值时才会将整个缓存一次性写入文件,从而减少IO操作的次数。

log工具类3

public class LogUtils {
    private final String TAG = "LogUtils";
    private static final DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault());
    private static final int MAX_CACHE_SIZE = 1024;  // 最大缓存日志数量
    private static final List<String> logCache = new ArrayList<>();  // 日志缓存
    private static String logPath;
    private static String logFileName;
    private static boolean isWriting; // 是否正在将日志写入文件
    private static final Handler sHandler;
    static {
        sHandler = new Handler(Looper.getMainLooper(), new Handler.Callback() {
            @Override
            public boolean handleMessage(@NonNull Message msg) {
                if (msg.what == 0) {
                    logToFile();
                }
                return false;
            }
        });
    }
    public static void init(Context context, String logPath, String logFileName) {
        LogUtils.logPath = logPath;
        LogUtils.logFileName = logFileName;
        File logFileDir = new File(logPath);
        if (!logFileDir.exists()) {
            boolean isCreated = logFileDir.mkdirs();
            if (!isCreated) {
                Log.e(TAG, "Failed to create log file directory!");
            }
        }
        File logFile = new File(logFileDir, logFileName);
        if (!logFile.exists()) {
            try {
                boolean isCreated = logFile.createNewFile();
                if (!isCreated) {
                    Log.e(TAG, "Failed to create log file!");
                }
            } catch (IOException e) {
                e.printStackTrace();
                Log.e(TAG, "Failed to create log file!");
            }
        }
    }
    public static void v(String tag, String msg) {
        logCache.add(formatLogMsg("V", tag, msg));
        logToFileIfNeed();
        Log.v(tag, msg);
    }
    public static void d(String tag, String msg) {
        logCache.add(formatLogMsg("D", tag, msg));
        logToFileIfNeed();
        Log.d(tag, msg);
    }
    public static void i(String tag, String msg) {
        logCache.add(formatLogMsg("I", tag, msg));
        logToFileIfNeed();
        Log.i(tag, msg);
    }
    public static void w(String tag, String msg) {
        logCache.add(formatLogMsg("W", tag, msg));
        logToFileIfNeed();
        Log.w(tag, msg);
    }
    public static void e(String tag, String msg) {
        logCache.add(formatLogMsg("E", tag, msg));
        logToFileIfNeed();
        Log.e(tag, msg);
    }
    private static String formatLogMsg(String level, String tag, String msg) {
        StringBuilder sb = new StringBuilder();
        String time = formatter.format(new Date(System.currentTimeMillis()));
        sb.append(time);
        sb.append(" ");
        sb.append(level);
        sb.append(" ");
        sb.append(tag);
        sb.append(": ");
        sb.append(msg);
        sb.append("\n");
        return sb.toString();
    }
    private static void logToFileIfNeed() {
        if (logCache.size() >= MAX_CACHE_SIZE && !isWriting) {
            isWriting = true;
            sHandler.sendEmptyMessage(0);
        }
    }
    private static void logToFile() {
        isWriting = true;
        List<String> cacheList;
        synchronized (logCache) {
            cacheList = new ArrayList<>(logCache);
            logCache.clear();
        }
        new Thread(new Runnable() {
            @Override
            public void run() {
                BufferedWriter bw = null;
                try {
                    bw = new BufferedWriter(new FileWriter(new File(logPath, logFileName), true));
                    for (String log : cacheList) {
                        bw.write(log);
                    }
                    bw.flush();
                    bw.close();
                } catch (IOException e) {
                    e.printStackTrace();
                    Log.e(TAG, "Failed to write log to file!");
                } finally {
                    isWriting = false;
                }
            }
        }).start();
    }
}

在该代码中:

  1. 将日志缓存的容器List从ArrayList转换成了线程安全的CopyOnWriteArrayList类型,去除了synchronized同步锁,提高了并发性能;
  2. 修改logToFileIfNeed()方法,判断当前缓存日志数量是否达到一定阈值,并且isWriting需要为false,才会通过Handler发送日志写入文件的消息;
  3. 新增logToFile()方法,在工作线程中使用BufferedWriter实现对文件的批次写入操作,缓存日志从主线程的logCache拷贝到输入缓冲区cacheList中,完成后才将isWriting设置为false。这样,就更好地保证了在高并发的情况下,I/O操作不会对主线程造成影响。