好用高扩展性的Android平台日志框架Slog

Slog

GitHub项目地址:https://github.com/shenbibo/Slog

概述

Slog是一个轻量级的Android平台的日志库,其是基于对当前开源的日志框架LoggerTimber的一个组合与扩展。具有极大的可扩展性,相比于原生Android Log,有以下新特性。

  • 支持对日志的格式化排版输出,显示效果更清晰,更方便查看。
  • 支持输出打印日志方法的栈和当前线程信息。
  • 支持打印对象,支持自定义对象解析器,默认提供对数组,集合等解析。
  • 支持使用多个自定义日志适配器,以决定日志的不同处理方式,默认提供LogcatTree适配器。
  • 支持每次打印日志之前自定义日志输出配置,从而达到不同的日志输出效果。
  • 支持自定义日志组装器,从而显示不同效果的格式化字符串。

引用方法

compile 'com.sky.slog:slog:0.4.0'

用法示例

初始化Slog

一般地在应用的Application类的onCreate()方法中调用如下类似代码。

Slog.init(new LogcatTree());

初始化时,至少需要传入一个日志适配器,当然我们也可以添加多个适配器,这个后面详解。

以上是最简单的初始化方式,我们还可以在初始化的时候对日志输出做全局配置,如下。

Slog.init(new LogcatTree())     // 初始化,设置适配器
    .showThreadInfo(true)       // 设置是否打印日志的线程的信息
    .prefixTag("test")          // 设置全局日志的前缀
    .logPriority(Slog.FULL)     // 设置日志输出级别
    .methodCount(2)             // 显示栈中方法的个数,默认从调用日志接口的方法往stack下计算
    .methodOffset(1)            // 显示从调用日志打印接口的方法往stack下计算的偏移数
    .simpleMode(false);         // 设置简单模式,无任何格式,等同于调用logcat,默认值为false.

以上方法的作用与默认值。

方法 默认值 作用
prefixTag "Android" 设置全局日志前缀。
logPriority Slog.FULL 日志的输出级别,FULL表示可以输出任何级别的日志, NONE,表示不输出任何日志。
methodCount 1 设置显示栈中方法到最终组装的日志中的个数,默认从调用日志接口的方法往stack下计算。
methodOffset 0 设置从调用日志打印接口的方法往stack下的偏移数。
showThreadInfo false 设置是否打印日志的线程的信息。
simpleMode false 设置简单模式,无任何格式,不显示线程信息,方法调用,等同于调用logcat。

注意,上表中,如果simpleModetrue,则methodCount, methodOffset, showThreadInfo将无效

另外我们还可以通过Slog.getSetting()获取Setting对象之后,在任何时候对全局Log输出配置项进行修改。

基本使用

使用以下方式初始化Slog

Slog.init(LogcatTree()).perfixTag("TestSlog").showThreadInfo(true);

打印普通日志

// 打印普通日志
Slog.d("sky debug");
Slog.i("sky info");

// 打印格式化字符串
Slog.d("this is a format string log, str1 = %s, int value2 = %d, boolean3 = %b", "string1", 2, true);

normal_log

打印错误日志

// 打印throwable
Slog.e(new Throwable());
Slog.w(new RuntimeException(), "test log with warn priority = %d", Slog.WARN);
error_warn_log

打印jsonxml

jsonxml字符串采用的日志级别都是Debug的。

打印json字符串

String jsonEmpty = "";
String jsonEmpty2 = "{}";
String jsonNull = null;
Slog.json(jsonEmpty);
Slog.json(jsonEmpty2);
Slog.json(jsonNull);

String json = "{'xyy1':[{'test1':'test1'},{'test2':'test2'}],'xyy2':{'test3':'test3','test4':'test4'}}";

Slog.json(json);

String jsonArray =
        "{ 'employees': [ {'firstName':'John', 'lastName':'Doe'}, {'firstName':'Anna', 'lastName':'Smith'}, "
                + "{'firstName':'Peter', 'lastName':'Jones'}]}";
