Android開發Log最佳實踐-一個簡單、漂亮、功能強大的Android日誌程式:logger

王世暉發表於2016-04-02

Log最佳實踐

概要:使用更好的log來除錯應用。 
本文會不定期更新,推薦watch下專案。如果喜歡請star,如果覺得有紕漏請提交issue,如果你有更好的點子可以提交pull request。本文的示例程式碼主要是基於loggerLogUtilstimber進行編寫的,如果想了解更多請檢視他們的詳細解釋。我很推薦大家多多進行對比,選擇適合你自己的庫來使用。

本文固定連線:https://github.com/tianzhijiexian/Android-Best-Practices

本文推薦的庫:https://github.com/tianzhijiexian/logger

一、需求背景

Android中log是這麼寫的:

  1. Log.d(TAG, "This is a debug log");

我覺得不爽,而且tag連空校驗都沒做!

二、需求

  1. 我才不要每次打log都去想tag叫什麼名字呢
  2. 我希望可以自動把當前類名作為預設的tag
  3. 如果我真的要寫tag了,你就必須顯示我定義的tag
  4. 我希望我寫的模板程式碼越少越好,一個logd就能列印一切
  5. 我要我的log變的好看,直觀,就是美
  6. 我打出的log後面要根上這個log的檔案源頭的連線,我可以直接點選跳轉到log的位置
  7. log中還能提示我當前的執行緒名,方便我除錯
  8. 我還要列印出list,map,json,pojo這樣的物件
  9. release包中不能洩漏我高傲的log
  10. 只要我想讓我的log顯示,release版本也阻擋不了我
  11. log資訊過長後應該要自動換行,我不允許我的log列印不全
  12. 在release版本中殘留的log程式碼應該對效率無影響

注意:我希望只要寫真正有意義的內容!

回看這些需求,不合理麼?很合理,我們的宗旨就是讓無意義的重複程式碼去死,如果死不掉就交給機器來做。我們應該做那些真正需要我們做的事情,而不是像一個沒思想的猿猴一般整天寫模板式程式碼。這才是程式設計師思維,而不是程式猿思維!

三、實現

無論一個第三方庫有多好,我還是推薦不要直接使用它,因為你很有可能會去替換這個第三方庫,而且還可能會有各種意想不到的需求。對於網路請求、圖片請求和log,是應該事先考慮到後續的擴充套件和替換的。

建立包裝類

這個包裝類用來包裹logger(logger是本文介紹的一個log庫),下面是一個程式碼片段:

  1. public static void d(@Nullable String info, Object... args) {
  2. if (!mIsOpen) { // 如果把開關關閉了,那麼就不進行列印
  3. return;
  4. }
  5. Logger.d(info, args);
  6. }

對於包裝類的起名最好不要和“Log”這個名字類似,能有明顯的區別最好,一是防止自己手抖寫錯了,二是方便review的時候方便自己檢查有沒有誤用原始的Log。

自動打tag

現在索性把當前類名作為這樣一個TAG的標識。我們可以通過下面程式碼來設定tag:

  1. private static String getClassName() {
  2. String result;
  3. // 這裡的陣列的index2是根據你工具類的層級做不同的定義,這裡僅僅是關鍵程式碼
  4. StackTraceElement thisMethodStack = (new Exception()).getStackTrace()[2];
  5. result = thisMethodStack.getClassName();
  6. int lastIndex = result.lastIndexOf(".");
  7. result = result.substring(lastIndex + 1, result.length());
  8. return result;
  9. }

這樣我們就輕易的擺脫了tag的糾纏。

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

自定義tag

我們想要偶爾打打log的tag方便做其他的處理:

  1. public static void d(@NonNull String tag, String info, Object... args) {
  2. Logger.t(tag).d(info, args);
  3. }

我上面的做法是把tag用getSimpleName的方式來得到,但會因為混淆的問題在混淆的包裡出現a.b.c這樣的類名。如果你的log是要出現在混淆的包裡的,強烈建議去手動設定tag值,否則你完全就沒辦法過濾了。至於如何手動設定tag的值,下面會講到logt這個快捷命令。

將Log程式碼快捷模板

有人說我們IDE不都有程式碼提示了麼,你還想怎麼簡化log的輸入呢?這裡可以利用as的一個模板提示的功能: 
QQ截圖20151214150341.png-112.7kB

