Android StackTraceElement与Log工具类设计

StackTraceElement引入

StackTraceElement这个类在开发中并不常见,但是做过Java2EE开发的人员肯定不陌生java.util.logging.Logger类或log4j工具包,当我们使用日志功能的时候,系统为我们打印很丰富的信息,格式如下:

运行时间] [当前类名] [方法名]

INFO: [用户信息]

java.util.logging.Logger打印log信息

十月 24, 2016 9:06:33 下午 com.demo.MainTest main
警告: hello world warning
十月 24, 2016 9:06:33 下午 com.demo.MainTest main
信息: hello world info
使用log4j的tomcat日志
十月 24, 2016 9:04:24 下午 org.apache.catalina.startup.VersionLoggerListener log
信息: Server version:        Apache Tomcat/7.0.70
十月 24, 2016 9:04:24 下午 org.apache.catalina.startup.VersionLoggerListener log
信息: Server built:          Jun 15 2016 16:27:45 UTC
十月 24, 2016 9:04:24 下午 org.apache.catalina.startup.VersionLoggerListener log
信息: Server number:         7.0.70.0
十月 24, 2016 9:04:24 下午 org.apache.catalina.startup.VersionLoggerListener log
信息: OS Name:               Windows 7

相比较而言Android的日志输出就有些简单粗暴了,虽然时间日期可有LogCat自动输出,可是方法名或类名都没有,需要我们自己将方法名或者类名手动输入tag标签或者msg才可以。

问题来了,为什么java.util.logging.Logger类或log4j工具包可以输出很规范详细的信息呢?接下来我们查看一下源码设计来追本溯源。

在JDK的源代码里面java.util.logging.LogRecord类的inferCaller方法。

private void inferCaller() {
	needToInferCaller = false;
	JavaLangAccess access = SharedSecrets.getJavaLangAccess();
	Throwable throwable = new Throwable();
	int depth = access.getStackTraceDepth(throwable);

	boolean lookingForLogger = true;
	for (int ix = 0; ix < depth; ix++) {
		// Calling getStackTraceElement directly prevents the VM
		// from paying the cost of building the entire stack frame.
		StackTraceElement frame =
			access.getStackTraceElement(throwable, ix);
		String cname = frame.getClassName();
		boolean isLoggerImpl = isLoggerImplFrame(cname);
		if (lookingForLogger) {
			// Skip all frames until we have found the first logger frame.
			if (isLoggerImpl) {
				lookingForLogger = false;
			}
		} else {
			if (!isLoggerImpl) {
				// skip reflection call
				if (!cname.startsWith("java.lang.reflect.") && !cname.startsWith("sun.reflect.")) {
				   // We've found the relevant frame.
				   setSourceClassName(cname);
				   setSourceMethodName(frame.getMethodName());
				   return;
				}
			}
		}
	}
	// We haven't found a suitable frame, so just punt.  This is
	// OK as we are only committed to making a "best effort" here.
}

log4j中LocationInfo类中一个静态代码块设计如下:

static {
      try {
          Class[] noArgs = null;
          getStackTraceMethod = Throwable.class.getMethod("getStackTrace", noArgs);
          Class stackTraceElementClass = Class.forName("java.lang.StackTraceElement");
          getClassNameMethod = stackTraceElementClass.getMethod("getClassName", noArgs);
          getMethodNameMethod = stackTraceElementClass.getMethod("getMethodName", noArgs);
          getFileNameMethod = stackTraceElementClass.getMethod("getFileName", noArgs);
          getLineNumberMethod = stackTraceElementClass.getMethod("getLineNumber", noArgs);
      } catch(ClassNotFoundException ex) {
          LogLog.debug("LocationInfo will use pre-JDK 1.4 methods to determine location.");
      } catch(NoSuchMethodException ex) {
          LogLog.debug("LocationInfo will use pre-JDK 1.4 methods to determine location.");
      }
  }

两个日志工具类都使用了一个类StackTraceElement,只是获取方式不一样,下面我们介绍一下该类。

StackTraceElement介绍

堆栈跟踪中的元素,它由Throwable.getStackTrace()返回。每个元素表示单独的一个堆栈帧。所有的堆栈帧(堆栈顶部的那个堆栈帧除外)都表示一个方法调用。堆栈顶部的帧表示生成堆栈跟踪的执行点。通常,这是创建对应于堆栈跟踪的 throwable 的点。

