Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android 從StackTraceElement反觀Log庫

Android 從StackTraceElement反觀Log庫

編輯:關於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 ~~~

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved