Android面试题 怎么写一个又好又快的日志库?_安卓面试题 如何去设计一个日志框架的软件(1)

先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7

深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
img
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以添加V获取:vip204888 (备注Android)
img

正文

高可扩展

看完刚才的砖之后,我再抛一个砖。

object EasyLog {
    private const val VERBOSE = 2
    private const val DEBUG = 3
    private const val INFO = 4
    private const val WARN = 5
    private const val ERROR = 6
    private const val ASSERT = 7
    // 拦截器列表
    private val logInterceptors = mutableListOf<LogInterceptor>()

    fun d(message: String, tag: String = "", vararg args: Any) {
        log(DEBUG, message, tag, *args)
    }
    fun e(message: String, tag: String = "", vararg args: Any, throwable: Throwable? = null) {
        log(ERROR, message, tag, *args, throwable = throwable)
    }
    fun w(message: String, tag: String = "", vararg args: Any) {
        log(WARN, message, tag, *args)
    }
    fun i(message: String, tag: String = "", vararg args: Any) {
        log(INFO, message, tag, *args)
    }
    fun v(message: String, tag: String = "", vararg args: Any) {
        log(VERBOSE, message, tag, *args)
    }
    fun wtf(message: String, tag: String = "", vararg args: Any) {
        log(ASSERT, message, tag, *args)
    }
    // 注入拦截器
    fun addInterceptor(interceptor: LogInterceptor) {
        logInterceptors.add(interceptor)
    }
    // 从头部注入拦截器
    fun addFirstInterceptor(interceptor: LogInterceptor) {
        logInterceptors.add(0, interceptor)
    }
}

日志库对上层的接口封装在一个单例 EasyLog 里。
日志库提供了和 android.util.Log 几乎一样的打印接口,但增加了一个可变参数args,这是为了方便地为字串的通配符赋值。

假责任链模式

日志库还提供了一个新接口addInterceptor(),用于动态地注入日志拦截器:

interface LogInterceptor {
    // 进行日志
    fun log(priority: Int, tag: String, log: String)
    // 是否允许进行日志
    fun enable():Boolean
}

日志拦截器是一个接口,定义了两个抽象的行,为分别是进行日志是否允许日志
所有的日志接口都将打印日志委托给了log()方法:

object EasyLog {
    @Synchronized
    private fun log(
        priority: Int,
        message: String,
        tag: String,
        vararg args: Any,
        throwable: Throwable? = null
    ) {
        // 为日志通配符赋值
        var logMessage = message.format(*args)
        // 如果有异常,则读取异常堆栈拼接在日志字串后面
        if (throwable != null) {
            logMessage += getStackTraceString(throwable)
        }
        // 遍历日志拦截器,将日志打印分发给所有拦截器
        logInterceptors.forEach { interceptor ->
            if (interceptor.enable()) interceptor.log(priority, tag, logMessage)
        }
    }
    // 对 String.format() 的封装,以求简洁
    fun String.format(vararg args: Any) =
        if (args.isNullOrEmpty()) this else String.format(this, *args)

    // 读取堆栈
    private fun getStackTraceString(tr: Throwable?): String {
        if (tr == null) {
            return ""
        }
        var t = tr
        while (t != null) {
            if (t is UnknownHostException) {
                return ""
            }
            t = t.cause
        }
        val sw = StringWriter()
        val pw = PrintWriter(sw)
        tr.printStackTrace(pw)
        pw.flush()
        return sw.toString()
    }
}

这里用了同步方法,为了防止多线程调用时日志乱序。
这里还运用了责任链模式(假的),使得 EasyLog 和日志处理的具体逻辑解耦,它只是持有一组日志拦截器,当有日志请求时,就分发给所有的拦截器。

这样做的好处就是,可以在业务层动态地为日志组件提供新的功能
当然日志库得提供一些基本的拦截器,比如将日志输出到 Logcat:

open class LogcatInterceptor : LogInterceptor {
    override fun log(priority: Int, tag: String, log: String){
        Log.println(priority, tag, log)
    }

    override fun enable(): Boolean {
       return true
    }
}

Log.println(priority, tag, log)android.util.Log 提供的,按优先级将日志输出到 Logcat 的方法。使用这个方法可以降低复杂度,因为不用写类似下面的代码:

when(priority){
    VERBOSE -> Log.v(...)
    ERROR -> Log.e(...)
}

