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

​背景

先上代码

arduino 复制代码
Log.d(TAG, "some log message");

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

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

ini 复制代码
private final Sting TAG = "MainActivity";
// Kotlin: private val TAG = "MainActivity"

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

arduino 复制代码
Log.d("TAG", "some log message");

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

Kotlin优化的单文件LogUtils

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

我觉得还不够轻量,最好一个文件就搞定,于是和朋友QC-uncle(武汉KotlinUserGroup负责人,有想讨论交流Kotlin技术的可以联系我们)一起闲暇时讨论出了这样一个工具类。

需求确定

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

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

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

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

不同方案的尝试

扩展函数方案

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

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

比如:

kotlin 复制代码
fun String.getLenth(){
    return this.lenth
}

val lenth = "Stephen".getLenth()

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

kotlin 复制代码
fun Any.infoLog(message:String){
    Log.i(this.javaClass.simpleName, message)
}

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

顶层函数方案

获取调用栈

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

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

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

vbscript 复制代码
Thread.currentThread().stackTrace.forEach { 
    Log.d("StephenTest", it.toString()) 
}

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

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

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

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

TAG前缀和等级设置

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

kotlin 复制代码
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操作符来执行备选方案。嗯,又省下来好几行。

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

kotlin 复制代码
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的生命周期打点,或者只想知道这个函数是否调用,这种调用方法更加方便。

使用时:

kotlin 复制代码
// 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来延时,进行收尾的工作和打印死亡前的日志。

完整代码及注释

kotlin 复制代码
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)
    }
}
相关推荐
zhangphil11 小时前
Android叠加双RecyclerView ScaleGestureDetector AnimatorSet动态放大缩小,Kotlin(1)
android·kotlin
zhangphil2 天前
Android Glide load origin Bitmap, Kotlin
android·kotlin·glide
老码沉思录2 天前
Android开发实战班 - Android开发基础之 Kotlin语言基础与特性
android·微信·kotlin
闲暇部落3 天前
‌Kotlin中的?.和!!主要区别
android·开发语言·kotlin
长亭外的少年4 天前
Kotlin 编译失败问题及解决方案:从守护进程到 Gradle 配置
android·开发语言·kotlin
JIAY_WX4 天前
kotlin
开发语言·kotlin
麦田里的守望者江4 天前
KMP 中的 expect 和 actual 声明
android·ios·kotlin
菠菠萝宝5 天前
【YOLOv8】安卓端部署-1-项目介绍
android·java·c++·yolo·目标检测·目标跟踪·kotlin
恋猫de小郭5 天前
Kotlin Multiplatform 未来将采用基于 JetBrains Fleet 定制的独立 IDE
开发语言·ide·kotlin
枫__________5 天前
kotlin 协程 job的cancel与cancelAndJoin区别
android·开发语言·kotlin