平时在实际的开发时,经常性的需要用到跟踪日志来调试问题(包括在业务上的调试和技术上的调试),本着避免重复造轮子的原则首先都是想到找个开源的,就比如log4j 这些类型,原先用的是log4j1,后面想着试试看log4j2,尝试按照网上各种配置什么初始化啊,xml配置啊折腾一通最终硬是没整成功(考虑是可能各种版本问题或者是一些细节方面,具体的配置情况说实话真的描述的不是很细节),算了,自己整一个简单实用点的吧。
TODO
文章中的所有内容都是kotlin
的形式编写,在java中调用的话可以直接调用,调用的形式上来讲也是不变的
首先来个前后对比:
使用Android原生日志工具进行打印日志的方式:
//声明该类的tag,每到一个新的类中都要重新进行声明一次
var tag="tag"
Log.d(tag,"消息内容")
//输出结果:2019-04-08 10:14:53.876 29455-29455/? D/String: 消息内容
期望进行日志打印的方式:
//不再需要声明什么tag啊乱七八糟的
log.d("输出内容”)
//输出结果:2019-04-08 10:17:50.819 20167-20167/ D/MyImageView(打印日志位置类名)->getBimtap(打印日志位置的方法名).702(打印日志所在文件中的行数): 输出内容
期望的形式就是在任何地方都可以打印日志,并且可以详细的标注打印日志的具体位置信息,具体问题就在于如何获取这个具体位置信息,可以使用线程的stackTrace
来进行获取,比如:
//获取stackTrace数组
var trace=Thread.currentThread().stackTrace
//打印这个trace信息
for (i in 0 until trace.size){
println(trace[i].toString)
}
//输出信息为:
java.lang.Thread.getStackTrace(Thread.java:1556)
//这一行是我们执行方法的位置,可以观察到有方法名,类名,以及对应的行数位置信息
com.test.xxx.ExampleUnitTest.testLog(ExampleUnitTest.kt:21)
//这一行是我们调用方法的位置,即 在 t 方法中调用的testLog方法,这些所有的输出操作都是在testLog方法中执行的
com.test.papatiyu.ExampleUnitTest.t(ExampleUnitTest.kt:30)
sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
java.lang.reflect.Method.invoke(Method.java:498)
org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
org.junit.runners.ParentRunner.run(ParentRunner.java:363)
org.junit.runner.JUnitCore.run(JUnitCore.java:137)
com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
/**
其实现在观察上面的输出信息已经可以发现怎么获取这些信息了,下面就贴上具体的实现逻辑
*/
具体的实现类如下:
package com.dobai.abroad.dongbysdk
import android.util.Log
/**
*@author wangxiaochen
*@Date: 2019/3/30
* 日志调试输出类,支持类名+methodName+lineNum输出
* 混淆 -keep public class com.dobai.abroad.dongbysdk.log{*;}
*/
object log {
/**
* 打印集合的内容
*/
fun Any.logList(something:String,list:List<Any>?) {
if (list == null) {
e("$something,list is null,can't log list")
} else {
var str = StringBuffer("[")
list.forEach { str.append("$it,") }
str.append("]")
d("$something,${str.toString()}")
}
}
/**
* 打印集合的内容
*/
fun Any.logList(something:String,list:Set<Any>?) {
if (list == null) {
e("$something,list is null,can't log list")
} else {
var str = StringBuffer("[")
list.forEach { str.append("$it,") }
str.append("]")
d("$something,${str.toString()}")
}
}
/**
* 全局控制是否输出日志
*/
var isDebug = true
/**
* 针对某个类进行管控的统一配置集合
* 支持仅针对某个类的输出的日志不进行输出,方便与对某个类进行调试(像在调试一些自定义view类的时候,坐标日志刷的蛮多的,所以这个还是可以考虑整一个)
* 后续其实如果需要进行扩展的话,也可进行配置为某个类的某个方法的日志进行屏蔽
*/
@JvmStatic
private var configMap = HashMap<String, Boolean>()
/**
* 是否简化tagName,如果简化的话代表只会输出类名不会输出详细数据内容
*/
@JvmStatic
private var simpleTagName = false
/**
* 针对单独的某个类进行设置是否需要输出该日志的类信息
* 如果调用设置了一般都是想屏蔽的
* @param isOut ->false 不可以输出 true->可以进行输出
*/
@JvmStatic
fun setIgnoreWithClass(clazzName: String, isOut: Boolean = false) {
var valueIfContains= configMap[clazzName]
//相同的属性不再过度设置
if (valueIfContains!=isOut) {
Log.d("${javaClass.`package`?.name}:", "log->setIgnoreWithClass,class:$clazzName 的日志输出状态被设置为:$isOut")
configMap[clazzName] = isOut
}
}
@JvmStatic
fun d(msg: String) {
if (isDebug) {
//之前设置过特殊内容
if (configMap.size > 0) {
if (getClassNameConfigValue(getClassName())){
logResult(getMsgOfLog(),msg,"d")
}
} else {
//没有特殊进行拦截的内容
logResult(getMsgOfLog(),msg,"d")
}
}
}
/**
* 在子类中的调用,类名可能是 `ChoosePicView$startAnimator$1` 这样的形式,一般我们屏蔽一个类的日志输出后,内部类的类名同样需要进行屏蔽
* 判断调用类是否被屏蔽了日志输出
* @return true ->可以进行日志输出
* false ->不可以进行日志输出
* 默认是可以进行输出的
* ChoosePicView$startAnimator$1
*/
fun getClassNameConfigValue(clazzName: String):Boolean {
for (key in configMap.keys) {
if (key.equals(clazzName.split("$").first())) {
//包含这个key值,直接返回对应的结果
return configMap[key]!!
}
}
return true
}
@JvmStatic
fun w(msg: String) {
if (isDebug) {
//之前设置过特殊内容
if (configMap.size > 0) {
if (getClassNameConfigValue(getClassName())){
logResult(getMsgOfLog(),msg,"w")
}
}else {
//没有特殊进行拦截的内容
logResult(getMsgOfLog(),msg,"w")
}
}
}
@JvmStatic
fun i(msg: String) {
if (isDebug) {
//之前设置过特殊内容
if (configMap.size > 0) {
if (getClassNameConfigValue(getClassName())){
logResult(getMsgOfLog(),msg,"i")
}
}else {
//没有特殊进行拦截的内容
logResult(getMsgOfLog(),msg,"i")
}
}
}
@JvmStatic
fun e(msg: String) {
if (isDebug) {
//之前设置过特殊内容
if (configMap.size > 0) {
if (getClassNameConfigValue(getClassName())){
logResult(getMsgOfLog(),msg,"e")
}
} else {
//没有特殊进行拦截的内容
logResult(getMsgOfLog(),msg,"e")
}
}
}
@JvmStatic
fun v(msg: String) {
if (isDebug) {
//之前设置过特殊内容
if (configMap.size > 0) {
if (getClassNameConfigValue(getClassName())){
logResult(getMsgOfLog(),msg,"v")
}
} else {
//没有特殊进行拦截的内容
logResult(getMsgOfLog(),msg,"v")
}
}
}
private fun logResult(tag:String, msg:String, level:String) {
var dealTag = tag
var dealMsg = msg
if (tag.length > 23) {
dealMsg="$dealTag::$dealMsg"
dealTag= dealTag.substring(0,22)
}
when (level) {
"i"->{Log.i(dealTag,dealMsg)}
"v"->{Log.v(dealTag,dealMsg)}
"e"->{Log.e(dealTag,dealMsg)}
"w"->{Log.w(dealTag,dealMsg)}
"d"->{Log.d(dealTag,dealMsg)}
else->{
loge("未知的level等级:$level,$dealMsg")
}
}
}
/**
* 获取调用当前线度的类名
*/
private fun getClassName(): String {
val trace = Thread.currentThread().stackTrace
var className = "unknowClassName"
//根据stack 层级+调用log的层级,需要向上偏移俩个单位
for (i in 0 until trace.size) {
if ("getClassName".equals(trace[i].methodName)) {
//获取到当前方法的执行栈具体位置,上一次是D,在上一层是具体的调用位置
var truelyTrace = trace[i + 2]
className = truelyTrace.className.split(".").last()
break
}
}
if ("unknowClassName".equals(className)) {
loge("getClassName->未找到调用方法的具体栈位信息!")
}
return className
}
//获取调用者位置的具体信息内容
private fun getMsgOfLog(): String {
//获取当前方法执行堆栈
val trace = Thread.currentThread().stackTrace
var className = "unknowClassName"
var methodName = "unknowClassName"
var lineNumber = "unknowLineNumber"
//根据stack 层级+调用log的层级,需要向上偏移俩个单位
for (i in 0 until trace.size) {
//混淆了的话注意这行代码不要混淆,不然会匹配不到
if ("getMsgOfLog".equals(trace[i].methodName)) {
//获取到当前方法的执行栈具体位置,上一次是D,在上一层是具体的调用位置
var truelyTrace = trace[i + 2]
className = truelyTrace.className.split(".").last()
methodName = truelyTrace.methodName
lineNumber = truelyTrace.lineNumber.toString()
break
}
}
if ("unknowClassName".equals(className)) {
loge("getMsgOfLog->未找到调用方法的栈位信息")
}
if (simpleTagName) {
return className
} else {
return "check->${className}.${methodName}:$lineNumber"
}
}
/**
* 本类的错误信息调试
*/
private fun loge(msg: String) {
Log.e("log->loge", msg)
}
}