使用Kotlin实现一个单文件的LogUtils

背景

先上代码

Log.d(TAG, "some log message");

这一句,各位Android开发者绝对都很熟悉了,我们在项目中需要记录调用链、回溯查看数据、查看界面生命周期等,都需要打印LOG。合理打印日志,可以方便地定位问题(shuaiguo),明确实际出问题的是哪一个链路,云端还是客户端,应用层还是系统层,对于车机开发,还有可能是下游MCU控制器。

这种最原始的打印方式,只能说能用,但是肯定不好用。平常在开发过程中,每次我们新建一个类,一想到有可能会打印一些日志,都要考虑给他新建一个TAG标志。一般来说,日志TAG都使用打印处的类名,就像下面这样:

private final Sting TAG = "MainActivity";
// Kotlin: private val TAG = "MainActivity"

多这样一个念想,总归是耗费了一些精力。甚至,接收老项目时,有的人还不用变量传递,直接使用硬编码,在调用打印log时来一句:

Log.d("TAG", "some log message");

不要怀疑,我真遇到过这种写法。

Kotlin优化的单文件LogUtils

原始方案打印log在后续出问题查看log和调试时都极其不方便,就像笔者是车机Android应用层,日志打印都会由系统存到文件里,来了bug,拿到从系统取出来的log文件后,再自己去搜索log,分析问题根源。我们看到,网上也有一些优秀的日志框架,比如github上的pretty_logger。

我觉得还不够轻量,最好一个文件就搞定,于是和朋友一起闲暇时讨论出了这样一个工具类。

需求确定

首先,最直接的需求就是干掉TAG这个参数,希望可以直接传递进一个String类型的message就实现打印。暂时为了简约性,没有设置Array类型的判断兼容。

其次,希望有基本的前缀preffix添加和等级筛选。

最后,需要自动的显示调用处的信息,比如,方法名和行数。

其实网上的一些框架也是这些功能点,在此基础上进行各类扩展。我们希望这些核心功能可以以一种优雅的形式,以100行以内的代码来实现。

不同方案的尝试

扩展函数方案

第一版我们是使用扩展函数的方案来做的,这个写起来相当简约,直接设置一个Any的扩展函数,任何类都可以使用,并且在函数作用域里可以通过this关键字拿到Any调用者的信息。

扩展函数:为现有类定义扩展函数 , 可以在不修改原有类的情况下增加类的功能,Kotlin中如果类没有被open关键字修饰, 则该类不能被继承 , 如果想要扩展该类 , 可以使用扩展函数。

比如:

fun String.getLenth(){
    return this.lenth
}

val lenth = "Stephen".getLenth()

我们在设计的时候,就像下面这样:

fun Any.infoLog(message:String){
    Log.i(this.javaClass.simpleName, message)
}

使用时,直接打印调用类的简短类名,再带一个message。完成了省略TAG的需求,但是熟悉Kotlin的朋友都知道,Kt还有一个“顶层函数”的设计,就是直接写在文件中的,不属于任何一个Kotlin类的函数,用来代替Java的static方法。如果我们在这些函数内有打印log的需求,因为其不隶属于任何一个类,这个函数压根引用不到这个扩展的log方法了。

顶层函数方案

获取调用栈

扩展函数用不了,我们遂直接改为顶层函数top function来实现了,代码的任何地方都可以引用到这个static函数,更符合log的使用场景。

但是顶层函数不像扩展函数,我们无法通过Any扩展this关键字来获取调用方的信息了,也就无法省略TAG这个参数,这又是一个强需求,不能妥协。还有另外一种获取调用方信息的方案,其实比之前的扩展函数方式要更加全面,可以在函数里获取到整个链路全部调用信息,那就是通过获取当前线程调用堆栈的Thread.currentThread().stackTrace来实现。

这个方法会返回一个StackTraceElement[]列表,里面的元素StackTraceElement,通过这个对象可以获取调用栈当中的调用过程信息,包括方法的类名、方法名、文件名以及调用的行数。那些热门的开源log库也都是通过这个调用栈信息来显示详细的信息的。

