Android Debug 之 Log 最佳實(shí)踐

本文微信公眾號(hào)「AndroidTraveler」首發(fā)。

背景

在開(kāi)發(fā)過(guò)程中辉巡,調(diào)試是必不可少的一項(xiàng)工作。

當(dāng)我們要確定項(xiàng)目的邏輯時(shí)蕊退,當(dāng)我們要了解界面的生命周期時(shí)郊楣,當(dāng)我們發(fā)現(xiàn)新寫(xiě)的邏輯與期望效果不一致時(shí)憔恳,當(dāng)我們覺(jué)得數(shù)據(jù)有問(wèn)題時(shí)......

而調(diào)試有兩種方式:

第一種就是使用 debug 模式運(yùn)行 APP,然后通過(guò)斷點(diǎn)讓程序運(yùn)行到指定位置進(jìn)行分析净蚤。

第二種就是打日志的方式钥组,通過(guò)觀察輸出來(lái)確定程序是否運(yùn)行到該位置以及此時(shí)的數(shù)據(jù)。

本篇文章主要聚焦在第二種方式上面今瀑。

在 Android 里面程梦,打日志使用的系統(tǒng) API 是 Log,你以為直接使用就完了嗎橘荠?

封裝

假設(shè)你在需要打印日志的地方直接使用系統(tǒng)的 API屿附,那么當(dāng)遇到下面情況時(shí),會(huì)「牽一發(fā)而動(dòng)全身」哥童。

場(chǎng)景一:如果我打印日志要用三方庫(kù)的日志 API挺份,那么我要查找項(xiàng)目所有使用位置,并一一替換贮懈。

場(chǎng)景二:如果我希望在開(kāi)發(fā)環(huán)境下打印日志匀泊,release 環(huán)境不打印,這個(gè)時(shí)候每個(gè)位置都需要單獨(dú)做處理朵你。

因此我們需要在使用 Log 進(jìn)行日志打印之前各聘,做一層封裝。

假設(shè)我們的類名字為 ZLog抡医,代碼如下:

import android.util.Log;

/**
 * Created on 2019-10-26
 *
 * @author Zengyu.Zhan
 */
public class ZLog {
    public static int v(String tag, String msg) {
        return Log.v(tag, msg);
    }

    public static int d(String tag, String msg) {
        return Log.d(tag, msg);
    }

    public static int i(String tag, String msg) {
        return Log.i(tag, msg);
    }

    public static int w(String tag, String msg) {
        return Log.w(tag, msg);
    }

    public static int e(String tag, String msg) {
        return Log.e(tag, msg);
    }
}

這樣處理之后躲因,對(duì)于場(chǎng)景一和場(chǎng)景二,我們需要修改的只是 ZLog 這個(gè)類魂拦,而不需要到具體使用 ZLog 的所有地方去修改毛仪。

提供日志打印控制

我們知道,日志打印可能包含敏感信息芯勘,而且過(guò)多的日志打印可能影響 APP 的性能箱靴,因此我們一般是在開(kāi)發(fā)時(shí)候打開(kāi)日志,在發(fā)布 APP 之前關(guān)閉荷愕。

因此我們這邊需要提供一個(gè)標(biāo)志位來(lái)控制日志的打印與否衡怀。

import android.util.Log;

/**
 * Created on 2019-10-26
 *
 * @author Zengyu.Zhan
 */
public class ZLog {
    private static boolean isDebugMode = false;
    public static void setDebugMode(boolean debugMode) {
        isDebugMode = debugMode;
    }

    public static int v(String tag, String msg) {
        return isDebugMode ? Log.v(tag, msg) : -1;
    }

    public static int d(String tag, String msg) {
        return isDebugMode ? Log.d(tag, msg) : -1;
    }

    public static int i(String tag, String msg) {
        return isDebugMode ? Log.i(tag, msg) : -1;
    }

    public static int w(String tag, String msg) {
        return isDebugMode ? Log.w(tag, msg) : -1;
    }

    public static int e(String tag, String msg) {
        return isDebugMode ? Log.e(tag, msg) : -1;
    }
}

