Android 從StackTraceElement反觀Log庫

本文已授權微信公衆號:鴻洋(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),各種詞彙符號應該都會有。

所以,我們的需求是這樣的:

  1. 可以對Log封裝,通過debug開關來控制正常日誌信息的輸出
  2. 在修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
(歡迎關注,不要錯過每一篇乾貨,支持投稿)

發佈了213 篇原創文章 · 獲贊 2萬+ · 訪問量 2069萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章