Thread.currentThread().stackTrace.forEach { 
    Log.d("StephenTest", it.toString()) 
}

我们在调用的地方获取这个列表并遍历打印看看:

可以看到,我们需要的调用方详细信息。安卓侧相比于其他平台,加了一个dalvik的调用栈,我们的直接调用元素是第三个,也就是index为2的位置。我们需要根据获取stacktrace的地方来合理寻找index,logutils内部看作一个整体,那么外部的直接调用方就需要往后移。上图为例,Application类里的信息就往后移到了index为四的位置。

获取栈信息的我们抽出来一个方法:

/**
 * 获取调用栈信息
 */
fun getStackInfo(stackTrace: Array<StackTraceElement>) =
    Pair(
    // 拿取类名并删除前面的包名,格式化
        stackTrace[4].className.split(".").last(),
    // 获取行数和调用的方法名
        "line:${stackTrace[4].lineNumber} ${stackTrace[4].methodName}"
    )

TAG前缀和等级设置

这两个比较好实现,我们只需在Application初始化时赋值两个变量即可,还要把两个参数的set权限设置为文件内。这一点我们可以另外抽取一个object单例类,作为前缀和等级的初始化入口:

object LogSetting {

    const val LOG_VERBOSE = 1
    const val LOG_DEBUG = 2
    const val LOG_INFO = 3
    const val LOG_WARNING = 4
    const val LOG_ERROR = 5

    var COMMON_TAG = ""
        private set

    var LOGLEVEL = 0
        private set

    /**
     * 大于此LEVEL的才会打出来
     */
    fun initLogSettings(tagPreffix: String, logLevel: Int) {
        COMMON_TAG = tagPreffix
        LOGLEVEL = logLevel
    }
}

然后实际外部打印log的static函数,可以以这两个变量来进行设置与判断。

兼容有tag和无tag

Kotlin另一个非常好用的特性就是默认函数参数。可以在定义时给参数设置默认值,调用处若不进行设置,就以这个默认值来使用。我们把tag设为null,进行区分打印。为空就获取堆栈自动设置TAG,不为空就优先以传进来的TAG来打印。同时,在代码写法上,我们使用let函数判空,和elvis操作符来执行备选方案。嗯,又省下来好几行。

一个打印的函数就像下面这样:

fun infoLog(message: String, tag: String? = null) {
    // 打印等级判断
    if (LogSetting.LOGLEVEL > LogSetting.LOG_INFO) return
    //tag不为空直接打印tag 
    tag?.let {
        printLog(it, message, LogSetting.LOG_INFO)
    //tag为空则获取调用栈,以类名和方法名,行数打印
    } ?: run {
        val stackTracePair = getStackInfo(Thread.currentThread().stackTrace)
        printLog(stackTracePair.first, "${stackTracePair.second}: $message", LogSetting.LOG_INFO)
    }
}

甚至,我们可以给message也设置一个默认参数空字符串,在调用时什么都不用传,直接一个单infoLog()。给Activity的生命周期打点,或者只想知道这个函数是否调用,这种调用方法更加方便。

使用

// Application 初始化
  LogSetting.initLogSettings(
            "RedfinDemo[${BuildConfig.VERSION_NAME}]",
            if (BuildConfig.BUILD_TYPE == "release") LogSetting.LOG_INFO else 
            LogSetting.LOG_VERBOSE
        )

// 打印log
infoLog("my message")

infoLog("my message", "my TAG")

至此,我们单文件的日志工具类就完成了,加上注释只需98行。而且,在发生异常时,我们可以搭配UncaughtExceptionHandler来延时,进行收尾的工作和打印死亡前的日志。

查看打印效果

Android Studio调试效果

还是用我的RedfinDemo,对这个app不了解的可以看下我前几篇,在手机上开发车机应用的文章。在调试时,我们在AS里实时地查看,通过preffix来筛选,就是下面的效果:

从电鳗Electric Eel版本之后的新logcat界面还蛮花哨好看的。

