本文微信公眾號(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");
輸出如下:
我們現(xiàn)在增加如下代碼:
String nullString = null;
if (nullString.equals("null")) {
}
運(yùn)行之后控制臺(tái)會(huì)顯示空指針異常奔潰棧,如下:
可以看到奔潰棧信息會(huì)顯示具體是哪個(gè)文件出現(xiàn)了空指針荐类,以及具體哪一行怖现。在我們這個(gè)例子里面就是 MainActivity.java 的 24 行。
而且點(diǎn)擊藍(lán)色鏈接光標(biāo)會(huì)直接定位到錯(cuò)誤位置。
如果我們普通的日志也可以點(diǎn)擊就跳轉(zhuǎn)到對(duì)應(yīng)位置屈嗤,對(duì)于我們開(kāi)發(fā)來(lái)說(shuō)效率是有很大提升的潘拨。
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)輸出如下:
可以看到代碼所在的類和行數(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)輸出如下:
可以看到確實(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