设计日志系统
问题
如何设计 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。