前言
基于安卓平台的日志工具组件Timber ( https://github.com/JakeWharton/timber), 实现鸿蒙的功能化迁移和重构。代码已经开源到(https://gitee.com/isrc_ohos/timber_ohos),欢迎各位开发者提出宝贵意见。
背景
Timber_ohos是一个带有小型可扩展API的日志工具组件,它可以给开发者提供统一的API接口,来记录不同类型的日志,帮助开发者管理不同类型的log。同时,Timber_ohos是项目开发时的log开关,通过此开关控制log的打印与关闭,从而形成不同的软件版本。该组件功能丰富且使用简单高效,可以被广泛应用于软件项目开发中。
组件效果展示
1、测试界面。
如图1所示,这是一个为了测试Timber_ohos功能而简单构建的UI页面。点击“测试”按钮即可输出相应的log。
图1 测试界面UI图
2、Log打印
Timber类的静态方法调用如图2中的(a)图所示。运行项目后查看HiLog显示,可以看到实时打印出来的日志,如图2中的(b)图所示。
图2 HiLog日志打印
Sample解析
1、Tree的使用
Timber_ohos将不同的日志操作以树(Tree)的概念进行表示,种植一种树就拥有一种日志记录功能,种植多种树就拥有多种日志记录的功能,树的种类有很多,常见的树有:DebugTree、RealeseTree、FileTree、CrashReportingTree等,这些树都是继承自Tree类。
- DebugTree:对所有的日志进行记录。
- RealeseTree:只对 warn,error,wtf 信息进行记录。
- FileTree:在运行时将日志记录到文件中。
- CrashReportingTree:对应用崩溃时的信息进行记录。
Timber_ohos中默认已经种植了DebugTree,由于Timber_ohos本身是一个可扩展的框架,因此开发者想得到其他类型的Log日志时,就需要自己实现一个日志记录类 ,然后种植到Timber_ohos中即可。
2、Sample的实现
Sample部分需要添加日志记录种类,并负责整体显示布局的搭建。首先为Timber_ohos组件添加想要的任何Tree子类实例(这里使用的是DebugTree),然后设置简单的按钮监听器,当按动按钮时在鸿蒙常规HiLog中出现调试日志。下面将详细介绍组件的使用方法。
步骤1. 种树(添加Tree子类实例)。
步骤2. 创建整体的显示布局。
步骤3. 导入相关类并设置按钮监听。
步骤4. 使用Tree实例。
(1)种树(添加Tree子类实例)
本步骤是在ExampleApp类的onInitialize()方法中实现的。首先需要创建Tree子类实例,然后调用Timber的plant()方法,同时将实例作为plant()方法的参数,这个过程叫做“种树”。
- Timber.plant(new Timber.DebugTree(0x001f00));
复制(2)创建整体的显示布局 在XML文件中创建一个DirectionalLayout作为整体显示布局,宽度和高度都跟随父控件变化而调整。创建两个组件,分别是Text组件和Button组件,用于控制组件效果显示。整体显示布局如图1所示。
- <DirectionalLayout
- xmlns:ohos="http://schemas.huawei.com/res/ohos"
- ohos:height="match_parent"
- ohos:width="match_parent"
- ohos:orientation="vertical"
- ohos:padding="32vp"
- ohos:background_element="#ffffff"
- ohos:alignment="horizontal_center">
- <Text //“测试”提示
- ohos:height="match_content"
- ohos:width="match_content"
- ohos:layout_alignment="horizontal_center"
- ohos:text="Timber测试"
- ohos:text_size="35fp"/>
- <Button //控制按钮
- ohos:id="$+id:btn1"
- ohos:height="match_content"
- ohos:width="match_content"
- ohos:top_margin="35vp"
- ohos:text_size="25fp"
- ohos:background_element="#FF51A8DD"
- ohos:padding="10vp"
- ohos:text="测试"/>
- </DirectionalLayout>
(3)导入显示布局并设置按钮监听
在MainAbilitySlice中,整体显示布局也需要通过super.setUIContent()方法进行设置,才能生效并成功显示。然后给按钮设置点击事件,当用户需要使用Tree子类实例时,可通过手指进行点击。
- super.setUIContent(ResourceTable.Layout_ability_main);//设置整体显示布局
- findComponentById(ResourceTable.Id_btn1).setClickedListener(new Component.ClickedListener() {
- ...//按钮的点击事件
- }
(4)使用Tree实例
当用户需要打印调试日志的时候,调用Timber的静态方法,就会在鸿蒙常规HiLog上出现调试日志。调试日志如组件效果展示部分的图2所示。
- Timber.e ("Timber.e 测试成功!!!");
- Timber.d ("Timber.d 测试成功!!!");
- Timber.i ("Timber.i 测试成功!!!");
- Timber.w ("Timber.w 测试成功!!!");
- Timber.wtf ("Timber.wtf测试成功!!!");
Library解析
Library主要为Timber_ohos组件提供日志输出的统一接口。以Sample中种植的调试树(DebugTree)为例,当使用Timber的静态方法Timber.e时,从MainAbilitySlice到Timber.e打印log的地方可以分为5个步骤,整体调用的流程如图3所示。
图3 调用顺序图
下面我们着重介绍树(Tree类)在Library中的实现,核心算法prepareLog()内部的逻辑结构这两个方面的内容。
1.树(Tree)的实现
Tree类是一种概念形式的日志操作,具体可分为(DebugTree、ReleaseTree、FileTree等)。而在Library内部,Tree类也实现了一系列方法,以便于对森林中的各类树进行增加、删除、修改等操作。
(1)在Timber_ohos组件中维护一个森林对象(FOREST)。
森林对象由不同类型的日志树组合而成,并提供对外的接口进行日志的打印。每种类型的树都可以通过种植操作来把自己添加到森林对象中,或者通过移除操作从森林对象中删除,从而实现该类型日志记录的开启和关闭。
- private static final List<Tree> FOREST = new ArrayList<>();
(2)种树。
调用plant()方法,把Tree实例添加进FOREST里面 可以种植一棵树,也可以种植多棵树。这里以种一棵树为例。可以看到,树的种植是在plant()静态方法的synchronized 同步代码块中进行的。具体流程是先将树对象添加到 FOREST 列表中,然后将日志树保存到 forestAsArray 数组中(将树种植到森林中)。
需要注意的是,如果树为空,则抛出空指针异常的错误;如果开发者手动种植灵魂之树(TREE_OF_SOULS),Timber_ohos将会抛出非法数据异常。
- public static void plant(@NotNull Tree tree) {
- if (tree == null) {
- throw new NullPointerException("tree == null");
- }
- if (tree == TREE_OF_SOULS) {
- throw new IllegalArgumentException("Cannot plant Timber into itself.");
- }
- synchronized (FOREST) {
- FOREST.add(tree);
- forestAsArray = FOREST.toArray(new Tree[FOREST.size()]);
- }
- }
(3)移除Tree实例
同样的,树的移除也是在静态方法uproot()中的synchronized 同步代码块中进行的。如果没有该树可以移除,则Timber_ohos组件将抛出一个非法数据异常;反之,Timber_ohos组件将根据移除该树后的 FOREST列表生成 新的forestAsArray 数组。
- public static void uproot(@NotNull Tree tree) {
- synchronized (FOREST) {
- if (!FOREST.remove(tree)) {
- throw new IllegalArgumentException("Cannot uproot tree which is not planted: " + tree);
- }
- forestAsArray = FOREST.toArray(new Tree[FOREST.size()]);
- }
- }
(4)清除森林里面全部的Tree实例
移除森林里所有的Tree实例,首先使用FOREST的clear()方法清除所有的Tree实例,将会自动生成一个对应的新的Tree数组,而forestAsArray就是这个数组的引用。因此forestAsArray 数组被设置为空数组。
- public static void uprootAll() {
- synchronized (FOREST) {
- FOREST.clear();
- forestAsArray = TREE_ARRAY_EMPTY;
- }
- }
(5) 灵魂之树(TREE_OF_SOULS)
估计很多同学好奇上述TREE_OF_SOULS。代码实现中,在这里运用的是经典设计模式中的代理模式,TREE_OF_SOULS 本质上是一个代理对象,森林中所有其他普通的树对象都是被代理对象,代理对象通过 for 循环来依次调用被代理对象的同名方法,从而实现不同类型的日志记录,如下所示。
- private static final Tree TREE_OF_SOULS = new Tree() {
- @Override public void v(String message, Object... args) {
- Tree[] forest = forestAsArray;
- for (Tree tree : forest) {
- tree.v(message, args);
- }
- }
2.核心算法( prepareLog)
Timber_ohos组件的日志记录功能的核心算法在抽象类 Tree 的私有化 prepareLog()方法中,该方法接收四个参数,如图4所示:
图4 参数表
prepareLog()中首先判断了打log的条件,然后将要打印的message信息进行了处理,最后调用了抽象方法log进行日志输出。总体而言 prepareLog()算法流程如下:
(1)获取当前线程的 tag。
(2)当正常信息message不为null且信息长度为0时,这时正常信息message为null。
(3)当正常信息message和异常信息t都是 null 时,说明没有信息可以记录,方法直接返回。
(4)异常信息t通过getStackTraceString方法转换为字符串。
(5)正常信息message和可选格式化参数 args 通过formatMessage方法拼装成一个字符串。
(6)调用抽象方法 log 进行日志记录,这个方法由Tree的子类来实现。
- private void prepareLog(int priority, Throwable t, String message, Object... args) {
- //获取当前线程的 tag
- String tag = getTag();
- //当正常信息message不为null且信息长度为0时,这时正常信息message为null
- if (message != null && message.length() == 0) {
- message = null;
- }
- //当正常信息 message 和异常信息 t 都是 null 时,说明没有信息可以记录,方法直接返回
- if (message == null) {
- if (t == null) {
- return; // Swallow message if it's null and there's no throwable.
- }
- //异常信息 t 通过 getStackTraceString 方法转换为字符串
- message = getStackTraceString(t);
- } else {
- if (args != null && args.length > 0) {
- //正常信息 message 和可选格式化参数 args 通过 formatMessage 方法拼装成一个字符串
- message = formatMessage(message, args);
- }
- if (t != null) {
- message += "\n" + getStackTraceString(t);
- }
- }
- //调用抽象方法 log 进行日志记录,这个方法由 Tree 的子类来实现
- log(priority, tag, message, t);
- }