我們可以模仿這裡原有的模板來做自己的程式碼模板,簡化模板式程式碼的輸入。至於具體模仿的方式我就不手把手教了,簡單到爆。下面僅展示下自帶的log模板的使用方式: 
寫tag: 
log1.gif-64.8kB 
自動填寫引數和方法名: 
log2.gif-83.7kB

讓log更加美觀

我要美,要直觀,要夠酷!做到這點也簡單,就是在輸出前做點字串拼接的工作,比如加上下面這行橫線。

  1. private static final String BOTTOM_BORDER = "╚═══════════════════════════";

因為做了很多拼接的工作,所以好看的log也是消耗效能的。我的習慣是除錯完畢後立刻刪除無用的log,這樣既能減少效能影響,也減少同事的閱讀程式碼的負擔,效果如下: 
QQ截圖20160327093141.png-197.6kB

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

這個功能其實ide是原生支援的,不相信的話你隨便用原生的log列印出onCreate: (MainActivity.java:39)試試。只不過我們可以通過一些神奇的方法來做到更好的效果:

  1. private static String callMethodAndLine() {
  2. String result = "at ";
  3. StackTraceElement thisMethodStack = (new Exception()).getStackTrace()[1];
  4. result += thisMethodStack.getClassName()+ "."; // 當前的類名(全名)
  5. result += thisMethodStack.getMethodName();
  6. result += "(" + thisMethodStack.getFileName();
  7. result += ":" + thisMethodStack.getLineNumber() + ") ";
  8. return result;
  9. }

這裡同樣需要注意的是在混淆後是得不到正確的類名的,所以可以酌情讓activity、fragment、view不被混淆,具體方案還是看自己的取捨。

支援POJO、Map、Collection、jsonStr、Array

這個需求實現起來也比較容易,如果是簡單的POJO的物件,我們可用反射得到物件的類變數,通過字串拼接的方式最終輸出值。如果是map等陣列結構,那麼就用其內部的遍歷依次輸出值和內容。如果是json的字串,就需要判斷json的{},[]這樣的特殊字元進行換行處理。至於具體的程式碼是怎樣了,大家移步去看原始碼就好,這個不是重點。重點是結果: 
QQ截圖20160327093200.png-156.6kB

增加自動化或強制開關

區分release和debug版本有系統自帶的BuildConfig.DEBUG變數,用這個就可以控制是否顯示log了。強制開關也很簡單,在log初始化的最後判斷強制開關是否開啟,如果開啟那麼就覆蓋之前的顯示設定,直接顯示log。轉為程式碼就是這樣:

  1. public class BaseApplication extends Application {
  2. // 定義是否是強制顯示log的模式
  3. protected static final boolean LOG = false;
  4. @Override
  5. public void onCreate() {
  6. Logger.initialize(
  7. Settings.getInstance()
  8. .setLogPriority(BuildConfig.DEBUG ? Log.VERBOSE : Log.ASSERT)
  9. );
  10. // 如果是強制顯示log,那麼無論在什麼模式下都顯示log
  11. if (LOG) {
  12. Logger.setLogPriority(Log.VERBOSE)
  13. }
  14. }
  15. }

支援超長的log資訊

有時候網路的返回值是很長的,android.util.Log類是有最大長度限制的,為了解決這個問題。我們只需要判斷這個字串的長度,然後手動讓其換行即可。

  1. private static final int CHUNK_SIZE = 4000;
  2. if (length <= CHUNK_SIZE) {
  3. logContent(logType, tag, msg);
  4. } else {
  5. for (int i = 0; i < length; i += CHUNK_SIZE) {
  6. int count = Math.min(length - i, CHUNK_SIZE);
  7. //create a new String with system's default charset (which is UTF-8 for Android)
  8. logContent(logType, tag, new String(bytes, i, count));
  9. }
  10. }

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

多引數log資訊應該這樣列印,避免拼接好後再列印。這樣在關閉log後就不會進行字串的拼接工作了,減少log語句在release版本中的效能影響。

  1. Logger.d("test %s%s", "v", 5); // test v5

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

各種需求和應對方案

雖然提出了上面的思路和方案,但我並不能確保可以滿足所有的需求,我給出下面的思維流程,方便大家隨機應變:

