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_MONOTONIC与persist.logd.timestamp | 第一篇 |
| logcat 卡住 | logd 慢速 reader 被阻塞 | ps -T -p $(pidof logd)查看线程状态 | 第二/六篇 |
| 内核日志看不到 | klogd 转发未工作 | cat /proc/kmsg或dmesg | 第二篇 |
二、案例一:开发中突然看不到日志
现象
应用正常运行,但logcat输出突然停止。重启 logcat 也没用。
排查步骤
# 步骤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 shellkill-HUP$(pidof logd)# 步骤4:如果是某个进程滥用日志adb logcat-vthreadtime-bmain *:V|grep"chatty"# 查找 chatty 消息,定位被裁剪合并的日志来源原理分析
LogBuffer::prune()的裁剪策略:
- 优先裁剪黑名单(naughty)UID/PID 的条目
- 其次裁剪占用超过缓冲区 12.5% 的"最坏 UID"条目(保留 dropped 占位符)
- 最后从最旧条目开始过期,白名单(nice)条目最后才删除
如果缓冲区满了且日志写入速度 > logcat 读取速度,日志就会被裁剪。慢速 reader 会被 logd 触发跳过(triggerSkip),极端情况下会被释放(release)。
解决方案
// 方案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 消息淹没了关键日志
现象
logcat 输出中频繁出现: I chatty:uid=10123(com.example.app)0x3039 expire47lines格式说明:
I= INFO 级别,chatty= 固定 tag,uid=10123= 写入 UID,(com.example.app)= 包名,0x3039= 线程 TID(十六进制),expire 47 lines= 被裁剪合并了 47 条相同日志。
原因
应用中有循环日志代码:
voidonSensorChanged(SensorEventevent){// 高频传感器回调,每秒 100 次Log.d(TAG,"sensor value: "+event.values[0]);// 每条日志完全相同 → 裁剪时被设置为 dropped → chatty 合并计数}排查
# 查看 chatty 来源adb logcat-vthreadtime|grepchatty# 按 PID 过滤(★ logcat 没有 --uid 选项,只有 --pid)adb logcat-vthreadtime--pid=$(adb shell pidof com.example.app)解决方案
// 方案1:用采样率控制privateintcounter=0;voidonSensorChanged(SensorEventevent){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 正常可见排查
# 检查内核日志(dmesg)adb shelldmesg|head-50# 检查 pstore(上次崩溃后保留的日志)adb shellls/sys/fs/pstore/# console-ramoops:内核日志# pmsg-ramoops-0:用户态日志(pmsg_writer 写入的)# 查看是否有上次的重启日志adb shellcat/sys/fs/pstore/console-ramoops|head-50# ★ logcat -L 读取 pstore 中的日志adb logcat-L解决方案
# 1. 增大内核日志缓冲区# 在内核命令行添加:log_buf_len=1M# 2. 确保 pstore 配置已启用# CONFIG_PSTORE=y, CONFIG_PSTORE_RAM=y# 3. 抓取 logd 相关的启动事件adb logcat-bevents|grep"logd"原理
参考第三篇的pmsg_writer:系统崩溃时日志通过write("/dev/pmsg0")写入 pstore。重启后logcat -L可以读取上次的日志(logd 没有-L参数,该功能由 logcat 的ANDROID_LOG_PSTORE模式实现)。
五、案例四:系统服务日志不出现
现象
修改了 framework 代码添加Slog.d(TAG, "xxxx"),但logcat -b system看不到。
排查
# 步骤1:确认日志是否真的写入了(搜索所有缓冲区)adb logcat-ball|grep"xxxx"# 步骤2:检查 SELinux 是否阻止adb shelldmesg|grep"avc.*denied.*logd"# 步骤3:检查进程 UIDadb shellps|grep<service_name># 系统服务 UID 通常为 1000 (system)# 步骤4:确认写入了正确的缓冲区# Slog.d → system 缓冲区 (LOG_ID_SYSTEM=3)# Log.d → main 缓冲区 (LOG_ID_MAIN=0)常见错误
// ❌ 错误:用了 Log 而非 SlogLog.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。
排查
# 查看当前的 tag 映射文件adb shellcat/system/etc/event-log-tags|grep<tag_number># 查看 logcat 是否能正确解析adb logcat-bevents-vthreadtime|grep"<数字tag>"解决方案
# 在 event-log-tags 文件中添加对应条目# 格式:<tag编号> <tag名称> (<字段名>|<类型>|<字节数>),...echo"123456 my_custom_event (Event Value|3)">>/system/etc/event-log-tags# 重启 logcat 即可生效(logcat 重启时重新加载 EventTagMap)# 不需要重启 logd七、日志系统的性能影响分析
logd 内存占用
# ★ logd 是 native 守护进程,不能用 dumpsys meminfo# 正确方式:查看 /proc/$(pidof logd)/statusadb shellcat/proc/$(pidof logd)/status|grep-E"VmRSS|VmSize"# 或用 procrankadb shell procrank|greplogd典型内存占用(缓冲区部分):
| 缓冲区 | 默认大小 | 说明 |
|---|---|---|
| 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 上下文
# logd 进程的 SELinux 上下文adb shellps-Z|greplogd# logdw socket 的 SELinux 上下文adb shellls-Z/dev/socket/logd*# 查看日志相关的 AVC 拒绝记录adb shelldmesg|grep"avc.*denied.*logd"常见 SELinux 问题
# 问题:自定义 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:实时监控特定进程
# 按 PID 过滤(★ 注意是 --pid,不是 --uid)adb logcat--pid=$(adb shell pidof com.example.app)# 同时监控多个缓冲区adb logcat-vthreadtime-bmain-bsystem-bevents技巧2:查看被裁剪的日志
# 搜索 chatty 消息(裁剪合并标记)adb logcat-vthreadtime-bmain|grep"chatty"# 统计 chatty 出现频率adb logcat-d-bmain|grep-c"chatty"技巧3:对比各缓冲区
# 同时打开四个窗口:# 窗口1:mainadb logcat-vtime-bmain MyTag:* *:S# 窗口2:systemadb logcat-vtime-bsystem MyTag:* *:S# 窗口3:events(注意看二进制事件解码)adb logcat-vthreadtime-bevents# 窗口4:内核adb shellcat/proc/kmsg技巧4:分析日志"大户"
# 统计某个 TAG 的出现频率adb logcat-d-vraw|grep-oP'(?<=/)[^\(]+'|sort|uniq-c|sort-rn|head-20# 用 logcat -S 直接查看统计(最准确)adb logcat-S技巧5:崩溃日志分析
# 搜索 FATAL 级别日志adb logcat-vthreadtime-bcrash# 搜索 ANR 相关事件adb logcat-vthreadtime-bevents|grep"am_anr"# 搜索 Native Crashadb logcat-vthreadtime|grep-A20"FATAL SIGNAL"技巧6:日志持久化到文件
# 将日志保存到文件adb logcat-vthreadtime-f/data/local/tmp/mylog.txt# 持续记录,限制大小(轮转 10 个文件,每个 1MB)adb logcat-vthreadtime-r1024-n10-f/data/local/tmp/logcat.txt十、logd 运行时配置
# 查看所有 logd 相关属性adb shell getprop|greplogd# ★ 动态调整缓冲区大小(注意属性名有 persist. 前缀)adb shell setprop persist.logd.size.main 512K adb shell setprop persist.logd.size.system 512K# 发送 SIGHUP 让 logd 重新加载配置(★ 是 SIGHUP,不是 SIGUSR1)adb shellkill-HUP$(pidof logd)# 或者用 logcat -G 直接设置(无需重启 logd)adb logcat-G512K# ★ 启用统计功能(属性名是 logd.statistics,布尔值)adb shell setprop logd.statisticstrue# 启用日志级别控制adb shell setprop log.tag.MyTag DEBUGlogd 支持的系统属性一览
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
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
# DropBoxManager 是 ServiceManager 中的一个 Java 服务# 它记录 crash/anr/watchdog 等关键事件# 查看 dropbox 条目adb shell dumpsys dropboxlogd + tombstone
# Native crash 会在 /data/tombstones/ 产生 tombstone 文件adb shellls-la/data/tombstones/# tombstone 相关的崩溃信息也同时写入 logd 的 crash 缓冲区adb logcat-bcrashlogd + pstore
# 系统崩溃(kernel panic)时,pmsg 通道的日志刷入 pstore# 重启后通过 logcat -L 读取adb logcat-L# 或直接读取文件adb shellcat/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 中添加调试日志
// ★ 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 机制)
