本文會不定期更新,推薦watch下專案。
如果喜歡請star,如果覺得有紕漏請提交issue,如果你有更好的點子可以提交pull request。
本文的示例程式碼主要是基於logger、LogUtils和timber進行編寫的,如果想了解更多請檢視他們的詳細解釋。
我很推薦大家多多進行對比,選擇適合你自己的庫來使用。
本文固定連線:github.com/tianzhijiex…
一、背景
Android中的log是這麼寫的:
Log.d(TAG, "This is a debug log");複製程式碼
android.util.Log
類做的事情很簡單,符合kiss原則,但是隨著業務的不斷髮展,logcat中就會有多個部門的各種log,不同手機系統自己的一些log也會參雜進來,逼迫我們要擴充套件log類。
二、需求
- 我才不要每次打log都去想tag叫什麼名字呢
- 通常情況下請自動把當前類名作為預設的tag,但也允許我自由指定
- 我希望我寫的模板式程式碼越少越好,一個
logd
就能列印一切 - 我要列印出list,map,json,pojo這樣的物件
- 我的log絕對不要和其餘的雜亂log混在一起
- log資訊過長後應該要自動換行,我不允許我的log列印不全
- 我要我的log變的好看,直觀,就是美
- log中還要能顯示我當前的執行緒名,方便我除錯多執行緒
- 我打出的log後面要根上這個log的地址,可以直接外鏈到log的位置
- release包中不能洩漏我高傲的log,但只要我想讓它顯示,release版本也阻擋不了我
- 在release版本中殘留的log程式碼應該對app執行效率影響極低
- 它能自動將try-catch住的crash通過log上傳到Crashlytics
回看這些需求,不合理麼?其實很合理,我們的宗旨就是讓無意義的重複程式碼去死,如果死不掉就交給機器來做。我們應該做那些真正需要我們做的事情,而不是像一個沒思想的猿猴一般整天寫模板式程式碼。這才是程式設計師思維,而不是程式猿思維!
注意:我希望只要寫真正有意義的內容!
三、實現
分析上述的需求後,我將其分為四類: 使用、顯示和擴充套件。
使用篇
建立包裝類
無論一個第三方庫有多好,我還是推薦不直接使用它,因為你很有可能會去替換這個第三方庫,而且一個第三方庫肯定無法滿足各種奇葩需求。所以,對於網路庫、圖片庫和log庫來說,我們應該事先考慮在上面封裝一層。
我們建立一個包裝類,用這個包裝類用來包裹Logger(logger是本文介紹的一個log庫),下面是包裝類的程式碼片段:
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呢?
在這種情況下我們可以採用如下兩種方案:
- 自行除錯時關閉無關部門的log輸出
- 每個部門有自定義的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/logger和elvishew/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.gradle
z中啟用混淆:
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++;
}
);
}
}複製程式碼
通過console熱部署列印log資訊
我通過debug工具,可以在任意位置列印出任意物件的值,通過這種方式就可以精準除錯一些資訊了。
下圖是我讓其在不中斷執行的情況下列印index的值。
動態設定值
有時候某種分支需要在某個情況下才能走到,我可以利用debug的setValue(F12)方法動態設定值,比如我把下面的123改成了520,最終在終端列印出的資訊也會變成520。整個過程對原本程式碼完全遮蔽,無入侵。
PS:更多的除錯技巧可以檢視Android-Best-Practices中的推薦的除錯技巧的文章。
因地制宜的使用log
雖然我提出了上面的思路和方案,但我並不能確保可以滿足所有的需求,我給出下面的思維流程,方便大家隨機應變:
- 儘量用as的debug模式下的log系統,無入侵。不用寫程式碼就能打log,十分方便。
- 如果真的要打log做除錯,先用debug和error級別,提交程式碼時務必記得清除。
- 如果提交的程式碼中需要在某個關鍵點打log,或者要持續除錯,可以用info以上的log。
- 在realse中用自己的log包裝類的開關做處理,這樣方便在公司內部測試時可以檢視到log。
- 如果一些資訊需要在使用者版本中保留,優先考慮資料統計的方式進行關鍵點的打點。
- 如果真的要在釋出出去的apk中帶著log,只保留info級別以上的,不輕易把info級別之下的資訊漏出去。
四、總結
我們可以看到即使一行程式碼的log都有很多點是可優化的,還明白了我們之前一直寫的模板式程式碼是多麼的枯燥乏味。
通過這篇文章,希望大家可以看到一個優化編碼的思維過程,也希望大家去嘗試下logger這個庫。當然,我知道還是有很多人不喜歡,那麼不妨提出更好的解決方案來一起討論,不滿意可以提issue。
要知道精品永遠是個位數,而中庸的東西永遠是層出不窮的。我希望大家多提意見齊心協力優化出一個精品,而不是花時間去在平庸的選項中做著選擇難題。
五、尾聲
在文章中我給出了通過idea的debug模式下列印log的方法,目的是即使你有了這個log庫,但我仍舊希望你可以能找到更好的方法來達到除錯的目的。擁有技巧,使用技巧,最終化為無形才是最高境界。相信我們的最終目的是一致的,那就是讓開發越來越簡便,越來越優雅~
最後說下我沒直接用文章開頭那幾個庫的原因,logger的庫很漂亮,但是冗餘行數過多,除錯多行的資料就會受到資訊干擾。timber的本身設計就是一個log的框架,列印是交給開發者自定義的,所以我將timber的框架和logger的美觀實現進行了結合。這當然還要感謝logUtils的作者,讓log支援了object型別。
有朋友問,你為什麼不自己實現log框架,而是依賴於timber做呢,這樣會不會太重?其實logger的1.1.6
版本中,我確實是自己實現了所有的功能,沒有依賴於任何庫。當我看到了timber後,我發現我做的工作和這個庫的重疊性太高了,而且它的設計也很值得學習。於是我直接依賴於它做了重構,我現在只關心log的美化和功能的擴充套件,log分發的事情就交給timber了。
參考文章: