背景
先上代码
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)
}
}