Logus:另一种简单、优雅、高效打Log的方式

起源:

最近项目中有使用Uber的zap(Go语言生态中一种高效打印Log的实用库,久经考验)打印log,用的顺手,于是借鉴其中的设计思想,在Android环境下封装Log类提供相似功能。

设计思想:

分离消息与数据域,避免字符串拼接和效率低的字符串格式化

  • 把log信息分两段:消息和数据域;消息是必须的, 数据域是个数可变的键-值对
  • 消息仅仅是String,不带任何格式化或字符串拼接
  • 数据域是以key-value的形式成对作为参数传给打印函数,忽略最后一个不成对参数
  • 内部实现是使用StringBuilder组合最终要打印的信息,避免过多的字符串拼接导致log打印频繁时给gc过多压力
  • StackTrace信息是可以设置打印或不打印

用例:

// file: MainActivity.java
@Override
protected void onResume() {
    super.onResume();
    Logus.d("just a message");
    Logus.e("a field message", "a", 1);
    Logus.w("3 fields but see 2", "a", 1, "b", true, "c");
    Logus.V("Main", "with prefix just a message");
    Logus.E("Main", "with prefix 2 fields", "a", 10, "b", false);
    Logus.E("Main", "with prefix 3 fields but log 2", "a", 10, "b", true, "c");
    Logus.setStackTrace(false);
    Logus.E("Main", "no stack trace with prefix");
}

// 输出:
04-19 22:12:56.209 3256-3256/net.gility.note  D/Logus: MainActivity.java(425)#onResume > just a message
04-19 22:12:56.210 3256-3256/net.gility.note  E/Logus: MainActivity.java(426)#onResume > a field message { a=1; }
04-19 22:12:56.210 3256-3256/net.gility.note  W/Logus: MainActivity.java(427)#onResume > 3 fields but see 2 { a=1; b=true; }
04-19 22:12:56.211 3256-3256/net.gility.note  V/Logus: MainActivity.java(428)#onResume Main> with prefix just a message
04-19 22:12:56.212 3256-3256/net.gility.note  E/Logus: MainActivity.java(429)#onResume Main> with prefix 2 fields { a=10; b=false; }
04-19 22:12:56.213 3256-3256/net.gility.note  E/Logus: MainActivity.java(430)#onResume Main> with prefix 3 fields but log 2 { a=10; b=true; }
04-19 22:12:56.213 3256-3256/net.gility.note  D/Logus: > no stack trace no prefix
04-19 22:12:56.213 3256-3256/net.gility.note  E/Logus: Main> no stack trace with prefix

具体实现:

public final class Logus {
    // 数据域长度调整掩码,用于计算数据域长度;aInt & FIXED_LENGTH_MASK 相当于 aInt % 2 == 0 ? aInt : aInt - 1;
    private static final int FIXED_LENGTH_MASK = ~1;

    private static String sPreTag = "Logus";        // 预设TAG
    private static String sPrePrefix = "";          // 预设消息前缀
    private static boolean sIsStackTrace = true;    // 是否打印StackTrace Info

    /**
     * 设置TAG
     *
     * @param tag
     */
    public static void setTag(final String tag) {
        if (!TextUtils.isEmpty(tag)) {
            sPreTag = tag;
        }
    }

    /**
     * 设置消息前缀
     *
     * @param prefix
     */
    public static void setPrefix(final String prefix) {
        if (!TextUtils.isEmpty(prefix)) {
            sPrePrefix = prefix;
        } else {
            sPrePrefix = "";
        }
    }

    /**
     * 设置是否打印StackTrace信息 (显示文件名(行数)#函数名:)
     *
     * @param isStackTrace true/false
     */
    public static void setStackTrace(boolean isStackTrace) {
        sIsStackTrace = isStackTrace;
    }

    /**
     * Verbose level log
     *
     * @param message 消息
     * @param fields  数据域,键-值连续的数据对,必须确保fields的长度为偶数,否则最后一个数据将不会打印
     */
    public static void v(final String message, Object... fields) {
        printMsg(Log.VERBOSE, sPrePrefix, message, fields);
    }

    /**
     * Debug level log print
     *
     * @param message 消息
     * @param fields  数据域,键-值连续的数据对,必须确保fields的长度为偶数,否则最后一个数据将不会打印
     */
    public static void d(final String message, Object... fields) {
        printMsg(Log.DEBUG, sPrePrefix, message, fields);
    }