之所以要将 LogcatInterceptor 声明为 open,是因为业务层有动态重写enable()方法的需求:

EasyLog.addInterceptor(object : LogcatInterceptor() {
    override fun enable(): Boolean {
        return BuildConfig.DEBUG 
    }
})

这样就把日志输出到 Logcat 的开关 和 build type 联系起来了,就不需要将 build type 作为 EasyLog 的一个配置字段了。
除了输出到 Logcat,另一个基本需求就是日志文件化,新建一个拦截器:

class FileWriterLogInterceptor 
    private constructor(private var dir: String) : LogInterceptor {
    private val handlerThread = HandlerThread("log_to_file_thread")
    private val handler: Handler
    private val dispatcher: CoroutineDispatcher
    // 带参单例
    companion object {
        @Volatile
        private var INSTANCE: FileWriterLogInterceptor? = null
        fun getInstance(dir: String): FileWriterLogInterceptor =
            INSTANCE ?: synchronized(this) {
                INSTANCE ?: FileWriterLogInterceptor(dir).apply { INSTANCE = this }
            }
    }

    init {
        // 启动日志线程
        handlerThread.start()
        handler = Handler(handlerThread.looper)
        // 将 handler 转换成 Dispatcher
        dispatcher = handler.asCoroutineDispatcher("log_to_file_dispatcher")
    }

    override fun log(priority: Int, tag: String, log: String) {
        // 启动协程串行地将日志写入文件
        if (!handlerThread.isAlive) handlerThread.start()
        GlobalScope.launch(dispatcher) {
            FileWriter(getFileName(), true).use {
                it.append("[$tag] $log")
                it.append("\n")
                it.flush()
            }
        }
    }

    override fun enable(): Boolean {
        return true
    }

    private fun getToday(): String =
        SimpleDateFormat("yyyy-MM-dd").format(Calendar.getInstance().time)

    private fun getFileName() = "$dir${File.separator}${getToday()}.log"
}

日志写文件的思路是:“异步串行地将字串通过流输出到文件中”。

异步化是为了不阻塞主线程,串行化是为了保证日志顺序。

HandlerThread 就很好地满足了异步化串行的要求。

为了简化“将日志作为消息发送到异步线程中”这段代码,使用了协程,这样代码就转变成:每次日志请求到来时,启动协程,在其中完成创建流、输出到流、关闭流。隐藏了收发消息的复杂度。
日志拦截器被设计为单例,目的是让 App 内存中只存在一个写日志的线程。
日志文件的路径由构造方法传入,这样就避免了日志拦截器和 Context 的耦合。

use()Closeable的扩展方法,它隐藏了流操作的try-catch,降低了复杂度,关于这方面的详细介绍可以点击

然后业务层就可以像这样动态地为日志组件添加写文件功能:
class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        // 日志文件路径
        val dir = this.filesDir.absolutePath
        // 构造日志拦截器单例
        val interceptor = FileWriterLogInterceptor.getInstance(dir)
        // 注入日志拦截器单例
        EasyLog.addInterceptor(interceptor)
    }
}

真责任链模式

还有一个对日志库的基本诉求就是“美化日志”。
还是重新定义一个拦截器:

// 调用堆栈拦截器
class CallStackLogInterceptor : LogInterceptor {
    companion object {
        private const val HEADER =
            "┌──────────────────────────────────────────────────────────────────────────────────────────────────────"
        private const val FOOTER =
            "└──────────────────────────────────────────────────────────────────────────────────────────────────────"
        private const val LEFT_BORDER = '│'
        // 用于过滤日志调用栈(将EasyLog中的类过滤)
        private val blackList = listOf(
            CallStackLogInterceptor::class.java.name,
            EasyLog::class.java.name
        )
    }

    override fun log(priority: Int, tag: String, log: String) {
        // 打印头部
        Log.println(priority, tag, HEADER)
        // 打印日志
        Log.println(priority, tag, "$LEFT_BORDER$log")
        // 打印堆栈信息
        getCallStack(blackList).forEach {
            val callStack = StringBuilder()
                .append(LEFT_BORDER)
                .append("\t${it}").toString()
            Log.println(priority, tag, callStack)
        }
        // 打印尾部
        Log.println(priority, tag, FOOTER)
    }

    override fun enable(): Boolean {
        return true
    }
}

对于业务层的一条日志,调用堆栈拦截器输出了好几条日志,依次是头部、日志、堆栈、尾部。
为了降低复杂度,把获取调用栈的逻辑单独抽象为一个方法:

