Log最佳實踐

本文會不按期更新,推薦watch下項目。
若是喜歡請star,若是以爲有紕漏請提交issue,若是你有更好的點子能夠提交pull request。
本文的示例代碼主要是基於loggerLogUtilstimber進行編寫的,若是想了解更多請查看他們的詳細解釋。
我很推薦你們多多進行對比,選擇適合你本身的庫來使用。 javascript

本文固定鏈接:github.com/tianzhijiex…php


1、背景

Android中的log是這麼寫的:html

Log.d(TAG, "This is a debug log");複製代碼

android.util.Log類作的事情很簡單,符合kiss原則,可是隨着業務的不斷髮展,logcat中就會有多個部門的各類log,不一樣手機系統本身的一些log也會參雜進來,逼迫咱們要擴展log類。java

2、需求

  1. 我纔不要每次打log都去想tag叫什麼名字呢
  2. 一般狀況下請自動把當前類名做爲默認的tag,但也容許我自由指定
  3. 我但願我寫的模板式代碼越少越好,一個logd就能打印一切
  4. 我要打印出list,map,json,pojo這樣的對象
  5. 個人log絕對不要和其他的雜亂log混在一塊兒
  6. log信息過長後應該要自動換行,我不容許個人log打印不全
  7. 我要個人log變的好看,直觀,就是美
  8. log中還要能顯示我當前的線程名,方便我調試多線程
  9. 我打出的log後面要根上這個log的地址,能夠直接外鏈到log的位置
  10. release包中不能泄漏我高傲的log,但只要我想讓它顯示,release版本也阻擋不了我
  11. 在release版本中殘留的log代碼應該對app運行效率影響極低
  12. 它能自動將try-catch住的crash經過log上傳到Crashlytics

回看這些需求,不合理麼?其實很合理,咱們的宗旨就是讓無心義的重複代碼去死,若是死不掉就交給機器來作。咱們應該作那些真正須要咱們作的事情,而不是像一個沒思想的猿猴通常成天寫模板式代碼。這纔是程序員思惟,而不是程序猿思惟!android

注意:我但願只要寫真正有意義的內容!git

3、實現

分析上述的需求後,我將其分爲四類: 使用、顯示和擴展。程序員

使用篇

創建包裝類

不管一個第三方庫有多好,我仍是推薦不直接使用它,由於你頗有可能會去替換這個第三方庫,並且一個第三方庫確定沒法知足各類奇葩需求。因此,對於網絡庫、圖片庫和log庫來講,咱們應該事先考慮在上面封裝一層。github

咱們創建一個包裝類,用這個包裝類用來包裹Logger(logger是本文介紹的一個log庫),下面是包裝類的代碼片斷:json

public static void d(@Nullable String info, Object... args) {
    if (!mIsOpen) { // 若是把開關關閉了,那麼就不進行打印
        return;
    }
    Logger.d(info, args);
}複製代碼

對於包裝類的起名最好不要和「Log」這個相似,能有明顯的區別最好,一是防止本身手抖寫錯了,二是方便review的時候能快速檢查出有沒有誤用原始的Log。後端

自動打tag

默認狀況下能夠把當前類名做爲TAG的默認值,咱們能夠經過下面代碼來獲得當前類名:

private static String getClassName() {
    // 這裏的數組的index,即2,是根據你工具類的層級取的值,可根據需求改變
    StackTraceElement thisMethodStack = (new Exception()).getStackTrace()[2]; 
    String result = thisMethodStack.getClassName();
    int lastIndex = result.lastIndexOf(".");
    result = result.substring(lastIndex + 1, result.length());
    return result;
}複製代碼

這樣咱們就輕易的擺脫了tag的糾纏。
須要注意的是,獲取堆棧的方法是有性能消耗的,因此在主線程的log可能會引發一些卡頓,因此強烈建議在release版本中不要使用這個方法。

這個方法來自於豪哥的建議,這裏感謝豪哥的意見。

自定義tag

除了自動打tag外,咱們確定要讓其支持自定義tag:

public static void d(@NonNull String tag, String info, Object... args) {
    Logger.t(tag).d(info, args);
}複製代碼

這個d(tag, info, args...)是上面d(info, args...)的擴展,這裏要注意的是tag的選取。

經常使用的作法是用getSimpleName的方式來獲得tag,但若是你加了混淆,不少類(Activity、View不必定會被混淆)就會被混淆爲a/b/c這樣的單詞。所以,若是你的log要出如今混淆的包裏的,我強烈建議去手動設置tag值,不然打出來的log就是很難過濾的了。

至於如何手動設置tag的值,下面會講到logt這個快捷命令。

