本文会不按期更新,推荐watch下项目。
若是喜欢请star,若是以为有纰漏请提交issue,若是你有更好的点子能够提交pull request。
本文的示例代码主要是基于logger、LogUtils和timber进行编写的,若是想了解更多请查看他们的详细解释。
我很推荐你们多多进行对比,选择适合你本身的库来使用。 javascript
本文固定链接:github.com/tianzhijiex…php
Android中的log是这么写的:html
Log.d(TAG, "This is a debug log");复制代码
android.util.Log
类作的事情很简单,符合kiss原则,可是随着业务的不断发展,logcat中就会有多个部门的各类log,不一样手机系统本身的一些log也会参杂进来,逼迫咱们要扩展log类。java
logd
就能打印一切回看这些需求,不合理么?其实很合理,咱们的宗旨就是让无心义的重复代码去死,若是死不掉就交给机器来作。咱们应该作那些真正须要咱们作的事情,而不是像一个没思想的猿猴通常成天写模板式代码。这才是程序员思惟,而不是程序猿思惟!android
注意:我但愿只要写真正有意义的内容!git
分析上述的需求后,我将其分为四类: 使用、显示和扩展。程序员
不管一个第三方库有多好,我仍是推荐不直接使用它,由于你颇有可能会去替换这个第三方库,并且一个第三方库确定没法知足各类奇葩需求。因此,对于网络库、图片库和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
的默认值,咱们能够经过下面代码来获得当前类名:
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:
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
这个快捷命令。
若是你的项目很庞大或者采用了插件化和组件化方案,那么你确定会涉及到多人开发的问题。底层平台是暴露统一的log接口,可是上层开发人员种类繁多,如何在繁杂的log中找到本身部门的本身关心的log呢?
在这种状况下咱们能够采用以下两种方案:
对于方案一,咱们自己的log系统底层采用的是timber,它自己就是经过“种树”的方式进行log分发的,咱们只须要在咱们项目的最开始调用
Logger.uprootAll();
// or
Timber.uprootAll();复制代码
将全部以前的log通道移除,这样就清空了无用的log了。
相比起方案一的简单粗暴,方案二却是温和实用的多。咱们经过在logger初始化设置一个tagPrefix
,这个前缀就会伴随着咱们私有项目的全部log了,之后直接搜索这个前缀就能够过滤出想要的信息了。
有时候在调试过程当中可能会要支持测试同窗的动态关闭和开启log的功能。
Logger.closeLog();
Logger.openLog(Log.INFO);复制代码
这个操做能够支持在应用运行的时的任什么时候候进行开关。
有人说咱们IDE不都有代码提示了么,你还想怎么简化log的输入呢?这里能够利用as的模板提示的功能:
咱们能够模仿原有的模板来作本身的代码模板,简化模板式代码的输入。至于具体模仿的方式我就不手把手教了,至关简单。下面仅展现下自带的log模板的使用:
生成TAG:
让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的尾巴上会带有线程的名字,方便你们进行调试。
这个需求实现起来也比较容易:
{}
,[]
这样的特殊字符进行换行处理至于具体是如何实现的,你们移步去看源码就好,这个不是重点,重点是结果:
不推荐打印每次网络请求的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样式,既保证了清晰易辨认又不会带来过多的冗余信息。
有时候网络的返回值是很长的,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语句在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代码在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。这样虽然让其没崩溃,可是也隐藏了错误,以致于咱们始终没有办法弄懂错误出现的缘由。
我但愿能够经过把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可能须要分发到不一样的系统,这也是我采用timber的缘由。咱们除了将线上的错误分发到崩溃统计系统外,也可能要将log保存到sd卡或是作其余的处理,因此目前logger利用timber的tree实现了分发的功能。
Logger内部的实现:
public static void plant(Timber.Tree tree) {
Timber.plant(tree);
}复制代码
关于如何plant能够参考下Timber的具体代码。
大多数团队会定义本身的log类来进行log的打印,咱们最好能够经过自定义的lint来在代码编写时防止开发者错用log类。
详细的内容能够参考:《Android自定义Lint实践》
上文中我就提到了能够利用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都有不少点是可优化的,还明白了咱们以前一直写的模板式代码是多么的枯燥乏味。
经过这篇文章,但愿你们能够看到一个优化编码的思惟过程,也但愿你们去尝试下logger这个库。固然,我知道仍是有不少人不喜欢,那么不妨提出更好的解决方案来一块儿讨论,不满意能够提issue。
要知道精品永远是个位数,而中庸的东西永远是层出不穷的。我但愿你们多提意见齐心合力优化出一个精品,而不是花时间去在平庸的选项中作着选择难题。
在文章中我给出了经过idea的debug模式下打印log的方法,目的是即便你有了这个log库,但我仍旧但愿你能够能找到更好的方法来达到调试的目的。拥有技巧,使用技巧,最终化为无形才是最高境界。相信咱们的最终目的是一致的,那就是让开发愈来愈简便,愈来愈优雅~
最后说下我没直接用文章开头那几个库的缘由,logger的库很漂亮,可是冗余行数过多,调试多行的数据就会受到信息干扰。timber的自己设计就是一个log的框架,打印是交给开发者自定义的,因此我将timber的框架和logger的美观实现进行告终合。这固然还要感谢logUtils的做者,让log支持了object类型。
有朋友问,你为何不本身实现log框架,而是依赖于timber作呢,这样会不会过重?其实logger的1.1.6
版本中,我确实是本身实现了全部的功能,没有依赖于任何库。当我看到了timber后,我发现我作的工做和这个库的重叠性过高了,并且它的设计也很值得学习。因而我直接依赖于它作了重构,我如今只关心log的美化和功能的扩展,log分发的事情就交给timber了。
参考文章: