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

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

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

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

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

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

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

正文

    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): List {
return Thread.currentThread()
.stackTrace.drop(3) // 获取调用堆栈,过滤上面的3个,因为它们就是这里看到3个方法
.filter { it.className !in blackList } // 过滤黑名单
.map { “ i t . c l a s s N a m e . {it.className}. it.className.{it.methodName}( i t . f i l e N a m e : {it.fileName}: it.fileName:{it.lineNumber})” }
}


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



EasyLog.addFirstInterceptor(CallStackLogInterceptor())


打印效果是这样的:  
 ![](https://img-blog.csdnimg.cn/2dfbf82215884486843688ea72801af4.png)


看上去还不错~,但当我打开日志文件后,却只有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,
// 当前拦截器索引
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()
// 将所有日志拦截器传递给链条
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  
 没想到瞬间就被回复了:  
 ![](https://img-blog.csdnimg.cn/82b4b3479b6742e09171597f6d5e543f.png)


我的提问的本意是想确认下使用 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!!
}

}



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


## 总结


* “**简单**”和“**弹性**”是库设计中首要关注的两个方面。
* Kotlin 有诸多语法特性能极大地降低代码的复杂度。
* 真责任链模式非常适用于为日志库提供弹性。它使得动态为日志库新
* 功能成为可能,它使得每次的处理结果得以在传递给后续处理者。
* 用HandlerThread实现异步串行日志输出。
* 用Okio实现高性能的日志文件化。
* 在高频日志文件化的场景下,复用输出流能极大地提高性能。同时需要延迟消息机制保证日志的完整性。



> 
> 作者:唐子玄  
>  链接:https://juejin.cn/post/7074734658277081102
> 
> 
> 


### 最后


这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。  
 ![在这里插入图片描述](https://img-blog.csdnimg.cn/06e41b3932164f0db07014d54e6e5626.png)  
 相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照知识体系编排的。


#### 一、架构师筑基必备技能


1、深入理解Java泛型  
 2、注解深入浅出  
 3、并发编程  
 4、数据传输与序列化  
 5、Java虚拟机原理  
 6、高效IO  
 ![在这里插入图片描述](https://img-blog.csdnimg.cn/079bc2315e7e4f73b8fe703c3c51ae8d.png)


#### 二、Android百大框架源码解析


1.Retrofit 2.0源码解析  
 2.Okhttp3源码解析  
 3.ButterKnife源码解析  
 4.MPAndroidChart 源码解析  
 5.Glide源码解析  
 6.Leakcanary 源码解析  
 7.Universal-lmage-Loader源码解析  
 8.EventBus 3.0源码解析  
 9.zxing源码分析  
 10.Picasso源码解析  
 11.LottieAndroid使用详解及源码解析  
 12.Fresco 源码分析——图片加载流程  
 ![在这里插入图片描述](https://img-blog.csdnimg.cn/2206daa6ec0b4bdfb0ea7a908d1249e4.png)


#### 三、Android性能优化实战解析


* 腾讯Bugly:对字符串匹配算法的一点理解
* 爱奇艺:安卓APP崩溃捕获方案——xCrash
* 字节跳动:深入理解Gradle框架之一:Plugin, Extension, buildSrc
* 百度APP技术:Android H5首屏优化实践
* 支付宝客户端架构解析:Android 客户端启动速度优化之「垃圾回收」
* 携程:从智行 Android 项目看组件化架构实践
* 网易新闻构建优化:如何让你的构建速度“势如闪电”?  
 ![在这里插入图片描述](https://img-blog.csdnimg.cn/2139f00c8fc74031b9fd38257c96c22e.png)


#### 四、高级kotlin强化实战


1、Kotlin入门教程  
 2、Kotlin 实战避坑指南  
 3、项目实战《Kotlin Jetpack 实战》  
 ![在这里插入图片描述](https://img-blog.csdnimg.cn/d05bd9ae3a9e481fa317022bfe161c7d.png)


#### 五、Android高级UI开源框架进阶解密



## 总结

**其实要轻松掌握很简单,要点就两个:**

1.  找到一套好的视频资料,紧跟大牛梳理好的知识框架进行学习。
2.  多练。 (视频优势是互动感强,容易集中注意力)

**你不需要是天才,也不需要具备强悍的天赋,只要做到这两点,短期内成功的概率是非常高的。**

对于很多初中级Android工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。

> 以上就是总结的关于在面试的一些总结,希望对大家能有些帮助,除了这些面试中需要注意的问题,当然最重要的就是刷题了,这里放上我之前整理的一份超全的**面试专题PDF**

还有 **高级架构技术进阶脑图、Android开发面试专题资料**,高级进阶架构资料 帮助大家学习提升进阶,也节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。

**【Android核心高级技术PDF文档,BAT大厂面试真题解析】**

![](https://img-blog.csdnimg.cn/img_convert/6acaf601c71efd8fd9ef7d274c16e8e7.webp?x-oss-process=image/format,png)

> 这里只是整理出来的部分面试题,后续会持续更新,希望通过这些高级面试题能够降低面试Android岗位的门槛,让更多的Android工程师理解Android系统,掌握Android系统。喜欢的话麻烦点击一个喜欢在关注一下~



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

**需要这份系统化的资料的朋友,可以添加V获取:vip204888 (备注Android)**
![img](https://img-blog.csdnimg.cn/img_convert/7c00504e292010180f283ee0adae42c1.png)

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

otlin Jetpack 实战》  
 ![在这里插入图片描述](https://img-blog.csdnimg.cn/d05bd9ae3a9e481fa317022bfe161c7d.png)


#### 五、Android高级UI开源框架进阶解密



## 总结

**其实要轻松掌握很简单,要点就两个:**

1.  找到一套好的视频资料,紧跟大牛梳理好的知识框架进行学习。
2.  多练。 (视频优势是互动感强,容易集中注意力)

**你不需要是天才,也不需要具备强悍的天赋,只要做到这两点,短期内成功的概率是非常高的。**

对于很多初中级Android工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。

> 以上就是总结的关于在面试的一些总结,希望对大家能有些帮助,除了这些面试中需要注意的问题,当然最重要的就是刷题了,这里放上我之前整理的一份超全的**面试专题PDF**

还有 **高级架构技术进阶脑图、Android开发面试专题资料**,高级进阶架构资料 帮助大家学习提升进阶,也节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。

**【Android核心高级技术PDF文档,BAT大厂面试真题解析】**

[外链图片转存中...(img-ljWKE14F-1713462376647)]

> 这里只是整理出来的部分面试题,后续会持续更新,希望通过这些高级面试题能够降低面试Android岗位的门槛,让更多的Android工程师理解Android系统,掌握Android系统。喜欢的话麻烦点击一个喜欢在关注一下~



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

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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值