跳到主要内容

设计日志系统

问题

如何设计 Android 客户端的日志收集系统?

答案

架构设计

日志分级

级别用途Release 输出
VERBOSE开发调试
DEBUG调试信息
INFO关键流程✅ 写文件
WARN异常但可恢复✅ 写文件
ERROR错误✅ 写文件 + 上报

高性能日志写入(mmap)

传统的文件写入(FileOutputStream):

write() → 用户态 → 内核态切换 → 文件系统缓冲 → 磁盘

mmap 方案(MMKV / xLog 采用):

write() → 直接写入映射的内存区域 → 内核异步刷盘
  • 性能:避免频繁的用户态/内核态切换
  • 可靠:即使进程 crash,已写入 mmap 的数据也不会丢失(内核会负责刷盘)

日志框架设计

object AppLogger {
private val dispatchers = mutableListOf<LogDispatcher>()

fun addDispatcher(dispatcher: LogDispatcher) {
dispatchers.add(dispatcher)
}

fun i(tag: String, message: String) = log(Level.INFO, tag, message)
fun w(tag: String, message: String) = log(Level.WARN, tag, message)
fun e(tag: String, message: String, t: Throwable? = null) =
log(Level.ERROR, tag, message, t)

private fun log(level: Level, tag: String, message: String, t: Throwable? = null) {
val logEntry = LogEntry(
level = level,
tag = tag,
message = message,
throwable = t,
timestamp = System.currentTimeMillis(),
threadName = Thread.currentThread().name
)
dispatchers.forEach { it.dispatch(logEntry) }
}
}

interface LogDispatcher {
fun dispatch(entry: LogEntry)
}

// Console 输出(仅 Debug)
class ConsoleDispatcher : LogDispatcher {
override fun dispatch(entry: LogEntry) {
if (BuildConfig.DEBUG) {
Log.println(entry.level.priority, entry.tag, entry.message)
}
}
}

// 文件写入(mmap)
class FileDispatcher(private val logDir: File) : LogDispatcher {
override fun dispatch(entry: LogEntry) {
if (entry.level >= Level.INFO) {
writeToFile(entry) // mmap 写入
}
}
}

上报策略

策略触发条件说明
定时上报每 4 小时 / WiFi 时正常日志
崩溃上报App 重启后上一次的崩溃日志
用户反馈点击"反馈"按钮附带最近 N 条日志
远程命令服务端推送线上问题排查

常见面试问题

Q1: 为什么日志写入推荐 mmap?

答案

传统 write() 每次调用都涉及用户态→内核态切换,频繁写入会有 I/O 性能问题。mmap 将文件映射到进程的虚拟内存地址空间,写日志变成写内存操作(memcpy),性能极高。且进程异常退出时,已映射的数据由内核负责刷盘,不会丢失。代表实现:美团的 Logan、微信的 XLOG。

相关链接