仿logger建造自己的log打印

作者: 求闲居士 | 来源:发表于2016-10-26 16:13 被阅读225次

1,前言

在开发过程中,log是不可缺少的助手,但直接用log,如果打印的信息过多或多个地方调用同一方法打印log,这种情况就很难定位打印出的数据是哪里调用的。就算知道哪里打印的,跳转到打印log的界面也麻烦。

像打印错误的那种定位功能就感觉很不错,点击错误的log就能跳转到调用的地点上。

网络有个打印封装Logger,就做的很不错,实现了以上功能。这里我就仿照它实现自己需要的简洁点的log打印。

2,如何实现定位日志打印

在Logger中,它是在LoggerPrinter类中的logHeaderContent方法中实现的:

private void logHeaderContent(int logType, String tag, int methodCount) {
    StackTraceElement[] trace = Thread.currentThread().getStackTrace();
    if (settings.isShowThreadInfo()) {
      logChunk(logType, tag, HORIZONTAL_DOUBLE_LINE + " Thread: " + Thread.currentThread().getName());
      logDivider(logType, tag);
    }
    String level = "";

    int stackOffset = getStackOffset(trace) + settings.getMethodOffset();

    //corresponding method count with the current stack may exceeds the stack trace. Trims the count
    if (methodCount + stackOffset > trace.length) {
      methodCount = trace.length - stackOffset - 1;
    }

    for (int i = methodCount; i > 0; i--) {
      int stackIndex = i + stackOffset;
      if (stackIndex >= trace.length) {
        continue;
      }
      StringBuilder builder = new StringBuilder();
      builder.append("║ ")
          .append(level)
          .append(getSimpleClassName(trace[stackIndex].getClassName()))
          .append(".")
          .append(trace[stackIndex].getMethodName())
          .append(" ")
          .append(" (")
          .append(trace[stackIndex].getFileName())
          .append(":")
          .append(trace[stackIndex].getLineNumber())
          .append(")");
      level += "   ";
      logChunk(logType, tag, builder.toString());
    }
  }

可以看到通过Thread.currentThread().getStackTrace()获取StackTraceElement,得到当前线程调用的栈帧集合。每调用一个方法,栈就会储存一个StackTraceElement用来储存该方法的相关信息,方法返回就出栈。通过getStackTrace()就能获得方法的调用栈,打印出文件名和行数,就能点击它定位到调用的方法了。打印出这样的信息:

MainActivity.onCreate  (MainActivity.java:33)

于是仿照这个方法,简单的写出自己想要打印的格式:

private static final int MIN_STACK_OFFSET = 5;      //调用MyLogger方法的最小层次
private static int logLevel = 2;        //打印几个等级的方法

     /**
     * 打印位置,根据logLevel来打印几层的方法,默认3层
     * @param logType   打印级别
     * @param tag   tag
     */
    private static void logLocation(int logType, String tag) {
        String level = "";
        StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace();
        int start = getStackOffset(stackTraceElements);
        for (int i = start; i < start + logLevel; i++) {   //除去本身logLocation方法的打印
            if (i == stackTraceElements.length) break;
            StringBuilder builder = new StringBuilder();
            builder.append(HORIZONTAL_DOUBLE_LINE).append(level)
                    .append(getSimpleClassName(stackTraceElements[i].getClassName()))
                    .append(".")
                    .append(stackTraceElements[i].getMethodName())
                    .append(" ")
                    .append(" (")
                    .append(stackTraceElements[i].getFileName())
                    .append(":")
                    .append(stackTraceElements[i].getLineNumber())
                    .append(")");
            level += "   ";
            logChunk(logType, tag, builder.toString());
        }
        logLevel = 3;
    }
    
    /**
     * Determines the starting index of the stack trace, after method calls made by this class.
     *
     * @param trace the stack trace
     *
     * @return the stack offset
     */
 private static int getStackOffset(StackTraceElement[] trace) {
        //经过debug发现,下标2开始是logLocation方法,0和1是线程方法,这个方法调用的层次是第3层 logLocation - log - d,所以MIN_STACK_OFFSET是2 + 3 = 5
        //但为了可扩展,一般设为3
        for (int i = MIN_STACK_OFFSET; i < trace.length; i++) {
            StackTraceElement e = trace[i];
            String name = e.getClassName();
            if (!name.equals(MyLogger.class.getName())) {
                return i;
            }
        }
        return -1;
    }

logLevel是设定打印出来的方法有几层;MIN_STACK_OFFSET是调用log打印方法在栈中的位置,0和1是线程方法,这个方法调用的层次是第3层 logLocation - log - d,所以MIN_STACK_OFFSET是2 + 3 = 5;HORIZONTAL_DOUBLE_LINE只是自己定义的打印样式'|'。

这样就实现了定位功能。主要靠获取StackTraceElement来获取调用的方法栈,再定位调用方法的位置,根据需要打印调用方法的层次。

要改变打印log的样式话,需要改变builder的拼接就行了。

打印内容:
/**
     * 打印内容
     * @param logType   类型
     * @param tag   tag
     * @param chunk 内容
     */
    private static void logContent(int logType, String tag, String chunk) {
        String[] lines = chunk.split(System.getProperty("line.separator"));
        for (String line : lines) {
            logChunk(logType, tag, HORIZONTAL_DOUBLE_LINE + " " + line);
        }
    }

根据自己的需要设计样式,这里就是换行打印。

3,Logger的简单解析