默認(rèn)是不開(kāi)啟日志打印,避免開(kāi)發(fā)者忘記設(shè)置安疗。

普通日志和奔潰棧系統(tǒng)日志在控制臺(tái)的輸出對(duì)比

現(xiàn)在我們?cè)?APP 里面使用 ZLog 打印日志抛杨,代碼為:

ZLog.setDebugMode(true);
ZLog.e("ZLog", "just test");

輸出如下:

image

我們現(xiàn)在增加如下代碼:

String nullString = null;
if (nullString.equals("null")) {
}

運(yùn)行之后控制臺(tái)會(huì)顯示空指針異常奔潰棧,如下:

image

可以看到奔潰棧信息會(huì)顯示具體是哪個(gè)文件出現(xiàn)了空指針荐类,以及具體哪一行怖现。在我們這個(gè)例子里面就是 MainActivity.java24 行。

而且點(diǎn)擊藍(lán)色鏈接光標(biāo)會(huì)直接定位到錯(cuò)誤位置。

如果我們普通的日志也可以點(diǎn)擊就跳轉(zhuǎn)到對(duì)應(yīng)位置屈嗤,對(duì)于我們開(kāi)發(fā)來(lái)說(shuō)效率是有很大提升的潘拨。

image

ZLogHelper

既然奔潰棧里面有鏈接可以跳轉(zhuǎn),那么我們可以通過(guò)棧信息來(lái)獲取日志的打印位置饶号。

我們直接上代碼:

public class ZLogHelper {
    private static final int CALL_STACK_INDEX = 1;
    private static final Pattern ANONYMOUS_CLASS = Pattern.compile("(\\$\\d+)+$");

    public static String wrapMessage(int stackIndex, String message) {
        // DO NOT switch this to Thread.getCurrentThread().getStackTrace().
        if (stackIndex < 0) {
            stackIndex = CALL_STACK_INDEX;
        }
        StackTraceElement[] stackTrace = new Throwable().getStackTrace();
        if (stackTrace.length <= stackIndex) {
            throw new IllegalStateException(
                    "Synthetic stacktrace didn't have enough elements: are you using proguard?");
        }
        String clazz = extractClassName(stackTrace[stackIndex]);
        int lineNumber = stackTrace[stackIndex].getLineNumber();
        message = ".(" + clazz + ".java:" + lineNumber + ") - " + message;
        return message;
    }

    /**
     * Extract the class name without any anonymous class suffixes (e.g., {@code Foo$1}
     * becomes {@code Foo}).
     */
    private static String extractClassName(StackTraceElement element) {
        String tag = element.getClassName();
        Matcher m = ANONYMOUS_CLASS.matcher(tag);
        if (m.find()) {
            tag = m.replaceAll("");
        }
        return tag.substring(tag.lastIndexOf('.') + 1);
    }
}

這里我們對(duì)外提供一個(gè) wrapMessage 方法铁追,看名字就知道是對(duì) Message 進(jìn)行包裝。

方法里面也是對(duì) StackTraceElement 進(jìn)行分析茫船。

這邊還做了一個(gè)控制琅束,避免 stackIndex 出現(xiàn)負(fù)數(shù)情況。

可能有小伙伴會(huì)好奇算谈,為什么要把 stackIndex 對(duì)外開(kāi)放呢涩禀?

因?yàn)槟愦蛴∪罩镜牡胤讲灰粯樱@里的 stackIndex 也需要對(duì)應(yīng)調(diào)整濒生。

方法里面是對(duì) StackTraceElement 做處理埋泵,而 StackTraceElement 跟你的方法層級(jí)有關(guān)系。

我們以最常用的兩種日志打印形式為例罪治,來(lái)說(shuō)明這里的 stackIndex 要怎么傳遞丽声,以及這個(gè) ZLogHelper 的用法。

直接代碼使用

我們?cè)?MainActivity.java 中直接使用觉义,stackIndex 傳入 1 即可雁社。

Log.e("ZLog", ZLogHelper.wrapMessage(1, "just test"));

控制臺(tái)輸出如下:


image

可以看到代碼所在的類和行數(shù)到顯示為鏈接文本,點(diǎn)擊會(huì)定位到具體的位置晒骇。

