編輯:關於Android編程
大家編寫項目的時候,肯定會或多或少的使用Log,尤其是發現bug的時候,會連續在多個類中打印Log信息,當問題解決了,然後又像狗一樣一行一行的去刪除剛才隨便添加的Log,有時候還要幾個輪回才能刪除干淨。
當然了,我們有很多方案可以不去刪除:
我們可以通過gradle去配置debug、release常量去區分 可以對Log進行一層封裝,通過debug開關常量來控制
當然了,更多時候我們是不得不刪除的,比如修bug著急的時候,一些
Log.e("TAG","馬丹,到底是不是null,obj = "+=obj),各種詞匯符號應該都會有。
所以,我們的需求是這樣的:
可以對Log封裝,通過debug開關來控制正常日志信息的輸出 在修bug時,用於定位的雜亂log日志,我們希望可以在bug解除後,很快的定位到,然後刪除滅跡。
ok,我們今天要談的就是Log的封裝,當然封裝不僅僅是是上述的好處,我們還可以讓使用更加便捷,打出來的Log信息展示的更加優雅。
比如:
https://github.com/orhanobut/logger
這個庫,就對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.
五、總結
那麼到此文章就結束了,雖然文章比較容易,不過我覺得也能解決一類問題,希望看了這個文章以後,對於任何的日志庫腦子裡對其實現的原理都非常清晰,看到其本質,很多時候就覺得這個東西很簡單了。
最後,文章中的代碼,和源碼略有不同,因為源碼可能會是封裝後的,文章中代碼是為了便於描述,都是越直觀越好。
源碼點擊下載:
Android/basetools">https://github.com/hongyangAndroid/basetools
have a nice day ~~~
本篇文章致那些從零開始學 Android 的或者正要學習還沒有勇氣出發的人, 希望通過我的經歷能夠讓你在學習的道路中堅持下來。我的第一份工作畢業之際通過學校的校招找到了一
Android開發的時候經常遇到端口號被占用的問題,經常使程序無法運行,很煩人。我總結了一個很好的方法,非常實用。方法如下: (1):方法1:第一步:1:netstat
一、命令篇 內存查看: 使用場景:跟蹤進程內存使用情況,看是否存在內存回收不了的問題,如果程序存在內存洩露問題,通過內存動態占用情況可以看出一些端倪。 2 查看系統d
隨著移動技術的深入發展,各種炫酷效果的更新,在我們追求UI與UE的同時一個不如忽視的問題逐漸暴露出來,那就是apk文件越來越大,可能有的童鞋會說現在都是wifi環境,ap