界面里,前缀RedfinDemo加Application等等类名就是我们自定义的TAG,message里包含line行数,methodName方法名,和我们需要打印的自定义的消息。核心信息还是蛮实用的,有需要的朋友还可以自定义扩展,加上threadName的打印。

系统LOG文件拉取

在我的Pixel设备上,Android Automotive系统因为是userdebug版本,我们可以直接将系统存储的日志文件拉出来,到windows里进行分析。在车机开发时,遇到bug这样查问题是很常见的场景。

在ASP编译出来的Automotive上因为没有系统供应商的定制,所以日志文件还是存储在安卓系统的默认位置/data/misc/logd下面,adb root后,通过shell进入到该目录下ls看一下

redfin:/data/misc/logd # ls 
event-log-tags logcat.03 logcat.07 logcat.11 
logcat.15 logcat.19 logcat.23 logcat.27 
logcat.id logcat logcat.04 logcat.08 
logcat.12 logcat.16 logcat.20 logcat.24 
logcat.28 logcat.01 logcat.05 logcat.09 
logcat.13 logcat.17 logcat.21 logcat.25 
logcat.29 logcat.02 logcat.06 logcat.10 
logcat.14 logcat.18 logcat.22 logcat.26 logcat.30

可以看到历史log全在这里。我们使用adb pull到windows里查看,并且可以用git bash来筛选我们需要的字段。adb相关基础也不赘述了,车机开发需要熟练掌握的。

 MINGW64 ~/Desktop/logd
$ find . -name "*logcat*" | xargs grep -i "RedfinDemo\[" > redfin.log