Slog.json(jsonArray);
json1

json2

打印xml字符串

String androidXml = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" +
        "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n" +
        "    package=\"com.sky.tools\" >\n" +
        "\n" +
        "    <application\n" +
        "        android:name=\".application.MainApplication\"\n" +
        "        android:allowBackup=\"true\"\n" +
        "        android:icon=\"@mipmap/ic_launcher\"\n" +
        "        android:label=\"@string/app_name\"\n" +
        "        android:roundIcon=\"@mipmap/ic_launcher_round\"\n" +
        "        android:supportsRtl=\"true\"\n" +
        "        android:theme=\"@style/AppTheme\" >\n" +
        "        <activity android:name=\".main.MainActivity\" >\n" +
        "            <intent-filter>\n" +
        "                <action android:name=\"android.intent.action.MAIN\" />\n" +
        "\n" +
        "                <category android:name=\"android.intent.category.LAUNCHER\" />\n" +
        "            </intent-filter>\n" +
        "        </activity>\n" +
        "    </application>\n" +
        "\n" +
        "</manifest>";

Slog.xml(androidXml);
xml_log

打印对象

Slog 支持对对象的打印,其原理是给每个不同的对象类型添加对应的对象解析器,默认提供对数组,集合等解析,支持自定义对象解析器。

打印null对象

Slog.i(null);
Slog.i("");
null_object

打印数组对象

// 全局多维对象数组
private static Object[] objectsArray = new Object[]{
        new boolean[]{false, true, true, false},
        new String[][]{
                new String[]{"22", "23", "24"},
                new String[]{"123", "456", "789"}},
        new int[][][]{new int[][]{
                new int[]{666, 555, 444},
                new int[]{111, 222, 333, 444}},
                new int[][]{
                        new int[]{1, 2, 3, 4, 5, 6},
                        new int[]{7878, 6565, 84155, 7542, 0}}}};


// 打印对象数组
Object[] objectArray = new Object[1024];
for (int i = 0; i < objectArray.length; i++) {
        objectArray[i] = i;
}

Slog.i(objectArray);

// 打印String
String[] stringArray = new String[1024];
for (int i = 1024; i < stringArray.length + 1024; i++) {
        stringArray[i - 1024] = "is " + i;
}
Slog.i(stringArray);

// 打印int数组
int[] intArray = new int[1024];
for (int i = 2048; i < intArray.length + 2048; i++) {
        intArray[i - 2048] = i;
}
Slog.i(intArray);

// 打印多维数组
Slog.i(objectsArray);
array_log

打印自定义对象解析器的对象

有如下一个Student类。

public class Student {
    private int number;
    private int age;
    private String name;
    private boolean isBoy;

    public Student(int number, int age, String name, boolean isBoy){
        this.number = number;
        this.age = age;
        this.name = name;
        this.isBoy = isBoy;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public boolean isBoy() {
        return isBoy;
    }

    public int getNumber() {
        return number;
    }
}

自定义一个Student对象解析器,所有的对象解析器都必须要实现Parser接口。

public class StudentParser implements Parser<Student> {
    @Override
    public Class<Student> getParseType() {
        return Student.class;
    }

