使用更加清晰的DebugLog工具开发调试

在开发应用过程中免不了开发调试找错的过程,相信很多做过iOS开发的程序猿都对Xcode的debug调试功能大加赞赏。但是在做Android开发过程中,却不那么方便,尽管IDE也提供了debug模式提供给开发者使用。就我个人而言eclipse的debug调试较之于Xcode可以说是一个天上,一个地下。因此,在日常开发中,常使用到的便是android.util包下的Log类进行调试打印输出。当然很多筒子们仍会继续沿用System.out.println来打印输出,在Android开发中并不推荐此种方式。不仅会代码冗余,而且在程序编译打包时去除Log会十分的繁琐。

下面我们先来看看一般情况下在LogCat输出日志信息所做的操作

public class MainActivity extends Activity {
	public static final String TAG = MainActivity.class.getSimpleName(); // "MainActivity"

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);

		android.util.Log.d("test", "我是测试信息");
	}
}

此时我们可以看到控制台会输出


上述情况是最基本的输出调试。此时我们知道有很大局限性。假如我们的项目非常的庞大,代码量达到几十万行的层级时。为了不消耗资源,在发布打包版本的时候需要去除打印输出语句,这是就显得很乏力。此时可能我们会想出很多办法比如定义一个布尔类型的debug开关,在需要打包的时候将其关闭。具体请看实现,下面是我一年前封装的一个Log类

package com.example.debuglog;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;

import android.os.Environment;
/**
 * @author J!nl!n
 * @date 2013-12-30
 * @time 下午12:29:18
 * @todo 提供扩展Log类
 */
public class Log {
//	private static boolean isOpen = isOpenLog();
	private static boolean	isOpen	= true;

	public static void e(String tag, String msg) {
		if (isOpen) {
			android.util.Log.e(tag, msg);
		}
	}

	public static void w(String tag, String msg) {
		if (isOpen) {
			android.util.Log.w(tag, msg);
		}
	}

	public static void d(String tag, String msg) {
		if (isOpen) {
			android.util.Log.d(tag, msg);
		}
	}

	public static void i(String tag, String msg) {
		if (isOpen) {
			android.util.Log.i(tag, msg);
		}
	}
	
	public static void v(String tag, String msg) {
		if (isOpen) {
			android.util.Log.v(tag, msg);
		}
	}

	public static void t(String tag, String msg) {
		if (isOpen) {
			android.util.Log.i(tag, msg + " : " + System.currentTimeMillis());
		}
	}

	public static void f(String fileName, String msg) {
		if (isOpen) {
			d(fileName, msg);
			File fileDir = new File(Environment.getExternalStorageDirectory(), "/mlogs/");
			File logFile = new File(fileDir, fileName);

			FileWriter fileOutputStream = null;
			try {
				if (!fileDir.exists()) {
					if (!fileDir.mkdirs()) {
						return;
					}
				}
				if (!logFile.exists()) {
					if (!logFile.createNewFile()) {
						return;
					}
				}
				fileOutputStream = new FileWriter(logFile, true);
				fileOutputStream.write(msg);
				fileOutputStream.flush();
			} catch (Exception e) {
			} finally {
				if (fileOutputStream != null) {
					try {
						fileOutputStream.close();
					} catch (IOException e) {
					}
				}
			}
		}
	}

	public static void printStackTrace(Exception e) {
		if (isOpen) {
			e.printStackTrace();
		}
	}

	public static boolean isOpenLog() {
		if (!isSDCardAvailable())
			return false;
		String path = Environment.getExternalStorageDirectory().getPath() + "/log.txt";
		return (new File(path).exists());
	}

	/**
	 * @TODO sdcard是否可用
	 */
	public static boolean isSDCardAvailable() {
		if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
			return true;
		}
		return false;
	}

}

一般我们会创建一个常量TAG,如:

public static final String TAG = MainActivity.class.getSimpleName(); // "MainActivity"

然后在需要输出的时候调用Log的静态方法d[debug-蓝色]、i[info-绿色]、w[warn-黄色]、e[error-红色]、v[verbose-黑色]、t[time-带时间的info]进行输出

public class MainActivity extends Activity {
	public static final String TAG = MainActivity.class.getSimpleName(); // "MainActivity"

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);

		android.util.Log.d("test", "我是测试信息");
		Log.d(TAG, "我是debug测试信息");
		Log.e(TAG, "我是error测试信息");
		Log.w(TAG, "我是warn测试信息");
		Log.i(TAG, "我是info测试信息");
		Log.t(TAG, "我是time测试信息");
	}
}

 
 
 
 

 
 

