本文微信公众号「AndroidTraveler」首发。

背景

在开发过程中,调试是必不可少的一项工作。

当我们要确定项目的逻辑时,当我们要了解界面的生命周期时,当我们发现新写的逻辑与期望效果不一致时,当我们觉得数据有问题时......

而调试有两种方式:

第一种就是使用 debug 模式运行 APP,然后通过断点让程序运行到指定位置进行分析。

第二种就是打日志的方式,通过观察输出来确定程序是否运行到该位置以及此时的数据。

本篇文章主要聚焦在第二种方式上面。

在 Android 里面,打日志使用的系统 API 是 Log,你以为直接使用就完了吗?

封装

假设你在需要打印日志的地方直接使用系统的 API,那么当遇到下面情况时,会「牵一发而动全身」。

场景一:如果我打印日志要用三方库的日志 API,那么我要查找项目所有使用位置,并一一替换。

场景二:如果我希望在开发环境下打印日志,release 环境不打印,这个时候每个位置都需要单独做处理。

因此我们需要在使用 Log 进行日志打印之前,做一层封装。

假设我们的类名字为 ZLog,代码如下:

import android.util.Log;

/**
* Created on 2019-10-26
*
* @author Zengyu.Zhan
*/
public class ZLog {
public static int v(String tag, String msg) {
return Log.v(tag, msg);
} public static int d(String tag, String msg) {
return Log.d(tag, msg);
} public static int i(String tag, String msg) {
return Log.i(tag, msg);
} public static int w(String tag, String msg) {
return Log.w(tag, msg);
} public static int e(String tag, String msg) {
return Log.e(tag, msg);
}
}

这样处理之后,对于场景一和场景二,我们需要修改的只是 ZLog 这个类,而不需要到具体使用 ZLog 的所有地方去修改。

提供日志打印控制

我们知道,日志打印可能包含敏感信息,而且过多的日志打印可能影响 APP 的性能,因此我们一般是在开发时候打开日志,在发布 APP 之前关闭。

因此我们这边需要提供一个标志位来控制日志的打印与否。

import android.util.Log;

/**
* Created on 2019-10-26
*
* @author Zengyu.Zhan
*/
public class ZLog {
private static boolean isDebugMode = false;
public static void setDebugMode(boolean debugMode) {
isDebugMode = debugMode;
} public static int v(String tag, String msg) {
return isDebugMode ? Log.v(tag, msg) : -1;
} public static int d(String tag, String msg) {
return isDebugMode ? Log.d(tag, msg) : -1;
} public static int i(String tag, String msg) {
return isDebugMode ? Log.i(tag, msg) : -1;
} public static int w(String tag, String msg) {
return isDebugMode ? Log.w(tag, msg) : -1;
} public static int e(String tag, String msg) {
return isDebugMode ? Log.e(tag, msg) : -1;
}
}

默认是不开启日志打印,避免开发者忘记设置。

普通日志和奔溃栈系统日志在控制台的输出对比

现在我们在 APP 里面使用 ZLog 打印日志,代码为:

ZLog.setDebugMode(true);
ZLog.e("ZLog", "just test");

输出如下:

我们现在增加如下代码:

String nullString = null;
if (nullString.equals("null")) {
}

运行之后控制台会显示空指针异常奔溃栈,如下:

可以看到奔溃栈信息会显示具体是哪个文件出现了空指针,以及具体哪一行。在我们这个例子里面就是 MainActivity.java24 行。

而且点击蓝色链接光标会直接定位到错误位置。

如果我们普通的日志也可以点击就跳转到对应位置,对于我们开发来说效率是有很大提升的。

ZLogHelper

既然奔溃栈里面有链接可以跳转,那么我们可以通过栈信息来获取日志的打印位置。

我们直接上代码:

