Logger源碼理解分析

安卓開發(fā)過程中,log日志是我們接觸最多的一部分星持。如何優(yōu)雅的獲取log日志呢抢埋?我個人推薦使用Logger

GitHub/Logger傳送門

Logger效果展示

備注:

log級別 顏色
Verbose BBBBBB
Debug 0070BB
Info 48BB31
Warm BBBB23
Error FF0006
Assert 8F0005

控制臺日志

代碼部分:
Logcat代碼部分.png
截圖部分:
Verbose.png
Debug.png
Info.png
Warm.png
Error.png
Assert.png
json.png

CsvFile文件日志

代碼部分
CsvFile代碼部分.png
截圖部分(文件保存在手機存儲logger目錄下)
CsvFile.png

源碼分析

這些是 Logger 最基礎(chǔ)的用法,同時還支持 xml 的打印督暂。而且在GitHub上的README中有自定義參數(shù)的用法:

FormatStrategy formatStrategy = PrettyFormatStrategy.newBuilder()
    .showThreadInfo(false)  // (Optional) Whether to show thread info or not. Default true
    .methodCount(0)         // (Optional) How many method line to show. Default 2
    .methodOffset(7)        // (Optional) Hides internal method calls up to offset. Default 5
    .logStrategy(customLog) // (Optional) Changes the log strategy to print out. Default LogCat
    .tag("My custom tag")   // (Optional) Global tag for every log. Default PRETTY_LOGGER
    .build();

Logger.addLogAdapter(new AndroidLogAdapter(formatStrategy));

不過揪垄,我們先從就從 v 方法開始分析:

Logger.java 部分代碼
     private static Printer printer = new LoggerPrinter();

  public static void v(String message, Object... args) {
        printer.v(message, args);
  }
  • 首先,我們進入 Logger 類中,看到 v() 方法調(diào)用了 printer.v()

  • Printer是接口逻翁,實現(xiàn)類是 LoggerPrinter,下一步進入 LoggerPrinter 類查看 v 方法的細節(jié)

LoggerPrinter.java 部分代碼
    private final ThreadLocal<String> localTag = new ThreadLocal<>();
    
    @Override 
    public Printer t(String tag) {
        if (tag != null) {
          localTag.set(tag);
        }
        return this;
    }
    
    @Override 
    public void v(String message, Object... args) {
        log(VERBOSE, null, message, args);
    }

    private synchronized void log(int priority, Throwable throwable, String msg, Object... args) {
        String tag = getTag();
        String message = createMessage(msg, args);
        log(priority, tag, message, throwable);
    }
    
    private String getTag() {
        String tag = localTag.get();
        if (tag != null) {
          localTag.remove();
          return tag;
        }
        return null;
    }
    
    @Override 
    public synchronized void log(int priority, String tag, String message, Throwable throwable) {
        if (throwable != null && message != null) {
          message += " : " + Utils.getStackTraceString(throwable);
        }
        if (throwable != null && message == null) {
          message = Utils.getStackTraceString(throwable);
        }
        if (Utils.isEmpty(message)) {
          message = "Empty/NULL log message";
        }
    
        for (LogAdapter adapter : logAdapters) {
          if (adapter.isLoggable(priority, tag)) {
            adapter.log(priority, tag, message);
          }
        }
    }
  • 從上面的代碼可以看出 LoggerPrinter 類中有一個ThreadLocal 用于存放標簽 (給線程設(shè)置局部變量,避免出現(xiàn)線程并發(fā)問題,在Handler源碼理解分析文末有簡單的介紹)饥努,同時可以發(fā)現(xiàn)有 t() 方法用于值的注入并且返回 Printer 對象,這說明可以鏈式調(diào)用八回。

  • log() 方法中取出做為 tag酷愧,不過細心的可以看出在 getTag() 的時候雖然取出了 tag驾诈,但是明顯取出 tag 之后就將其置空,說明tag只能使用一次

  • 注意:t()方法只是給當前的線程設(shè)置一個僅能使用一次的標簽參數(shù)

      Logger.t("lalala").d("測試t()方法1");
      Logger.d("測試t()方法2");
    
