在开发应用过程中免不了开发调试找错的过程,相信很多做过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测试信息"); } }
我们可以看到控制台输出的结果为这样的
注意:此处的时间我并没有做本地化处理,直接使用的当前的毫秒数。主要是因为用的不多,如有需要可以做一点点转化。
在该类中,我们可以使用isOpen这个开关对程序的log信息做相应的关闭处理。同时我们提供一个方法可以对打包成正式版本的apk进行调试。即如果SDcard根目录下下存在log.txt文件时就输出调试打印信息。这里我们打开
private static boolean isOpen = isOpenLog();
此时控制台LogCat是没有任何Log的打印输出的,如下图:
然后我们新建一个log.txt然后放到SDcard根目录下即可,此时我们可以看到熟悉的打印调试信息又出来了,这样就可以对打包完成后的应用进行调试了。
以上我们已经成功实现一个可动态关闭的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行发现,这结果尼玛绝对是在坑我
由于我使用的是API为16的4.1.1模拟器,得到的结果为5008行,但我关联的源码为API19,因此得到错误的结果属于正常情况。
修改关联API16的源码之后发现果然是5008行调用的onCreate方法
后续打开API为19的4.4.4的模拟器运行指挥得到的结果为5231行。也许有人会好奇为什么这次为什么这次会缺少诸如以下的日志信息。
这是因为我们换了模拟器之后,默认的SDcard根目录下是没有log.txt文件的所以debug开关是关闭状态所以不会打印信息,这更进一步说明其很好的实用性。
再一次通过快捷键Ctrl+L快速定位到5231行,我们发现结果完全正确
本篇到此就结束。
源码下载