我们可以看到控制台输出的结果为这样的

使用更加清晰的DebugLog工具开发调试_第1张图片

注意:此处的时间我并没有做本地化处理,直接使用的当前的毫秒数。主要是因为用的不多,如有需要可以做一点点转化。

在该类中,我们可以使用isOpen这个开关对程序的log信息做相应的关闭处理。同时我们提供一个方法可以对打包成正式版本的apk进行调试。即如果SDcard根目录下下存在log.txt文件时就输出调试打印信息。这里我们打开

private static boolean isOpen = isOpenLog();

此时控制台LogCat是没有任何Log的打印输出的,如下图:

使用更加清晰的DebugLog工具开发调试_第2张图片

然后我们新建一个log.txt然后放到SDcard根目录下即可,此时我们可以看到熟悉的打印调试信息又出来了,这样就可以对打包完成后的应用进行调试了。

使用更加清晰的DebugLog工具开发调试_第3张图片

以上我们已经成功实现一个可动态关闭的Log调试工具类。基本功能都已经成功实现,但是我觉得还不够。因为经过漫长的时间之后,这种方法的劣势明显暴露。我们已然忘记当初打log的地方,寻找起来十分繁琐。因此接下来将介绍本篇的主角DebugLog。我们先来试用一下,定义一个简单的方法

void mySecondFunc() {
	DebugLog.v("simple log from mySecondFunc()");
}

此时观察LogCat,可以发现我们并没有做任何操作,即打印出所在类、方法、甚至调用的行号。根据相应信息即可迅速定位到打印输出语句,此时即可对它进行修改、删除等处理操作。


我们来看下源码实现

/***
 * This is free and unencumbered software released into the public domain. Anyone is free to copy, modify, publish, use, compile, sell, or distribute this
 * software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means. For more information, please
 * refer to <http://unlicense.org/>
 */
package com.example.debuglog;

import android.util.Log;

/**
 * @author J!nl!n
 * @date 2014年11月19日
 * @time 下午9:05:46
 * @type DebugLog.java
 * @todo 多功能调试工具类
 */
public class DebugLog {
	/**
	 * Log输出所在类
	 */
	private static String className;
	/**
	 * Log输出所在方法
	 */
	private static String methodName;
	/**
	 * Log输出所行号
	 */
	private static int lineNumber;

	/**
	 * 是否可Debug状态
	 * 
	 * @return
	 */
	public static boolean isDebuggable() {
		return BuildConfig.DEBUG;
	}

	/**
	 * 创建Log输出的基本信息
	 * 
	 * @param log
	 * @return
	 */
	private static String createLog(String log) {
		StringBuffer buffer = new StringBuffer();
		buffer.append("[");
		buffer.append(methodName);
		buffer.append("()");
		buffer.append(" line:");
		buffer.append(lineNumber);
		buffer.append("] ");
		buffer.append(log);

		return buffer.toString();
	}

	/**
	 * 取得输出所在位置的信息 className methodName lineNumber
	 * 
	 * @param sElements
	 */
	private static void getMethodNames(StackTraceElement[] sElements) {
		// 拆分去除.java
		className = sElements[1].getFileName().split("\\.")[0];
		methodName = sElements[1].getMethodName();
		lineNumber = sElements[1].getLineNumber();
	}

	public static void e(String message) {
		if (!isDebuggable())
			return;

		getMethodNames(new Throwable().getStackTrace());
		Log.e(className, createLog(message));
	}

	public static void i(String message) {
		if (!isDebuggable())
			return;

		getMethodNames(new Throwable().getStackTrace());
		Log.i(className, createLog(message));
	}

	public static void d(String message) {
		if (!isDebuggable())
			return;

		getMethodNames(new Throwable().getStackTrace());
		Log.d(className, createLog(message));
	}

	public static void v(String message) {
		if (!isDebuggable())
			return;

		getMethodNames(new Throwable().getStackTrace());
		Log.v(className, createLog(message));
	}

	public static void w(String message) {
		if (!isDebuggable())
			return;

		getMethodNames(new Throwable().getStackTrace());
		Log.w(className, createLog(message));
	}