自定義全局tag和tag前綴

若是你的項目很龐大或者採用了插件化和組件化方案,那麼你確定會涉及到多人開發的問題。底層平臺是暴露統一的log接口,可是上層開發人員種類繁多,如何在繁雜的log中找到本身部門的本身關心的log呢?

在這種狀況下咱們能夠採用以下兩種方案:

  1. 自行調試時關閉無關部門的log輸出
  2. 每一個部門有自定義的tag前綴

對於方案一,咱們自己的log系統底層採用的是timber,它自己就是經過「種樹」的方式進行log分發的,咱們只須要在咱們項目的最開始調用

Logger.uprootAll();
// or
Timber.uprootAll();複製代碼

將全部以前的log通道移除,這樣就清空了無用的log了。

相比起方案一的簡單粗暴,方案二卻是溫和實用的多。咱們經過在logger初始化設置一個tagPrefix,這個前綴就會伴隨着咱們私有項目的全部log了,之後直接搜索這個前綴就能夠過濾出想要的信息了。

開啓和關閉log

有時候在調試過程當中可能會要支持測試同窗的動態關閉和開啓log的功能。

Logger.closeLog();
Logger.openLog(Log.INFO);複製代碼

這個操做能夠支持在應用運行的時的任什麼時候候進行開關。

將Log代碼快捷模板

有人說咱們IDE不都有代碼提示了麼,你還想怎麼簡化log的輸入呢?這裏能夠利用as的模板提示的功能:

咱們能夠模仿原有的模板來作本身的代碼模板,簡化模板式代碼的輸入。至於具體模仿的方式我就不手把手教了,至關簡單。下面僅展現下自帶的log模板的使用:

生成TAG:


自動填寫參數和方法名:

顯示篇

讓log更加美觀

讓log的輸出直觀、美觀其實很簡單,就是在輸出前作點字符串拼接的工做,好比加上下面這行橫線。

private static final String BOTTOM_BORDER = "╚═══════════════════════════";複製代碼

由於作了不少拼接的工做,因此好看的log也是消耗性能的。個人習慣是調試完畢後馬上刪除無用的log,這樣既能減小性能影響,也能減小同事的閱讀代碼的負擔。採用輕量級美化後效果以下:

顯示當前方法名、所在類並加超鏈

這個功能其實ide是原生支持的,不相信的話你隨便用原生的log打印出onCreate: (MainActivity.java:31)試試。

咱們能夠經過下面的方法來作到更好的效果:

private static String callMethodAndLine() {
        String result = "at ";
        StackTraceElement thisMethodStack = (new Exception()).getStackTrace()[1];
        result += thisMethodStack.getClassName()+ "."; // 當前的類名(全名)
        result += thisMethodStack.getMethodName();
        result += "(" + thisMethodStack.getFileName();
        result += ":" + thisMethodStack.getLineNumber() + ") ";
        return result;
    }複製代碼

這裏一樣須要注意的是類在混淆後是得不到正確的名稱的,因此能夠酌情讓activity、fragment、view不被混淆,具體方案仍是看本身的取捨。

增長當前線程的信息

當你調試過多線程,你就會發現log中帶有線程的信息是很方便的。

Thread.currentThread().getName()複製代碼

Logger的尾巴上會帶有線程的名字,方便你們進行調試。

支持POJO、Map、Collection、jsonStr、Array

這個需求實現起來也比較容易:

  • 若是是POJO,咱們可用反射獲得對象的類變量,經過字符串拼接的方式最終輸出值
  • 若是是map等數組結構,那麼就用其內部的遍歷依次輸出值和內容
  • 若是是json的字符串,就須要判斷json的{},[]這樣的特殊字符進行換行處理

至於具體是如何實現的,你們移步去看源碼就好,這個不是重點,重點是結果:

不推薦打印每次網絡請求的json,只推薦在調試某個數據的時候進行打印,不然信息太多,並且效率很低,不實用。

自定義輸出樣式

咱們看到了orhanobut/loggerelvishew/xLog都十分好看,可是tianzhijiexian/logger的log看起來就沒那麼美觀了,因此這個庫支持了自定的style,讓使用者能夠自定義輸出樣式。

PrintStyle.java

public abstract class PrintStyle {

    @Nullable
    protected abstract String beforePrint();

    @NonNull
    protected abstract String printLog(String message, int line, int wholeLineCount);

    @Nullable
    protected abstract String afterPrint();
}複製代碼

這個抽象類提供了三個方法,用來獲得log打印前,打印時,打印後的內容,咱們能夠經過它來實現自定義的樣式。

使用XLog樣式後的輸出:

