每个安卓开发者肯定都写过 Log.d(TAG, "hello")
但这行代码执行之后到底发生了什么? 日志是怎么从你的 App 进程跑到 logcat 的终端输出的?
在这篇文章中, 我将带你从 AOSP 源码出发, 完整追踪一条日志的生命周期.
全局视角
先给一张全链路的架构图:
graph TD
subgraph app ["App 进程"]
A["Log.d()"] --> B["println_native"]
B --> C["liblog"]
C --> D["LogdWrite()"]
C --> E["PmsgWrite()"]
end
D -->|"logdw"| F
E --> K[("pstore")]
subgraph logd ["logd 进程"]
F["LogListener"] --> G["SerializedLogBuffer"]
G --> H["LogReader"]
end
H -->|"logdr"| I
subgraph lcat ["logcat 进程"]
I["LogdRead()"] --> J["格式化输出"]
end整条链路横跨了三个进程, 并涉及到 Java 和 Native 两层, 通过 Unix Domain Socket 进行 IPC.
下面让我们逐层拆解.
Layer 1: Java API — android.util.Log
入口在 Log.java1.
public static int d(@Nullable String tag, @NonNull String msg) {
return println_native(LOG_ID_MAIN, DEBUG, tag, msg);
}所有的 Log.v/d/i/w/e 方法都是 one-liner, 最终调用同一个 native 方法:
public static native int println_native(int bufID, int priority, String tag, String msg);几个关键点:
LOG_ID_MAIN = 0— 普通应用日志写入 main buffer- 除 main 外还有
LOG_ID_RADIO(1),LOG_ID_EVENTS(2),LOG_ID_SYSTEM(3),LOG_ID_CRASH(4)等 buffer - 带
Throwable参数的重载版本会走printlns(), 内部用LineBreakBufferedWriter分段写入, 避免超长堆栈被截断
Layer 2: JNI — android_util_Log.cpp
android_util_Log.cpp2 负责 Java -> Native 的桥接:
static jint android_util_Log_println_native(JNIEnv* env, jobject clazz,
jint bufID, jint priority, jstring tagObj, jstring msgObj)
{
const char* tag = NULL;
const char* msg = NULL;
if (tagObj != NULL)
tag = env->GetStringUTFChars(tagObj, NULL);
msg = env->GetStringUTFChars(msgObj, NULL);
int res = __android_log_buf_write(bufID, (android_LogPriority)priority, tag, msg);
if (tag != NULL)
env->ReleaseStringUTFChars(tagObj, tag);
env->ReleaseStringUTFChars(msgObj, msg);
return res;
}JNI 层做的事情非常的简单:
- 把 Java String 转成 C 字符串 (通过
GetStringUTFChars) - 调用 liblog 的
__android_log_buf_write() - 释放字符串引用
JNI 方法注册表:
static const JNINativeMethod gMethods[] = {
{ "isLoggable", "(Ljava/lang/String;I)Z", (void*) android_util_Log_isLoggable },
{ "println_native", "(IILjava/lang/String;Ljava/lang/String;)I",
(void*) android_util_Log_println_native },
{ "logger_entry_max_payload_native", "()I",
(void*) android_util_Log_logger_entry_max_payload_native },
};Layer 3: liblog — 日志写入引擎
这一层是整个日志系统的核心.
3.1 入口: __android_log_buf_write
logger_write.cpp3:
int __android_log_buf_write(int log_id, int prio, const char* tag, const char* msg) {
// class ErrnoRestorer { ErrnoRestorer() : saved_errno_(errno) {} ~ErrnoRestorer() { errno = saved_errno_; } };
// 构造时保存当前 errno, 析构时恢复 -- 防止日志写入过程中意外修改调用者的 errno
ErrnoRestorer errno_restorer;
if (!__android_log_is_loggable(prio, tag, ANDROID_LOG_VERBOSE)) {
return -EPERM;
}
__android_log_message log_message = {
sizeof(__android_log_message), log_id, prio, tag, nullptr, 0, msg};
__android_log_write_log_message(&log_message);
return 1;
}首先检查 isLoggable — 这就是 setprop log.tag.YourTag DEBUG 能控制日志级别的原因.
通过检查后, 构造 __android_log_message 结构体, 交给 __android_log_write_log_message().
3.2 logger_function: 可插拔的日志后端
#ifdef __ANDROID__
static __android_logger_function logger_function = __android_log_logd_logger;
#else
static __android_logger_function logger_function = __android_log_stderr_logger;
#endif- Android 设备上, 默认的 logger 是
__android_log_logd_logger, 把日志发给 logd - 非 Android 环境 (host 工具) 则直接输出到 stderr
- 这个函数指针可以通过
__android_log_set_logger()替换, 提供了扩展性
3.3 写入 logd: LogdWrite
__android_log_logd_logger 会将日志数据组装成 iovec 数组:
void __android_log_logd_logger(const struct __android_log_message* log_message) {
struct iovec vec[3];
vec[0].iov_base = &log_message->priority; // 1 字节: 优先级
vec[0].iov_len = 1;
vec[1].iov_base = log_message->tag; // tag + '\0'
vec[1].iov_len = strlen(log_message->tag) + 1;
vec[2].iov_base = log_message->message; // message + '\0'
vec[2].iov_len = strlen(log_message->message) + 1;
write_to_log(static_cast<log_id_t>(buffer_id), vec, 3);
}write_to_log() 做两件事:
static int write_to_log(log_id_t log_id, struct iovec* vec, size_t nr) {
int ret = LogdWrite(log_id, timestamp, vec, nr); // 发给 logd
PmsgWrite(log_id, timestamp, vec, nr); // 写入 pstore
return ret;
}logd_writer.cpp4 中的 LogdWrite() 是真正的 IPC 发送端:
int LogdWrite(log_id_t logId, const struct timespec* ts,
const struct iovec* vec, size_t nr) {
// 构造 header
android_log_header_t header;
header.tid = gettid();
header.realtime.tv_sec = ts->tv_sec;
header.realtime.tv_nsec = ts->tv_nsec;
header.id = logId;
// header + payload 组装成新的 iovec
newVec[0].iov_base = &header;
newVec[0].iov_len = sizeof(header);
// ... 拷贝原始 vec ...
// 通过 Unix Domain Socket 发送
// #define TEMP_FAILURE_RETRY(exp) ({ do { _rc = (exp); } while (_rc == -1 && errno == EINTR); _rc; })
// 系统调用被信号中断 (EINTR) 时自动重试, 直到返回非 -1 或遇到其他错误
ret = TEMP_FAILURE_RETRY(writev(logd_socket.sock(), newVec, i));
// EAGAIN 说明 logd 过载, 其他错误则重连
if (ret < 0 && errno != EAGAIN) {
logd_socket.Reconnect();
ret = TEMP_FAILURE_RETRY(writev(logd_socket.sock(), newVec, i));
}
return ret;
}Socket 的细节:
- 连接
/dev/socket/logdw(UNIX Domain, SOCK_DGRAM) - 普通日志用 NonBlockingSocket — 写入不阻塞, logd 过载时直接丢弃 (计入 dropped 计数器)
- Security 日志用 BlockingSocket — 确保安全日志不丢失
- 发送失败时会自动 reconnect 并重试一次
3.4 旁路: PmsgWrite — pstore 持久化5
int PmsgWrite(log_id_t logId, const struct timespec* ts,
const struct iovec* vec, size_t nr) {
// 非 debuggable 设备只写 events 和 security
if (!ANDROID_DEBUGGABLE) {
if (logId != LOG_ID_EVENTS && logId != LOG_ID_SECURITY) {
return -1;
}
}
// 写入 /dev/pmsg0
ret = TEMP_FAILURE_RETRY(writev(pmsg_fd, newVec, i));
return ret;
}/dev/pmsg0 是 Linux pstore (persistent store) 的接口, 日志写入后即使设备异常重启也能保留. 这就是 “last kmsg” 等崩溃日志能在重启后被恢复的原因.
注意在 release 版本上, 只有 events 和 security 日志会写入 pstore.
Layer 4: logd — 日志守护进程
logd 是一个由 init 启动的 native daemon.
4.1 启动: main.cpp6
int main(int argc, char* argv[]) {
signal(SIGPIPE, SIG_IGN);
setenv("TZ", "UTC", 1);
// 创建 LogBuffer (默认 SerializedLogBuffer)
LogBuffer* log_buffer = new SerializedLogBuffer(&reader_list, &log_tags, &log_statistics);
// LogReader: 监听 /dev/socket/logdr, 把日志发给 logcat 等读者
LogReader* reader = new LogReader(log_buffer, &reader_list);
reader->startListener();
// LogListener: 监听 /dev/socket/logdw, 接收来自应用的日志
LogListener* swl = new LogListener(log_buffer);
swl->StartListener();
// CommandListener: 监听 /dev/socket/logd, 处理管理命令
CommandListener* cl = new CommandListener(log_buffer, ...);
cl->startListener();
TEMP_FAILURE_RETRY(pause()); // 主线程挂起, 工作线程各自运行
}logd 启动后创建三个 listener, 分别监听三个 socket:
| Socket | 类型 | 用途 |
|---|---|---|
/dev/socket/logdw | SOCK_DGRAM | 接收日志写入 |
/dev/socket/logdr | SOCK_SEQPACKET | 日志读取 (logcat) |
/dev/socket/logd | SOCK_STREAM | 管理命令 (clear, getLogSize 等) |
4.2 接收日志: LogListener
LogListener.cpp7 在独立线程中循环接收日志:
void LogListener::HandleData() {
// #define LOGGER_ENTRY_MAX_PAYLOAD 4068
char buffer[sizeof(android_log_header_t) + LOGGER_ENTRY_MAX_PAYLOAD + 1];
struct iovec iov = {buffer, sizeof(buffer) - 1};
// POSIX CMSG 宏 (bionic sys/socket.h):
// #define CMSG_ALIGN(len) (((len)+sizeof(long)-1) & ~(sizeof(long)-1)) -- 按 long 对齐
// #define CMSG_SPACE(len) (CMSG_ALIGN(sizeof(cmsghdr)) + CMSG_ALIGN(len)) -- header + payload 的总空间
// #define CMSG_FIRSTHDR(msg) (msg->msg_controllen >= sizeof(cmsghdr) ? msg->msg_control : NULL) -- 第一条辅助消息
// #define CMSG_DATA(cmsg) ((unsigned char*)(cmsg) + CMSG_ALIGN(sizeof(cmsghdr))) -- 跳过 header 取 payload
// #define CMSG_NXTHDR(mhdr,cm) __cmsg_nxthdr(mhdr, cm) -- 下一条辅助消息
alignas(4) char control[CMSG_SPACE(sizeof(struct ucred))];
struct msghdr hdr = { nullptr, 0, &iov, 1, control, sizeof(control), 0 };
ssize_t n = recvmsg(socket_, &hdr, 0);
// 提取凭据: 遍历辅助数据, 找到 SCM_CREDENTIALS 类型的 ucred
struct ucred* cred = nullptr;
struct cmsghdr* cmsg = CMSG_FIRSTHDR(&hdr);
while (cmsg != nullptr) {
if (cmsg->cmsg_level == SOL_SOCKET && cmsg->cmsg_type == SCM_CREDENTIALS) {
cred = (struct ucred*)CMSG_DATA(cmsg);
break;
}
cmsg = CMSG_NXTHDR(&hdr, cmsg);
}
// #define AID_LOGD 1036 -- log daemon
if (cred->uid == AID_LOGD) return; // 忽略 logd 自身日志, 防止递归
// 解析 header, 写入 LogBuffer
android_log_header_t* header = reinterpret_cast<android_log_header_t*>(buffer);
log_id_t logId = static_cast<log_id_t>(header->id);
char* msg = buffer + sizeof(android_log_header_t);
n -= sizeof(android_log_header_t);
logbuf_->Log(logId, header->realtime, cred->uid, cred->pid,
header->tid, msg, (uint16_t)n);
}关键机制:
- 使用
recvmsg()+SCM_CREDENTIALS获取发送者的 uid/pid — 这是内核级别的身份验证, 应用无法伪造 android_log_header_t包含 log_id, tid, realtime 时间戳- Security 日志会额外检查写入权限 (
clientCanWriteSecurityLog)
4.3 存储: SerializedLogBuffer
SerializedLogBuffer.cpp8 是默认的日志存储实现:
int SerializedLogBuffer::Log(log_id_t log_id, log_time realtime, uid_t uid,
pid_t pid, pid_t tid, const char* msg, uint16_t len) {
if (!ShouldLog(log_id, msg, len)) {
stats_->AddTotal(log_id, len);
return -EACCES;
}
auto sequence = sequence_.fetch_add(1, std::memory_order_relaxed);
auto lock = std::lock_guard{logd_lock};
auto entry = LogToLogBuffer(logs_[log_id], max_size_[log_id], sequence,
realtime, uid, pid, tid, msg, len);
stats_->Add(entry->ToLogStatisticsElement(log_id));
MaybePrune(log_id); // 超出容量则裁剪旧日志
reader_list_->NotifyNewLog(1 << log_id); // 通知等待中的读者
return len;
}存储结构:
- 每个 log_id 有独立的
std::list<SerializedLogChunk>链表 - 每个 Chunk 是一块连续内存, 内部存储多条
SerializedLogEntry - 日志条目有全局递增的
sequence编号, 用于读者定位 MaybePrune()在 buffer 超出max_size时按 FIFO 裁剪旧 ChunkNotifyNewLog()唤醒所有关注该 log_id 的LogReaderThread
Layer 5: logd -> logcat — 日志读取
5.1 logd 端: LogReader
LogReader.cpp9 监听 /dev/socket/logdr (SOCK_SEQPACKET):
bool LogReader::onDataAvailable(SocketClient* cli) {
char buffer[255];
int len = read(cli->getSocket(), buffer, sizeof(buffer) - 1);
buffer[len] = '\0';
// 解析命令字符串, 例如: "stream lids=0,3,4 tail=100 pid=1234"
unsigned long tail = 0;
// ... 解析 tail, start, timeout, logMask, pid, nonBlock ...
bool nonBlock = !fastcmp<strncmp>(buffer, "dumpAndClose", 12);
// 创建读者线程
auto entry = std::make_unique<LogReaderThread>(
log_buffer_, reader_list_, std::move(socket_log_writer),
nonBlock, tail, logMask, pid, start, sequence, deadline);
reader_list_->AddAndRunThread(std::move(entry));
return true;
}每个连接的 logcat 客户端都会获得一个 LogReaderThread, 该线程:
- 从
LogBuffer中FlushTo()匹配的日志条目 - 通过
SocketLogWriter::Write()发送给客户端 - 发完历史日志后, 阻塞等待
NotifyNewLog()唤醒
SocketLogWriter::Write() 的数据格式:
bool Write(const logger_entry& entry, const char* msg) override {
struct iovec iovec[2];
iovec[0].iov_base = const_cast<logger_entry*>(&entry);
iovec[0].iov_len = entry.hdr_size; // logger_entry header
iovec[1].iov_base = const_cast<char*>(msg);
iovec[1].iov_len = entry.len; // payload
return client_->sendDatav(iovec, 1 + (entry.len != 0)) == 0;
}5.2 logcat 端: liblog reader
logcat 通过 liblog 的 logd_reader.cpp10 读取日志:
// 连接 /dev/socket/logdr, 发送命令
static int logdOpen(struct logger_list* logger_list) {
int sock = socket_local_client("logdr", SOCK_SEQPACKET, set_timeout);
// 构造命令: "stream lids=0,3,4 tail=100"
// 或 nonBlock 模式: "dumpAndClose lids=0,3,4"
ret = TEMP_FAILURE_RETRY(write(sock, buffer, cp - buffer));
return sock;
}
// 读取一条日志
int LogdRead(struct logger_list* logger_list, struct log_msg* log_msg) {
int ret = logdOpen(logger_list);
// SOCK_SEQPACKET 保证每次 recv 恰好收到一条完整日志
ret = TEMP_FAILURE_RETRY(recv(ret, log_msg, LOGGER_ENTRY_MAX_LEN, 0));
return ret;
}SOCK_SEQPACKET 是一种可靠的、保持消息边界的 socket 类型, 非常适合这种 “一次传一条记录” 的场景.
Layer 6: logcat — 格式化输出
logcat.cpp11:
int main(int argc, char** argv) {
Logcat logcat;
return logcat.Run(argc, argv);
}Logcat::Run() 的核心流程:
- 解析命令行参数 (
-v,-s,-b,--pid等) - 调用
android_logger_list_alloc()+android_logger_open()配置要监听的 buffer - 循环调用
android_logger_list_read()读取日志 - 格式化为用户看到的文本输出
默认监听的 buffer:
if (id_mask == 0) {
id_mask = (1 << LOG_ID_MAIN) | (1 << LOG_ID_SYSTEM) |
(1 << LOG_ID_CRASH) | (1 << LOG_ID_KERNEL);
}这就是为什么 logcat 默认显示 main + system + crash + kernel 的日志, 而 events 和 radio 需要 -b events / -b radio 显式指定.
完整数据流总结
把上面的分析串起来, 一条 Log.d(TAG, "hello") 的完整时序:
sequenceDiagram
participant App as App (liblog)
participant logd as logd
participant logcat as logcat
App->>App: Log.d() → JNI → __android_log_buf_write()
Note over App: isLoggable 检查
组装 iovec [priority|tag|msg]
App->>logd: writev() /dev/socket/logdw
App-->>App: PmsgWrite() → /dev/pmsg0
Note over logd: LogListener.HandleData()
recvmsg() + SCM_CREDENTIALS
logd->>logd: SerializedLogBuffer.Log()
Note over logd: 分配 sequence → 写入 Chunk → MaybePrune()
logd->>logcat: LogReader → /dev/socket/logdr
Note over logcat: recv() 一条完整日志
格式化输出到终端附: 关键设计决策
为什么用 userspace 的 logd 而不是内核驱动? 早期 Android (4.x 之前) 确实用的是内核驱动
/dev/log/main等. 后来迁移到 userspace logd 的原因:- 内核驱动的 buffer 管理不够灵活
- 日志过滤、统计、裁剪策略在 userspace 更容易实现
- 减少内核代码量, 降低安全攻击面
为什么写入用 SOCK_DGRAM, 读取用 SOCK_SEQPACKET?
- 写入端 (DGRAM): 无连接, 低开销, 写入失败直接丢弃不阻塞应用 — 日志不应该影响应用性能
- 读取端 (SEQPACKET): 面向连接, 保持消息边界, 保证每条日志完整接收 — logcat 需要可靠读取
SCM_CREDENTIALS 的安全意义 logd 通过
recvmsg()的SCM_CREDENTIALS获取写入者的 uid / pid, 这是由内核填充的, 应用无法伪造. 这保证了:- 日志条目中的 uid / pid 信息是可信的
- Security 日志的写入权限检查有效
logcat --uid过滤基于真实身份
源码参考
AOSP 分支:
main(platform/superproject/main) | 查阅工具: cs.android.com
适用版本: Android 15+ (logging 子系统自 Android 11 引入 logd userspace 重构后, 整体架构稳定)
Footnotes
NOTE
文章作者: Catluo
文章链接: Android Log 全链路: 从 Log.d() 到 logcat 输出
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 授权协议。
转载请注明来源!