鴻蒙開源第三方組件—日志工具組件Timber_ohos
51CTO和華為官方合作共建的鴻蒙技術(shù)社區(qū)
前言
基于安卓平臺(tái)的日志工具組件Timber ( https://github.com/JakeWharton/timber), 實(shí)現(xiàn)鴻蒙的功能化遷移和重構(gòu)。代碼已經(jīng)開源到(https://gitee.com/isrc_ohos/timber_ohos),歡迎各位開發(fā)者提出寶貴意見。
背景
Timber_ohos是一個(gè)帶有小型可擴(kuò)展API的日志工具組件,它可以給開發(fā)者提供統(tǒng)一的API接口,來記錄不同類型的日志,幫助開發(fā)者管理不同類型的log。同時(shí),Timber_ohos是項(xiàng)目開發(fā)時(shí)的log開關(guān),通過此開關(guān)控制log的打印與關(guān)閉,從而形成不同的軟件版本。該組件功能豐富且使用簡單高效,可以被廣泛應(yīng)用于軟件項(xiàng)目開發(fā)中。
組件效果展示
1、測(cè)試界面。
如圖1所示,這是一個(gè)為了測(cè)試Timber_ohos功能而簡單構(gòu)建的UI頁面。點(diǎn)擊“測(cè)試”按鈕即可輸出相應(yīng)的log。

圖1 測(cè)試界面UI圖
2、Log打印
Timber類的靜態(tài)方法調(diào)用如圖2中的(a)圖所示。運(yùn)行項(xiàng)目后查看HiLog顯示,可以看到實(shí)時(shí)打印出來的日志,如圖2中的(b)圖所示。


圖2 HiLog日志打印
Sample解析
1、Tree的使用
Timber_ohos將不同的日志操作以樹(Tree)的概念進(jìn)行表示,種植一種樹就擁有一種日志記錄功能,種植多種樹就擁有多種日志記錄的功能,樹的種類有很多,常見的樹有:DebugTree、RealeseTree、FileTree、CrashReportingTree等,這些樹都是繼承自Tree類。
- DebugTree:對(duì)所有的日志進(jìn)行記錄。
- RealeseTree:只對(duì) warn,error,wtf 信息進(jìn)行記錄。
- FileTree:在運(yùn)行時(shí)將日志記錄到文件中。
- CrashReportingTree:對(duì)應(yīng)用崩潰時(shí)的信息進(jìn)行記錄。
Timber_ohos中默認(rèn)已經(jīng)種植了DebugTree,由于Timber_ohos本身是一個(gè)可擴(kuò)展的框架,因此開發(fā)者想得到其他類型的Log日志時(shí),就需要自己實(shí)現(xiàn)一個(gè)日志記錄類 ,然后種植到Timber_ohos中即可。
2、Sample的實(shí)現(xiàn)
Sample部分需要添加日志記錄種類,并負(fù)責(zé)整體顯示布局的搭建。首先為Timber_ohos組件添加想要的任何Tree子類實(shí)例(這里使用的是DebugTree),然后設(shè)置簡單的按鈕監(jiān)聽器,當(dāng)按動(dòng)按鈕時(shí)在鴻蒙常規(guī)HiLog中出現(xiàn)調(diào)試日志。下面將詳細(xì)介紹組件的使用方法。
步驟1. 種樹(添加Tree子類實(shí)例)。
步驟2. 創(chuàng)建整體的顯示布局。
步驟3. 導(dǎo)入相關(guān)類并設(shè)置按鈕監(jiān)聽。
步驟4. 使用Tree實(shí)例。
(1)種樹(添加Tree子類實(shí)例)
本步驟是在ExampleApp類的onInitialize()方法中實(shí)現(xiàn)的。首先需要?jiǎng)?chuàng)建Tree子類實(shí)例,然后調(diào)用Timber的plant()方法,同時(shí)將實(shí)例作為plant()方法的參數(shù),這個(gè)過程叫做“種樹”。
- Timber.plant(new Timber.DebugTree(0x001f00));
復(fù)制(2)創(chuàng)建整體的顯示布局 在XML文件中創(chuàng)建一個(gè)DirectionalLayout作為整體顯示布局,寬度和高度都跟隨父控件變化而調(diào)整。創(chuàng)建兩個(gè)組件,分別是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 //“測(cè)試”提示
- ohos:height="match_content"
- ohos:width="match_content"
- ohos:layout_alignment="horizontal_center"
- ohos:text="Timber測(cè)試"
- 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="測(cè)試"/>
- </DirectionalLayout>
(3)導(dǎo)入顯示布局并設(shè)置按鈕監(jiān)聽
在MainAbilitySlice中,整體顯示布局也需要通過super.setUIContent()方法進(jìn)行設(shè)置,才能生效并成功顯示。然后給按鈕設(shè)置點(diǎn)擊事件,當(dāng)用戶需要使用Tree子類實(shí)例時(shí),可通過手指進(jìn)行點(diǎn)擊。
- super.setUIContent(ResourceTable.Layout_ability_main);//設(shè)置整體顯示布局
- findComponentById(ResourceTable.Id_btn1).setClickedListener(new Component.ClickedListener() {
- ...//按鈕的點(diǎn)擊事件
- }
(4)使用Tree實(shí)例
當(dāng)用戶需要打印調(diào)試日志的時(shí)候,調(diào)用Timber的靜態(tài)方法,就會(huì)在鴻蒙常規(guī)HiLog上出現(xiàn)調(diào)試日志。調(diào)試日志如組件效果展示部分的圖2所示。
- Timber.e ("Timber.e 測(cè)試成功!??!");
- Timber.d ("Timber.d 測(cè)試成功?。。?quot;);
- Timber.i ("Timber.i 測(cè)試成功?。。?quot;);
- Timber.w ("Timber.w 測(cè)試成功?。。?quot;);
- Timber.wtf ("Timber.wtf測(cè)試成功!??!");
Library解析
Library主要為Timber_ohos組件提供日志輸出的統(tǒng)一接口。以Sample中種植的調(diào)試樹(DebugTree)為例,當(dāng)使用Timber的靜態(tài)方法Timber.e時(shí),從MainAbilitySlice到Timber.e打印log的地方可以分為5個(gè)步驟,整體調(diào)用的流程如圖3所示。

圖3 調(diào)用順序圖
下面我們著重介紹樹(Tree類)在Library中的實(shí)現(xiàn),核心算法prepareLog()內(nèi)部的邏輯結(jié)構(gòu)這兩個(gè)方面的內(nèi)容。
1.樹(Tree)的實(shí)現(xiàn)
Tree類是一種概念形式的日志操作,具體可分為(DebugTree、ReleaseTree、FileTree等)。而在Library內(nèi)部,Tree類也實(shí)現(xiàn)了一系列方法,以便于對(duì)森林中的各類樹進(jìn)行增加、刪除、修改等操作。
(1)在Timber_ohos組件中維護(hù)一個(gè)森林對(duì)象(FOREST)。
森林對(duì)象由不同類型的日志樹組合而成,并提供對(duì)外的接口進(jìn)行日志的打印。每種類型的樹都可以通過種植操作來把自己添加到森林對(duì)象中,或者通過移除操作從森林對(duì)象中刪除,從而實(shí)現(xiàn)該類型日志記錄的開啟和關(guān)閉。
- private static final List<Tree> FOREST = new ArrayList<>();
(2)種樹。
調(diào)用plant()方法,把Tree實(shí)例添加進(jìn)FOREST里面 可以種植一棵樹,也可以種植多棵樹。這里以種一棵樹為例。可以看到,樹的種植是在plant()靜態(tài)方法的synchronized 同步代碼塊中進(jìn)行的。具體流程是先將樹對(duì)象添加到 FOREST 列表中,然后將日志樹保存到 forestAsArray 數(shù)組中(將樹種植到森林中)。
需要注意的是,如果樹為空,則拋出空指針異常的錯(cuò)誤;如果開發(fā)者手動(dòng)種植靈魂之樹(TREE_OF_SOULS),Timber_ohos將會(huì)拋出非法數(shù)據(jù)異常。
- 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實(shí)例
同樣的,樹的移除也是在靜態(tài)方法uproot()中的synchronized 同步代碼塊中進(jìn)行的。如果沒有該樹可以移除,則Timber_ohos組件將拋出一個(gè)非法數(shù)據(jù)異常;反之,Timber_ohos組件將根據(jù)移除該樹后的 FOREST列表生成 新的forestAsArray 數(shù)組。
- 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實(shí)例
移除森林里所有的Tree實(shí)例,首先使用FOREST的clear()方法清除所有的Tree實(shí)例,將會(huì)自動(dòng)生成一個(gè)對(duì)應(yīng)的新的Tree數(shù)組,而forestAsArray就是這個(gè)數(shù)組的引用。因此forestAsArray 數(shù)組被設(shè)置為空數(shù)組。
- public static void uprootAll() {
- synchronized (FOREST) {
- FOREST.clear();
- forestAsArray = TREE_ARRAY_EMPTY;
- }
- }
(5) 靈魂之樹(TREE_OF_SOULS)
估計(jì)很多同學(xué)好奇上述TREE_OF_SOULS。代碼實(shí)現(xiàn)中,在這里運(yùn)用的是經(jīng)典設(shè)計(jì)模式中的代理模式,TREE_OF_SOULS 本質(zhì)上是一個(gè)代理對(duì)象,森林中所有其他普通的樹對(duì)象都是被代理對(duì)象,代理對(duì)象通過 for 循環(huán)來依次調(diào)用被代理對(duì)象的同名方法,從而實(shí)現(xiàn)不同類型的日志記錄,如下所示。
- 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()方法中,該方法接收四個(gè)參數(shù),如圖4所示:

圖4 參數(shù)表
prepareLog()中首先判斷了打log的條件,然后將要打印的message信息進(jìn)行了處理,最后調(diào)用了抽象方法log進(jìn)行日志輸出??傮w而言 prepareLog()算法流程如下:
(1)獲取當(dāng)前線程的 tag。
(2)當(dāng)正常信息message不為null且信息長度為0時(shí),這時(shí)正常信息message為null。
(3)當(dāng)正常信息message和異常信息t都是 null 時(shí),說明沒有信息可以記錄,方法直接返回。
(4)異常信息t通過getStackTraceString方法轉(zhuǎn)換為字符串。
(5)正常信息message和可選格式化參數(shù) args 通過formatMessage方法拼裝成一個(gè)字符串。
(6)調(diào)用抽象方法 log 進(jìn)行日志記錄,這個(gè)方法由Tree的子類來實(shí)現(xiàn)。
- private void prepareLog(int priority, Throwable t, String message, Object... args) {
- //獲取當(dāng)前線程的 tag
- String tag = getTag();
- //當(dāng)正常信息message不為null且信息長度為0時(shí),這時(shí)正常信息message為null
- if (message != null && message.length() == 0) {
- message = null;
- }
- //當(dāng)正常信息 message 和異常信息 t 都是 null 時(shí),說明沒有信息可以記錄,方法直接返回
- if (message == null) {
- if (t == null) {
- return; // Swallow message if it's null and there's no throwable.
- }
- //異常信息 t 通過 getStackTraceString 方法轉(zhuǎn)換為字符串
- message = getStackTraceString(t);
- } else {
- if (args != null && args.length > 0) {
- //正常信息 message 和可選格式化參數(shù) args 通過 formatMessage 方法拼裝成一個(gè)字符串
- message = formatMessage(message, args);
- }
- if (t != null) {
- message += "\n" + getStackTraceString(t);
- }
- }
- //調(diào)用抽象方法 log 進(jìn)行日志記錄,這個(gè)方法由 Tree 的子類來實(shí)現(xiàn)
- log(priority, tag, message, t);
- }
51CTO和華為官方合作共建的鴻蒙技術(shù)社區(qū)