Logger类图(来源 image

可以看出,Logger的功能实现都是通过LoggerPrinter类代理实现的。

Helper类可以说是工具类,有isEmpty,equals,getStackTraceString三个工具方法。

Settings方法,顾名思义,配置信息都通过它维护。在这个类中有个域是LogAdapter,这个接口真正的实现是AndroidLogAdapter,里面调用Log打印。

关键的实现就在LoggerPrinter里,主要的实现方法是log方法:
@Override public synchronized void log(int priority, String tag, String message, Throwable throwable) {
    if (settings.getLogLevel() == LogLevel.NONE) {
      return;
    }
    if (throwable != null && message != null) {
      message += " : " + Helper.getStackTraceString(throwable);
    }
    if (throwable != null && message == null) {
      message = Helper.getStackTraceString(throwable);
    }
    if (message == null) {
      message = "No message/exception is set";
    }
    int methodCount = getMethodCount();
    if (Helper.isEmpty(message)) {
      message = "Empty/NULL log message";
    }

    logTopBorder(priority, tag);
    logHeaderContent(priority, tag, methodCount);

    //get bytes of message with system's default charset (which is UTF-8 for Android)
    byte[] bytes = message.getBytes();
    int length = bytes.length;
    if (length <= CHUNK_SIZE) {
      if (methodCount > 0) {
        logDivider(priority, tag);
      }
      logContent(priority, tag, message);
      logBottomBorder(priority, tag);
      return;
    }
    if (methodCount > 0) {
      logDivider(priority, tag);
    }
    for (int i = 0; i < length; i += CHUNK_SIZE) {
      int count = Math.min(length - i, CHUNK_SIZE);
      //create a new String with system's default charset (which is UTF-8 for Android)
      logContent(priority, tag, new String(bytes, i, count));
    }
    logBottomBorder(priority, tag);
  }
  • logTopBorder打印顶部分割线。
  • logHeaderContent打印调用方法位置用于定位。
  • 如果打印内容大于可打印的最大长度CHUNK_SIZE,则拆分多段打印。logDivider打印分割线区分顶部域内容,logContent打印内容,logBottomBorder打印底部分割线。
打印方法logChunk:
private void logChunk(int logType, String tag, String chunk) {
    String finalTag = formatTag(tag);
    switch (logType) {
      case ERROR:
        settings.getLogAdapter().e(finalTag, chunk);
        break;
      case INFO:
        settings.getLogAdapter().i(finalTag, chunk);
        break;
      case VERBOSE:
        settings.getLogAdapter().v(finalTag, chunk);
        break;
      case WARN:
        settings.getLogAdapter().w(finalTag, chunk);
        break;
      case ASSERT:
        settings.getLogAdapter().wtf(finalTag, chunk);
        break;
      case DEBUG:
        // Fall through, log debug by default
      default:
        settings.getLogAdapter().d(finalTag, chunk);
        break;
    }
  }

它是通过Settings的LogAdapter,也就是AndroidLogAdapter调用系统Log打印的信息。

打印的Tag信息,可以通过 Logger.t(String tag)放入LoggerPrinter的localTag中

ThreadLocal<String> localTag = new ThreadLocal<>();

formatTag(tag)方法将创建LoggerPrinter时的tag拼接上通过Logger.t(String tag)的tag。最终打印出的是初始tag(Logger中的DEFAULT_TAG)加上Logger.t(String tag)中的tag。

private String formatTag(String tag) {
    if (!Helper.isEmpty(tag) && !Helper.equals(this.tag, tag)) {
      return this.tag + "-" + tag;
    }
    return this.tag;
  }

定位打印通过Thread.currentThread().getStackTrace()实现,前面说过就不说了。

xml和json打印:

xml就是解析xml在打印出来:

/**
   * Formats the json content and print it
   *
   * @param xml the xml content
   */
  @Override public void xml(String xml) {
    if (Helper.isEmpty(xml)) {
      d("Empty/Null xml content");
      return;
    }
    try {
      Source xmlInput = new StreamSource(new StringReader(xml));
      StreamResult xmlOutput = new StreamResult(new StringWriter());
      Transformer transformer = TransformerFactory.newInstance().newTransformer();
      transformer.setOutputProperty(OutputKeys.INDENT, "yes");
      transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
      transformer.transform(xmlInput, xmlOutput);
      d(xmlOutput.getWriter().toString().replaceFirst(">", ">\n"));
    } catch (TransformerException e) {
      e("Invalid xml");
    }
  }

json判断是Array还是对象进行解析打印:

/**
   * Formats the json content and print it
   *
   * @param json the json content
   */
  @Override public void json(String json) {
    if (Helper.isEmpty(json)) {
      d("Empty/Null json content");
      return;
    }
    try {
      json = json.trim();
      if (json.startsWith("{")) {
        JSONObject jsonObject = new JSONObject(json);
        String message = jsonObject.toString(JSON_INDENT);
        d(message);
        return;
      }
      if (json.startsWith("[")) {
        JSONArray jsonArray = new JSONArray(json);
        String message = jsonArray.toString(JSON_INDENT);
        d(message);
        return;
      }
      e("Invalid Json");
    } catch (JSONException e) {
      e("Invalid Json");
    }
  }

4,总结

Logger的功能足够满足日常开发了,各种级别打印和xml,json打印。通过仿照它,对于学习它的架构和实现方法是很不错的。

因为我需要的是普通信息打印并定位地点,而Logger打印的信息样式有些复杂,所以集成了一个类只实现少量功能。自己尝试后才发现,Logger的结构是多么好,一个类还是太臃肿了;实现细节上对于多线程的考虑Logger是充分考虑过的。

在别人的基础上学习,查看优秀的代码是很好的学习方式。

MyLogger是在一个项目中,地址

相关文章

网友评论

    本文标题:仿logger建造自己的log打印

    本文链接:https://www.haomeiwen.com/subject/ikaduttx.html