PS:Logger的不美觀實際上是折衷的結果。美觀必然會帶來數據的冗餘,但原始的log卻又不足夠清晰。Logger最終選擇了一個輕量的log樣式,既保證了清晰易辨認又不會帶來過多的冗餘信息。

支持超長的log信息

有時候網絡的返回值是很長的,android.util.Log類是有最大長度限制的。爲了解決這個問題,咱們只須要判斷這個字符串的長度,而後手動讓其換行便可。

private static final int CHUNK_SIZE = 4000;

if (length <= CHUNK_SIZE) {
    logContent(logType, tag, msg);
} else {
    for (int i = 0; i < length; i += CHUNK_SIZE) {
        int count = Math.min(length - i, CHUNK_SIZE);
        //create a new String with system's default charset (which is UTF-8 for Android)
        logContent(logType, tag, new String(bytes, i, count));
    }
}複製代碼

自定義過濾規則

當崩潰出現的時候,有時候會將咱們的log清屏,大大影響了咱們的調試工做。因此咱們能夠在合適的時候利用Edit Filter Configuration這個功能。

Edit Filter Configuration十分強大,而且支持正則。通常狀況下使用Show only selected application就搞定了,是否使用Edit Filter Configuration就看你的具體場景了。

擴展篇

增長自動化或強制開關

要區分release和debug版本,能夠用自帶的BuildConfig.DEBUG變量,用這個也就能夠控制是否顯示log了。作個強制開關也很簡單,在log初始化的最後判斷強制開關是否打開,若是打開那麼就覆蓋以前的顯示設置,直接顯示log。轉爲代碼就是這樣:

public class BaseApplication extends Application {

    // 定義是不是強制顯示log的模式
    protected static final boolean LOG = false;

    @Override
    public void onCreate() {
        Logger.initialize(
            new Settings()
                    .setLogPriority(BuildConfig.DEBUG ? Log.VERBOSE : Log.ASSERT)
        );

        // 若是是強制顯示log,那麼不管在什麼模式下都顯示log
        if (LOG) {
            Logger.getSettings().setLogPriority(Log.VERBOSE)
        }
    }
}複製代碼

之後要是須要作log的開關,那麼只須要經過settings重設log級別便可:

Logger.getSettings().setLogPriority(Log.ASSERT); // close log複製代碼

解決log字符拼接的效率影響

多參數log信息應該利用佔位符進行打印,儘可能避免手動拼接字符串。這樣好處是:在關閉log後就不會進行字符串的拼接工做了,減小log語句在release版本中的性能影響。

封裝類.d("test %s%s", "v", 5); // test v5複製代碼
public static void d(@Nullable String info, Object... args) {
    if (!mIsOpen) { // 若是把開關關閉了,天然就不進行字符串拼接
        return;
    }
    Logger.d(info, args); // 內部會作String.format()
}複製代碼

這條來自朋友helder的建議,感謝!

經過混淆剔除log代碼

若是你肯定你的log代碼在release版本中是無需存在的,那麼我分享一個方案來幫你幹掉它。

好比你的混淆配置文件叫proguard-rules.pro,裏面有以下代碼:

-assumenosideeffects class kale.log.LL { // 假設咱們的log類是LL
    public static *** d(...); // public static void d(...);
    public static *** i(...);
    public static *** v(...);
}複製代碼

而後在build.gradlez中啓用混淆:

buildTypes {
        release {
            minifyEnabled true
            shrinkResources true // 是否去除無效的資源文件
            // 注意是用proguard-android-optimize.txt而不是proguard-android.txt
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
            signingConfig signingConfigs.release
        }
    }複製代碼

要令assumenosideeffects生效,就須要開啓混淆中的優化選項,而默認的proguard-android.txt是不會開啓優化選項的。若是咱們須要開啓混淆的話,那麼建議咱們採用 proguard-android-optimize.txt。

proguard-android-optimize的所有內容以下:

# This is a configuration file for ProGuard.
# http://proguard.sourceforge.net/index.html#manual/usage.html

