仿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