本文已授權微信公衆號:鴻洋(hongyangAndroid)在微信公衆號平臺原創首發。
轉載請標明出處:
http://blog.csdn.net/lmj623565791/article/details/52506545;
本文出自:【張鴻洋的博客】
一、概述
大家編寫項目的時候,肯定會或多或少的使用Log
,尤其是發現bug的時候,會連續在多個類中打印Log信息,當問題解決了,然後又像狗一樣一行一行的去刪除剛纔隨便添加的Log,有時候還要幾個輪迴才能刪除乾淨。
當然了,我們有很多方案可以不去刪除:
- 我們可以通過gradle去配置debug、release常量去區分
- 可以對Log進行一層封裝,通過debug開關常量來控制
當然了,更多時候我們是不得不刪除的,比如修bug着急的時候,一些Log.e("TAG","馬丹,到底是不是null,obj = "+=obj)
,各種詞彙符號應該都會有。
所以,我們的需求是這樣的:
- 可以對Log封裝,通過debug開關來控制正常日誌信息的輸出
- 在修bug時,用於定位的雜亂log日誌,我們希望可以在bug解除後,很快的定位到,然後刪除滅跡。
ok,我們今天要談的就是Log的封裝,當然封裝不僅僅是是上述的好處,我們還可以讓使用更加便捷,打出來的Log信息展示的更加優雅。
比如:
這個庫,就對Log的信息的展示做了非常多的處理,展示給大家是一個非常nice的效果:
當然今天的博文不是去介紹該庫,或者是源碼解析,不過解析的文章我最後收到了投稿,可以關注我的公衆號,近期應該會推送。
今天文章的目標是:掌握這類庫的核心原理,以後只要遇到該類庫,大家都能說出其本質,以及可以自己去封裝一個適合自己的日誌庫。
二、可行性
對於好用,我覺得如下用法就可以:
L.e("heiheihei");
對於好定位,當然是可以通過日誌信息點擊,定位到具體行,所以今天demo代碼的效果是這樣的:
當然了,你可以根據自己喜好,去添加各種信息,以及裝飾。
那麼,現在最大的一個問題就是
- 我怎麼輸出具體的日誌調用行呢?
這個祕密就在:
Thread.currentThread().getStackTrace();
我們可以通過當前的線程,拿到當前調用的棧幀集合(稱呼不一定準備)。
- 這個棧幀集合是什麼玩意呢?
你可以理解爲當我們調用方法的時候,每進入一個方法,會將該方法的相關信息(例如:類名,方法名,方法調用行數等)存儲下來,壓入到一個棧中,當方法返回的時候再將其出棧。
下面看個具體的例子:
@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中,調用了a方法,然後a中調用的b方法。在b方法中打印出當前線程中的棧幀集合信息。
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)
...
可以看到我們整個方法的調用過程,底部的最先開始調用,順序爲onCreate->a->b->Thread.getStackTrace->VMStack.getThreadStackTrace.
最後兩個是因爲我們的stacks是在VMStack.getThreadStackTrace方法中獲取,然後返回的,所以包含了這兩個的內部調用信息。
這裏我們直接調用的StackTraceElement的toString方法,它內部有:
- getClassName
- getMethodName
- getFileName
- getLineNumber
看名字就知道什麼意思了,我們可以根據這些信息拼接要打印的信息。
所以,不管怎麼說,我們現在已經確定了,可以通過該種方式得到我們的調用某個方法的行數,而且是支持點擊跳轉到指定位置的。
到這裏相當於,方案的可行性就通過了,剩下就是碼代碼了。
三、實現
先寫個大致的代碼:
public class L{
private static boolean sDebug = true;
private static String sTag = "zhy";
public static void init(boolean debug, String tag){
L.sDebug = debug;
L.sTag = tag;
}
public static void e(String msg, Object... params){
e(null, msg, params);
}
public static void e(String tag, String msg, Object[] params){
if (!sDebug) return;
tag = getFinalTag(tag);
//TODO 通過stackElement打印具體log執行的行數
Log.e(tag, content);
}
private static String getFinalTag(String tag){
if (!TextUtils.isEmpty(tag)){
return tag;
}
return sTag;
}
}
因爲我平時基本上只用Log.e,所以我就不對其他方法進行處理了,你可以根據你的喜好來決定。
ok,那麼現在只有一個地方沒有處理,就是打印log執行的類以及代碼行。
我在onCreate的17行調用了:
L.e("Hello World");
然後在e()方法中,打印了所有的棧幀信息:
E/zhy: at dalvik.system.VMStack.getThreadStackTrace(Native Method)
at java.lang.Thread.getStackTrace(Thread.java:579)
at com.zxy.recovery.test.L.e(L.java:32)
at com.zxy.recovery.test.L.e(L.java:25)
at com.zxy.recovery.test.MainActivity.onCreate(MainActivity.java:19)
at android.app.Activity.performCreate(Activity.java:5231)
//...
E/zhy: Hello World
我們要輸出的就是上述的MainActivity.onCreate(MainActivity.java:19)
- 那麼我們如何定位呢?
觀察上面的信息,因爲我們的入口是L類的方法,所以,我們直接遍歷,L類相關的下一個非L類的棧幀信息就是具體調用的方法。
於是我們這麼寫:
private 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;
}
拿到確定的方法調用相關的棧幀之後,就是輸出啦~~
添加到e()方法中:
public static void e(String tag, String msg, Object... params) {
if (!sDebug) return;
String finalTag = getFinalTag(tag);
StackTraceElement targetStackTraceElement = getTargetStackTraceElement();
Log.e(finalTag, "(" + targetStackTraceElement.getFileName() + ":"
+ targetStackTraceElement.getLineNumber() + ")");
Log.e(finalTag, String.format(msg, params));
}
現在再看下輸出結果:
現在就可以迅速的定位到日誌輸出行,再也不要全局搜索去查找了~
到這裏,對於我個人的需求已經滿足了,如果你有特殊需要,比如也想像logger那樣搞個框,那就自己繪製吧,也可以參考它的源碼。
對了,還有json,有時候希望可以看json字符串更加的直觀,像looger那樣:
你可以參考它的做法,其實就是將json字符串,通過JsonArray和JsonObject進行了一個類似format這樣的操作。
private static String getPrettyJson(String jsonStr) {
try {
jsonStr = jsonStr.trim();
if (jsonStr.startsWith("{")) {
JSONObject jsonObject = new JSONObject(jsonStr);
return jsonObject.toString(JSON_INDENT);
}
if (jsonStr.startsWith("[")) {
JSONArray jsonArray = new JSONArray(jsonStr);
return jsonArray.toString(JSON_INDENT);
}
} catch (JSONException e) {
e.printStackTrace();
}
return "Invalid Json, Please Check: " + jsonStr;
}
重點就是文本的處理了,其他的和普通log一致。
你可以獨立一個L.json()方法。
L.json("{\"name\":\"張鴻洋\",\"age\":24}");
效果如下:
好了,我自己在每次輸出前後加了個橫線,根據自己的喜歡定製吧。
四、其他用法
StackElementStack在其他一些SDK裏面也會用到,比如處理app的crash,有時候會重新處理下信息。
還有就是一些統計PV相關的SDK,會強制要求在某些方法中執行某個方法,例如,必須在Activity.onResume中執行,PVSdk.onResume,如果你之前遇到過某個SDK給你拋了類似的異常,那麼它的原理就是這麼實現的。
大致的代碼如下,可能會有漏洞,隨手寫的:
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) {
// ignored
}
}
if (!result)
throw new RuntimeException("PVSdk.onResume must in Activity.onResume");
//do other things
}
}
大多時候上述代碼實在debug時候開啓的,發版狀態可能會關閉檢查,具體看自己的需求了。
包括自己再寫一些庫的時候,強綁定生命週期也能這麼去簡單的check.
五、總結
那麼到此文章就結束了,雖然文章比較容易,不過我覺得也能解決一類問題,希望看了這個文章以後,對於任何的日誌庫腦子裏對其實現的原理都非常清晰,看到其本質,很多時候就覺得這個東西很簡單了。
最後,文章中的代碼,和源碼略有不同,因爲源碼可能會是封裝後的,文章中代碼是爲了便於描述,都是越直觀越好。
源碼點擊下載:
have a nice day ~~~
歡迎關注我的微博:
http://weibo.com/u/3165018720
羣號: 497438697 ,歡迎入羣
微信公衆號:hongyangAndroid
(歡迎關注,不要錯過每一篇乾貨,支持投稿)