fun getCallStack(blackList: List<String>): List<String> {
    return Thread.currentThread()
        .stackTrace.drop(3) // 获取调用堆栈,过滤上面的3个,因为它们就是这里看到3个方法
        .filter { it.className !in blackList } // 过滤黑名单
        .map { "${it.className}.${it.methodName}(${it.fileName}:${it.lineNumber})" }
}

按照上面map()中那样的格式,在 Logcat 中就能有点击跳转效果。
然后把调用堆栈拦截器插入到所有拦截器的头部:

EasyLog.addFirstInterceptor(CallStackLogInterceptor())

打印效果是这样的:

看上去还不错~,但当我打开日志文件后,却只有DemoActivity.onCreate()这一行日志,并没有堆栈信息。。。
原因就在于我用了一个假的责任链模式!!

责任链模式就好比“老师发考卷”:

  1. 真责任链是这样发考卷的:老师将考卷递给排头同学,排头同学再传递给第二个同学,如此往复,直到考卷最终递到我的手里。
  2. 假责任链是这样发考卷的:老师站讲台不动,叫到谁的名字,谁就走上去拿自己的考卷。

学生时代当然希望老师用假责任链模式发考卷,因为若用真责任链,前排的每一个同学都可以看到我的分数,还能在我的考卷上乱涂乱画。
但在打日志这个场景中,用假责任链模式的后果就是,后续拦截器拿不到前序拦截器的处理结果

其实它根本称不上责任链,因为就不存在“链”,至多是一个“策略模式的遍历”
关于策略模式的详细解析可以点击一句话总结殊途同归的设计模式:工厂模式=?策略模式=?模版方法模式
所以只能用真责任链重构,为了向后传递拦截器的处理结果,拦截器就得持有其后续拦截器

interface LogInterceptor {
    // 后续拦截器
    var nextInterceptor:LogInterceptor?
    fun log(priority: Int, tag: String, log: String)
    fun enable():Boolean
}

然后在具体的拦截器实例中实现这个抽象属性:

open class LogcatInterceptor : LogInterceptor {
    override var nextInterceptor: LogInterceptor? = null
        get()= field
        set(value) {
            field = value
        }

    override fun log(priority: Int, tag: String, log: String) {
        if (enable()) {
            Log.println(priority, tag, log)
        }
        // 将log请求传递给下一个拦截器
        nextInterceptor?.log(priority, tag, log)
    }

    override fun enable(): Boolean {
        return true
    }
}

因为所有的拦截器都通过链的方式连接,所以 EasyLog 就不需要再持有一组拦截器,而只需要持有头拦截器就好了:

object EasyLog {
    // 头拦截器
    private val logInterceptor : LogInterceptor? = null

    // 注入拦截器
    fun setInterceptor(interceptor: LogInterceptor) {
        logInterceptor = interceptor
    }
}

注入拦截器的代码也需要做相应的变化:

class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        // 构建所有拦截器
        val dir = this.filesDir.absolutePath
        val fileInterceptor = FileWriterLogInterceptor.getInstance(dir)
        val logcatInterceptor = LogcatInterceptor()
        val callStackLogInterceptor = CallStackLogInterceptor()
        // 安排拦截器链接顺序
        callStackLogInterceptor.nextInterceptor = logcatInterceptor
        logcatInterceptor.nextInterceptor = fileInterceptor
        // 将头部拦截器注入
        EasyLog.setInterceptor(callStackLogInterceptor)
    }
}

这个设计能满足功能要求,但是对于接入方来说,复杂度有点高,因为不得不手动安排拦截器的顺序,而且如果想改动拦截器的顺序也非常麻烦。
我想到了 OkHttp,它也采用了真拦截器模式,但并不需要手动安排拦截器的顺序。它是怎么做到的?由于篇幅原因,源码分析不展开了,感兴趣的同学可以点开okhttp3.internal.http.RealInterceptorChain
下面运用这个思想,重构一下 EasyLog
首先要新建一条链:

class Chain(
    // 持有一组拦截器
    private val interceptors: List<LogInterceptor>,
    // 当前拦截器索引
    private val index: Int = 0
) {
    // 将日志请求在链上传递
    fun proceed(priority: Int, tag: String, log: String) {
        // 用一条新的链包裹链上的下一个拦截器
        val next = Chain(interceptors, index + 1)
        // 执行链上当前的拦截器
        val interceptor = interceptors.getOrNull(index)
        // 执行当前拦截器逻辑,并传入新建的链
        interceptor?.log(priority, tag, log, next)
    }
}

