系列目录 :第一篇:全景图与架构概览 | 第二篇:logd守护进程---启动、初始化与Socket通信 | 第三篇:liblog库---日志写入的完整链路 | 第四篇:日志写入接口---Java层与Native层 | 第五篇:日志读取---logcat源码深度分析 | 第六篇:日志缓冲区管理---容量、裁剪与统计机制 | 第七篇:实战调试与常见问题分析
前六篇讲透了原理,本篇用这些原理解决实际问题。所有案例均来自真实工程场景。
一、问题定位速查表
| 现象 | 可能原因 | 排查方法 | 参考原理篇 |
|---|---|---|---|
| logcat 没有输出 | 日志级别被过滤、缓冲区空了 | logcat -v threadtime -b all *:V |
第五篇 |
| 日志大量丢失 | 缓冲区溢出、裁剪过度 | logcat -g, logcat -S |
第六篇 |
| "chatty" 消息频繁 | 高频重复日志被裁剪合并 | 检查循环日志代码,grep chatty |
第六篇 |
| 应用日志不出现 | 误写了 system/events 缓冲区 | 检查 Log/Slog 使用,确认 bufID | 第四篇 |
| 开机日志丢失 | logd 启动晚于日志产生 | 检查 /sys/fs/pstore/ 或 dmesg |
第二篇 |
| 日志时间错乱 | 时钟跳变或 monotonic/realtime 切换 | 检查 CLOCK_MONOTONIC 与 persist.logd.timestamp |
第一篇 |
| logcat 卡住 | logd 慢速 reader 被阻塞 | ps -T -p $(pidof logd) 查看线程状态 |
第二/六篇 |
| 内核日志看不到 | klogd 转发未工作 | cat /proc/kmsg 或 dmesg |
第二篇 |
二、案例一:开发中突然看不到日志
现象
应用正常运行,但 logcat 输出突然停止。重启 logcat 也没用。
排查步骤
bash
# 步骤1:查看缓冲区是否满了
adb logcat -g
# 输出示例:main: ring buffer is 256Kb (250Kb consumed), max entry is 5120b, max payload is 4069b
# 如果 "consumed" 接近 "ring buffer is",说明缓冲区快满了
# 步骤2:查看是谁在大量写日志
adb logcat -S
# 找出 SIZE 最大的 UID 和 TAG
# 步骤3:如果缓冲区满了,临时增大
# ★ 注意:属性名是 persist.logd.size.<buffer>,不是 logd.size.<buffer>
adb shell setprop persist.logd.size.main 512K
# 发送 SIGHUP 让 logd 重新加载配置(★ 是 SIGHUP,不是 SIGUSR1)
adb shell kill -HUP $(pidof logd)
# 步骤4:如果是某个进程滥用日志
adb logcat -v threadtime -b main *:V | grep "chatty"
# 查找 chatty 消息,定位被裁剪合并的日志来源
原理分析
LogBuffer::prune() 的裁剪策略:
- 优先裁剪黑名单(naughty)UID/PID 的条目
- 其次裁剪占用超过缓冲区 12.5% 的"最坏 UID"条目(保留 dropped 占位符)
- 最后从最旧条目开始过期,白名单(nice)条目最后才删除
如果缓冲区满了且日志写入速度 > logcat 读取速度,日志就会被裁剪。慢速 reader 会被 logd 触发跳过(triggerSkip),极端情况下会被释放(release)。
解决方案
java
// 方案1:减少不必要的日志输出
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "heavy debug info: " + expensiveToString());
}
// 方案2:合理使用日志级别
Log.v(TAG, "极详细调试"); // 发布版关闭
Log.d(TAG, "一般调试信息"); // 开发阶段
Log.i(TAG, "值得记录的事件"); // 保留
Log.w(TAG, "异常但可恢复"); // 保留
Log.e(TAG, "错误"); // 保留
三、案例二:chatty 消息淹没了关键日志
现象
bash
logcat 输出中频繁出现:
I chatty : uid=10123(com.example.app) 0x3039 expire 47 lines
格式说明 :
I= INFO 级别,chatty= 固定 tag,uid=10123= 写入 UID,(com.example.app)= 包名,0x3039= 线程 TID(十六进制),expire 47 lines= 被裁剪合并了 47 条相同日志。
原因
应用中有循环日志代码:
java
void onSensorChanged(SensorEvent event) {
// 高频传感器回调,每秒 100 次
Log.d(TAG, "sensor value: " + event.values[0]);
// 每条日志完全相同 → 裁剪时被设置为 dropped → chatty 合并计数
}
排查
bash
# 查看 chatty 来源
adb logcat -v threadtime | grep chatty
# 按 PID 过滤(★ logcat 没有 --uid 选项,只有 --pid)
adb logcat -v threadtime --pid=$(adb shell pidof com.example.app)
解决方案
java
// 方案1:用采样率控制
private int counter = 0;
void onSensorChanged(SensorEvent event) {
if (counter++ % 100 == 0) {
Log.d(TAG, "sensor value: " + event.values[0]);
}
}
// 方案2:使用 VERBOSE 级别(发布版自动关闭)
Log.v(TAG, "sensor value: " + event.values[0]);
// 方案3:用 IF_ALOGV(Native 层)
IF_ALOGV() {
ALOGV("sensor value: %f", event.values[0]);
}
四、案例三:开机日志丢失问题
现象
设备启动过程中的日志(如 init 阶段、service 启动日志)在 logcat 中看不到。
原因
时间线:
T0: 内核启动 → printk 输出日志到内核 __log_buf
T1: init 进程启动 → 日志暂存(logd 还未启动)
T2: servicemanager 启动
T3: logd 启动 → 创建 LogBuffer → 开始接收日志
T4: zygote 启动 → Log.d() 可以正常记录
T0-T2 阶段的日志:只能通过内核日志(dmesg)或 pstore 查看
T3 之后的日志:logcat 正常可见
排查
bash
# 检查内核日志(dmesg)
adb shell dmesg | head -50
# 检查 pstore(上次崩溃后保留的日志)
adb shell ls /sys/fs/pstore/
# console-ramoops:内核日志
# pmsg-ramoops-0:用户态日志(pmsg_writer 写入的)
# 查看是否有上次的重启日志
adb shell cat /sys/fs/pstore/console-ramoops | head -50
# ★ logcat -L 读取 pstore 中的日志
adb logcat -L
解决方案
bash
# 1. 增大内核日志缓冲区
# 在内核命令行添加:
log_buf_len=1M
# 2. 确保 pstore 配置已启用
# CONFIG_PSTORE=y, CONFIG_PSTORE_RAM=y
# 3. 抓取 logd 相关的启动事件
adb logcat -b events | grep "logd"
原理
参考第三篇的 pmsg_writer:系统崩溃时日志通过 write("/dev/pmsg0") 写入 pstore。重启后 logcat -L 可以读取上次的日志(logd 没有 -L 参数,该功能由 logcat 的 ANDROID_LOG_PSTORE 模式实现)。
五、案例四:系统服务日志不出现
现象
修改了 framework 代码添加 Slog.d(TAG, "xxxx"),但 logcat -b system 看不到。
排查
bash
# 步骤1:确认日志是否真的写入了(搜索所有缓冲区)
adb logcat -b all | grep "xxxx"
# 步骤2:检查 SELinux 是否阻止
adb shell dmesg | grep "avc.*denied.*logd"
# 步骤3:检查进程 UID
adb shell ps | grep <service_name>
# 系统服务 UID 通常为 1000 (system)
# 步骤4:确认写入了正确的缓冲区
# Slog.d → system 缓冲区 (LOG_ID_SYSTEM=3)
# Log.d → main 缓冲区 (LOG_ID_MAIN=0)
常见错误
java
// ❌ 错误:用了 Log 而非 Slog
Log.d(TAG, "system service message"); // 去了 main 缓冲区
// ✅ 正确
Slog.d(TAG, "system service message"); // 去了 system 缓冲区
六、案例五:events 日志的 tag 编号问题
现象
logcat -b events 输出的 tag 是数字而非名称。
原因
events 日志使用 /system/etc/event-log-tags 文件将 tag 编号映射为名称。logcat 读取时通过 EventTagMap 做映射。如果运行时映射表未更新(例如新增了自定义 tag 编号),就会出现数字 tag。
排查
bash
# 查看当前的 tag 映射文件
adb shell cat /system/etc/event-log-tags | grep <tag_number>
# 查看 logcat 是否能正确解析
adb logcat -b events -v threadtime | grep "<数字tag>"
解决方案
bash
# 在 event-log-tags 文件中添加对应条目
# 格式:<tag编号> <tag名称> (<字段名>|<类型>|<字节数>),...
echo "123456 my_custom_event (Event Value|3)" >> /system/etc/event-log-tags
# 重启 logcat 即可生效(logcat 重启时重新加载 EventTagMap)
# 不需要重启 logd
七、日志系统的性能影响分析
logd 内存占用
bash
# ★ logd 是 native 守护进程,不能用 dumpsys meminfo
# 正确方式:查看 /proc/$(pidof logd)/status
adb shell cat /proc/$(pidof logd)/status | grep -E "VmRSS|VmSize"
# 或用 procrank
adb shell procrank | grep logd
典型内存占用(缓冲区部分):
| 缓冲区 | 默认大小 | 说明 |
|---|---|---|
| main | 256KB | 可配置 |
| system | 256KB | 可配置 |
| events | 256KB | 可配置 |
| radio | 256KB | 可配置 |
| crash | 256KB | 可配置 |
| 合计 | ~1.28MB | 仅缓冲区,不含日志条目开销 |
实际内存占用 = 缓冲区大小 + 每个
LogBufferElement对象的开销(元数据 + 消息体)。在日志密集场景下,实际内存可能达到 2-3MB。
写入性能开销
开销构成(从调用 Log.d() 到 logd 完成写入):
- 字符串格式化 (Java 层
printlns分块 + Native 层vsnprintf):~5-10μs - 锁竞争 (liblog 端
log_init_lock和 logd 端mLogElementsLock):1-5μs - Socket 发送 (
writev()写入/dev/socket/logdw):5-20μs - logd 端处理(epoll 唤醒 + 锁 + 时间排序插入 + 统计更新):10-30μs
总耗时约 20-65μs,在正常负载下对应用影响微乎其微。但高频日志(每秒数万条)会显著增加 CPU 占用。
八、SELinux 与日志权限
日志相关的 SELinux 上下文
bash
# logd 进程的 SELinux 上下文
adb shell ps -Z | grep logd
# logdw socket 的 SELinux 上下文
adb shell ls -Z /dev/socket/logd*
# 查看日志相关的 AVC 拒绝记录
adb shell dmesg | grep "avc.*denied.*logd"
常见 SELinux 问题
bash
# 问题:自定义 native 服务无法写入日志
# 典型错误信息:
# avc: denied { write } for comm="my_service" path="socket:[12345]"
# dev="sockfs" scontext=u:r:my_service:s0 tcontext=u:r:logd:s0
# 解决方案:添加 SELinux 策略
# allow my_service logd_socket:sock_file write;
# 或使用更精细的宏:unix_socket_send(my_service, logdw, logd)
九、调试技巧集锦
技巧1:实时监控特定进程
bash
# 按 PID 过滤(★ 注意是 --pid,不是 --uid)
adb logcat --pid=$(adb shell pidof com.example.app)
# 同时监控多个缓冲区
adb logcat -v threadtime -b main -b system -b events
技巧2:查看被裁剪的日志
bash
# 搜索 chatty 消息(裁剪合并标记)
adb logcat -v threadtime -b main | grep "chatty"
# 统计 chatty 出现频率
adb logcat -d -b main | grep -c "chatty"
技巧3:对比各缓冲区
bash
# 同时打开四个窗口:
# 窗口1:main
adb logcat -v time -b main MyTag:* *:S
# 窗口2:system
adb logcat -v time -b system MyTag:* *:S
# 窗口3:events(注意看二进制事件解码)
adb logcat -v threadtime -b events
# 窗口4:内核
adb shell cat /proc/kmsg
技巧4:分析日志"大户"
bash
# 统计某个 TAG 的出现频率
adb logcat -d -v raw | grep -oP '(?<=/)[^\(]+' | sort | uniq -c | sort -rn | head -20
# 用 logcat -S 直接查看统计(最准确)
adb logcat -S
技巧5:崩溃日志分析
bash
# 搜索 FATAL 级别日志
adb logcat -v threadtime -b crash
# 搜索 ANR 相关事件
adb logcat -v threadtime -b events | grep "am_anr"
# 搜索 Native Crash
adb logcat -v threadtime | grep -A 20 "FATAL SIGNAL"
技巧6:日志持久化到文件
bash
# 将日志保存到文件
adb logcat -v threadtime -f /data/local/tmp/mylog.txt
# 持续记录,限制大小(轮转 10 个文件,每个 1MB)
adb logcat -v threadtime -r 1024 -n 10 -f /data/local/tmp/logcat.txt
十、logd 运行时配置
bash
# 查看所有 logd 相关属性
adb shell getprop | grep logd
# ★ 动态调整缓冲区大小(注意属性名有 persist. 前缀)
adb shell setprop persist.logd.size.main 512K
adb shell setprop persist.logd.size.system 512K
# 发送 SIGHUP 让 logd 重新加载配置(★ 是 SIGHUP,不是 SIGUSR1)
adb shell kill -HUP $(pidof logd)
# 或者用 logcat -G 直接设置(无需重启 logd)
adb logcat -G 512K
# ★ 启用统计功能(属性名是 logd.statistics,布尔值)
adb shell setprop logd.statistics true
# 启用日志级别控制
adb shell setprop log.tag.MyTag DEBUG
logd 支持的系统属性一览
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
persist.logd.size |
number | 256K | 全局缓冲区默认大小 |
persist.logd.size.<buffer> |
number | 256K | 每个 buffer 独立大小 |
ro.logd.size |
number | 256K | 编译时默认值(persist 为空时使用) |
ro.logd.statistics |
bool | svelte+ | 启用 logcat -S 统计 |
persist.logd.security |
bool | false | 启用 security 缓冲区 |
ro.logd.kernel |
bool | svelte+ | 启用 klogd 内核日志转发 |
persist.logd.filter |
string | ~! ~1000/! |
裁剪过滤规则 |
persist.logd.timestamp |
string | realtime | 时间戳类型(monotonic/realtime) |
ro.config.low_ram |
bool | false | 低内存设备:日志缓冲区降为 64K |
十一、与其他日志系统的协同
logd + dropbox
bash
# DropBoxManager 是 ServiceManager 中的一个 Java 服务
# 它记录 crash/anr/watchdog 等关键事件
# 查看 dropbox 条目
adb shell dumpsys dropbox
logd + tombstone
bash
# Native crash 会在 /data/tombstones/ 产生 tombstone 文件
adb shell ls -la /data/tombstones/
# tombstone 相关的崩溃信息也同时写入 logd 的 crash 缓冲区
adb logcat -b crash
logd + pstore
bash
# 系统崩溃(kernel panic)时,pmsg 通道的日志刷入 pstore
# 重启后通过 logcat -L 读取
adb logcat -L
# 或直接读取文件
adb shell cat /sys/fs/pstore/pmsg-ramoops-0
十二、源码调试环境搭建
关键源码路径
system/core/logd/ # logd 守护进程全部源码
system/core/logcat/ # logcat 工具源码
system/core/liblog/ # liblog 库源码(写入/读取/格式化)
frameworks/base/core/java/android/util/Log.java # Java 层 Log
frameworks/base/core/java/android/util/Slog.java # Java 层 Slog
frameworks/base/core/java/android/util/EventLog.java # Java 层 EventLog
frameworks/base/core/jni/android_util_Log.cpp # Log JNI 桥接
frameworks/base/core/jni/android_util_EventLog.cpp # EventLog JNI 桥接
system/core/include/log/ # 日志相关头文件
system/core/rootdir/init.rc # logd service 定义
在 logd 中添加调试日志
cpp
// ★ logd 本身不能使用 ALOGD(会导致循环依赖),应使用内核日志
#include <cutils/klog.h>
KLOG_ERROR("logd", "prune: uid=%u size=%zu\n", uid, size);
// 输出到内核日志,通过 dmesg 或 cat /proc/kmsg 查看
十三、本篇总结与系列回顾
| 篇 | 主题 | 核心内容 |
|---|---|---|
| 一 | 架构概览 | 六层架构、五个缓冲区、两条数据流 |
| 二 | logd 守护进程 | 启动时序、Socket 设计、epoll 事件循环 |
| 三 | liblog 库 | 完整写入链路、传输器链表、双写兜底 |
| 四 | 写入接口 | Log/Slog/EventLog 与 ALOGD/SLOGD/RLOGD 宏 |
| 五 | logcat 源码 | 参数解析、格式切换、过滤机制 |
| 六 | 缓冲区管理 | 环形链表、三段式裁剪、chatty 合并、统计 |
| 七 | 实战调试 | 常见问题诊断、调试技巧、性能分析 |
从「会用日志」到「理解日志」
通过这七篇文章,希望你在面对以下场景时有清晰的思路:
- 日志丢失 → 查裁剪策略,看
logcat -S统计(第六篇) - logcat 没输出 → 查过滤器和缓冲区选择(第五篇)
- 自定义日志不生效 → 查缓冲区路由和 SELinux 权限(第四篇、第八节)
- 系统崩溃日志恢复 → 查 pstore/pmsg 通道(第三篇、第四篇案例三)
- 日志性能影响 → 查写入链路开销(第三篇、第七节)
- 修改 logd 行为 → 查具体模块源码 + 系统属性(第二篇、第六篇、第十节)
- chatty 消息过多 → 查循环日志代码,降低日志频率(第六篇 chatty 机制)