t方法使用.png
  • 從上圖可以看出 t() 方法可可設(shè)置 tag 標簽溶浴,不過是在默認 "PRETTY_KIGGER" 之后拼上添加的 tag (細節(jié)請往下看PrettyFormatStrategy類的formatTag()方法)

  • 回到 LoggerPrinterv() 方法翘鸭,它最終調(diào)用了 Printer 接口的 log() 方法,所以可以看出無論是 v,i,d,w 等方法最終都是調(diào)用 log()戳葵。當中要特別注意的是 for (LogAdapter adapter : logAdapters) 這說明了它可以配置多個 Adapter就乓。

  • LoggerPrinter最終調(diào)用了接口 LogAdapterlog() 方法,所以我們要找 LogAdapter 的實現(xiàn)類拱烁。不過在調(diào)用之前通過 isLoggable(priority, tag) 對輸出進行過濾生蚁,我們可以在初始化的時候通過重寫這個方法來定義自己的規(guī)則

       Logger.addLogAdapter(new AndroidLogAdapter(){
          @Override
          public boolean isLoggable(int priority, String tag) {
              return super.isLoggable(priority, tag);
          }
      });
    

我們在README中可以看到兩個實現(xiàn)類:

AndroidLogAdapterDiskLogAdapter

下面我們分別對它們進行分析

AndroidLogAdapter.java代碼

public class AndroidLogAdapter implements LogAdapter {

  private final FormatStrategy formatStrategy;

  public AndroidLogAdapter() {
    this.formatStrategy = PrettyFormatStrategy.newBuilder().build();
  }

  public AndroidLogAdapter(FormatStrategy formatStrategy) {
    this.formatStrategy = formatStrategy;
  }

  @Override public boolean isLoggable(int priority, String tag) {
    return true;
  }

  @Override public void log(int priority, String tag, String message) {
    formatStrategy.log(priority, tag, message);
  }

}
  • 可以看出 AndroidLogAdapter 有兩個構(gòu)造方法,從無參的構(gòu)造方法可以看出 PrettyFormatStrategy 使用的是 Builder設(shè)計模式戏自。明顯邦投,無參的是我們在篇首進行測試使用的,另一個是用于自定義屬性參數(shù)擅笔。

  • 我們再進一步觀察 PrettyFormatStrategy 類中發(fā)生了什么

PrettyFormatStrategy.java部分代碼
  private final int methodCount;
  private final int methodOffset;
  private final boolean showThreadInfo;
  private final LogStrategy logStrategy;
  private final String tag;
    
@Override public void log(int priority, String onceOnlyTag, String message) {
    String tag = formatTag(onceOnlyTag);

    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);
}   

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

private void logContent(int logType, String tag, String chunk) {
    String[] lines = chunk.split(System.getProperty("line.separator"));
    for (String line : lines) {
      logChunk(logType, tag, HORIZONTAL_LINE + " " + line);
    }
}

private void logChunk(int priority, String tag, String chunk) {
    logStrategy.log(priority, tag, chunk);
}
  • log() 方法中開始通過 formatTag() 拼接添加的 tag 標簽
  • 通過 logTopBorder() 打印輸出的頭部邊界
  • 通過 logHeaderContent() 打印輸出線程信息志衣,以及調(diào)用該方法的所在代碼位置
  • logContent() 之前先進行 message 超長處理,之后在 logContent() 中進行換行格式處理
  • System.getProperty("line.separator")//換行符,功能和"\n"是一致的,但是此種寫法避免了 Windows和Linux的沖突
  • 最后調(diào)用接口 LogStrategylog() 方法進行打印猛们,所以我們要去尋找它的實現(xiàn)類 LogcatLogStrategy
  • 明顯念脯,我們可以自定義打印的策略,通過Builder傳入弯淘,否則將使用默認的 LogcatLogStrategy
LogcatLogStrategy.java
public class LogcatLogStrategy implements LogStrategy {

  @Override public void log(int priority, String tag, String message) {
    Log.println(priority, tag, message);
  }

}
  • 看到這里調(diào)用了系統(tǒng)自帶的 Log 來打印

DiskLogAdapter.java部分代碼

public class DiskLogAdapter implements LogAdapter {

  private final FormatStrategy formatStrategy;