做了封裝的情況

一般我們對(duì) Log 都會(huì)做封裝霉撵,因此假設(shè)我們有一個(gè) LogUtils 類,我們?cè)?MainActivity.java 里面調(diào)用洪囤。

LogUtils.java:

class LogUtils {
    public static  void loge() {
        Log.e("ZLog", ZLogHelper.wrapMessage(2, "just test"));
    }
}

MainActivity.java:

LogUtils.loge();

我們先看下結(jié)果徒坡,再來(lái)分析×鏊酰控制臺(tái)輸出如下:


image

可以看到確實(shí)定位到了 MainActivity.java 中的具體使用地方喇完。

那么為什么這里傳入的 stackIndex 跟第一種不一樣,是 2 而不是 1 呢剥啤?

其實(shí)答案很簡(jiǎn)單锦溪,你改為 1 之后,輸出的控制臺(tái)顯示的會(huì)定位到 LogUtils 里面的日志打印語(yǔ)句處府怯。在這里就是:

Log.e("ZLog", ZLogHelper.wrapMessage(2, "just test"));

所以其實(shí)你可以看出一個(gè)規(guī)律刻诊,而這個(gè)從代碼也可以發(fā)現(xiàn)。

因?yàn)榇a里面解析調(diào)用位置是根據(jù)棧來(lái)的牺丙,對(duì) StackTraceElement 進(jìn)行分析则涯,因此情況一直接使用,傳入 1。而情況二多了一層函數(shù)調(diào)用粟判,通過(guò) loge 方法做了一層包裝肖揣。因此需要傳入 2。如果你再套一層浮入,那么需要傳入 3。了解了這一點(diǎn)羊异,我們下面的工具類相信你就看得懂了事秀。

ZLog

如果你不想自己手動(dòng)傳入 stackIndex,可以直接使用我們提供的工具類 ZLog野舶。

public class ZLog {
    private static boolean isDebugMode = false;
    public static void setDebugMode(boolean debugMode) {
        isDebugMode = debugMode;
    }

    private static boolean isLinkMode = true;
    public static void setLinkMode(boolean linkMode) {
        isLinkMode = linkMode;
    }

    private static final int CALL_STACK_INDEX = 3;

    public static int v(String tag, String msg) {
        return isDebugMode ? Log.v(tag, mapMsg(msg)) : -1;
    }

    public static int d(String tag, String msg) {
        return isDebugMode ? Log.d(tag, mapMsg(msg)) : -1;
    }

    public static int i(String tag, String msg) {
        return isDebugMode ? Log.i(tag, mapMsg(msg)) : -1;
    }

    public static int w(String tag, String msg) {
        return isDebugMode ? Log.w(tag, mapMsg(msg)) : -1;
    }

    public static int e(String tag, String msg) {
        return isDebugMode ? Log.e(tag, mapMsg(msg)) : -1;
    }

    private static String mapMsg(String msg) {
        return isLinkMode ? ZLogHelper.wrapMessage(CALL_STACK_INDEX, msg) : msg;
    }
}

相信有了前面的知識(shí)易迹,小伙伴對(duì)于這里為什么傳入 3 應(yīng)該了解了。

1 的話會(huì)定位到

return isLinkMode ? ZLogHelper.wrapMessage(CALL_STACK_INDEX, msg) : msg;

2 的話(以 e 為例)會(huì)定位到

return isDebugMode ? Log.e(tag, mapMsg(msg)) : -1;

3 的話才能夠定位到外面具體的調(diào)用處平道。

優(yōu)化

我們知道睹欲,雖然 ZLog 做了封裝,但是我們每次打日志都要傳入 ZLog一屋,有點(diǎn)麻煩窘疮?

能否提供一個(gè)默認(rèn)的 TAG,允許對(duì)外設(shè)置冀墨。

可以的闸衫,我們修改如下(以 e 為例):

private static String tag = "ZLOG";
public static void setTag(String tag) {
    if (!TextUtils.isEmpty(tag)) {
        ZLog.tag = tag;
    }
}