持有一组拦截器和索引,其中索引表示当前需要执行的是哪个拦截器。
包含一个proceed()方法,它是让日志请求在链上传递起来的关键。每次执行该方法都会新建一个链条并将索引+1,下次通过该链条获取的拦截器就是“下一个拦截器”。紧接着根据当前索引获取当前拦截器,将日志请求传递给它的同时,将“下一个拦截器”以链条的形式也传递给它。
重构之后的调用栈拦截器如下:

class CallStackLogInterceptor : LogInterceptor {
    companion object {
        private const val HEADER =
            "┌──────────────────────────────────────────────────────────────────────────────────────────────────────"
        private const val FOOTER =
            "└──────────────────────────────────────────────────────────────────────────────────────────────────────"
        private const val LEFT_BORDER = '│'
        private val blackList = listOf(
            CallStackLogInterceptor::class.java.name,
            EasyLog::class.java.name,
            Chain::class.java.name,
        )
    }

    override fun log(priority: Int, tag: String, log: String, chain: Chain) {
        // 将日志请求传递给下一个拦截器
        chain.proceed(priority, tag, HEADER)
        // 将日志请求传递给下一个拦截器
        chain.proceed(priority, tag, "$LEFT_BORDER$log")
        getCallStack(blackList).forEach {
            val callStack = StringBuilder()
                .append(LEFT_BORDER)
                .append("\t${it}").toString()
            // 将日志请求传递给下一个拦截器
            chain.proceed(priority, tag, callStack)
        }
        // 将日志请求传递给下一个拦截器
        chain.proceed(priority, tag, FOOTER)
    }

    override fun enable(): Boolean {
        return true
    }
}

和之前假责任链的区别在于,将已经美化过的日志传递给了后续拦截器,其中就包括文件日志拦截器,这样写入文件的日志也被美化了。
EasyLog 也需要做相应的改动:

object EasyLog {
    // 持有一组拦截器
    private val logInterceptors = mutableListOf<LogInterceptor>()
    // 将所有日志拦截器传递给链条
    private val interceptorChain = Chain(logInterceptors)
    
    fun addInterceptor(interceptor: LogInterceptor) {
        logInterceptors.add(interceptor)
    }

    fun addFirstInterceptor(interceptor: LogInterceptor) {
        logInterceptors.add(0, interceptor)
    }

    fun removeInterceptor(interceptor: LogInterceptor) {
        logInterceptors.remove(interceptor)
    }
    
    @Synchronized
    private fun log(
        priority: Int,
        message: String,
        tag: String,
        vararg args: Any,
        throwable: Throwable? = null
    ) {
        var logMessage = message.format(*args)
        if (throwable != null) {
            logMessage += getStackTraceString(throwable)
        }
        // 日志请求传递给链条
        interceptorChain.proceed(priority, tag, logMessage)
    }
}

这样一来上层注入拦截的代码不需要更改,还是按序 add 就好,将拦截器形成链条的复杂度被隐藏在 EasyLog 的内部。

高性能 I/O

有时候为了排查线上偶现问题,会尽可能地在关键业务点打 Log 并文件化,再上传到云以便排查。
在一些高强度业务场景中的高频模块,比如直播间中的 IM,瞬间产生几百上千条 Log 是家常便饭,这就对 Log 库的性能提出了要求(CPU 和内存)。

Okio

如何高性能地 I/O?
第一个想到的是Okio。关于为啥 Okio 的性能与好于 java.io 包的流,之后会专门写一篇分析文章。
重新写一个 Okio 版本的日志拦截器:

class OkioLogInterceptor private constructor(private var dir: String) : LogInterceptor {
    private val handlerThread = HandlerThread("log_to_file_thread")
    private val handler: Handler
    private val dispatcher: CoroutineDispatcher
    // 写日志的开始时间
    var startTime = System.currentTimeMillis()

    companion object {
        @Volatile
        private var INSTANCE: OkioLogInterceptor? = null
        fun getInstance(dir: String): OkioLogInterceptor =
            INSTANCE ?: synchronized(this) {
                INSTANCE ?: OkioLogInterceptor(dir).apply { INSTANCE = this }
            }
    }

