前言
之前在项目中将一些日志内容保存到sd卡文件的时候,发现公司一直使用的是Util.save(String tag, String text)
形式来记录的,不同的文件名或文件目录采用tag
进行区分,文件内容为text
,写入逻辑为:1、打开文件;2、写入内容;3、关闭文件;
但这样的逻辑存在以下两个主要问题:
1、如果需要保存多条内容就执行多次save()
方法,且直接在当前线程执行,这带来的一个明显的问题就是性能问题,日志记录功能很可能在release版本也需要保留,多次执行或者在主线程进行文件操作会在一定程度上影响app运行效率;
2、不同功能模块区分日志内容仅能通过tag
,不便于扩展;
3、安全问题,比如多线程操作同一文件,可能导致文件内容混乱;
基于这些原因,在工作时间之外自己动手写了一个简洁的日志记录框架TextRecorder
,现将其开源并分享出来。
介绍
项目地址:TextRecorder
项目特点:
- 简洁;
- 扩展性强;
- 主要适配Android平台;
- 线程安全;
日志记录其实每个项目中基础但小众的功能,所以TextRecorder
并不以提供非常强大的功能为目标,但通过它的扩展性,基本可实现大部分的功能需求。另外,虽然目前是Java项目,但其主要目标使用平台还是Android平台,当然你也可以完全用于Java项目中。
扩展:当开始进行这个工具类开发的时候,目标仍然是对日志进行文本保存,但后期发现通过它的扩展性,可实现的功能并不仅仅局限如此,它可以是数据库保存、网络保存或者仅仅只在控制台打印文本内容,甚至它能处理的并不只是日志内容,任何文本都可以,基于该原因,我将该框架命名为TextRecorder
,而不是FileRecorder
或者是LogRecorder
。
使用
添加引用
首先在项目中引入框架,项目目前发布在jcenter仓库上的,
repositories {
// ...
jcenter()
}
添加项目核心依赖(必须添加):
compile 'com.github.naturs.text.recorder:text-recorder:1.5.1'
项目还提供了几个扩展依赖,主要是实现对日志进行文件保存,你也可以完全不依赖它们而是自定义实现,后续会介绍到。
两个依赖需一起添加,
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
之前,先介绍一下涉及到的几个Java类及概念:
TextLine
:它是一个抽象类,代表的是一个文本记录,它可以包含一个字符串、一个Exception或者一个段落等等,注意:一个TextLine并不一定只是一行数据,它可以同时包含上面的内容;
GenericTextLine
:TextLine
的子类,它主要处理文本、异常、JSON、XML等信息;
TextLineConverter
:将一个TextLine
转换成字符串的工具;
TextLineProcessor
:处理TextLineConverter
转换后的字符串的工具;
TextRecorder
:文本操作入口,所有的操作都通过该类进行;
TAG
:这是一个抽象但很重要的概念,在使用TextRecorder
时,会要求传入一个tag,如TextRecorder.with(tag)
,这个tag类似于Android Log框架的tag标签,用来区分不同的日志类型,这里建议 日志按模块或功能划分,使用不同的tag来区分;
我们需要对TextRecorder
进行初始化以设置一些默认的配置,否则你需要在每次操作时都指定这些配置。
在你第一次使用TextRecorder
之前初始化即可,但在Android平台下,一般会选择在Application中初始化。
初始化方式如下:
TextRecorder.init(
Scheduler,
TextLineConverter.Factory,
TextLineProcessor.Factory,
LogPrinter
);
其中参数含义如下:
-
Scheduler
,代表处理文本的线程,默认使用Schedulers.io()
; -
TextLineConverter.Factory
,TextLineConverter
的工厂对象,每次需要的时候会生产一个TextLineConverter
对象; -
TextLineProcessor.Factory
,TextLineProcessor
的工厂对象,每次需要的时候会生产一个TextLineProcessor
对象; -
LogPrinter
,打印日志的接口,可根据运行环境来配置,比如Android下使用android.util.Log
来打印日志;
使用方式
// 参数tag代表日志标签,最终会在Converter或Processor中用到
TextRecorder recorder = TextRecorder.with("module");
// 每一个append都是一条记录,可同时记录多条
recorder.append(String)
.append(Throwable)
.appendJson(JSON)
.appendXml(XML)
.appendBlankLine()
.appendDivider();
// 同步提交
recorder.commit();
// OR 异步提交
recorder.apply();
TextRecorder.appendXX()
方法是指添加一条文本,每次append都添加一条,也就是说可以同时提交多条日志,最终会按提交的顺序保存。
如果使用默认提供的文本处理方式,最终文本保存效果如下图。
分析
扩展性
首先看一下执行流程:
1、首先通过TextRecorder
将文本内容提交,提交内容只要是TextLine
的子类即可;
2、提交后会通过TextLineConverter.Factory
生成一个TextLineConverter
,将TextLine
转换成String
;
3、最后通过TextLineProcessor.Factory
生成一个TextLineProcessor
,来处理步骤2中生成的String
;
执行流程很简单,接下来具体分析一下这3个步骤所带来的扩展性。
1、TextLine
的扩展性。一开始开发的时候,想法是封装一个类,里面包含所能考虑到的所有的文本内容,类似于目前的GenericTextLine
,但是再完整的封装也不可能满足所有人的需求,所以这里改为了现在的抽象类TextLine
,用户可以自定义文本内容,任何内容都可以。
2、TextLineConverter
的扩展性。既然文本内容可以自定义,那文本最终处理方式也应该可以自定义。我们可以直接对TextLine
进行处理,比如直接将一个TextLine
保存到文件中,但是显然在保存操作这个过程中,我们需要将TextLine
转换成一个我们可以进行保存操作的对象,所以为了将职责区分开来,使用TextLineConverter
来专门处理这一转换操作。
3、TextLineProcessor
的扩展性。TextLineProcessor
是我们处理文本的最后一步,最终处理TextLine
转换后的String
。
可能有同学对步骤2中TextLineConverter
的功能有疑惑,为什么不能通过给TextLine
添加抽象方法的形式来代替TextLineConverter
呢?比如:
public abstract class TextLine {
// ...
public abstract String convert();
// ...
}
public class MyTextLine {
// ...
@Override
public String convert() {
}
// ...
}
我们是否可以将TextLineConverter
的功能放入TextLine.convert()
方法中呢?
答案当然是可以的,这样我们可以省略掉步骤2,直接在TextLineProcessor
中处理TextLine.convert()
的结果就可以了。
但是,当我们需要更换转换方式时,比如之前是TextLine -> A_B_C
,现在想改为TextLine -> A-B-C
,我们就需要控制MyTextLine
这个类了,甚至需要直接替换掉该类。从实际开发的角度来看,这一成本是比较大的,因为TextLine
可能出现在全局大部分位置,改动困难且无法一次性全局更改。
而且,从设计的角度来看,TextLine
应该仅仅关心内容,而不应该关心内容的转换方式,尽量做到职责的单一。
这一设计灵感来源于 Retrofit,大家可以去研究它的源码。
项目提供了一个自定义逻辑的转换方式,用于将普通的文本内容转换为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();
文本内容渲染后的效果如下:
线程安全
文章开头提过,如果多线程同时操作一个文件,很有可能造成文件内容混乱,所以该框架是采用的单线程存储模式,具体控制逻辑在TextLineEmitLoop
类中。该逻辑中参考自 Operator 并发原语:串行访问(serialized access)(一),emitter-loop,就不具体分析了,参考原文即可。
结语
最后说一下项目提供的默认的TextLineConverter
和TextLineProcessor
的效果。
TextLineConverter
在上图已经展示出来了,最终是 时间+调用方法+内容 的格式。
TextLineProcessor
会 按模块分目录 存储文件,即不同模块的日志文件放入不同文件夹下,模块名通过TextRecorder.with(tag)
传入,最后文件会 按天保存,每天的文件会放入单独的文件中。