    @Override
    public String parseToString(Student student) {
        return student.getName() + " is a " + student.getAge() + " years old " + (student.isBoy() ? "boy" : "girl");
    }
}

上面的parseToString将在解析对象时调用,由它返回被解析的对象最终要表示的字符串内容。

// 没有添加解析器之前
Student s = new Student(12345, 54, "sky", true);
Slog.d(s);

// 添加解析器之后
Slog.addObjectParser(new StudentParser());
Slog.d(s);
object_parser

打印Collection对象

这里以Map举例。

// empty map
Map<Integer, Student> map = new HashMap<>();
Slog.d(map);

// int map
Map<Integer, Integer> intMap = new ConcurrentHashMap<>();
intMap.put(1, 2);
intMap.put(1543, 2745867);
intMap.put(17687, 27678);
intMap.put(76781, 27678);
intMap.put(1786768, 26786);
Slog.d(intMap);

// Object Map
Map<Object, String> objectStringMap = new LinkedHashMap<>();
objectStringMap.put(new Object(), "11223786");
objectStringMap.put(new Object(), "475775486");
objectStringMap.put(new Object(), "7856874757");
Slog.d(objectStringMap);

// student Map
map.put(12345, new Student(12345, 54, "sky", true));
map.put(123456, new Student(123456, 56, "sky2", true));
map.put(1234567, new Student(1234567, 15, "sky3", true));
map.put(12345678, new Student(12345678, 25, "sky4", true));
map.put(1234555, new Student(1234555, 35, "sky5", true));
map.put(12345444, new Student(12345444, 45, "sky6", true));
Slog.d(map);

// map itself
Map map1 = new Hashtable<>();
//noinspection CollectionAddedToSelf,unchecked
map1.put(map1, map1);
Slog.d(map1);
map_log

指定临时的配置项打印日志

前面的示例都是不修改输出配置项时打印的日志,但是在很多时候我们可能根据不同的场景,需要采用不同的日志输出配置。

除了前面说的使用Slog.getSetting()方法获取Setting对象之后,修改全局输出配置外,Slog框架还支持指定临时输出配置,只在下次当前线程打印日志时生效一次的方式更改日志输出效果。

具体包括以下几个方法。

Slog.t()        // 指定下一次当前线程打印日志的tag,最终输出的日志的Tag组合为: prefixTag-tag
Slog.th()       // 指定下一次当前线程打印日志是否显示线程信息
Slog.m()        // 指定下一次当前线程打印日志显示的调用stack中方法的数目
Slog.o()        // 指定下一次当前线程打印日志显示stack方法时的偏移值
Slog.s()        // 指定下一次当前线程打印日志是否采用简单模式输出

上面的配置方法可以单个使用也可以组合使用。

单个使用

Slog.t("custom22").i("set tag to custom");
Slog.th(false).i("hide the threadInfo");
Slog.m(0).i("test 0 method count print, so hide track");
Slog.m(3).i("test three method count println");
Slog.o(1).i("method offset 1");
Slog.s(true).i("set to simple mode");
temp_setting_log

联合使用

Slog.s(false).t("fiveSetting").th(true).m(5).o(2).i("this time set five temp setting for test");
five_temp_setting_log

添加日志适配器

Slog框架目前只提供一个实现的日志适配器LogcatTree,本框架支持自定义日志适配器,所有的日志适配器都必须要继承Tree抽象类或者其子类,为了保证足够的扩展性,我们在Tree的接口中除了可以接收到封装处理好的日志之外,也可以对原始的日志数据进行处理。

自定义一个FileTree

public class FileTree extends Tree {

    // ... 还有其他的方法也可以根据需要复写

    // 处理对象类型的日志,注意该接口方法,也可以根据原始的`originalObject`参数进行自定义处理
    @Override
    protected void prepareObjectLog(int priority, String tag, String[] compoundMessages, @Nullable Object originalObject) {
        super.prepareObjectLog(priority, tag, compoundMessages, originalObject);
    }

    // 处理String类型的日志,注意该接口方法,也可以根据原始的`originalMessage`参数进行自定义处理
    @Override
    protected void prepareStringLog(int priority, String tag, Throwable t, String[] compoundMessages, @Nullable String originalMessages, @Nullable Object... args) {
        super.prepareStringLog(priority, tag, t, compoundMessages, originalMessages, args);
    }

    // 该方法为必须要实现的父类抽象方法
    @Override
    protected void log(int priority, String tag, String message) {
        // ... 省略代码将日志保存到文件中
    }
}

将其添加到日志适配器列表中,以后就可以正常使用了。

Slog.plantTree(new FileTree());

注意: 在每个日志适配器中,我们可以根据需要最终自己确定将组装之后的日志或者原始日志如何处理。

自定义日志组装器

通过继承LogAssembler抽象类,我们可以实现自己定义的日志组装器。

public class SimpleLogAssembler extends LogAssembler {