# Optimizations: If you don't want to optimize, use the
# proguard-android.txt configuration file instead of this one, which
# turns off the optimization flags.  Adding optimization introduces
# certain risks, since for example not all optimizations performed by
# ProGuard works on all versions of Dalvik.  The following flags turn
# off various optimizations known to have issues, but the list may not
# be complete or up to date. (The "arithmetic" optimization can be
# used if you are only targeting Android 2.0 or later.)  Make sure you
# test thoroughly if you go this route.
-optimizations !code/simplification/arithmetic,!code/simplification/cast,!field/*,!class/merging/*
-optimizationpasses 5
-allowaccessmodification
-dontpreverify

# The remainder of this file is identical to the non-optimized version
# of the Proguard configuration file (except that the other file has
# flags to turn off optimization).

-dontusemixedcaseclassnames
-dontskipnonpubliclibraryclasses
-verbose

-keepattributes *Annotation*
-keep public class com.google.vending.licensing.ILicensingService
-keep public class com.android.vending.licensing.ILicensingService

# For native methods, see http://proguard.sourceforge.net/manual/examples.html#native
-keepclasseswithmembernames class * {
    native <methods>;
}

# keep setters in Views so that animations can still work.
# see http://proguard.sourceforge.net/manual/examples.html#beans
-keepclassmembers public class * extends android.view.View {
   void set*(***);
   *** get*();
}

# We want to keep methods in Activity that could be used in the XML attribute onClick
-keepclassmembers class * extends android.app.Activity {
   public void *(android.view.View);
}

# For enumeration classes, see http://proguard.sourceforge.net/manual/examples.html#enumerations
-keepclassmembers enum * {
    public static **[] values();
    public static ** valueOf(java.lang.String);
}

-keepclassmembers class * implements android.os.Parcelable {
  public static final android.os.Parcelable$Creator CREATOR;
}

-keepclassmembers class **.R$* {
    public static <fields>;
}

# The support library contains references to newer platform versions.
# Don't warn about those in case this app is linking against an older
# platform version.  We know about them, and they are safe.
-dontwarn android.support.**

# Understand the @Keep support annotation.
-keep class android.support.annotation.Keep

-keep @android.support.annotation.Keep class * {*;}

-keepclasseswithmembers class * {
    @android.support.annotation.Keep <methods>;
}

-keepclasseswithmembers class * {
    @android.support.annotation.Keep <fields>;
}

-keepclasseswithmembers class * {
    @android.support.annotation.Keep <init>(...);
}複製代碼

上面的註釋就是採用優化方案來剔除log的風險點,因此要慎重使用!!!

這裏也提到了通常推薦用proguard-android.txt來作混淆方案,若是你要是用了proguard-android-optimize.txt的話,請必定要測試充分在發佈app。

將try-catch的信息經過log上傳到Crashlytics

咱們有時候爲了防護某個未知緣由的崩潰,常常會進行try-catch。這樣雖然讓其沒崩潰,可是也隱藏了錯誤,以致於咱們始終沒有辦法弄懂錯誤出現的緣由。
我但願能夠經過把catch的異常經過log系統分發到崩潰分析網站上(如:Crashlytics),這樣既能防護問題,又能夠幫助開發者知道崩潰產生的緣由,方便之後針對性的進行處理。

代碼參考自:blog.xmartlabs.com/2015/07/09/…

模擬

/** * 這裏模擬後端給客戶端傳值的狀況。 * * 這裏的id來自外部輸入,若是外部輸入的值有問題,那麼就可能崩潰。 * 但理論上是不會有數據異常的,爲了避免崩潰,這裏加try-catch */
    private void setRes(@StringRes int resId) {
        TextView view = new TextView(this);

        try {
            view.setText(resId); // 若是出現了崩潰,那麼就會調用崩潰處理機制
        } catch (Exception e) {
            // 防護了崩潰
            e.printStackTrace();

            // 把崩潰的異常和當前的上下文經過log系統分發
            Logger.e(e, "res id = " + resId);
        }
    }複製代碼

接下來,咱們創建一個crash分發tree:

public class CrashlyticsTree extends Timber.Tree {

    @Override
    protected void log(int priority, @Nullable String tag, @Nullable String message, @Nullable Throwable t) {
        if (priority == Log.VERBOSE || priority == Log.DEBUG || priority == Log.INFO) {
            // 只分發異常
            return;
        }

        if (t == null && message != null) {
            Crashlytics.logException(new Exception(message));
        } else if (t != null && message != null) {
            Crashlytics.logException(new Exception(message, t));
        } else if (t != null) {
            Crashlytics.logException(t);
        }
    }
}

// ---------------

if (!BuildConfig.DEBUG) {    // for release 
    Logger.plant(new CrashlyticsTree()); // plant a tree
}複製代碼

一旦用戶發生了崩潰,咱們如今就能夠經過Crashlytics進行分析,這樣的錯誤會自動歸檔在Crashlytics報表的non-fatals中。經過這樣的方式,能夠方便咱們排查出真正的問題,解決後就能夠真正去掉這個try-catch了。

注意:
由於咱們有些錯誤是不但願上傳的,有些是但願上傳的,因此我建議在使用Logger.e()的時候,經過你的包裝類來作個處理(加參數或加方法),讓使用者明確這個log將通向何方,不但願引發理解混亂。