    init {
        handlerThread.start()
        handler = Handler(handlerThread.looper)
        dispatcher = handler.asCoroutineDispatcher("log_to_file_dispatcher")
    }

    override fun log(priority: Int, tag: String, log: String, chain: Chain) {
        if (!handlerThread.isAlive) handlerThread.start()
        GlobalScope.launch(dispatcher) {
            // 使用 Okio 写文件
            val file = File(getFileName())
            file.sink(true).buffer().use {
                it.writeUtf8("[$tag] $log")
                it.writeUtf8("\n")
            }
            //  写日志结束时间
            if (log == "work done") Log.v("ttaylor1","log() work is done=${System.currentTimeMillis() - startTime}")
        }
        chain.proceed(priority, tag, log)
    }

    private fun getToday(): String =
        SimpleDateFormat("yyyy-MM-dd").format(Calendar.getInstance().time)

    private fun getFileName() = "$dir${File.separator}${getToday()}.log"
}

FileWriter vs Okio 性能 PK

为了测试 Okio 和 FileWriter 的性能差异,编写了如下测试代码:

class LogActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // 添加 FileWriter 或者 Okio 日志拦截器
        EasyLog.addInterceptor(OkioLogInterceptor.getInstance(this.filesDir.absolutePath))
//        EasyLog.addInterceptor(FileWriterLogInterceptor.getInstance(this.filesDir.absolutePath))
        // 重复输出 1 万条短 log
        MainScope().launch(Dispatchers.Default) { count ->
            repeat(10000){
                EasyLog.v("test log count=$count")
            }
            EasyLog.v("work done")
        }
    }
}

测试方案:重复输入 1 万条短 log,并且从写第一条 log 开始计时,直到最后一条 log 的 I/O 完成时输出耗时。
Okio 和 FileWriter 三次测试的对比耗时如下:

// okio
ttaylor1: log() work is done=9600
ttaylor1: log() work is done=9822
ttaylor1: log() work is done=9411

// FileWriter
ttaylor1: log() work is done=10688
ttaylor1: log() work is done=10816
ttaylor1: log() work is done=11928

看上去 Okio 在耗时上有微弱的优势。
但当我把单条 Log 的长度增加 300 倍之后,测试结果出现了反转:

// FileWriter
ttaylor1: log() work is done=13569
ttaylor1: log() work is done=12654
ttaylor1: log() work is done=13152

// okio
ttaylor1: log() work is done=14136
ttaylor1: log() work is done=15451
ttaylor1: log() work is done=15292

也就是说 Okio 在高频少量 I/O 场景性能好于 FileWriter,而高频大量 I/O 场景下没有性能优势。
这个结果让我很困惑,于是乎我在 Github 上提了 issue:
Okio is slower when writing long strings into file frequently compared with FileWriter · Issue #1098 · square/okio
没想到瞬间就被回复了:

我的提问的本意是想确认下使用 Okio 的姿势是否得当,但没想到官方回答却是:“Okio 库就是这样的,你的测试数据是符合预期的。我们在“用自己的 UTF-8 编码以减少大量垃圾回收”和性能上做了权衡。所以导致有些测试场景下性能好,有些场景下性能没那么好。我们的期望是在真实的业务场景下 Okio 的性能会表现的好。”
不知道我翻译的准不准确,若有误翻望英语大佬指点~

这样的结果不能让我满意,隐约觉得 Okio 的使用方式有优化空间,于是我一直凝视下面这段代码:

override fun log(priority: Int, tag: String, log: String) {
    GlobalScope.launch(dispatcher) {
        val file = File(getFileName())
        file.sink(true).buffer().use {
            it.writeUtf8("[$tag] $log")
            it.writeUtf8("\n")
        }
    }
    return false
}

代码洁癖告诉我,这里有几个可以优化的地方:

  1. 不用每次打 Log 都新建 File 对象。
  2. 不用每次打 Log 都新建输出流,每次打完就关闭流。

于是我改造了 Okio 日志拦截器:

class OkioLogInterceptor private constructor(private var dir: String) : LogInterceptor {
    private val handlerThread = HandlerThread("log_to_file_thread")
    private val handler: Handler
    private val dispatcher: CoroutineDispatcher
    private var bufferedSink: BufferedSink? = null
    // 初始化时,只新建一次文件
    private var logFile = File(getFileName())

    var startTime = System.currentTimeMillis()