public class ZLogHelper {
private static final int CALL_STACK_INDEX = 1;
private static final Pattern ANONYMOUS_CLASS = Pattern.compile("(\\$\\d+)+$"); public static String wrapMessage(int stackIndex, String message) {
// DO NOT switch this to Thread.getCurrentThread().getStackTrace().
if (stackIndex < 0) {
stackIndex = CALL_STACK_INDEX;
}
StackTraceElement[] stackTrace = new Throwable().getStackTrace();
if (stackTrace.length <= stackIndex) {
throw new IllegalStateException(
"Synthetic stacktrace didn't have enough elements: are you using proguard?");
}
String clazz = extractClassName(stackTrace[stackIndex]);
int lineNumber = stackTrace[stackIndex].getLineNumber();
message = ".(" + clazz + ".java:" + lineNumber + ") - " + message;
return message;
} /**
* Extract the class name without any anonymous class suffixes (e.g., {@code Foo$1}
* becomes {@code Foo}).
*/
private static String extractClassName(StackTraceElement element) {
String tag = element.getClassName();
Matcher m = ANONYMOUS_CLASS.matcher(tag);
if (m.find()) {
tag = m.replaceAll("");
}
return tag.substring(tag.lastIndexOf('.') + 1);
}
}

这里我们对外提供一个 wrapMessage 方法,看名字就知道是对 Message 进行包装。

方法里面也是对 StackTraceElement 进行分析。

这边还做了一个控制,避免 stackIndex 出现负数情况。

可能有小伙伴会好奇,为什么要把 stackIndex 对外开放呢?

因为你打印日志的地方不一样,这里的 stackIndex 也需要对应调整。

方法里面是对 StackTraceElement 做处理,而 StackTraceElement 跟你的方法层级有关系。

我们以最常用的两种日志打印形式为例,来说明这里的 stackIndex 要怎么传递,以及这个 ZLogHelper 的用法。

直接代码使用

我们在 MainActivity.java 中直接使用,stackIndex 传入 1 即可。

Log.e("ZLog", ZLogHelper.wrapMessage(1, "just test"));

控制台输出如下:



可以看到代码所在的类和行数到显示为链接文本,点击会定位到具体的位置。

做了封装的情况

一般我们对 Log 都会做封装,因此假设我们有一个 LogUtils 类,我们在 MainActivity.java 里面调用。

LogUtils.java:

class LogUtils {
public static void loge() {
Log.e("ZLog", ZLogHelper.wrapMessage(2, "just test"));
}
}

MainActivity.java:

LogUtils.loge();

我们先看下结果,再来分析。控制台输出如下:

可以看到确实定位到了 MainActivity.java 中的具体使用地方。

那么为什么这里传入的 stackIndex 跟第一种不一样,是 2 而不是 1 呢?

其实答案很简单,你改为 1 之后,输出的控制台显示的会定位到 LogUtils 里面的日志打印语句处。在这里就是:

Log.e("ZLog", ZLogHelper.wrapMessage(2, "just test"));

所以其实你可以看出一个规律,而这个从代码也可以发现。

因为代码里面解析调用位置是根据栈来的,对 StackTraceElement 进行分析,因此情况一直接使用,传入 1。而情况二多了一层函数调用,通过 loge 方法做了一层包装。因此需要传入 2。如果你再套一层,那么需要传入 3。了解了这一点,我们下面的工具类相信你就看得懂了。

ZLog

如果你不想自己手动传入 stackIndex,可以直接使用我们提供的工具类 ZLog。

public class ZLog {
private static boolean isDebugMode = false;
public static void setDebugMode(boolean debugMode) {
isDebugMode = debugMode;
} private static boolean isLinkMode = true;
public static void setLinkMode(boolean linkMode) {
isLinkMode = linkMode;
} private static final int CALL_STACK_INDEX = 3; public static int v(String tag, String msg) {
return isDebugMode ? Log.v(tag, mapMsg(msg)) : -1;
} public static int d(String tag, String msg) {
return isDebugMode ? Log.d(tag, mapMsg(msg)) : -1;
} public static int i(String tag, String msg) {
return isDebugMode ? Log.i(tag, mapMsg(msg)) : -1;
} public static int w(String tag, String msg) {
return isDebugMode ? Log.w(tag, mapMsg(msg)) : -1;
} public static int e(String tag, String msg) {
return isDebugMode ? Log.e(tag, mapMsg(msg)) : -1;
} private static String mapMsg(String msg) {
return isLinkMode ? ZLogHelper.wrapMessage(CALL_STACK_INDEX, msg) : msg;
}
}