  public DiskLogAdapter() {
    formatStrategy = CsvFormatStrategy.newBuilder().build();
  }

  public DiskLogAdapter(FormatStrategy formatStrategy) {
    this.formatStrategy = formatStrategy;
  }

  @Override public boolean isLoggable(int priority, String tag) {
    return true;
  }

  @Override public void log(int priority, String tag, String message) {
    formatStrategy.log(priority, tag, message);
  }
}
  • AndroidLogAdapter 一樣我們直接分析分析 CsvFormatStrategy
CsvFormatStrategy.java部分代碼
    private final Date date;
  private final SimpleDateFormat dateFormat;
  private final LogStrategy logStrategy;
  private final String tag;

  private CsvFormatStrategy(Builder builder) {
    date = builder.date;
    dateFormat = builder.dateFormat;
    logStrategy = builder.logStrategy;
    tag = builder.tag;
  }

  public static Builder newBuilder() {
    return new Builder();
  }

  @Override public void log(int priority, String onceOnlyTag, String message) {
    String tag = formatTag(onceOnlyTag);

    date.setTime(System.currentTimeMillis());

    StringBuilder builder = new StringBuilder();

    // machine-readable date/time
    builder.append(Long.toString(date.getTime()));

    // human-readable date/time
    builder.append(SEPARATOR);
    builder.append(dateFormat.format(date));

    // level
    builder.append(SEPARATOR);
    builder.append(Utils.logLevel(priority));

    // tag
    builder.append(SEPARATOR);
    builder.append(tag);

    // message
    if (message.contains(NEW_LINE)) {
      // a new line would break the CSV format, so we replace it here
      message = message.replaceAll(NEW_LINE, NEW_LINE_REPLACEMENT);
    }
    builder.append(SEPARATOR);
    builder.append(message);

    // new line
    builder.append(NEW_LINE);

    logStrategy.log(priority, tag, builder.toString());
  }
  
  public static final class Builder {
    .
    .
    .
    public CsvFormatStrategy build() {
      if (date == null) {
        date = new Date();
      }
      if (dateFormat == null) {
        dateFormat = new SimpleDateFormat("yyyy.MM.dd HH:mm:ss.SSS", Locale.UK);
      }
      if (logStrategy == null) {
        String diskPath = Environment.getExternalStorageDirectory().getAbsolutePath();
        String folder = diskPath + File.separatorChar + "logger";

        HandlerThread ht = new HandlerThread("AndroidFileLogger." + folder);
        ht.start();
        Handler handler = new DiskLogStrategy.WriteHandler(ht.getLooper(), folder, MAX_BYTES);
        logStrategy = new DiskLogStrategy(handler);
      }
      return new CsvFormatStrategy(this);
    }
    
  }
  • 有了 PrettyFormatStrategy 的分析绿店,相比較這個反而會更簡單一點
  • 主要是對字符串的拼接,格式的調(diào)整
  • 所有重點就落到了 LogSrategy 的實現(xiàn)類了,在 Builder 中的builder()方法中問題最大的應(yīng)該是 HandlerThreadDiskLogStrategy
  • HandlerThread 實際上還是一個普通的Thread庐橙,不過內(nèi)部實現(xiàn)了 Looper 循環(huán)假勿。好處: 在子線程中實現(xiàn)Looper,減輕了UI線程looper的壓力态鳖。如有問題可以結(jié)合Handler源碼理解分析進行理解
  • 并且可以看出日志文件保存在 Environment.getExternalStorageDirectory().getAbsolutePath() + "logger" 文件夾下
  • 接下來我們開始分析 DiskLogStrategy
DiskLogStrategy.java部分代碼
private final Handler handler;

 public DiskLogStrategy(Handler handler) {
    this.handler = handler;
 }

 @Override public void log(int level, String tag, String message) {
 // do nothing on the calling thread, simply pass the tag/msg to the background thread
 handler.sendMessage(handler.obtainMessage(level, message));
}