    companion object {
        @Volatile
        private var INSTANCE: OkioLogInterceptor? = null

        fun getInstance(dir: String): OkioLogInterceptor =
            INSTANCE ?: synchronized(this) {
                INSTANCE ?: OkioLogInterceptor(dir).apply { INSTANCE = this }
            }
    }

    init {
        handlerThread.start()
        handler = Handler(handlerThread.looper)
        dispatcher = handler.asCoroutineDispatcher("log_to_file_dispatcher")
    }

   
    override fun log(priority: Int, tag: String, log: String, chain: Chain){
        if (!handlerThread.isAlive) handlerThread.start()
        GlobalScope.launch(dispatcher) {
            val sink = checkSink()
            sink.writeUtf8("[$tag] $log")
            sink.writeUtf8("\n")
            if (log == "work done") Log.v("ttaylor1","log() work is ok done=${System.currentTimeMillis() - startTime}")
        }
      chain.proceed(priority, tag, log)
    }

    private fun getToday(): String =
        SimpleDateFormat("yyyy-MM-dd").format(Calendar.getInstance().time)

    private fun getFileName() = "$dir${File.separator}${getToday()}.log"
    
    // 不关流,复用 bufferedSink 对象
    private fun checkSink(): BufferedSink {
        if (bufferedSink == null) {
            bufferedSink = logFile.appendingSink().buffer()
        }
        return bufferedSink!!
    }
}

新增了成员变量 logFile 和 bufferedSink,避免了每次打 Log 时重新构建它们。而且我没有在每次打完 Log 就把输出流关闭掉。

重新跑一下测试代码,奇迹发生了:

ttaylor1: log() work is done=832

1万条 Log 写入的时间从 9411 ms 缩短到 832 ms,整整缩短了 11 倍!!

这个结果有点难以置信,我连忙从手机中拉取了日志文件,非常担心是因为程序 bug 导致日志输出不全。
果不其然,正确的日志文件应该包含 1 万行,而现在只有 9901 行。
转念一下,不对啊,一次flush()都没有调用,为啥文件中会有日志?
哦~,肯定是因为内存中的输出缓冲区满了之后自动进行了 flush 操作。
然后我用同样的思路写了一个 FileWriter 的版本,跑了一下测试代码:

ttaylor1: FileWriter log() work is done=1239

这下可把 FileWriter 和 Okio 的差距显现出来了,将近 50 %的速度差距。
但是。。。我才意识到这个比对是不公平的。Okio 使用了缓存,而 java.io 没使用。
改了下测试代码,在 FileWriter 外套了一层 BufferedWriter,再跑一下:

ttaylor1: BufferedWriter log() work is done=1023

速度果然有提升,但还是比 Okio 慢了 25% 左右。
算是为技术选型 Okio 提供了数据支持!

感知日志打印结束?

但还有一个问题急需解决:Log 输出不全。

这是因为当最后一条日志写入缓冲区时,若缓冲区未满就不会执行 flush 操作。这种情况下需要手动 flush。
如何感知到最后一条 Log?做不到!因为打 Log 是业务层行为,底层 Log 库无法感知上层行为。

这个场景让我联想到 “搜索框防抖”,即当你在键入关键词后,自动发起搜索的行为。

的思想理解上面的场景:输入框是流数据的生产者,其内容每变化一次,就是在流上生产了一个新数据。但并不是每一个数据都需要被消费,所以得做“限流”,即丢弃一切发射间隔过短的数据,直到生产出某个数据之后一段时间内不再有新数据。

Flow 的操作符debounce()就非常契合这个场景。
它的背后机制是:每当流产生新数据时,开启倒计时,如果在倒计时归零之前没有新数据,则将最后那个数据发射出去,否则重新开启倒计时。关于 Flow 限流的应用场景及原理分析可以点击

知道了背后的机制,就不需要拘泥于具体的实现方式,使用 Android 中的消息机制也能实现同样的效果(日志拦截器正好使用了 HandlerThread,现成的消息机制)。

每当新日志到来时,将其封装为一条消息发送给 Handler,紧接着再发送一条延迟消息,若有后续日志,则移除延迟消息,并重发一条新延迟消息。若无后续日志,Handler 终将收到延迟消息,此时就执行 flush 操作。
(Android 中判定 Activity 生命周期超时也是用这套机制,感兴趣的可以搜索com.android.server.wm.ActivityStack.STOP_TIMEOUT_MSG)
改造后的日志拦截器如下:

