前言
之前在項(xiàng)目中將一些日志內(nèi)容保存到sd卡文件的時(shí)候,發(fā)現(xiàn)公司一直使用的是Util.save(String tag, String text)形式來(lái)記錄的,不同的文件名或文件目錄采用tag進(jìn)行區(qū)分,文件內(nèi)容為text,寫入邏輯為:1、打開(kāi)文件;2、寫入內(nèi)容;3、關(guān)閉文件;
但這樣的邏輯存在以下兩個(gè)主要問(wèn)題:
1、如果需要保存多條內(nèi)容就執(zhí)行多次save()方法,且直接在當(dāng)前線程執(zhí)行,這帶來(lái)的一個(gè)明顯的問(wèn)題就是性能問(wèn)題,日志記錄功能很可能在release版本也需要保留,多次執(zhí)行或者在主線程進(jìn)行文件操作會(huì)在一定程度上影響app運(yùn)行效率;
2、不同功能模塊區(qū)分日志內(nèi)容僅能通過(guò)tag,不便于擴(kuò)展;
3、安全問(wèn)題,比如多線程操作同一文件,可能導(dǎo)致文件內(nèi)容混亂;
基于這些原因,在工作時(shí)間之外自己動(dòng)手寫了一個(gè)簡(jiǎn)潔的日志記錄框架TextRecorder,現(xiàn)將其開(kāi)源并分享出來(lái)。
介紹
項(xiàng)目地址:TextRecorder
項(xiàng)目特點(diǎn):
- 簡(jiǎn)潔;
- 擴(kuò)展性強(qiáng);
- 主要適配Android平臺(tái);
- 線程安全;
日志記錄其實(shí)每個(gè)項(xiàng)目中基礎(chǔ)但小眾的功能,所以TextRecorder并不以提供非常強(qiáng)大的功能為目標(biāo),但通過(guò)它的擴(kuò)展性,基本可實(shí)現(xiàn)大部分的功能需求。另外,雖然目前是Java項(xiàng)目,但其主要目標(biāo)使用平臺(tái)還是Android平臺(tái),當(dāng)然你也可以完全用于Java項(xiàng)目中。
擴(kuò)展:當(dāng)開(kāi)始進(jìn)行這個(gè)工具類開(kāi)發(fā)的時(shí)候,目標(biāo)仍然是對(duì)日志進(jìn)行文本保存,但后期發(fā)現(xiàn)通過(guò)它的擴(kuò)展性,可實(shí)現(xiàn)的功能并不僅僅局限如此,它可以是數(shù)據(jù)庫(kù)保存、網(wǎng)絡(luò)保存或者僅僅只在控制臺(tái)打印文本內(nèi)容,甚至它能處理的并不只是日志內(nèi)容,任何文本都可以,基于該原因,我將該框架命名為TextRecorder,而不是FileRecorder或者是LogRecorder。
使用
添加引用
首先在項(xiàng)目中引入框架,項(xiàng)目目前發(fā)布在jcenter倉(cāng)庫(kù)上的,
repositories {
// ...
jcenter()
}
添加項(xiàng)目核心依賴(必須添加):
compile 'com.github.naturs.text.recorder:text-recorder:1.5.1'
項(xiàng)目還提供了幾個(gè)擴(kuò)展依賴,主要是實(shí)現(xiàn)對(duì)日志進(jìn)行文件保存,你也可以完全不依賴它們而是自定義實(shí)現(xiàn),后續(xù)會(huì)介紹到。
兩個(gè)依賴需一起添加,
compile 'com.github.naturs.text.recorder:text-recorder-converter:1.5.1'
compile 'com.github.naturs.text.recorder:text-recorder-processor:1.5.1'
初始化
在正式使用TextRecorder之前,先介紹一下涉及到的幾個(gè)Java類及概念:
TextLine:它是一個(gè)抽象類,代表的是一個(gè)文本記錄,它可以包含一個(gè)字符串、一個(gè)Exception或者一個(gè)段落等等,注意:一個(gè)TextLine并不一定只是一行數(shù)據(jù),它可以同時(shí)包含上面的內(nèi)容;
GenericTextLine:TextLine的子類,它主要處理文本、異常、JSON、XML等信息;
TextLineConverter:將一個(gè)TextLine轉(zhuǎn)換成字符串的工具;
TextLineProcessor:處理TextLineConverter轉(zhuǎn)換后的字符串的工具;
TextRecorder:文本操作入口,所有的操作都通過(guò)該類進(jìn)行;
TAG:這是一個(gè)抽象但很重要的概念,在使用TextRecorder時(shí),會(huì)要求傳入一個(gè)tag,如TextRecorder.with(tag),這個(gè)tag類似于Android Log框架的tag標(biāo)簽,用來(lái)區(qū)分不同的日志類型,這里建議 日志按模塊或功能劃分,使用不同的tag來(lái)區(qū)分;
我們需要對(duì)TextRecorder進(jìn)行初始化以設(shè)置一些默認(rèn)的配置,否則你需要在每次操作時(shí)都指定這些配置。
在你第一次使用TextRecorder之前初始化即可,但在Android平臺(tái)下,一般會(huì)選擇在Application中初始化。
初始化方式如下:
TextRecorder.init(
Scheduler,
TextLineConverter.Factory,
TextLineProcessor.Factory,
LogPrinter
);
其中參數(shù)含義如下:
-
Scheduler,代表處理文本的線程,默認(rèn)使用Schedulers.io(); -
TextLineConverter.Factory,TextLineConverter的工廠對(duì)象,每次需要的時(shí)候會(huì)生產(chǎn)一個(gè)TextLineConverter對(duì)象; -
TextLineProcessor.Factory,TextLineProcessor的工廠對(duì)象,每次需要的時(shí)候會(huì)生產(chǎn)一個(gè)TextLineProcessor對(duì)象; -
LogPrinter,打印日志的接口,可根據(jù)運(yùn)行環(huán)境來(lái)配置,比如Android下使用android.util.Log來(lái)打印日志;
使用方式
// 參數(shù)tag代表日志標(biāo)簽,最終會(huì)在Converter或Processor中用到
TextRecorder recorder = TextRecorder.with("module");
// 每一個(gè)append都是一條記錄,可同時(shí)記錄多條
recorder.append(String)
.append(Throwable)
.appendJson(JSON)
.appendXml(XML)
.appendBlankLine()
.appendDivider();
// 同步提交
recorder.commit();
// OR 異步提交
recorder.apply();
TextRecorder.appendXX()方法是指添加一條文本,每次append都添加一條,也就是說(shuō)可以同時(shí)提交多條日志,最終會(huì)按提交的順序保存。
如果使用默認(rèn)提供的文本處理方式,最終文本保存效果如下圖。

