对于应用程序而言,日志是非常重要的功能,通过日志,我们可以跟踪应用程序的数据状态,记录 Crash 的日志可以帮助我们分析应用程序崩溃的原因,我们甚至可以通过日志来进行性能的监控。总之,日志的好处很多,特别是对 Release 之后的线上版本进行异常的跟踪。
在平常开发时,我们通常喜欢在 Debug 模式下进行调试,通过断点,可以跟踪数据的变化。除了调试,另一种直观的方式是使用控制台输出,比如 Java 的
,.NET 的 Console.WriteLine(),Swift 的 print() 等等。在 Untiy 中,为我们提供了 Debug.Log() 方式来记录。
- system.out.println()
而对于线上的版本,上述两种调试都不行,那我们怎么来跟踪数据呢?
从日志的存储分类上来看,可以分为四类:控制台,文件系统,数据库,第三方平台
为了可以更加灵活的跟踪线上的变化,可以使用第三方的 Analysis,也可以自建日志组件。我偏向于混合使用,所以接下来,谈谈一个日志组件的基本设计理念,如下图所示:
从上图可以看出,整个入口由工厂 LogFactory 来创建 LogStrategy 子类实例,LogStrategy 是个抽象的模板类,定义了公共的处理方法,但并不知道怎样写日志(比如是写入到数据库呢还是到文件),写日志的行为交给子类去完成。
有了日志组件的设计图,接下来就是将理念落实到行动,让我们来实现它吧!
LogFactory 是一个简单工厂,封装创建 LogStrategy 对象的代码。
- public class LogFactory
- {
- public static LogFactory Instance=new LogFactory();
- private LogFactory(){}
- private readonly Dictionary<string,LogStrategy> _strategies=new Dictionary<string, LogStrategy>()
- {
- {typeof(ConsoleLogStrategy).Name,new ConsoleLogStrategy() },
- {typeof(FileLogStrategy).Name,new FileLogStrategy() },
- {typeof(DatabaseLogStrategy).Name,new DatabaseLogStrategy() }
- };
- public LogStrategy Resolve<T>() where T:LogStrategy
- {
- return _strategies[typeof(T).Name];
- }
- }
LogFactory 内部定义了一个字典,Key 为 LogStrategy 子类的类名,Value 为具体的 LogStrategy 对象。通过一个公共接口 Resolve<T> 来获取相关对象。
使用字典比 switch..case 更直观,也更加容易扩展其他选项。更重要的是,不会对公共接口 Resolve<T> 进行修改。
LogStrategy 是一个抽象类,即模板类。
它定义了一个公共的 API,即 Log。在方法 Log 中,定义了一些对内容的公共操作,因为对于日志来说,不管是记录在数据库还是文件系统,都将对内容拼接上设备类型、设备名称、操作系统、创建时间等基本信息。
同时还定义了一个抽象方法 RecordMessage,对于需要写入的类型(文件系统 Or 数据库 Or 控制台)延迟到子类决定。
- public abstract class LogStrategy
- {
- private readonly StringBuilder _messageBuilder=new StringBuilder();
- protected IContentWriter Writer { get; set; }
- /// <summary>
- /// 模板方法
- /// </summary>
- protected
- abstract
- void
- RecordMessage
- (
- string message
- )
- ;
- protected
- abstract
- void
- SetContentWriter
- (
- )
- ;
- /// <summary>
- /// 公共的API
- /// </summary>
- public void Log(string message,bool verbose=false)
- {
- if (verbose)
- {
- //公共方法
- RecordDateTime();
- RecordDeviceModel();
- RecordDeviceName();
- RecordOperatingSystem();
- }
- //抽象方法,交由子类实现
- RecordMessage(_messageBuilder.AppendLine(string.Format("Message:{0}", message)).ToString());
- }
- private void RecordDateTime()
- {
- _messageBuilder.AppendLine(string.Format("DateTime:{0}", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")));
- }
- private void RecordDeviceModel()
- {
- _messageBuilder.AppendLine(string.Format("Device Model:{0}",SystemInfo.deviceModel));
- }
- private void RecordDeviceName()
- {
- _messageBuilder.AppendLine(string.Format("Device Name:{0}", SystemInfo.deviceName));
- }
- private void RecordOperatingSystem()
- {
- _messageBuilder
- .AppendLine(string.Format("Operating System:{0}", SystemInfo.operatingSystem))
- .AppendLine();
- }
模板方法模式:在一个方法中定义算法的骨架,而将一些步骤延迟到子类。模板方法使得子类可以在不改变算法的结构下,重新定义算法中的某些步骤。
当在控制台 Debug 时,我们其实不需要设备类型,设备名称等信息,故公共接口 Log 提供了一个开关 verbose 来开启是否需要详细信息,默认为 false,即关闭状态。
继承 LogStrategy,创建自定义的日志策略
比如实现 FileLogStrategy,除了 override 了 RecordMessage 方法之外,还需要提供一个实现了 IContentWriter 接口的类,你可以直接在 RecordMessage 方法中写入日志,但可能有一些公共的操作,比如在异步线程,批量将 10 条数据写到文件或者数据库中,所以提供一个 IContentWriter 更加容易扩展。
- public class FileLogStrategy:LogStrategy
- {
- public FileLogStrategy()
- {
- SetContentWriter();
- }
- protected sealed override void SetContentWriter()
- {
- Writer = new FileContentWriter();
- }
- protected override void RecordMessage(string message)
- {
- Writer.Write(message);
- }
- }
创建一个 BaseContentWriter, 提供了公共的写入方法,比如为了提高性能,文件的 IO 并不是马上写入文件,而是批量 Flush。同样数据库记录日志也是一样,像 Unit Of Work 那样,批量向数据库写入数据,提高它的吞吐率。
根据需求使用不同的日志类
- LogFactory.Instance.Resolve < FileLogStrategy > ().Log("Welcome");
不同于服务器端的日志组件,比如 Log4J,只需要将日志写在本地文件系统中,客户端的日志相对来说复杂点,因为记录的日志是发生在用户的客户端,所以你必须要想办法把日志传到服务器,比如一些 Crash 的异常。既然要把日志发回来,在应用闪退时,必须能够持久化到本地,故我们会将日志写到文件系统或者数据库,然后在合适的时候将日志发送到服务器进行分析。当然,你也可以使用第三方的服务,比如友盟或者 Unity Analytics 来分析数据。 源代码托管在 Github 上, 点击此了解
欢迎关注我的公众号:
来源: https://juejin.im/post/5a467d12518825696f7e5382