public static int e(String tag, String msg) {
    return isDebugMode ? Log.e(mapTag(tag), mapMsg(msg)) : -1;
}

public static int e(String msg) {
    return isDebugMode ? Log.e(tag, mapMsg(msg)) : -1;
}

private static String mapTag(String tag) {
    return TextUtils.isEmpty(tag) ? ZLog.tag : tag;
}

項(xiàng)目實(shí)戰(zhàn)

按照下面兩步引入開(kāi)源庫(kù)。

Step 1. Add the JitPack repository to your build file
Add it in your root build.gradle at the end of repositories:

allprojects {
  repositories {
    ...
    maven { url 'https://jitpack.io' }
  }
}

Step 2. Add the dependency

dependencies {
  implementation 'com.github.nesger:AndroidWheel:1.0.0'
}

使用時(shí)先打開(kāi)開(kāi)關(guān):

ZLog.setDebugMode(true);

然后就可以直接使用了诽嘉。

溫馨提示

由于帶鏈接的 debug 對(duì)性能有一定影響蔚出,因此建議開(kāi)發(fā)使用,上線關(guān)閉虫腋。

結(jié)語(yǔ)

這邊在完善一個(gè)開(kāi)源倉(cāng)庫(kù) AndroidWheel骄酗,跟名字一樣,避免重復(fù)造輪子悦冀。

目前 1.0.0 版本提供日志相關(guān)工具類趋翻,1.0.1 增加了防抖動(dòng) EditText。

后續(xù)會(huì)繼續(xù)更新迭代雏门,功能會(huì)更完善更全面嘿歌。

覺(jué)得不錯(cuò),歡迎給個(gè) star 哈~

參考鏈接:
Android Studio Pro Tip: go to source from logcat output

image
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末茁影,一起剝皮案震驚了整個(gè)濱河市宙帝,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌募闲,老刑警劉巖步脓,帶你破解...
    沈念sama閱讀 221,695評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡靴患,警方通過(guò)查閱死者的電腦和手機(jī)仍侥,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,569評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)鸳君,“玉大人农渊,你說(shuō)我怎么就攤上這事』蚣眨” “怎么了砸紊?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,130評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)囱挑。 經(jīng)常有香客問(wèn)我醉顽,道長(zhǎng),這世上最難降的妖魔是什么平挑? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,648評(píng)論 1 297
  • 正文 為了忘掉前任游添,我火速辦了婚禮,結(jié)果婚禮上通熄,老公的妹妹穿的比我還像新娘唆涝。我一直安慰自己,他們只是感情好棠隐,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,655評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布石抡。 她就那樣靜靜地躺著,像睡著了一般助泽。 火紅的嫁衣襯著肌膚如雪啰扛。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 52,268評(píng)論 1 309
  • 那天嗡贺,我揣著相機(jī)與錄音隐解,去河邊找鬼。 笑死诫睬,一個(gè)胖子當(dāng)著我的面吹牛煞茫,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播摄凡,決...
    沈念sama閱讀 40,835評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼续徽,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了亲澡?” 一聲冷哼從身側(cè)響起钦扭,我...
    開(kāi)封第一講書(shū)人閱讀 39,740評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎床绪,沒(méi)想到半個(gè)月后客情,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體其弊,經(jīng)...
    沈念sama閱讀 46,286評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,375評(píng)論 3 340
  • 正文 我和宋清朗相戀三年膀斋,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了梭伐。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,505評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡仰担,死狀恐怖糊识,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情摔蓝,我是刑警寧澤技掏,帶...
    沈念sama閱讀 36,185評(píng)論 5 350
  • 正文 年R本政府宣布,位于F島的核電站项鬼,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏劲阎。R本人自食惡果不足惜绘盟,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,873評(píng)論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望悯仙。 院中可真熱鬧龄毡,春花似錦、人聲如沸锡垄。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,357評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)货岭。三九已至路操,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間千贯,已是汗流浹背屯仗。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,466評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留搔谴,地道東北人魁袜。 一個(gè)月前我還...
    沈念sama閱讀 48,921評(píng)論 3 376
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像敦第,于是被迫代替她去往敵國(guó)和親峰弹。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,515評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容