Android 7系统日志(七)实战调试与常见问题分析

系列目录第一篇:全景图与架构概览 | 第二篇: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_MONOTONICpersist.logd.timestamp 第一篇
logcat 卡住 logd 慢速 reader 被阻塞 ps -T -p $(pidof logd) 查看线程状态 第二/六篇
内核日志看不到 klogd 转发未工作 cat /proc/kmsgdmesg 第二篇

二、案例一:开发中突然看不到日志

现象

应用正常运行,但 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() 的裁剪策略:

  1. 优先裁剪黑名单(naughty)UID/PID 的条目
  2. 其次裁剪占用超过缓冲区 12.5% 的"最坏 UID"条目(保留 dropped 占位符)
  3. 最后从最旧条目开始过期,白名单(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 完成写入):

  1. 字符串格式化 (Java 层 printlns 分块 + Native 层 vsnprintf):~5-10μs
  2. 锁竞争 (liblog 端 log_init_lock 和 logd 端 mLogElementsLock):1-5μs
  3. Socket 发送writev() 写入 /dev/socket/logdw):5-20μs
  4. 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 机制)