優先用ida的debug系統實在不行就在debug和error級別打log資訊不夠時才在info、warning、error的級別打提交測試時,刪除無用log,預留測試環境中的log開關正式版中優先用資料統計平臺或應用打點工具來代替log日誌如果必須出現log,正式版中僅洩漏info級別以上的log,即warn和erro

說明: 
1. 儘量用as的debug模式下的log系統,無入侵。不用寫程式碼就能打log,十分方便。(下文會介紹) 
2. 如果真的要打log做除錯,先放在debug和error級別,提交程式碼時務必記得清除。 
3. 如果提交的程式碼中需要在某個關鍵點打log,或者要給同事看這些log,可以放在在info級別以上。 
4. 在realse中推薦用自己的log包裝類的開關做處理,這樣方便在公司內部測試時可以檢視到log。 
5. 如果一些資訊需要在發出去的使用者版本中出現,優先考慮資料統計的方式進行關鍵點的資料打點。 
6. 如果真的要在正式釋出的apk中還帶著log,只保留info級別以上的,不把info級別之下的資訊漏出去。

四、IDEA的超強debug技巧

上文中我就提到了可以利用as的除錯模式來加速debug,下面分享下兩個和log有關的經驗。 
測試程式碼:

  1. public class MainActivity extends AppCompatActivity {
  2. private static final String TAG = "MainActivity";
  3. private int index = 0;
  4. @Override
  5. protected void onCreate(Bundle savedInstanceState) {
  6. super.onCreate(savedInstanceState);
  7. Button button = (Button) findViewById(R.id.button);
  8. button.setOnClickListener(v-> {
  9. index = 123;
  10. Log.d(TAG, "onClick: index = " + index);
  11. index++;
  12. }
  13. );
  1. 通過console熱部署列印log資訊 
    我通過debug工具,可以在任意位置列印出任意物件的值,通過這種方式就可以精準除錯一些資訊了。這回我是讓其在不中斷執行的情況下列印index的值。 
    debug1.gif-158.1kB

  2. 動態設定值 
    有時候某種分支需要在某個情況下才能走到,我可以利用debug的setValue(F12)方法動態設定值,比如我把下面的123改成了520,最終在終端列印出的資訊也會變成520。整個過程對原本程式碼完全遮蔽,無入侵。 
    debug2.gif-273.6kB

五、最終成果

依賴庫: 
https://github.com/tianzhijiexian/logger 
如果你覺得這個庫不好,請提交issue,萬不可冷嘲熱諷。要知道精品永遠是個位數,而中庸的東西永遠是層出不窮的。我希望大家多提意見齊心協力優化出一個精品,而不是花時間去在平庸的選項中做著選擇難題。

六、後記

我們可以看到即使一行程式碼的log都有很多點是可優化的,還明白了我們之前一直寫的模板式程式碼是多麼的枯燥乏味。 
通過這篇文章,希望大家可以看到一個優化編碼的思維過程,也希望大家去嘗試下logger這個庫。當然,我知道還是有很多人不喜歡,那麼不妨提出更好的解決方案來一起討論,不滿意可以提嘛。 
寧信書則不如無書,具體如何使用還得看自己的需求,歡迎通過郵件或者是gitter的方式進行交流。

在文章後面我也給出了通過idea的debug模式下列印log的方法,意思是即使你有了這個log庫,但是我仍舊希望你可以能找到更好的方法來達到目的,擁有技巧,使用技巧,最終化為無形才是最高境界。相信我們的最終目的是一致的,那就是讓開發越來越簡便,越來越優雅~

最後說下我沒直接用文章開頭那幾個庫的原因,logger的庫很漂亮,但是冗餘行數過多,除錯多行的資料就會受到資訊干擾,timber的本身設計就是一個log的框架,列印是交給開發者自定義的。所以我將timber的框架和logger的美觀實現進行了結合。這當然還要感謝logUtils的作者,讓log支援了object型別。

參考文章

http://ihongqiqu.com/blog/2014/10/16/android-log/ 
https://github.com/pengwei1024/LogUtils 
https://github.com/orhanobut/logger 
http://droidyue.com/blog/2015/11/01/thinking-about-android-log/ 
https://github.com/JakeWharton/timber

作者

avatar.png-55.5kB

developer_kale@foxmail.com 
@天之界線2010

相關文章