相信有了前面的知识,小伙伴对于这里为什么传入 3 应该了解了。

1 的话会定位到

return isLinkMode ? ZLogHelper.wrapMessage(CALL_STACK_INDEX, msg) : msg;

2 的话(以 e 为例)会定位到

return isDebugMode ? Log.e(tag, mapMsg(msg)) : -1;

3 的话才能够定位到外面具体的调用处。

优化

我们知道,虽然 ZLog 做了封装,但是我们每次打日志都要传入 ZLog,有点麻烦?

能否提供一个默认的 TAG,允许对外设置。

可以的,我们修改如下(以 e 为例):

private static String tag = "ZLOG";
public static void setTag(String tag) {
if (!TextUtils.isEmpty(tag)) {
ZLog.tag = tag;
}
} public static int e(String tag, String msg) {
return isDebugMode ? Log.e(mapTag(tag), mapMsg(msg)) : -1;
} public static int e(String msg) {
return isDebugMode ? Log.e(tag, mapMsg(msg)) : -1;
} private static String mapTag(String tag) {
return TextUtils.isEmpty(tag) ? ZLog.tag : tag;
}

项目实战

按照下面两步引入开源库。

Step 1. Add the JitPack repository to your build file

Add it in your root build.gradle at the end of repositories:

allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}

Step 2. Add the dependency

dependencies {
implementation 'com.github.nesger:AndroidWheel:1.0.0'
}

使用时先打开开关:

ZLog.setDebugMode(true);

然后就可以直接使用了。

温馨提示

由于带链接的 debug 对性能有一定影响,因此建议开发使用,上线关闭。

结语

这边在完善一个开源仓库 AndroidWheel,跟名字一样,避免重复造轮子。

目前 1.0.0 版本提供日志相关工具类,1.0.1 增加了防抖动 EditText。

后续会继续更新迭代,功能会更完善更全面。

觉得不错,欢迎给个 star 哈~

参考链接:

Android Studio Pro Tip: go to source from logcat output

Android Debug 之 Log 最佳实践的更多相关文章

  1. Google Developing for Android 三 - Performance最佳实践

    Google Developing for Android 三 - Performance最佳实践 发表于 2015-06-07   |   分类于 Android最佳实践 原文 Developing ...

  2. Google Developing for Android 二 - Memory 最佳实践 // lightSky‘Blog

    Google Developing for Android 二 - Memory 最佳实践   |   分类于 Android最佳实践 原文:Developing for Android, II Th ...

  3. Android和PHP开发最佳实践

    Android和PHP开发最佳实践 <Android和PHP开发最佳实践>基本信息作者: 黄隽实丛书名: 移动应用开发技术丛书出版社:机械工业出版社ISBN:9787111410508上架 ...

  4. Android 路由框架ARouter最佳实践

    转载请标明出处:http://blog.csdn.net/zhaoyanjun6/article/details/76165252 本文出自[赵彦军的博客] 一:什么是路由? 说简单点就是映射页面跳转 ...

  5. android: SQLite 数据库的最佳实践

    6.5.1    使用事务 前面我们已经知道,SQLite 数据库是支持事务的,事务的特性可以保证让某一系列的操 作要么全部完成,要么一个都不会完成.那么在什么情况下才需要使用事务呢?想象以下场 景, ...

  6. android:活动的最佳实践

    2.6.1    知晓当前是在哪一个活动 这个技巧将教会你,如何根据程序当前的界面就能判断出这是哪一个活动.可能你会觉 得挺纳闷的,我自己写的代码怎么会不知道这是哪一个活动呢?很不幸的是,在你真正进入 ...

  7. Android中活动的最佳实践(如何很快的看懂别人的代码activity)

    这种方法主要在你拿到别人的代码时候很多activity一时半会儿看不懂,用了这个方法以后就可以边实践操作就能够知道具体哪个activity是干什么用的 1.新建一个BaseActivity的类,让他继 ...

  8. Android Jetpack 架构组件最佳实践之“网抑云”APP

    背景 近几年,Android 相关的新技术层出不穷.往往这个技术还没学完,下一个新技术又出来了.很多人都是一脸黑人问号? 不少开发者甚至开始哀嚎:"求求你们别再创造新技术了,我们学不动了!& ...

  9. Android应用中MVP最佳实践

    转自:http://www.jianshu.com/p/ed2aa9546c2c 文/Jude95(简书作者)原文链接:http://www.jianshu.com/p/ed2aa9546c2c著作权 ...