class OkioLogInterceptor private constructor(private var dir: String) : LogInterceptor {
    private val handlerThread = HandlerThread("log_to_file_thread")
    private val handler: Handler
    private var startTime = System.currentTimeMillis()
    private var bufferedSink: BufferedSink? = null
    private var logFile = File(getFileName())

    // 日志消息处理器
    val callback = Handler.Callback { message ->
        val sink = checkSink()
        when (message.what) {
            // flush 日志
            TYPE_FLUSH -> {
                sink.use {
                    it.flush()
                    bufferedSink = null
                }
            }
            // 写日志
            TYPE_LOG -> {
                val log = message.obj as String
                sink.writeUtf8(log)
                sink.writeUtf8("\n")
            }
        }
        // 统计耗时
        if (message.obj as? String == "work done") Log.v(
            "ttaylor1",
            "log() work is done=${System.currentTimeMillis() - startTime}"
        )
        false
    }

    companion object {
        private const val TYPE_FLUSH = -1
        private const val TYPE_LOG = 1
        // 若 3000 ms 内没有新日志请求,则执行 flush
        private const val FLUSH_LOG_DELAY_MILLIS = 3000L

        @Volatile
        private var INSTANCE: OkioLogInterceptor? = null

        fun getInstance(dir: String): OkioLogInterceptor =
            INSTANCE ?: synchronized(this) {
                INSTANCE ?: OkioLogInterceptor(dir).apply { INSTANCE = this }
            }
    }

    init {
        handlerThread.start()
        handler = Handler(handlerThread.looper, callback)
    }

    override fun log(priority: Int, tag: String, log: String, chain: Chain) {
        if (!handlerThread.isAlive) handlerThread.start()
        handler.run {
            // 移除上一个延迟消息
            removeMessages(TYPE_FLUSH)
            // 将日志作为一条消息发送出去
            obtainMessage(TYPE_LOG, "[$tag] log").sendToTarget()
            // 构建延迟消息并发送
            val flushMessage = handler.obtainMessage(TYPE_FLUSH)
            sendMessageDelayed(flushMessage, FLUSH_LOG_DELAY_MILLIS)
        }
        chain.proceed(priority, tag, log)
    }

    private fun getToday(): String =
        SimpleDateFormat("yyyy-MM-dd").format(Calendar.getInstance().time)

    private fun getFileName() = "$dir${File.separator}${getToday()}.log"

    private fun checkSink(): BufferedSink {
        if (bufferedSink == null) {
            bufferedSink = logFile.appendingSink().buffer()
        }
        return bufferedSink!!
    }
}

后续
目前只是搭了一个高可扩展,高性能的日子组件的框架,后续文章会逐步介绍如何进行下一步的优化,欢迎关注~

新的开始

改变人生,没有什么捷径可言,这条路需要自己亲自去走一走,只有深入思考,不断反思总结,保持学习的热情,一步一步构建自己完整的知识体系,才是最终的制胜之道,也是程序员应该承担的使命。

《系列学习视频》

《系列学习文档》

《我的大厂面试之旅》

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip204888 (备注Android)
img

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

=
SimpleDateFormat(“yyyy-MM-dd”).format(Calendar.getInstance().time)

private fun getFileName() = "$dir${File.separator}${getToday()}.log"

private fun checkSink(): BufferedSink {
    if (bufferedSink == null) {
        bufferedSink = logFile.appendingSink().buffer()
    }
    return bufferedSink!!
}

}



> 
> 后续  
>  目前只是搭了一个高可扩展,高性能的日子组件的框架,后续文章会逐步介绍如何进行下一步的优化,欢迎关注~


### 新的开始

改变人生,没有什么捷径可言,这条路需要自己亲自去走一走,只有深入思考,不断反思总结,保持学习的热情,一步一步构建自己完整的知识体系,才是最终的制胜之道,也是程序员应该承担的使命。

**《系列学习视频》**
[外链图片转存中...(img-FgdnSAu3-1713243142753)]

**《系列学习文档》**

[外链图片转存中...(img-wPLv5FUF-1713243142753)]

**《我的大厂面试之旅》**

[外链图片转存中...(img-VflmoGXk-1713243142754)]




**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**

**需要这份系统化的资料的朋友,可以添加V获取:vip204888 (备注Android)**
[外链图片转存中...(img-4YnKnGTi-1713243142754)]

**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**

  • 9
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值