仿logger建造自己的log打印

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是在一个项目中,地址

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,635评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,628评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,971评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,986评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,006评论 6 394
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,784评论 1 307
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,475评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,364评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,860评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,008评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,152评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,829评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,490评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,035评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,156评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,428评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,127评论 2 356

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 172,195评论 25 707
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,672评论 18 139
  • 本文会不定期更新,推荐watch下项目。如果喜欢请star,如果觉得有纰漏请提交issue,如果你有更好的点子可以...
    天之界线2010阅读 7,173评论 11 29
  • 在应用程序中添加日志记录总的来说基于三个目的:监视代码中变量的变化情况,周期性的记录到文件中供其他应用进行统计分析...
    时待吾阅读 5,053评论 1 13
  • 曾经的爱人 两年前 我们在一个城市 一个工作单位不期而遇…… 那时你给我的印象是一个奔三的大叔。哈哈 慵懒的衬衫 ...
    扯淡青春1995阅读 215评论 0 0