这个命令的含义就是查找这个目录下所有名称带logcat的文件里,含有Redfin[字段的那一行的数据,并且重定向输出成一个redfin.log的文件下,如果文件已存在将被覆盖。

我们打开这个新生成的log文件,里面就是我们自己应用打印的log了:

./logcat:01-04 11:06:55.093971 10511 10511 I RedfinDemo[2.0]RedfinApplication: line:25 onCreate: 
./logcat:01-04 11:06:55.172279 10511 10511 I RedfinDemo[2.0]MainActivity: line:26 onCreate: 
./logcat:01-04 11:06:55.172424 10511 10511 I RedfinDemo[2.0]MainActivity: line:42 changeFragment: fragmentTag: 0
./logcat:01-04 11:06:55.214022 10511 10511 I RedfinDemo[2.0]DeviceFragment: line:24 onViewCreated: 
./logcat:01-04 11:06:55.236448 10511 10534 I RedfinDemo[2.0]DeviceInfoManager$getDeviceInfo$2: line:37 invokeSuspend: getDeviceInfo
./logcat:01-04 11:06:55.236598 10511 10534 I RedfinDemo[2.0]DeviceInfoManager: line:61 getCpuModel: getCpuModel
./logcat:01-04 11:06:55.264662 10511 10534 I RedfinDemo[2.0]DeviceInfoManager: line:190 getExternalMounts: outsize: []
./logcat:01-04 11:06:55.346790 10511 10511 I RedfinDemo[2.0]DeviceInfoManager: line:69 getBatteryPercent: batteryPercent: 100.0
./logcat.02:01-04 11:01:19.539168  6757  6757 I RedfinDemo[2.0]RedfinApplication: line:25 onCreate: 
./logcat.02:01-04 11:01:19.615792  6757  6757 I RedfinDemo[2.0]MainActivity: line:26 onCreate: ==========>onCreate<===========
./logcat.02:01-04 11:01:19.615940  6757  6757 I RedfinDemo[2.0]MainActivity: line:42 changeFragment: fragmentTag: 0
./logcat.02:01-04 11:01:19.657746  6757  6757 I RedfinDemo[2.0]DeviceFragment: line:24 onViewCreated: ========>onViewCreated<========
./logcat.02:01-04 11:01:19.680941  6757  6778 I RedfinDemo[2.0]DeviceInfoManager$getDeviceInfo$2: line:37 invokeSuspend: getDeviceInfo
./logcat.02:01-04 11:01:19.681350  6757  6778 I RedfinDemo[2.0]DeviceInfoManager: line:61 getCpuModel: getCpuModel
./logcat.02:01-04 11:01:19.722580  6757  6778 I RedfinDemo[2.0]DeviceInfoManager: line:190 getExternalMounts: outsize: []
./logcat.02:01-04 11:01:19.792299  6757  6757 I RedfinDemo[2.0]DeviceInfoManager: line:69 getBatteryPercent: batteryPercent: 100.0
./logcat.02:01-04 11:01:35.672238  7039  7039 I RedfinDemo[2.0]RedfinApplication: line:25 onCreate: 
./logcat.02:01-04 11:01:35.751160  7039  7039 I RedfinDemo[2.0]MainActivity: line:26 onCreate: ==========>onCreate<===========
./logcat.02:01-04 11:01:35.751300  7039  7039 I RedfinDemo[2.0]MainActivity: line:42 changeFragment: fragmentTag: 0
./logcat.02:01-04 11:01:35.792833  7039  7039 I RedfinDemo[2.0]DeviceFragment: line:24 onViewCreated: ========>onViewCreated<========
./logcat.02:01-04 11:01:35.815270  7039  7070 I RedfinDemo[2.0]DeviceInfoManager$getDeviceInfo$2: line:37 invokeSuspend: getDeviceInfo
./logcat.02:01-04 11:01:35.815418  7039  7070 I RedfinDemo[2.0]DeviceInfoManager: line:61 getCpuModel: getCpuModel
./logcat.02:01-04 11:01:35.922051  7039  7039 I RedfinDemo[2.0]DeviceInfoManager: line:69 getBatteryPercent: batteryPercent: 100.0
./logcat.02:01-04 11:01:58.368946  7340  7340 I RedfinDemo[2.0]RedfinApplication: line:25 onCreate: 
./logcat.02:01-04 11:01:58.446966  7340  7340 I RedfinDemo[2.0]MainActivity: line:26 onCreate: 
./logcat.02:01-04 11:01:58.447106  7340  7340 I RedfinDemo[2.0]MainActivity: line:42 changeFragment: fragmentTag: 0
./logcat.02:01-04 11:01:58.488774  7340  7340 I RedfinDemo[2.0]DeviceFragment: line:24 onViewCreated: 
./logcat.02:01-04 11:01:58.511274  7340  7361 I RedfinDemo[2.0]DeviceInfoManager$getDeviceInfo$2: line:37 invokeSuspend: getDeviceInfo
./logcat.02:01-04 11:01:58.511416  7340  7361 I RedfinDemo[2.0]DeviceInfoManager: line:61 getCpuModel: getCpuModel
./logcat.02:01-04 11:01:58.540320  7340  7361 I RedfinDemo[2.0]DeviceInfoManager: line:190 getExternalMounts: outsize: []
./logcat.02:01-04 11:01:58.624703  7340  7340 I RedfinDemo[2.0]DeviceInfoManager: line:69 getBatteryPercent: batteryPercent: 100.0
./logcat.03:01-04 11:01:01.964042  6466  6466 I RedfinDemo[2.0]RedfinApplication: line:25 onCreate: 
./logcat.03:01-04 11:01:02.040996  6466  6466 I RedfinDemo[2.0]MainActivity: line:26 onCreate: ==========>onCreate<===========
./logcat.03:01-04 11:01:02.041136  6466  6466 I RedfinDemo[2.0]MainActivity: line:42 changeFragment: fragmentTag: 0
./logcat.03:01-04 11:01:02.082829  6466  6466 I RedfinDemo[2.0]DeviceFragment: line:24 onViewCreated: ========>onViewCreated<========
./logcat.03:01-04 11:01:02.105905  6466  6487 I RedfinDemo[2.0]DeviceInfoManager$getDeviceInfo$2: line:37 invokeSuspend: getDeviceInfo
./logcat.03:01-04 11:01:02.106293  6466  6487 I RedfinDemo[2.0]DeviceInfoManager: line:61 getCpuModel: getCpuModel
./logcat.03:01-04 11:01:02.141612  6466  6487 I RedfinDemo[2.0]DeviceInfoManager: line:190 getExternalMounts: outsize: []
./logcat.03:01-04 11:01:02.256756  6466  6466 I RedfinDemo[2.0]DeviceInfoManager: line:69 getBatteryPercent: batteryPercent: 100.0

完整代码及注释

import android.util.Log

object LogSetting {

    const val LOG_VERBOSE = 1
    const val LOG_DEBUG = 2
    const val LOG_INFO = 3
    const val LOG_WARNING = 4
    const val LOG_ERROR = 5

    var COMMON_TAG = ""
        private set

    var LOGLEVEL = 0
        private set

    /**
     * 大于此LEVEL的才会打出来
     */
    fun initLogSettings(tagPreffix: String, logLevel: Int) {
        COMMON_TAG = tagPreffix
        LOGLEVEL = logLevel
    }
}

fun verboseLog(message: String, tag: String? = null) {
    if (LogSetting.LOGLEVEL > LogSetting.LOG_VERBOSE) return
    tag?.let {
        printLog(it, message, LogSetting.LOG_VERBOSE)
    } ?: run {
        val stackTracePair = getStackInfo(Thread.currentThread().stackTrace)
        printLog(stackTracePair.first, "${stackTracePair.second}: $message", LogSetting.LOG_VERBOSE)
    }
}

fun debugLog(message: String, tag: String? = null) {
    if (LogSetting.LOGLEVEL > LogSetting.LOG_DEBUG) return
    tag?.let {
        printLog(it, message, LogSetting.LOG_DEBUG)
    } ?: run {
        val stackTracePair = getStackInfo(Thread.currentThread().stackTrace)
        printLog(stackTracePair.first, "${stackTracePair.second}: $message", LogSetting.LOG_DEBUG)
    }
}

fun infoLog(message: String, tag: String? = null) {
    if (LogSetting.LOGLEVEL > LogSetting.LOG_INFO) return
    tag?.let {
        printLog(it, message, LogSetting.LOG_INFO)
    } ?: run {
        val stackTracePair = getStackInfo(Thread.currentThread().stackTrace)
        printLog(stackTracePair.first, "${stackTracePair.second}: $message", LogSetting.LOG_INFO)
    }
}

fun warningLog(message: String, tag: String? = null) {
    if (LogSetting.LOGLEVEL > LogSetting.LOG_WARNING) return
    tag?.let {
        printLog(it, message, LogSetting.LOG_WARNING)
    } ?: run {
        val stackTracePair = getStackInfo(Thread.currentThread().stackTrace)
        printLog(stackTracePair.first, "${stackTracePair.second}: $message", LogSetting.LOG_WARNING)
    }
}

fun errorLog(message: String, tag: String? = null) {
    if (LogSetting.LOGLEVEL > LogSetting.LOG_ERROR) return
    tag?.let {
        printLog(it, message, LogSetting.LOG_ERROR)
    } ?: run {
        val stackTracePair = getStackInfo(Thread.currentThread().stackTrace)
        printLog(stackTracePair.first, "${stackTracePair.second}: $message", LogSetting.LOG_ERROR)
    }
}

/**
 * 获取调用栈信息
 */
fun getStackInfo(stackTrace: Array<StackTraceElement>) =
    Pair(
        stackTrace[4].className.split(".").last(),
        "line:${stackTrace[4].lineNumber} ${stackTrace[4].methodName}"
    )

/**
 * 实际打印处,根据等级打印log
 */
fun printLog(tag: String, message: String, logLevel: Int) {
    when (logLevel) {
        LogSetting.LOG_ERROR -> Log.e(LogSetting.COMMON_TAG + tag, message)
        LogSetting.LOG_WARNING -> Log.w(LogSetting.COMMON_TAG + tag, message)
        LogSetting.LOG_INFO -> Log.i(LogSetting.COMMON_TAG + tag, message)
        LogSetting.LOG_DEBUG -> Log.d(LogSetting.COMMON_TAG + tag, message)
        LogSetting.LOG_VERBOSE -> Log.v(LogSetting.COMMON_TAG + tag, message)
    }
}

  • 15
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值