随机推荐

  1. python自动化测试三部曲之request+django实现接口测试

    国庆期间准备写三篇博客,介绍和总结下接口测试,由于国庆期间带娃,没有按照计划完成,今天才完成第二篇,惭愧惭愧. 这里我第一篇博客的地址:https://www.cnblogs.com/bainianm ...

  2. Java内存模型总结

    Java内存模型 内存模型可以理解为在特定的操作协议下,对特定的内存或者高速缓存进行读写访问的过程抽象,不同架构下的物理机拥有不一样的内存模型,Java虚拟机也有自己的内存模型,即Java内存模型(J ...

  3. JS相关实训

    今天又是无聊的一天,我的脑袋一直在嗡嗡叫,想着一些奇怪的问题,比如我为什么总是感到这么失落,为什么我喜欢的女孩不喜欢我呢,真是头大啊.不过既然有作业了我这个五好公民当然要认真写了.没时间让我思考这么复 ...

  4. [JZOJ5778]【NOIP提高A组模拟2018.8.8】没有硝烟的战争

    Description 被污染的灰灰草原上有羊和狼.有N只动物围成一圈,每只动物是羊或狼.该游戏从其中的一只动物开始,报出[1,K]区间的整数,若上一只动物报出的数是x,下一只动物可以报[x+1,x+ ...

  5. 备战双 11!蚂蚁金服万级规模 K8s 集群管理系统如何设计?

    作者 | 蚂蚁金服技术专家 沧漠 关注『阿里巴巴云原生』公众号,回复关键词"1024",可获取本文 PPT. 前言 Kubernetes 以其超前的设计理念和优秀的技术架构,在容器 ...

  6. JAVA aio简单使用

    使用aio,实现客户端和服务器 对一个数进行轮流累加 //服务器端 public class Server { private static ExecutorService executorServi ...

  7. PHP spl_autoload和class_exsits使用技能

    本文章的PHP使用版本:5.4.7 PHP建议使用: spl_autoload_register 那么写了一种实现 文件路径 core core.php ChildrenClass.php Paren ...

  8. 3D切割轮播图

    预览图: 实现原理:将图片切割构建一个和ul(电脑屏幕)同一个轴的立方体,利用延时旋转实现切割效果 知识点:transform-style属性(必须搭配transform属性使用) 值 描述 flat ...

  9. vue中改变数组的值视图无变化

    今天开发的时候遇到一个多选取消点击状态的,渲染的时候先默认都选中,然后可以取消选中,自建了一个全为true的数组,点击时对应下标的arr[index]改为false,数据改变了状态没更新,突然想起来单 ...

  10. SpringBoot是如何启动的?

    本文是通过查看SpringBoot源码整理出来的SpringBoot大致启动流程,整体大方向是以简单为出发点,不说太多复杂的东西,内部实现细节本文不深扣因为每个人的思路.理解都不一样,我个人看的理解跟 ...