分析
擴(kuò)展性
首先看一下執(zhí)行流程:
1、首先通過(guò)TextRecorder將文本內(nèi)容提交,提交內(nèi)容只要是TextLine的子類即可;
2、提交后會(huì)通過(guò)TextLineConverter.Factory生成一個(gè)TextLineConverter,將TextLine轉(zhuǎn)換成String;
3、最后通過(guò)TextLineProcessor.Factory生成一個(gè)TextLineProcessor,來(lái)處理步驟2中生成的String;
執(zhí)行流程很簡(jiǎn)單,接下來(lái)具體分析一下這3個(gè)步驟所帶來(lái)的擴(kuò)展性。
1、TextLine的擴(kuò)展性。一開(kāi)始開(kāi)發(fā)的時(shí)候,想法是封裝一個(gè)類,里面包含所能考慮到的所有的文本內(nèi)容,類似于目前的GenericTextLine,但是再完整的封裝也不可能滿足所有人的需求,所以這里改為了現(xiàn)在的抽象類TextLine,用戶可以自定義文本內(nèi)容,任何內(nèi)容都可以。
2、TextLineConverter的擴(kuò)展性。既然文本內(nèi)容可以自定義,那文本最終處理方式也應(yīng)該可以自定義。我們可以直接對(duì)TextLine進(jìn)行處理,比如直接將一個(gè)TextLine保存到文件中,但是顯然在保存操作這個(gè)過(guò)程中,我們需要將TextLine轉(zhuǎn)換成一個(gè)我們可以進(jìn)行保存操作的對(duì)象,所以為了將職責(zé)區(qū)分開(kāi)來(lái),使用TextLineConverter來(lái)專門處理這一轉(zhuǎn)換操作。
3、TextLineProcessor的擴(kuò)展性。TextLineProcessor是我們處理文本的最后一步,最終處理TextLine轉(zhuǎn)換后的String。
可能有同學(xué)對(duì)步驟2中TextLineConverter的功能有疑惑,為什么不能通過(guò)給TextLine添加抽象方法的形式來(lái)代替TextLineConverter呢?比如:
public abstract class TextLine {
// ...
public abstract String convert();
// ...
}
public class MyTextLine {
// ...
@Override
public String convert() {
}
// ...
}
我們是否可以將TextLineConverter的功能放入TextLine.convert()方法中呢?
答案當(dāng)然是可以的,這樣我們可以省略掉步驟2,直接在TextLineProcessor中處理TextLine.convert()的結(jié)果就可以了。
但是,當(dāng)我們需要更換轉(zhuǎn)換方式時(shí),比如之前是TextLine -> A_B_C,現(xiàn)在想改為TextLine -> A-B-C,我們就需要控制MyTextLine這個(gè)類了,甚至需要直接替換掉該類。從實(shí)際開(kāi)發(fā)的角度來(lái)看,這一成本是比較大的,因?yàn)?code>TextLine可能出現(xiàn)在全局大部分位置,改動(dòng)困難且無(wú)法一次性全局更改。
而且,從設(shè)計(jì)的角度來(lái)看,TextLine應(yīng)該僅僅關(guān)心內(nèi)容,而不應(yīng)該關(guān)心內(nèi)容的轉(zhuǎn)換方式,盡量做到職責(zé)的單一。
這一設(shè)計(jì)靈感來(lái)源于 Retrofit,大家可以去研究它的源碼。
項(xiàng)目提供了一個(gè)自定義邏輯的轉(zhuǎn)換方式,用于將普通的文本內(nèi)容轉(zhuǎn)換為markdown形式的文本,添加如下依賴即可:
compile 'com.github.naturs.text.recorder:text-recorder-markdown:1.5.1'
使用方式如下:
TextRecorder recorder = TextRecorder.with("markdown");
MarkdownTextLine textLine = MarkdownTextLine.with().text("I'm a text.");
recorder.append(textLine);
RuntimeException exception = new RuntimeException("mock an exception.");
textLine = MarkdownTextLine.with().throwable("I'm an exception.", exception);
recorder.append(textLine);
textLine = MarkdownTextLine.with().divider();
recorder.append(textLine);
textLine = MarkdownTextLine.with().json("I'm a json.", Sample.JSON);
recorder.append(textLine);
textLine = MarkdownTextLine.with().xml("I'm a xml.", Sample.XML);
recorder.append(textLine);
recorder.apply();
文本內(nèi)容渲染后的效果如下:

線程安全
文章開(kāi)頭提過(guò),如果多線程同時(shí)操作一個(gè)文件,很有可能造成文件內(nèi)容混亂,所以該框架是采用的單線程存儲(chǔ)模式,具體控制邏輯在TextLineEmitLoop類中。該邏輯中參考自 Operator 并發(fā)原語(yǔ):串行訪問(wèn)(serialized access)(一),emitter-loop,就不具體分析了,參考原文即可。
結(jié)語(yǔ)
最后說(shuō)一下項(xiàng)目提供的默認(rèn)的TextLineConverter和TextLineProcessor的效果。
TextLineConverter在上圖已經(jīng)展示出來(lái)了,最終是 時(shí)間+調(diào)用方法+內(nèi)容 的格式。
TextLineProcessor會(huì) 按模塊分目錄 存儲(chǔ)文件,即不同模塊的日志文件放入不同文件夾下,模塊名通過(guò)TextRecorder.with(tag)傳入,最后文件會(huì) 按天保存,每天的文件會(huì)放入單獨(dú)的文件中。