引言
對(duì)于日志打印严嗜,之前都是使用Log.i()
的方式。今天來(lái)探究下StackTraceElement
獲取方法調(diào)用棧的信息仪媒。
痛點(diǎn):編寫(xiě)項(xiàng)目的時(shí)候狸眼,肯定會(huì)或多或少的使用Log捻爷,尤其是發(fā)現(xiàn)bug的時(shí)候,會(huì)連續(xù)在多個(gè)類(lèi)中打印Log信息份企,當(dāng)問(wèn)題解決了也榄,然后需要一行一行地去刪除剛才隨便添加的Log,有時(shí)候還要幾個(gè)輪回才能刪除干凈司志。
當(dāng)然了甜紫,我們有很多方案可以不去刪除:
- 我們可以通過(guò)gradle去配置debug、release常量去區(qū)分
- 可以對(duì)Log進(jìn)行一層封裝骂远,通過(guò)debug開(kāi)關(guān)常量來(lái)控制
所以囚霸,我們的需求是這樣的:
- 可以對(duì)Log封裝,通過(guò)debug開(kāi)關(guān)來(lái)控制正常日志信息的輸出
- 在修bug時(shí)激才,用于定位雜亂的log日志拓型,我們希望可以在bug解除后额嘿,很快的定位到,然后刪除滅跡劣挫。
一册养、什么是StackTrace
StackTrace(堆棧軌跡)存放的就是方法調(diào)用棧的信息,異常處理中常用的printStackTrace()實(shí)質(zhì)就是打印異常調(diào)用的堆棧信息压固。
二球拦、StackTraceElement介紹
StackTraceElement
表示StackTrace
(堆棧軌跡)中的一個(gè)方法對(duì)象,屬性包括方法的類(lèi)名帐我、方法名坎炼、文件名以及調(diào)用的行數(shù)。
public final class StackTraceElement implements java.io.Serializable {
// Normally initialized by VM (public constructor added in 1.5)
private String declaringClass;
private String methodName;
private String fileName;
private int lineNumber;
}
StackTraceElement
被定義為final
拦键,可見(jiàn)其作為一個(gè)Java的基礎(chǔ)類(lèi)不允許被繼承谣光。
獲取StackTraceElement的方法有兩種,均返回StackTraceElement數(shù)組芬为,也就是這個(gè)棧的信息萄金。
1、Thread.currentThread().getStackTrace()
2碳柱、new Throwable().getStackTrace()
StackTraceElement
數(shù)組包含了StackTrace
(堆棧軌跡)的內(nèi)容,通過(guò)遍歷它可以得到方法間的調(diào)用過(guò)程熬芜,即可以得到當(dāng)前方法以及其調(diào)用者的方法名莲镣、調(diào)用行數(shù)等信息
StackTraceElement
數(shù)組,你可以理解為當(dāng)我們調(diào)用方法的時(shí)候涎拉,每進(jìn)入一個(gè)方法瑞侮,會(huì)將該方法的相關(guān)信息(例如:類(lèi)名,方法名鼓拧,方法調(diào)用行數(shù)等)存儲(chǔ)下來(lái)半火,壓入到一個(gè)棧中,當(dāng)方法返回的時(shí)候再將其出棧季俩。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
a();
}
void a() {
b();
}
void b() {
StringBuffer err = new StringBuffer();
StackTraceElement[] stack = Thread.currentThread().getStackTrace();
for (int i = 0; i < stack.length; i++) {
err.append("\tat ");
err.append(stack[i].toString());
err.append("\n");
}
Log.e("TAG", err.toString());
}
我在onCreate中钮糖,調(diào)用了a方法,然后a中調(diào)用的b方法酌住。在b方法中打印出當(dāng)前線程中的棧幀集合信息店归。
at dalvik.system.VMStack.getThreadStackTrace(Native Method)
at java.lang.Thread.getStackTrace(Thread.java:579)
at com.zxy.recovery.test.MainActivity.b(MainActivity.java:26)
at com.zxy.recovery.test.MainActivity.a(MainActivity.java:21)
at com.zxy.recovery.test.MainActivity.onCreate(MainActivity.java:17)
at android.app.Activity.performCreate(Activity.java:5231)
...
可以看到我們整個(gè)方法的調(diào)用過(guò)程,底部的最先開(kāi)始調(diào)用酪我,順序?yàn)閛nCreate->a->b->Thread.getStackTrace->VMStack.getThreadStackTrace.
最后兩個(gè)是因?yàn)槲覀兊膕tacks是在VMStack.getThreadStackTrace方法中獲取消痛,然后返回的,所以包含了這兩個(gè)的內(nèi)部調(diào)用信息都哭。
上面的例子便于理解在文末代碼中的編寫(xiě)邏輯秩伞。
三逞带、用途
1、我們可以封裝一個(gè)日志庫(kù)纱新,在打印目標(biāo)日志的時(shí)候展氓,也可以通過(guò)這個(gè)調(diào)用棧打印出這個(gè)日志所在的行數(shù),這樣就可以迅速的定位到日志輸出行怒炸,再也不要全局搜索去查找了带饱。
public static void d(String tag, String msg, Object... params) {
StackTraceElement targetStackTraceElement = getTargetStackTraceElement();
Log.d(tag, "(" + targetStackTraceElement.getFileName() + ":"
+ targetStackTraceElement.getLineNumber() + ")");
Log.d(tag, String.format(msg, params));
}
2、如果我們寫(xiě)了一個(gè)SDK
阅羹,希望某個(gè)方法在固定的位置被調(diào)用勺疼,我們也可以在這個(gè)方法被調(diào)用的時(shí)候,進(jìn)行檢查捏鱼,看這個(gè)方法的調(diào)用位置是否正確执庐。
例如,必須在Activity.onResume中執(zhí)行导梆,PVSdk.onResume轨淌,所以我們?cè)谡{(diào)用PVSdk.onResume方法的時(shí)候,在PVSdk.onResume方法里面來(lái)通過(guò)獲取調(diào)用棧的信息檢測(cè)這個(gè)方法是否在Activity的onResume方法中調(diào)用的看尼。
public class PVSdk {
public static void onResume() {
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
boolean result = false;
for (StackTraceElement stackTraceElement : stackTrace) {
String methodName = stackTraceElement.getMethodName();
String className = stackTraceElement.getClassName();
try {
boolean assignableFromClass = Class.forName(className).isAssignableFrom(Activity.class);
if (assignableFromClass && "onResume".equals(methodName)) {
result = true;
break;
}
} catch (ClassNotFoundException e) {
}
}
if (!result)
throw new RuntimeException("PVSdk.onResume must in Activity.onResume");
}
}
3递鹉、我們?cè)谶M(jìn)行源碼分析的時(shí)候,如果想分析整個(gè)代碼的執(zhí)行流程藏斩,我們可以進(jìn)行通過(guò)打印棧的信息來(lái)獲取躏结,這個(gè)在源碼分析的時(shí)候還是挺有用的。
推薦一下
我們今天要談的就是Log的封裝狰域,當(dāng)然封裝不僅僅是是上述的好處媳拴,我們還可以讓使用更加便捷,打出來(lái)的Log信息展示的更加優(yōu)雅兆览。
比如:
這個(gè)庫(kù)屈溉,就對(duì)Log的信息的展示做了非常多的處理,展示給大家是一個(gè)非常nice的效果:
最后附上核心代碼:
public static void i(String msg) {
if (!isDebug) return;
StackTraceElement targetStackTraceElement = getTargetStackTraceElement();
Log.i(TAG, "(" + targetStackTraceElement.getFileName() + ":"
+ targetStackTraceElement.getLineNumber() + ")");
Log.i(TAG, msg);
}
private static StackTraceElement getTargetStackTraceElement() {
// find the target invoked method
StackTraceElement targetStackTrace = null;
boolean shouldTrace = false;
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
for (StackTraceElement stackTraceElement : stackTrace) {
boolean isLogMethod = stackTraceElement.getClassName().equals(L.class.getName());
if (shouldTrace && !isLogMethod) {
targetStackTrace = stackTraceElement;
break;
}
shouldTrace = isLogMethod;
}
return targetStackTrace;
}