    @Override
    protected void onFormatModeLogMethodStackTrace(List<String> compoundMessagesList, int methodCount, int stackOffset,
            StackTraceElement[] trace) {
        for (int i = methodCount; i > 0; i--) {
            int stackIndex = i + stackOffset;
            if (stackIndex >= trace.length) {
                continue;
            }

            //noinspection StringBufferReplaceableByString
            StringBuilder builder = new StringBuilder();
            builder.append(getSimpleClassName(trace[stackIndex].getClassName()))
                   .append(".")
                   .append(trace[stackIndex].getMethodName())
                   .append("(")
                   .append(trace[stackIndex].getFileName())
                   .append(":")
                   .append(trace[stackIndex].getLineNumber())
                   .append(")");

            compoundMessagesList.add(builder.toString());
        }
    }

    @Override
    protected void onFormatModeLogThreadInfo(List<String> compoundMessagesList, Thread curThread) {
        compoundMessagesList.add(Helper.createThreadInfo(curThread));
    }

    @Override
    protected void onFormatModeLogContent(List<String> compoundMessagesList, Throwable t, Object originalObject, Object[] args) {
        String[] compoundMessages = compoundMessage(t, originalObject, args);
        for (String compoundMessage : compoundMessages) {
            String[] splitMessages = compoundMessage.split(LINE_SEPARATOR);
            Collections.addAll(compoundMessagesList, splitMessages);
        }
    }
}

以上三个方法是子类必须要实现的,还有其他的方法子类可以选择性的复写。

以下是调用实例。

Slog.setLogAssembler(new SimpleLogAssembler());
Slog.i("set log assembler to simple");
Slog.m(10).i("simple log assembler set methodCount to 10");

// 设置为null,将使用默认的日志组装器
Slog.setLogAssembler(null);
Slog.i("Slog.setLogAssembler(null), so turn to default log assembler");
set_log_assembler

另外,我们也可以在初始化时调用Slog.init(Tree, LogAssembler)方法时进行指定日志组装器。

SlogTest测试用例集

更多的用法可以参考slog/src/androidTest/java/com.sky.slog/SlogTest.java

结构概述

Slog打印日志的基本流程可以归纳为以下几个步骤。

  • 打印日志,调用对应的Slog接口。
  • 根据当前日志全局配置,判断是否对需要输出日志(当前是只判断允许输出的日志级别Priority)。
  • 结合全局日志配置和单次指定的日志配置(单次优先级高于全局),对原始日志进行组装。
  • 将组装好的日志和原始日志数据通过日志分发器分发到各个日志适配器。
  • 每个日志适配器最终根据自身实现对日志进行处理。

简单的流程图。

slog_流程图
slog_流程图

简单的类图。

Slog结构类图
Slog结构类图
  • LogAssembler,日志组装器的抽象类,负责对日志进行组装,调用分发器将组装好的日志进行分发。
  • LogDispatcher,日志分发器接口。
  • LogController,分别实现了TreeMangerLogDispatcher接口,通过其分发日志功能,将日志分发到其管理的日志适配器中。

致谢

本库最终形成,分别参考了以下三个库,本库的设计借鉴了它们的设计思想与代码实现,十分感谢。

Logger : https://github.com/orhanobut/logger

Timber : https://github.com/JakeWharton/timber

ViseLog : https://github.com/xiaoyaoyou1212/ViseLog

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

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,517评论 18 139
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,140评论 25 707
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,680评论 6 342
  • 晓风拂柳絮,化声芦苇中。旭日当空照,留影松柏下。汪月今将现,此刻祝中秋。——by Aman正义 招财进宝,不负年华...
    Lii阅读 498评论 0 0
  • 悟在蓝莲花下 忘记了忧伤带来的疼痛,遗忘了痛苦带来的清晰的伤疤,丢掉了沉重包袱这个累赘,把卑微放在孤寂的门口,孤单...
    孙子曰阅读 446评论 4 5