StackTraceElement是JDK1.4开始引入的,而java.util.logging.Logger类也是JDK1.4开始引入的,某种意义上来讲StackTraceElement就是为java.util.logging.Logger的实现而设计的。在初始引入是是通过Throwable类通过getStackTrace()方法获取一个StackTraceElement数组,在JDK1.5中Thread类引入了getStackTrace()getAllStackTraces()两个方法。这下子,我们不用 (new Throwable()).getStackTrace();可以调用Thread.getCurrentThread().getStackTrace()来获得当前线程的运行栈信息。不仅如此,只要权限允许,还可以获得其它线程的运行栈信息。

Throwable类部分代码:

public StackTraceElement[] getStackTrace() {
	return getOurStackTrace().clone();
}
private synchronized StackTraceElement[] getOurStackTrace() {
	// Initialize stack trace field with information from
	// backtrace if this is the first call to this method
	if (stackTrace == UNASSIGNED_STACK ||
		(stackTrace == null && backtrace != null) /* Out of protocol state */) {
		int depth = getStackTraceDepth();
		stackTrace = new StackTraceElement[depth];
		for (int i=0; i < depth; i++)
			stackTrace[i] = getStackTraceElement(i);
	} else if (stackTrace == null) {
		return UNASSIGNED_STACK;
	}
	return stackTrace;
} 

Thread类部分代码:

public StackTraceElement[] getStackTrace() {
	if (this != Thread.currentThread()) {
		// check for getStackTrace permission
		SecurityManager security = System.getSecurityManager();
		if (security != null) {
			security.checkPermission(
				SecurityConstants.GET_STACK_TRACE_PERMISSION);
		}
		// optimization so we do not call into the vm for threads that
		// have not yet started or have terminated
		if (!isAlive()) {
			return EMPTY_STACK_TRACE;
		}
		StackTraceElement[][] stackTraceArray = dumpThreads(new Thread[] {this});
		StackTraceElement[] stackTrace = stackTraceArray[0];
		// a thread that was alive during the previous isAlive call may have
		// since terminated, therefore not having a stacktrace.
		if (stackTrace == null) {
			stackTrace = EMPTY_STACK_TRACE;
		}
		return stackTrace;
	} else {
		// Don't need JVM help for current thread
		return (new Exception()).getStackTrace();
	}
}
public static Map getAllStackTraces() {
	// check for getStackTrace permission
	//...
	// Get a snapshot of the list of all threads
	Thread[] threads = getThreads();
	StackTraceElement[][] traces = dumpThreads(threads);
	Map<Thread, StackTraceElement[]> m = new HashMap<>(threads.length);
	for (int i = 0; i < threads.length; i++) {
		StackTraceElement[] stackTrace = traces[i];
		if (stackTrace != null) {
			m.put(threads[i], stackTrace);
		}
		// else terminated so we don't put it in the map
	}
	return m;
}

上面已经说了StackTraceElement就是一个堆栈帧,在方法栈中每个方法都有一个StackTraceElement,每个StackTraceElement中保存了方法的相关信息,如方法所处的类、方法名、方法所处类文件名称以及该方法执行行号。

  • String getClassName() 返回类的完全限定名,该类包含由该堆栈跟踪元素所表示的执行点。
  • String getFileName() 返回源文件名,该文件包含由该堆栈跟踪元素所表示的执行点。
  • int getLineNumber() 返回源行的行号,该行包含由该堆栈该跟踪元素所表示的执行点。
  • String getMethodName() 返回方法名,此方法包含由该堆栈跟踪元素所表示的执行点。
public static void main(String[] args) {
	test();
}
private static void test(){
	StackTraceElement[] caller = new Throwable().getStackTrace();
	for(int i=0;i<caller.length;i++){
		System.out.println("====> "+caller[i].getClassName());
		System.out.println("====> "+caller[i].getMethodName());
		System.out.println("====> "+caller[i].getFileName());
		System.out.println("====> "+caller[i].getLineNumber());
		System.out.println("----------------------------------");
	}
}

上面方法打印结果如下:

====< com.demo.MainTest
====< test
====< MainTest.java
====< 11
----------------------------------
====< com.demo.MainTest
====< main
====< MainTest.java
====< 7
----------------------------------

在MainTest类中首先定义了一个test方法,然后在main方法中调用,由打印信息知道该次执行方法栈深度为2,方法调用的相关信息也如数打印了出来。上述方法是使用Throwable类获取的方法栈信息,如果使用Thread获取会多出来一个层级,示例代码如下:

