平时在实际的开发时,经常性的需要用到跟踪日志来调试问题(包括在业务上的调试和技术上的调试),本着避免重复造轮子的原则首先都是想到找个开源的,就比如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)
    }
}