一、日志系统概述:Android日志系统的发展历程、核心组件与缓冲区概念
大家好,我是老张。今天咱们聊聊Android日志系统。说实话,我入行那会儿,Android还叫 cupcake(纸杯蛋糕)版本。那时候调试程序,全靠 Log.d 和 adb logcat 硬扛。现在回想起来,真是又亲切又感慨。
日志系统这东西,说白了就是系统的「黑匣子」。飞机有黑匣子,手机系统也有。你想想看,App 崩溃了、系统卡顿了、网络断了,第一反应是什么?肯定是翻日志。所以,搞懂日志系统,是每个 Android 工程师的必修课。
1.1 发展历程:从简单到复杂
Android 日志系统经历了三个阶段。我把它总结为「石器时代」、「工业时代」和「智能时代」。
- 早期(Android 1.0 - 4.4):日志直接写内核缓冲区,用 /dev/log/* 节点读取。简单粗暴,但有个致命问题——日志多了会丢。我记得有一次线上 bug,日志量太大,关键信息被冲掉了,查了三天才定位到问题。
- 中期(Android 5.0 - 7.0):引入了 logd 守护进程。日志不再直接写内核,而是通过 logd 统一管理。缓冲区也变成了环形缓冲区,不会丢日志了。嗯,这里要注意,环形缓冲区满了会覆盖旧日志,所以抓日志要趁早。
- 现代(Android 8.0+):logd 功能更强大,支持了 per-buffer 大小动态调整、日志压缩、权限控制等。我个人最喜欢的是「日志标签白名单」功能,可以过滤掉不关心的日志,减少干扰。
核心变化一句话总结:从「裸写内核」到「守护进程统一管理」,再到「精细化控制」。每一步都是为了解决实际痛点。
1.2 核心组件:logd 和 logcat
这两个组件,是日志系统的左膀右臂。一个负责写,一个负责读。
logd:日志守护进程
logd 是 Android 系统的一个 native 守护进程,在 init.rc 中启动。它的职责很简单:接收日志、存储日志、分发日志。
我在项目中遇到过一个问题:某款低端机,logd 进程 CPU 占用率高达 30%。查了半天,发现是某个三方 App 疯狂打日志,每秒几千条。logd 忙着处理,反而拖慢了系统。解决方案?限制单个 App 的日志速率。嗯,这个后面会细讲。
logd 的核心数据结构是 LogBuffer,它是一个环形缓冲区。每个缓冲区有独立的读写指针。写日志时,如果缓冲区满了,就覆盖最旧的日志。读日志时,logcat 通过 socket 连接 logd,按需拉取。
// logd 的启动参数(来自 init.rc)
service logd /system/bin/logd
class core
socket logd stream 0666 logd logd
socket logdr stream 0666 logd logd
socket logdw stream 0222 logd logd
// ... 其他参数
注意看这三个 socket:logd 是控制通道,logdr 是读取通道,logdw 是写入通道。权限控制得很细。
logcat:日志查看工具
logcat 是命令行工具,也是我们最常用的调试利器。它的本质是一个 socket 客户端,连接 logd 的 logdr 端口,拉取日志并格式化输出。
我曾经犯过一个低级错误:在线上环境跑了 logcat -v long,结果日志量太大,把磁盘撑爆了。后来学乖了,线上抓日志一定加 -b 指定缓冲区,加 -s 过滤标签,加 -t 限制条数。
个人习惯:我一般用 logcat -b all -v threadtime -s MyTag。这样能看到所有缓冲区的日志,带线程时间戳,只显示我关心的标签。效率高很多。
1.3 日志缓冲区:main、system、crash 等
缓冲区是日志系统的「仓库」。Android 定义了多个缓冲区,每个缓冲区存不同类型的日志。为什么要分?你想想看,如果把系统日志和 App 日志混在一起,查问题就像大海捞针。
| 缓冲区名称 | 存储内容 | 默认大小 | 典型用途 |
|---|---|---|---|
| main | App 应用日志(Log.i/d/w/e) | 256KB | 调试 App 业务逻辑 |
| system | 系统组件日志(SystemServer、AMS、WMS 等) | 256KB | 分析系统行为 |
| crash | 崩溃日志(Java 异常、native 崩溃) | 256KB | 定位崩溃原因 |
| events | 事件日志(Activity 生命周期、按键事件等) | 256KB | 分析用户操作流程 |
| kernel | 内核日志(dmesg 内容) | 128KB | 分析驱动、内存、电源问题 |
| security | 安全日志(SELinux 拒绝、权限检查失败) | 64KB | 排查权限问题 |
这里有个坑:不同厂商可能会修改缓冲区大小。比如某米手机,把 main 缓冲区改成了 512KB。所以,不要迷信默认值。用 logcat -g 可以查看当前缓冲区大小。
避坑指南:我曾经在调试一个 native 崩溃时,怎么都抓不到日志。后来发现,crash 缓冲区默认只有 256KB,崩溃日志太大被截断了。解决方案?用 adb logcat -b crash -G 1M 临时扩大缓冲区。记住,这是临时方案,重启后恢复默认。
1.4 日志的写入与读取流程
搞清楚了组件和缓冲区,咱们看看日志从产生到显示,到底经历了什么。我画个流程图(用文字描述):
- 写入端:App 调用
Log.i()→ 通过 JNI 调用__android_log_print()→ 写入/dev/socket/logdw→ logd 接收 → 写入对应缓冲区。 - 读取端:logcat 连接
/dev/socket/logdr→ 发送读取请求(指定缓冲区、过滤条件) → logd 从缓冲区读取 → 返回给 logcat → 格式化输出到终端。
整个过程是异步的。写入端不会阻塞,读取端可以实时拉取。这也是为什么 logcat 能实时显示日志的原因。
嗯,这里要注意:日志写入是有开销的。每次 Log.d() 调用,都会触发一次 socket 通信。如果日志量太大,会拖慢 App 性能。所以,生产环境一定要控制日志量,或者干脆关闭日志。
1.5 实战:快速定位问题
说了这么多理论,来点实际的。假设你遇到一个 App 闪退的问题,怎么用日志系统快速定位?
我的套路是这样的:
- 先看 crash 缓冲区:
adb logcat -b crash -v threadtime。这里能看到完整的崩溃堆栈。 - 如果崩溃堆栈不完整,看 main 缓冲区:
adb logcat -b main -v threadtime -s MyAppTag。这里能看到崩溃前的业务日志。 - 如果怀疑是系统问题,看 system 缓冲区:
adb logcat -b system -v threadtime。比如 AMS 的 ANR 日志、WMS 的窗口异常等。 - 如果还不行,看 events 缓冲区:
adb logcat -b events -v threadtime。这里能看到 Activity 的启动、暂停、销毁事件,帮你还原操作流程。
核心思路:先看 crash 找原因,再看 main 找上下文,最后看 system/events 找系统级线索。不要一上来就 logcat -b all,信息太多反而找不到重点。
好了,这一章就到这里。日志系统是 Android 调试的基石,搞懂它,你就能在问题面前游刃有余。下一章,咱们深入 logd 的源码,看看它到底是怎么工作的。