增長log的擴展性

正如上面提到的,咱們的log可能須要分發到不一樣的系統,這也是我採用timber的緣由。咱們除了將線上的錯誤分發到崩潰統計系統外,也可能要將log保存到sd卡或是作其餘的處理,因此目前logger利用timber的tree實現了分發的功能。

Logger內部的實現:

public static void plant(Timber.Tree tree) {
    Timber.plant(tree);
}複製代碼

關於如何plant能夠參考下Timber的具體代碼。

經過自定義lint來規範log

大多數團隊會定義本身的log類來進行log的打印,咱們最好能夠經過自定義的lint來在代碼編寫時防止開發者錯用log類。

詳細的內容能夠參考:《Android自定義Lint實踐》

利用IDEA的debug工具打log

上文中我就提到了能夠利用as的調試模式來加速debug,下面分享下兩個和log有關的經驗。

public class MainActivity extends AppCompatActivity {

    private static final String TAG = "MainActivity";

    private int index = 0;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        Button button = (Button) findViewById(R.id.button);
        button.setOnClickListener(v-> {
                index = 123;
                Log.d(TAG, "onClick: index = " + index);
                index++;
            }
        );
    }
}複製代碼
  1. 經過console熱部署打印log信息
    我經過debug工具,能夠在任意位置打印出任意對象的值,經過這種方式就能夠精準調試一些信息了。
    下圖是我讓其在不中斷運行的狀況下打印index的值。

  2. 動態設置值
    有時候某種分支須要在某個狀況下才能走到,我能夠利用debug的setValue(F12)方法動態設置值,好比我把下面的123改爲了520,最終在終端打印出的信息也會變成520。整個過程對本來代碼徹底屏蔽,無入侵。

PS:更多的調試技巧能夠查看Android-Best-Practices中的推薦的調試技巧的文章。

因地制宜的使用log

雖然我提出了上面的思路和方案,但我並不能確保能夠知足全部的需求,我給出下面的思惟流程,方便你們隨機應變:

  1. 儘可能用as的debug模式下的log系統,無入侵。不用寫代碼就能打log,十分方便。
  2. 若是真的要打log作調試,先用debug和error級別,提交代碼時務必記得清除。
  3. 若是提交的代碼中須要在某個關鍵點打log,或者要持續調試,能夠用info以上的log。
  4. 在realse中用本身的log包裝類的開關作處理,這樣方便在公司內部測試時能夠查看到log。
  5. 若是一些信息須要在用戶版本中保留,優先考慮數據統計的方式進行關鍵點的打點。
  6. 若是真的要在發佈出去的apk中帶着log,只保留info級別以上的,不輕易把info級別之下的信息漏出去。

4、總結

咱們能夠看到即便一行代碼的log都有不少點是可優化的,還明白了咱們以前一直寫的模板式代碼是多麼的枯燥乏味。
經過這篇文章,但願你們能夠看到一個優化編碼的思惟過程,也但願你們去嘗試下logger這個庫。固然,我知道仍是有不少人不喜歡,那麼不妨提出更好的解決方案來一塊兒討論,不滿意能夠提issue。
要知道精品永遠是個位數,而中庸的東西永遠是層出不窮的。我但願你們多提意見齊心合力優化出一個精品,而不是花時間去在平庸的選項中作着選擇難題。

5、尾聲

在文章中我給出了經過idea的debug模式下打印log的方法,目的是即便你有了這個log庫,但我仍舊但願你能夠能找到更好的方法來達到調試的目的。擁有技巧,使用技巧,最終化爲無形纔是最高境界。相信咱們的最終目的是一致的,那就是讓開發愈來愈簡便,愈來愈優雅~

最後說下我沒直接用文章開頭那幾個庫的緣由,logger的庫很漂亮,可是冗餘行數過多,調試多行的數據就會受到信息干擾。timber的自己設計就是一個log的框架,打印是交給開發者自定義的,因此我將timber的框架和logger的美觀實現進行告終合。這固然還要感謝logUtils的做者,讓log支持了object類型。

有朋友問,你爲何不本身實現log框架,而是依賴於timber作呢,這樣會不會過重?其實logger的1.1.6版本中,我確實是本身實現了全部的功能,沒有依賴於任何庫。當我看到了timber後,我發現我作的工做和這個庫的重疊性過高了,並且它的設計也很值得學習。因而我直接依賴於它作了重構,我如今只關心log的美化和功能的擴展,log分發的事情就交給timber了。

developer-kale@foxmail.com

微博:@天之界線2010

參考文章:

相關文章
相關標籤/搜索