static class WriteHandler extends Handler {

private final String folder;
private final int maxFileSize;

WriteHandler(Looper looper, String folder, int maxFileSize) {
  super(looper);
  this.folder = folder;
  this.maxFileSize = maxFileSize;
}

@SuppressWarnings("checkstyle:emptyblock")
@Override public void handleMessage(Message msg) {
  String content = (String) msg.obj;

  FileWriter fileWriter = null;
  File logFile = getLogFile(folder, "logs");

  try {
    fileWriter = new FileWriter(logFile, true);

    writeLog(fileWriter, content);

    fileWriter.flush();
    fileWriter.close();
  } catch (IOException e) {
    if (fileWriter != null) {
      try {
        fileWriter.flush();
        fileWriter.close();
      } catch (IOException e1) { /* fail silently */ }
    }
  }
}

/**
 * This is always called on a single background thread.
 * Implementing classes must ONLY write to the fileWriter and nothing more.
 * The abstract class takes care of everything else including close the stream and catching IOException
 *
 * @param fileWriter an instance of FileWriter already initialised to the correct file
 */
private void writeLog(FileWriter fileWriter, String content) throws IOException {
  fileWriter.append(content);
}

private File getLogFile(String folderName, String fileName) {
  .
  .
  .
  return newFile;
}
  • 從上面的代碼可以看出 DiskLogStrategy 類中有個靜態(tài)內(nèi)部類繼承自 Handler, Looper 卻是從 HandlerThread 中得到的转培,說明handleMessage將會在一個子線程中執(zhí)行
  • 通過 handleMessage 則是將 log() 方法發(fā)送的 msg 中的內(nèi)容寫入文件
  • 個人理解:使用這種模式是為了保證日志的有序性避免多個線程對同一個文件進行編輯,且在子線程中保證不阻塞UI線程

How it works

how_it_works.png

在GitHub上浸须,官方給出了原理圖兆蕉,我們分析的方向也大致如此羽戒。注意:Printer與LogAdapter的關(guān)系為1對多

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末虎韵,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子缸废,更是在濱河造成了極大的恐慌驶社,老刑警劉巖测萎,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件亡电,死亡現(xiàn)場離奇詭異,居然都是意外死亡硅瞧,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進店門或辖,熙熙樓的掌柜王于貴愁眉苦臉地迎上來枣接,“玉大人,你說我怎么就攤上這事但惶。” “怎么了膀曾?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵添谊,是天一觀的道長。 經(jīng)常有香客問我碉钠,道長,這世上最難降的妖魔是什么祝高? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任污筷,我火速辦了婚禮,結(jié)果婚禮上陆蟆,老公的妹妹穿的比我還像新娘惋增。我一直安慰自己,他們只是感情好林束,可當我...
    茶點故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著缕题,像睡著了一般胖腾。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上咸作,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天性宏,我揣著相機與錄音,去河邊找鬼毫胜。 笑死,一個胖子當著我的面吹牛荐吉,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播口渔,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼缺脉,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了攻礼?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤知举,失蹤者是張志新(化名)和其女友劉穎太伊,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體锰提,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了芭概。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡踢故,死狀恐怖惹苗,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情淋纲,我是刑警寧澤院究,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站伙窃,受9級特大地震影響样漆,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜放祟,卻給世界環(huán)境...
    茶點故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一跪妥、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧确徙,春花似錦执桌、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽洲愤。三九已至顷锰,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間官紫,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工酝陈, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留毁涉,地道東北人。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓遇西,卻偏偏與公主長得像严嗜,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子茄蚯,可洞房花燭夜當晚...
    茶點故事閱讀 44,713評論 2 354

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,102評論 25 707
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理渗常,服務(wù)發(fā)現(xiàn)汗盘,斷路器,智...
    卡卡羅2017閱讀 134,654評論 18 139
  • 想一步步地抽離生活的泥沼癌椿,好不容易快出來了菱阵,一不小心,卻陷得更深了
    龔思源閱讀 163評論 0 0
  • 阿里推客新推出不到3個月啦桌,在瀏覽器插件和群發(fā)軟件推出的情況下歼培,就馬不停蹄的進入到了APP的研發(fā)階段茸塞,在經(jīng)過不斷地努...
    亂流年中閱讀 1,573評論 0 0
  • 正則表達式 純文本做限制處理.可以用來檢查一個字符串是否包含某種子串,將匹配的子串做替換或者從某個串中取出符合某個...
    BigBossZhu閱讀 498評論 0 2