    /**
     * Info level log print
     *
     * @param message 消息
     * @param fields  数据域,键-值连续的数据对,必须确保fields的长度为偶数,否则最后一个数据将不会打印
     */
    public static void i(final String message, Object... fields) {
        printMsg(Log.INFO, sPrePrefix, message, fields);
    }

    /**
     * Warning level log print
     *
     * @param message 消息
     * @param fields  数据域,键-值连续的数据对,必须确保fields的长度为偶数,否则最后一个数据将不会打印
     */
    public static void w(final String message, Object... fields) {
        printMsg(Log.WARN, sPrePrefix, message, fields);
    }

    /**
     * Error level log print
     *
     * @param message 消息
     * @param fields  数据域,键-值连续的数据对,必须确保fields的长度为偶数,否则最后一个数据将不会打印
     */
    public static void e(final String message, Object... fields) {
        printMsg(Log.ERROR, sPrePrefix, message, fields);
    }

    /**
     * Verbose level log
     *
     * @param prefix  消息前缀
     * @param message 消息
     * @param fields  数据域,键-值连续的数据对,必须确保fields的长度为偶数,否则最后一个数据将不会打印
     */
    public static void V(final String prefix, final String message, Object... fields) {
        printMsg(Log.VERBOSE, prefix, message, fields);
    }

    /**
     * Debug level log print
     *
     * @param prefix  消息前缀
     * @param message 消息
     * @param fields  数据域,键-值连续的数据对,必须确保fields的长度为偶数,否则最后一个数据将不会打印
     */
    public static void D(final String prefix, final String message, Object... fields) {
        printMsg(Log.DEBUG, prefix, message, fields);
    }

    /**
     * Info level log print
     *
     * @param prefix  消息前缀
     * @param message 消息
     * @param fields  数据域,键-值连续的数据对,必须确保fields的长度为偶数,否则最后一个数据将不会打印
     */
    public static void I(final String prefix, final String message, Object... fields) {
        printMsg(Log.INFO, prefix, message, fields);
    }

    /**
     * Warning level log print
     *
     * @param prefix  消息前缀
     * @param message 消息
     * @param fields  数据域,键-值连续的数据对,必须确保fields的长度为偶数,否则最后一个数据将不会打印
     */
    public static void W(final String prefix, final String message, Object... fields) {
        printMsg(Log.WARN, prefix, message, fields);
    }

    /**
     * Error level log print
     *
     * @param prefix  消息前缀
     * @param message 消息
     * @param fields  数据域,键-值连续的数据对,必须确保fields的长度为偶数,否则最后一个数据将不会打印
     */
    public static void E(final String prefix, final String message, Object... fields) {
        printMsg(Log.ERROR, prefix, message, fields);
    }

    /**
     * 打印log
     *
     * @param level   log级别
     * @param prefix  前缀
     * @param message 消息体(必须的)
     * @param fields  数据域
     */
    private static void printMsg(final int level, final String prefix, final String message, Object... fields) {
        StringBuilder sb = new StringBuilder();
        // 添加栈信息(文件及函数信息)
        if (sIsStackTrace) {
            StackTraceElement[] elements = Thread.currentThread().getStackTrace();
            if (elements != null && elements.length > 4) {
                sb.append(elements[4].getFileName());
                sb.append("(");
                sb.append(elements[4].getLineNumber());
                sb.append(")#");
                sb.append(elements[4].getMethodName());
                sb.append(" ");
            }
        }
        sb.append(prefix);
        sb.append("> ");
        sb.append(message);
        if (fields.length > 1) {
            sb.append(" { ");
            for (int i = 0, j = fields.length & FIXED_LENGTH_MASK; i < j; ) {
                sb.append(String.valueOf(fields[i++]));
                sb.append("=");
                sb.append(String.valueOf(fields[i++]));
                sb.append("; ");
            }
            sb.append("}");
        }
        switch (level) {
            case Log.VERBOSE:
                Log.v(sPreTag, sb.toString());
                break;
            case Log.DEBUG:
                Log.d(sPreTag, sb.toString());
                break;
            case Log.INFO:
                Log.i(sPreTag, sb.toString());
                break;
            case Log.WARN:
                Log.w(sPreTag, sb.toString());
                break;
            case Log.ERROR:
                Log.e(sPreTag, sb.toString());
                break;
            default:
                break;
        }
    }
}
 rsync: 基本命令和用法 NewSQL: 分布式数据库TiDB、CockroachDB 

Comments