Android Debug 之 Log 最佳实践

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

背景

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

当咱们要肯定项目的逻辑时,当咱们要了解界面的生命周期时,当咱们发现新写的逻辑与指望效果不一致时,当咱们以为数据有问题时......git

而调试有两种方式:github

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

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

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

在 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

相关文章
相关标签/搜索