public static void main(String[] args) {
	test01();
	System.out.println("---------------------------------------");
	test02();
}
private static void test01(){
	StackTraceElement[] caller = new Throwable().getStackTrace();
	for(int i=0;i<caller.length;i++){
		System.out.println("Throwable: "+caller[i].toString());
	}
}
private static void test02(){
	StackTraceElement[] caller = Thread.currentThread().getStackTrace();
	for(int i=0;i<caller.length;i++){
		System.out.println("Thread: "+caller[i].toString());
	}
}
Throwable: com.demo.MainTest.test01(MainTest.java:39)
Throwable: com.demo.MainTest.main(MainTest.java:7)
---------------------------------------
Thread: java.lang.Thread.getStackTrace(Unknown Source)
Thread: com.demo.MainTest.test02(MainTest.java:45)
Thread: com.demo.MainTest.main(MainTest.java:9)

Android Log的设计

在Android的原生Log需要两个参数一个是tag标签一个是msg,我们只需要将tag重新设计一下然后作为原生Log相关方法中tag标签,从LogCat中我们开发中最想知道的信息给出来就可以了,如类名、方法名还有方法调用的行数,时间信息我们不需要从新赋值,因为LogCat会自动打印出Log执行时的时间信息。

整个类的设计很简单,核心就在tag的设计上面,LogUtils类的代码如下:

public class LogUtils {

	private static final boolean DEBUG = true;
	public static String tagPrefix = "";

	private LogUtils() {
	}

	private static String generateTag() {
		StackTraceElement caller = new Throwable().getStackTrace()[2];
		String tag = "%s.%s(L:%d)";
		String callerClazzName = caller.getClassName();
		callerClazzName = callerClazzName.substring(callerClazzName.lastIndexOf(".") + 1);
		tag = String.format(tag, callerClazzName, caller.getMethodName(), caller.getLineNumber());
		tag = TextUtils.isEmpty(tagPrefix) ? tag : tagPrefix + ":" + tag;
		return tag;
	}

	public static void d(String content) {
		if (!DEBUG)
			return;
		String tag = generateTag();

		Log.d(tag, content);
	}

	public static void d(String content, Throwable tr) {
		if (!DEBUG)
			return;
		String tag = generateTag();

		Log.d(tag, content, tr);
	}

	public static void e(String content) {
		if (!DEBUG)
			return;
		String tag = generateTag();

		Log.e(tag, content);
	}

	public static void e(String content, Throwable tr) {
		if (!DEBUG)
			return;
		String tag = generateTag();

		Log.e(tag, content, tr);
	}

	public static void i(String content) {
		if (!DEBUG)
			return;
		String tag = generateTag();

		Log.i(tag, content);
	}

	public static void i(String content, Throwable tr) {
		if (!DEBUG)
			return;
		String tag = generateTag();

		Log.i(tag, content, tr);
	}

	public static void v(String content) {
		if (!DEBUG)
			return;
		String tag = generateTag();

		Log.v(tag, content);
	}

	public static void v(String content, Throwable tr) {
		if (!DEBUG)
			return;
		String tag = generateTag();

		Log.v(tag, content, tr);
	}

	public static void w(String content) {
		if (!DEBUG)
			return;
		String tag = generateTag();

		Log.w(tag, content);
	}

	public static void w(String content, Throwable tr) {
		if (!DEBUG)
			return;
		String tag = generateTag();

		Log.w(tag, content, tr);
	}

	public static void w(Throwable tr) {
		if (!DEBUG)
			return;
		String tag = generateTag();

		Log.w(tag, tr);
	}

	public static void wtf(String content) {
		if (!DEBUG)
			return;
		String tag = generateTag();

		Log.wtf(tag, content);
	}

	public static void wtf(String content, Throwable tr) {
		if (!DEBUG)
			return;
		String tag = generateTag();

		Log.wtf(tag, content, tr);
	}

	public static void wtf(Throwable tr) {
		if (!DEBUG)
			return;
		String tag = generateTag();

		Log.wtf(tag, tr);
	}
}
写一个简单的测试类验证一下,测试类代码如下:
public class MainActivity extends Activity {
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		LogUtils.tagPrefix="test";
		LogUtils.d("hello world01");
		test01();
	}
	private void test01(){
		LogUtils.d("hello world02");
	}
}
10-25 23:00:24.063: D/test:MainActivity.onCreate(L:13): hello world01
10-25 23:00:24.063: D/test:MainActivity.test01(L:17): hello world02

参考资料

JDK Logging模块深入分析

Log信息获取调用类和调用方法名的实现原理

StackTraceElement类使用说明

xUtils LogUtil类设计

评论

您确定要删除吗?删除之后不可恢复