	public static void wtf(String message) {
		if (!isDebuggable())
			return;

		getMethodNames(new Throwable().getStackTrace());
		Log.wtf(className, createLog(message));
	}

}

原理其实很简单,在调用方法的地方得到该方法的调用栈(StackTraceElement),然后就可以得出调用此方法所在位置的 类、方法、行号、文件名等信息。这里补充说明一下,我们如果想要关闭打印输出进行打包时该如何操作。通过分析可以看到如下代码

/**
 * 是否可Debug状态
 * 
 * @return
 */
public static boolean isDebuggable() {
	return BuildConfig.DEBUG;
}

直接返回的是gen目录下BuildConfig.java文件中的DEBUG常量。如果想要关闭,改为false即可。

/** Automatically generated file. DO NOT MODIFY */
package com.example.debuglog;

public final class BuildConfig {
    public final static boolean DEBUG = true;
}

扩展:

我们可以根据这个原理来查看源码中方法被调用的位置。例如,我们需要查看Activity的onCreate方法在哪里被调用便可以使用此方法实现目的。

/********************************************************** 
 * @文件名称:MainActivity.java 
 * @创建时间:2014年11月19日 下午9:30:06
 * @修改历史:2014年11月20日
 **********************************************************/
package com.example.debuglog;
import android.app.Activity;
import android.os.Bundle;
/**
 * @author J!nl!n
 * @date 2014年11月19日
 * @time 下午9:30:06
 * @type MainActivity.java
 * @todo
 */
public class MainActivity extends Activity {
	public static final String TAG = MainActivity.class.getSimpleName(); // "MainActivity"

	void myFunc() {
		android.util.Log.i(TAG, "my message");
	}
	void mySecondFunc() {
		DebugLog.v("simple log from mySecondFunc()");
	}
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);

		android.util.Log.d("test", "我是测试信息");

		Log.d(TAG, "我是debug测试信息");
		Log.e(TAG, "我是error测试信息");
		Log.w(TAG, "我是warn测试信息");
		Log.i(TAG, "我是info测试信息");
		Log.t(TAG, "我是time测试信息");

		myFunc();
		mySecondFunc();
		getMethodNames(new Throwable().getStackTrace());
		DebugLog.i("onCreate的调用位置: " + className + "-" + methodName + "-" + lineNumber);
	}

	private static String className;
	private static String methodName;
	private static int lineNumber;

	/**
	 * 取得输出所在位置的信息 className methodName lineNumber
	 * 
	 * @param sElements
	 */
	private void getMethodNames(StackTraceElement[] sElements) {
		className = sElements[1].getFileName().split("\\.")[0];
		methodName = sElements[1].getMethodName();
		lineNumber = sElements[1].getLineNumber();
	}

	@Override
	protected void onResume() {
		super.onResume();

		DebugLog.v("v log");
		DebugLog.w("w log");
		DebugLog.wtf("wtf log");
	}
}

 
 

观察LogCat可以得到打印结果


我们清楚的知道onCreate方法是在Activity类中performCreate方法中调用的。其所在位置在5008行,但当我满心欢喜打开Activity源码通过快捷键Ctrl+L定位到5008行发现,这结果尼玛绝对是在坑我

使用更加清晰的DebugLog工具开发调试_第4张图片

由于我使用的是API为16的4.1.1模拟器,得到的结果为5008行,但我关联的源码为API19,因此得到错误的结果属于正常情况。

使用更加清晰的DebugLog工具开发调试_第5张图片

修改关联API16的源码之后发现果然是5008行调用的onCreate方法

使用更加清晰的DebugLog工具开发调试_第6张图片

后续打开API为19的4.4.4的模拟器运行指挥得到的结果为5231行。也许有人会好奇为什么这次为什么这次会缺少诸如以下的日志信息。

使用更加清晰的DebugLog工具开发调试_第7张图片

这是因为我们换了模拟器之后,默认的SDcard根目录下是没有log.txt文件的所以debug开关是关闭状态所以不会打印信息,这更进一步说明其很好的实用性。

使用更加清晰的DebugLog工具开发调试_第8张图片

再一次通过快捷键Ctrl+L快速定位到5231行,我们发现结果完全正确

使用更加清晰的DebugLog工具开发调试_第9张图片

本篇到此就结束。

源码下载

你可能感兴趣的:(使用更加清晰的DebugLog工具开发调试)