文章目录

  • 0x01 [Android Log框架推荐](https://www.jianshu.com/p/64b63e51fd4c)
    • 1、 [logger](https://github.com/orhanobut/logger)
    • 2、[timber](https://github.com/JakeWharton/timber)
    • 3、[Hugo](https://github.com/JakeWharton/hugo)
    • 4、[xLog](https://github.com/elvishew/xLog)
    • 5、 [LogUtils](https://github.com/pengwei1024/LogUtils)
    • 6、[Log4a](https://github.com/pqpo/Log4a)
  • 0x02 Android 原生 Log 系统
    • 1、Java 层逻辑
    • 2、Native 层逻辑
    • 3、liblog.so 逻辑
    • 4、logd 逻辑
      • 4.1、LogBuffer 逻辑
      • 4.2、LogListener 逻辑
      • 4.3、LogReader 逻辑
    • 5、log 流程小结
    • 6、logcat 逻辑
      • 6.1、 logcat 逻辑
      • 6.2、读日志逻辑
      • 6.3、打日志逻辑

0x01 Android Log框架推荐

1、 logger

提供了一些格式化输出、美观

// 添加依赖
implementation 'com.orhanobut:logger:2.2.0'// 初始化
Logger.addLogAdapter(new AndroidLogAdapter());
// 使用
Logger.d("hello,Android");

2、timber

基于原生Log类的小型可扩展的log框架

3、Hugo

使用注解形式的调试版本log框架

4、xLog

可扩展,支持多种数据格式,支持线程和调用栈信息

5、 LogUtils

支持多种数据结构,支持系统对象,支持高性能写入文件(mmap)

6、Log4a

基于mmap内存映射,最大化保证日志完整性

0x02 Android 原生 Log 系统

1、Java 层逻辑

Android 系统 Java 层 Log 定义在 /frameworks/base/core/java/android/util/Log.java

此外还提供了 EventLog 和 SLog【todo】

Log 提供了六种日志级别,并定义了一系列静态方法:

public static int v/d/i/w/e/wtf(String tag, String msg) {return println_native(LOG_ID_MAIN, VERBOSE, tag, msg);
}public static int v/d/i/w/e/wtf(String tag, String msg, Throwable tr) {return printlns(LOG_ID_MAIN, VERBOSE, tag, msg, tr);
}

下面这种调用带异常记录,会调用 Log 类内部类 ImediateLogWriter 来写入日志消息,最终也会调用 println_native

说明:Android 中不同的 log 会指定不同的缓冲区然后被写入到不同的设备中,包括 system(系统相关)、radio(无线/电话相关)、event(事件相关)、main(主缓冲区,默认)

本地实现定义在 /frameworks/base/core/jni/android_util_Log.cpp

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 },
};

android_util_Log_isLoggable

  • 检查 tag,取tag,调用 isLoggable 判断

    • 调用 __android_log_is_loggable 获取 logLevel【/system/core/liblog/properties.c,编译成 liblog.so】
    • 比较 prio 和 logLevel

android_util_Log_println_native

  • 检查bufID,检查 tag 和 msg,取 tag 和 msg,最后调用 __android_log_buf_write【/system/core/liblog/logger_write.c,编译到 liblog.so】

2、Native 层逻辑

Native 层通过定义一系列宏的方式提供 log 功能,全部是调用了 __android_log_print

LIBLOG_ABI_PUBLIC int __android_log_print(int prio, const char* tag,const char* fmt, ...) {va_list ap;char buf[LOG_BUF_SIZE];va_start(ap, fmt);vsnprintf(buf, LOG_BUF_SIZE, fmt, ap);va_end(ap);return __android_log_write(prio, tag, buf);
}

这个过程使用了可变参数,va 就是 variable-argument,相关宏定义在 stdarg.h 中

  • 首先定义 va_list 类型变量,是一个指向参数的指针
  • va_start 初始化变量,此函数的第一个参数是指针,第二个参数是固定参数
  • vsnprintf 将可变参数格式化输出到一个字符数组
  • 另外还有 va_arg 获取可变的参数,此函数的第一个参数是指针,第二个参数是可变参数类型
  • va_end 结束可变参数的获取

最后函数会调用 __android_log_write

LIBLOG_ABI_PUBLIC int __android_log_write(int prio, const char* tag,const char* msg) {return __android_log_buf_write(LOG_ID_MAIN, prio, tag, msg);
}

这里 Java/C++ 层就走到同一个函数,在这个函数中会实现写设备文件

3、liblog.so 逻辑

liblog.so 会被所有需要日志操作的进程加载,负责处理打印和读取日志的流程

主要代码及逻辑都在 /system/core/liblog/logger_write.c 中,__android_log_buf_write 里面做了下面这几件事:

  1. 定义 iovec 结构数组 vec[3],三个元素分别存放 prio、tag、msg

    struct iovec {void* iov_base;size_t iov_len;
    };
    

    iovec 结构体包含一个指向缓冲区的指针和读/写的长度

  2. 判断 bufID 修改 tag(bufID 这个变量表示不同的 log 缓冲区,或者理解成写到不同的文件里)

  3. 最后调用 write_to_log

write_to_log 是一个函数指针,初始设置指向 __write_to_log_init,进入 __write_to_log_init 后,首先会去调用 __write_to_log_initialize,然后将 write_to_log 设置为指向 __write_to_log_daemon,然后又调用一次 write_to_log

static int (*write_to_log)(log_id_t, struct iovec* vec, size_t nr) = __write_to_log_init;static int __write_to_log_init(log_id_t log_id, struct iovec* vec, size_t nr) {__android_log_lock();if (write_to_log == __write_to_log_init) {int ret;ret = __write_to_log_initialize();if (ret < 0) {__android_log_unlock();if (!list_empty(&__android_log_persist_write)) {__write_to_log_daemon(log_id, vec, nr);}return ret;}write_to_log = __write_to_log_daemon;}__android_log_unlock();return write_to_log(log_id, vec, nr);
}

__write_to_log_initialize__write_to_log_daemon 实现太复杂了,总结下来就是 initialize 基于默认配置构造结构体链表,daemon 从链表中取出节点,就是 android_log_transport_write,节点的结构体的定义在liblog/logger.h 中

struct android_log_transport_write {struct listnode node;const char* name;                  /* human name to describe the transport */unsigned logMask;                  /* mask cache of available() success */union android_log_context context; /* Initialized by static allocation */int (*available)(log_id_t logId); /* Does not cause resources to be taken */int (*open)();   /* can be called multiple times, reusing current resources */void (*close)(); /* free up resources */int (*write)(log_id_t logId, struct timespec* ts, struct iovec* vec, size_t nr); /* write log to transport, returns number of bytes propagated, or -errno */
};

初始化 的时候会调用 __android_log_config_write 会基于不同的场景定义不同的结构去写日志,包括 localLoggerWrite、logdLoggerWrite、pmsgLoggerWrite、fakeLoggerWrite、stderrLoggerWrite,调用 retval = (*node->write)(log_id, &ts, vec, nr) 进行 write 操作

LIBLOG_HIDDEN void __android_log_config_write() {if (__android_log_transport & LOGGER_LOCAL) {extern struct android_log_transport_write localLoggerWrite;__android_log_add_transport(&__android_log_transport_write,&localLoggerWrite);}if ((__android_log_transport == LOGGER_DEFAULT) || (__android_log_transport & LOGGER_LOGD)) {#if (FAKE_LOG_DEVICE == 0)extern struct android_log_transport_write logdLoggerWrite;extern struct android_log_transport_write pmsgLoggerWrite;__android_log_add_transport(&__android_log_transport_write, &logdLoggerWrite);__android_log_add_transport(&__android_log_persist_write, &pmsgLoggerWrite);
#elseextern struct android_log_transport_write fakeLoggerWrite;__android_log_add_transport(&__android_log_transport_write, &fakeLoggerWrite);
#endif}if (__android_log_transport & LOGGER_STDERR) {extern struct android_log_transport_write stderrLoggerWrite;if (list_empty(&__android_log_transport_write)) {__android_log_add_transport(&__android_log_transport_write, &stderrLoggerWrite);} else {struct android_log_transport_write* transp;write_transport_for_each(transp, &__android_log_transport_write) {if (transp == &stderrLoggerWrite) {return;}}__android_log_add_transport(&__android_log_persist_write, &stderrLoggerWrite);}}
}

看其中比较重要的几个,logdLoggerWrite 的定义在 /system/core/liblog/logd_writer.c

LIBLOG_HIDDEN struct android_log_transport_write logdLoggerWrite = {.node = { &logdLoggerWrite.node, &logdLoggerWrite.node },.context.sock = -EBADF,.name = "logd",.available = logdAvailable,.open = logdOpen,.close = logdClose,.write = logdWrite,
};

其中,logdOpen 方法创建 sockaddr_un 结构体并将 “/dev/socket/logdw” 写入 sun_path 成员变量中,然后调用 connect 去建立连接,并将套接字标识放到 logdLoggerWrite.context.sock 中;write 函数指针指向 logdWrite 方法,会调用 writev 非阻塞地写入日志信息

同理,pmsgLoggerWrite 打开的是 “/dev/pmsg0” 的 socket

所以,liblog.so 的一个主要工作就是写入日志

4、logd 逻辑

使用 file 命令查看 /dev/socket/logdw 发现是一个 socket,这个 socket 是由 logd 创建的,见 /system/corelogd/logd.rc

service logd /system/bin/logdsocket logd stream 0666 logd logdsocket logdr seqpacket 0666 logd logdsocket logdw dgram+passcred 0222 logd logdfile /proc/kmsg rfile /dev/kmsg wuser logdgroup logd system package_info readprocwritepid /dev/cpuset/system-background/tasks

logd 是 C 层的守护进程,由 init 进程创建(创建 servicemanager 时同时创建),可以看到启动 logd 后会创建三个 socket,分别为 logd 用来监听命令、logdr 用于读日志、logdw 用于写日志,还打开了两个文件,修改了 user id 和 group id,并把 pid 写文件

logd 启动后会从 main 函数开始执行,见 /system/core/logd/main.cpp

第一步、打开 /dev/kmsg 用于写内核 log 的,在 logd 还未启动或出错时,只能写到内核日志中

int main(int argc, char* argv[]) {......static const char dev_kmsg[] = "/dev/kmsg";fdDmesg = android_get_control_file(dev_kmsg);if (fdDmesg < 0) {fdDmesg = TEMP_FAILURE_RETRY(open(dev_kmsg, O_WRONLY | O_CLOEXEC));}......}

android_get_control_file 里面先调用 __android_get_control_from_env 拼接 Android 文件前缀 ANDROID_FILE_ 和路径 /dev/kmsg,然后做符号转换得到 ANDROID_FILE__dev_kmsg,接下来通过 getenv 获取这个环境变量的值,最后通过 strtol 将这个值转换成 long 类型就是文件描述符,还要通过 fcntl 去验证下文件是不是开着


这里用了三种方法去验证:

  1. 通过访问文件表获取 flag,最快;
  2. 通过文件表去拿 file 对象,然后获取 flag,其次;
  3. 通过文件表拿到 file 对象,然后进一步拿到 inode 节点数据,再获取 stat,最慢。

__android_get_control_from_env 返回文件描述符后,还会调用 /proc/self/fd/fd_num 验证一次

第二步、打开 /proc/kmsg 用于读内核日志

int main(int argc, char* argv[]) {......bool klogd = __android_logger_property_get_bool("logd.kernel", BOOL_DEFAULT_TRUE | BOOL_DEFAULT_FLAG_PERSIST |BOOL_DEFAULT_FLAG_ENG | BOOL_DEFAULT_FLAG_SVELTE);if (klogd) {static const char proc_kmsg[] = "/proc/kmsg";fdPmesg = android_get_control_file(proc_kmsg);if (fdPmesg < 0) {fdPmesg = TEMP_FAILURE_RETRY(open(proc_kmsg, O_RDONLY | O_NDELAY | O_CLOEXEC));}if (fdPmesg < 0) android::prdebug("Failed to open %s\n", proc_kmsg);}......}

第三步、启动 reinit 线程,处理 --reinit 命令,此外这个线程还会完成 uid 转 name

第四步、设置运行时优先级和权限【略】

第五步、启动 log 监听

  • 创建 LogBuffer 对象,负责管理所有的 log entries,可以理解为存放 log,对应线程“logd.auditd”
  • 创建 LogReader 对象,监听 /dev/socket/logdr,有客户端接入时就把 logBuf 中的 log entry 给客户端,即管理客户端读取 log
  • 创建 LogListener 对象,监听 /dev/socket/logdw,负责将 log entry 写入 logBuf,即写入 log
  • 创建 CommandListener 对象,监听 /dev/socket/logd,接收各种命令
  • LogAudit 读取 selinux 的日志
  • LogKlog 读取 kernel 的日志,写到 logBuf 中

看一下 logd 的线程,可以看到 logd 进程 pid = 574,ppid = 1,即 init 进程孵化,reinit_thread_start 函数启动线程“logd.daemon”,LogReader 启动线程“logd.reader”监听 /dev/socket/logdr,LogListener 启动线程“logd.writer”监听 /dev/socket/logdw,CommandListener 启动线程“logd.control”监听 /dev/socket/logd,LogAudit 启动线程“logd.auditd”,LogKlog 启动线程“logd.klogd”,LogTimeEntry 启动线程“logd.reader.per”

logd 574   574   1   32428   4912 SyS_rt_sigsuspend 753d7b1634 S logd
logd    574   577   1   32428   4912 futex_wait_queue_me 753d7644b0 S logd.daemon
logd    574   578   1   32428   4912 do_select  753d7b15a4 S logd.reader
logd    574   579   1   32428   4912 do_select  753d7b15a4 S logd.writer
logd    574   580   1   32428   4912 do_select  753d7b15a4 S logd.control
logd    574   582   1   32428   4912 do_select  753d7b15a4 S logd.auditd
logd    574 14402   1   32428   4912 futex_wait_queue_me 753d7644b0 S logd.reader.per
4.1、LogBuffer 逻辑

LogBuffer 继承自 LogBufferInterface,类内部定义了很多成员变量和一些函数,包括:

  • LogBufferElementCollection
  • LogBufferElement 指针变量:lastLoggedElements 和 droppedElements
  • LastLogTimes
  • 构造和析构方法,init/log 方法

LogBufferElementCollection 是一个 LogBufferElement 指针类型的 list

LogBufferElement 中存放 LogBuffer 元素的相关信息,包括 uint32_t 类型的 uid/pid/tid,还有 realTime、消息内容 msg,logId 等;

LastLogTimes 是一个 LogTimeEntry 指针类型的 list,两者都定义在 LogTime 中,LogTimeEntry 里面包含很多线程相关的变量及方法,还有 SocketClient 对象,表示读取日志的客户端

LogBuffer 的构造过程如下,传入参数为 LastLogTimes指针

LogBuffer* logBuf = nullptr;
LastLogTimes* times = new LastLogTimes();
logBuf = new LogBuffer(times);

继续看 LogBuffer 的构造函数

LogBuffer::LogBuffer(LastLogTimes* times): monotonic(android_log_clockid() == CLOCK_MONOTONIC), mTimes(*times) {pthread_rwlock_init(&mLogElementsLock, nullptr);log_id_for_each(i) {lastLoggedElements[i] = nullptr;droppedElements[i] = nullptr;}init();
}

monotonic 表示时间格式,即 CPU 通电时间或实际时间,mLogElementsLock 为读写锁,log_id_for_each 会通过 ID 遍历所有日志初始化 lastLoggedElements 和 droppedElements,最后调用 init 进行初始化

void LogBuffer::init() {......LogTimeEntry::wrlock();LastLogTimes::iterator times = mTimes.begin();while (times != mTimes.end()) {LogTimeEntry* entry = (*times);if (entry->owned_Locked()) {entry->triggerReader_Locked();}times++;}LogTimeEntry::unlock();
}

init 函数中会先遍历 logID 设置日志最大容量,然后检查时间格式并作类型转换,最后依次取出 LastLogTimes 中的元素,即一个 LogTimeEntry 对象,调用 triggerReader_Locked,这个方法的作用是,发送条件变量去唤醒另一个处于阻塞状态的线程,辅助后续的日志读写

4.2、LogListener 逻辑

LogListener 调用逻辑很简单,就是初始化一个监听对象,然后开启监听

LogListener* swl = new LogListener(logBuf, reader);
if (swl->startListener(600)) {exit(1);
}

LogListener 继承自 SocketListener,这个是 sysutils 库提供的类,用于监听客户端的 socket 连接,包含两个成员变量 LogBufferInterface 和 LogReader,也是构造函数的参数,还有一个 onDataAvailable 的回调,以及一个获取 getLogSocket 函数。

class LogListener : public SocketListener {LogBufferInterface* logbuf;LogReader* reader;public:LogListener(LogBufferInterface* buf, LogReader* reader /* nullable */);protected:virtual bool onDataAvailable(SocketClient* cli);private:static int getLogSocket();
};

getLogSocket 会构造路径 /dev/socket/logdw 作为 socketName,然后调用 socket_local_server 启动一个 socket 接收客户端连接

int LogListener::getLogSocket() {static const char socketName[] = "logdw";int sock = android_get_control_socket(socketName);if (sock < 0) {  // logd started up in init.shsock = socket_local_server(socketName, ANDROID_SOCKET_NAMESPACE_RESERVED, SOCK_DGRAM);int on = 1;if (setsockopt(sock, SOL_SOCKET, SO_PASSCRED, &on, sizeof(on))) {return -1;}}return sock;
}

再看 LogListener 构造函数

LogListener::LogListener(LogBufferInterface* buf, LogReader* reader): SocketListener(getLogSocket(), false), logbuf(buf), reader(reader) {}

会调用 SocketListener 构造函数,然后进一步调用 init 函数,里面只是做了变量赋值,然后初始化了一个 SocketClientCollection 类型指针

SocketListener::SocketListener(int socketFd, bool listen) {init(NULL, socketFd, listen, false);
}void SocketListener::init(const char *socketName, int socketFd, bool listen, bool useCmdNum) {mListen = listen;mSocketName = socketName;mSock = socketFd;mUseCmdNum = useCmdNum;pthread_mutex_init(&mClientsLock, NULL);mClients = new SocketClientCollection();
}

SocketClientCollection 是 SocketClient 指针类型的 list ,两者都定义在 SocketClient 中,SocketClient 包含一些客户端 socket 连接的变量。

LogListener 构造完成后就会调用父类的 startListener 方法开始监听,根据参数主要调用了这几个方法

int SocketListener::startListener(int backlog) {......mClients->push_back(new SocketClient(mSock, false, mUseCmdNum));if (pipe(mCtrlPipe)) {......if (pthread_create(&mThread, NULL, SocketListener::threadStart, this)) {......
}

首先会构造一个 SocketClient 对象,然后把这个对象放到 mClients 里面。

SocketClient 构造函数会调用 init 函数,里面会做一些变量赋值,注意这里调用 getsockopt返回成功,但是这个 socket 还是服务器 socket,所以 creds 相关参数都是无效的

SocketClient::SocketClient(int socket, bool owned, bool useCmdNum) {init(socket, owned, useCmdNum);
}void SocketClient::init(int socket, bool owned, bool useCmdNum) {mSocket = socket;mSocketOwned = owned;mUseCmdNum = useCmdNum;pthread_mutex_init(&mWriteMutex, NULL);pthread_mutex_init(&mRefCountMutex, NULL);mPid = -1;mUid = -1;mGid = -1;mRefCount = 1;mCmdNum = 0;struct ucred creds;socklen_t szCreds = sizeof(creds);memset(&creds, 0, szCreds);int err = getsockopt(socket, SOL_SOCKET, SO_PEERCRED, &creds, &szCreds);if (err == 0) {mPid = creds.pid;mUid = creds.uid;mGid = creds.gid;}
}

接着会创建一个 mCtrlPipe 的管道,用于唤醒 select 系统调用,最后创建一个线程开启监听

void *SocketListener::threadStart(void *obj) {SocketListener *me = reinterpret_cast<SocketListener *>(obj);me->runListener();pthread_exit(NULL);return NULL;
}

SocketListener::threadStart 里面会调用 SocketListener 的 runListener

这个函数会监听 mCtrlPipe,将套接口放到 read_fds 数组中,然后开启 select 进行监听。如果 mCtrlPipe 中写入了 CtrlPipe_Shutdown 则退出线程;如果 mSock 可监听且可读,则表示有客户端连接,使用 accept 接收客户端连接的 socket,使用此 socket 构造 SocketClient 并放到 mClients 中,最后依次处理 SocketClient 的 socket,即回调子类的 onDataAvailable 函数

fd_set 数据结构,为 long 类型的数组,存放一组等待检查的套接口,select 返回查询到的满足状态的套接口数目

void SocketListener::runListener() {SocketClientCollection pendingList;while(1) {SocketClientCollection::iterator it;fd_set read_fds;FD_ZERO(&read_fds);......FD_SET(mCtrlPipe[0], &read_fds);if (mCtrlPipe[0] > max)max = mCtrlPipe[0];pthread_mutex_lock(&mClientsLock);for (it = mClients->begin(); it != mClients->end(); ++it) {int fd = (*it)->getSocket();FD_SET(fd, &read_fds);if (fd > max) {max = fd;}}pthread_mutex_unlock(&mClientsLock);SLOGV("mListen=%d, max=%d, mSocketName=%s", mListen, max, mSocketName);if ((rc = select(max + 1, &read_fds, NULL, NULL, NULL)) < 0) {......if (FD_ISSET(mCtrlPipe[0], &read_fds)) {char c = CtrlPipe_Shutdown;TEMP_FAILURE_RETRY(read(mCtrlPipe[0], &c, 1));if (c == CtrlPipe_Shutdown) {break;}continue;}if (mListen && FD_ISSET(mSock, &read_fds)) {int c = TEMP_FAILURE_RETRY(accept4(mSock, nullptr, nullptr, SOCK_CLOEXEC));......pthread_mutex_lock(&mClientsLock);mClients->push_back(new SocketClient(c, true, mUseCmdNum));pthread_mutex_unlock(&mClientsLock);}pendingList.clear();pthread_mutex_lock(&mClientsLock);for (it = mClients->begin(); it != mClients->end(); ++it) {SocketClient* c = *it;int fd = c->getSocket();if (FD_ISSET(fd, &read_fds)) {pendingList.push_back(c);c->incRef();}}pthread_mutex_unlock(&mClientsLock);while (!pendingList.empty()) {it = pendingList.begin();SocketClient* c = *it;pendingList.erase(it);if (!onDataAvailable(c)) {release(c, false);}c->decRef();}}
}

LogListener 的 onDataAvailable 会完成对日志的读取操作。

bool LogListener::onDataAvailable(SocketClient* cli) {static bool name_set;if (!name_set) {prctl(PR_SET_NAME, "logd.writer");name_set = true;}......ssize_t n = recvmsg(socket, &hdr, 0);struct ucred* cred = NULL;struct cmsghdr* cmsg = CMSG_FIRSTHDR(&hdr);while (cmsg != NULL) {if (cmsg->cmsg_level == SOL_SOCKET &&cmsg->cmsg_type == SCM_CREDENTIALS) {cred = (struct ucred*)CMSG_DATA(cmsg);break;}cmsg = CMSG_NXTHDR(&hdr, cmsg);}......if (logbuf != nullptr) {int res = logbuf->log((log_id_t)header->id, header->realtime, cred->uid, cred->pid, header->tid, msg, ((size_t)n <= USHRT_MAX) ? (unsigned short)n :USHRT_MAX);if (res > 0 && reader != nullptr) {reader->notifyNewLog();}}return true;
}

首先调用系统调用 prctl 设置线程名称为 logd.writer,,然后调用 recvmsg 从 socket 中读取日志数据到 msghdr 这么一个结构体中,接着调用 CMSG_FIRSTHDR 获取指向 cmsghdr 结构的辅助信息,进而拿到应用相关的信息放到 ucred 结构中,最后调用 logbuf->log 写入数据,如果发现 reader 不为空,即客户端在等待读取数据,则调用 notifyNewLog 通知有新日志。

msghdr 对应的内核结构为 user_msghdr,结构为

struct user_msghdr {void        __user *msg_name;    /* ptr to socket address structure */int        msg_namelen;        /* size of socket address structure */struct iovec    __user *msg_iov;    /* scatter/gather array */__kernel_size_t    msg_iovlen;        /* # elements in msg_iov */void        __user *msg_control;    /* ancillary data */__kernel_size_t    msg_controllen;        /* ancillary data buffer length */unsigned int    msg_flags;        /* flags on received message */
};

继续看 LogBuffer::log 方法如何写入日志,先用传入的参数构造 LogBufferElement 对象,然后根据 prio 和 tag 判断日志是否可以写入

int LogBuffer::log(log_id_t log_id, log_time realtime, uid_t uid, pid_t pid,pid_t tid, const char* msg, unsigned short len) {......LogBufferElement* elem =new LogBufferElement(log_id, realtime, uid, pid, tid, msg, len);if (log_id != LOG_ID_SECURITY) {int prio = ANDROID_LOG_INFO;const char* tag = nullptr;if (log_id == LOG_ID_EVENTS) {tag = tagToName(elem->getTag());} else {prio = *msg;tag = msg + 1;}if (!__android_log_is_loggable(prio, tag, ANDROID_LOG_VERBOSE)) {// Log traffic received to totalwrlock();stats.addTotal(elem);unlock();delete elem;return -EACCES;}}......
}

接下来使用一个状态机去除重复的日志,过程比较复杂没看懂先略过,最后会调另一个 log 方法去写日志

int LogBuffer::log(log_id_t log_id, log_time realtime, uid_t uid, pid_t pid,pid_t tid, const char* msg, unsigned short len) {......wrlock();......log(elem);unlock();return len;
}

这个 log 方法传入的对象就是我们上面构造好的那个 LogBufferElement 对象,大致逻辑就是把这个构造好的 LogBufferElement 插入到 mLogElements 列表正确的位置

小结:

(logd)main.cpp

  • new LogListener()

    • new SocketListener()

      • SocketListener::init()
  • LogListener->startListener()
    • SocketListener::startListener()

      • SocketListener::threadStart()

        • SocketListener::runListener()

          • LogListener::onDataAvailable()

            • LogBuffer::log()

              • push_back(LogBufferElement)
            • LogReader::notifyNewLog()
4.3、LogReader 逻辑

LogReader 调用跟 LogListener 类似,也是初始化一个监听对象然后开启监听

LogReader* reader = new LogReader(logBuf);
if (reader->startListener()) {exit(1);
}

LogReader 也是继承自 SocketListener,成员变量为 LogBuffer,也是构造函数的参数,还有一个 onDataAvailable 的回调,一个获取 getLogSocket 函数,以及一些其他的函数

class LogReader : public SocketListener {LogBuffer& mLogbuf;public:explicit LogReader(LogBuffer* logbuf);void notifyNewLog();LogBuffer& logbuf(void) const {return mLogbuf;}protected:virtual bool onDataAvailable(SocketClient* cli);private:static int getLogSocket();void doSocketDelete(SocketClient* cli);
};

这里的 getLogSocket 会构造路径 /dev/socket/logdr 作为 socketName,然后调用 socket_local_server 启动一个 socket 接收客户端连接

int LogReader::getLogSocket() {static const char socketName[] = "logdr";int sock = android_get_control_socket(socketName);if (sock < 0) {sock = socket_local_server(socketName, ANDROID_SOCKET_NAMESPACE_RESERVED, SOCK_SEQPACKET);}return sock;
}

LogReader 构造函数会调用 SocketListener 构造函数,然后进一步调用 init 函数进行初始化,构造完成后就会调用父类的 startListener 方法开始监听,接着就会回调子类的 onDataAvailable

bool LogReader::onDataAvailable(SocketClient* cli) {static bool name_set;if (!name_set) {prctl(PR_SET_NAME, "logd.reader");name_set = true;}......int len = read(cli->getSocket(), buffer, sizeof(buffer) - 1);......FlushCommand command(*this, nonBlock, tail, logMask, pid, sequence, timeout);// Set acceptable upper limit to wait for slow reader processing b/27242723struct timeval t = { LOGD_SNDTIMEO, 0 };setsockopt(cli->getSocket(), SOL_SOCKET, SO_SNDTIMEO, (const char*)&t,sizeof(t));command.runSocketCommand(cli);return true;
}

首先调用系统调用 prctl 设置线程名称为 logd.reader,然后调用 read 读取 socket 并提取 " tail="、" start=" 等相关字符串来设置 tail、start、timeout、logMask、pid、nonBlock 这些参数,最后会利用这些参数构造一个 FlushCommand 对象,并调用 runSocketCommand 方法来真正读取日志

FlushCommand 继承自 SocketClientCommand,构造函数就是初始化参数

class FlushCommand : public SocketClientCommand {LogReader& mReader;bool mNonBlock;unsigned long mTail;unsigned int mLogMask;pid_t mPid;log_time mStart;uint64_t mTimeout;public:explicit FlushCommand(LogReader& mReader, bool nonBlock = false,unsigned long tail = -1, unsigned int logMask = -1,pid_t pid = 0, log_time start = log_time::EPOCH,uint64_t timeout = 0);virtual void runSocketCommand(SocketClient* client);static bool hasReadLogs(SocketClient* client);static bool hasSecurityLogs(SocketClient* client);
};

前面 LogListener 的 onDataAvailable 函数在对日志做读取操作时,判断 reader 不为空就会调用 reader->notifyNewLog() 来通知 LogReader 读取日志,

void LogReader::notifyNewLog() {FlushCommand command(*this);runOnEachSocket(&command);
}

这里 runOnEachSocket 由 SocketListener 实现,里面实际上就是会针对每一个 SocketClient 去执行 runSocketCommand

void SocketListener::runOnEachSocket(SocketClientCommand *command) {SocketClientCollection safeList;/* Add all active clients to the safe list first */safeList.clear();pthread_mutex_lock(&mClientsLock);SocketClientCollection::iterator i;for (i = mClients->begin(); i != mClients->end(); ++i) {SocketClient* c = *i;c->incRef();safeList.push_back(c);}pthread_mutex_unlock(&mClientsLock);while (!safeList.empty()) {/* Pop the first item from the list */i = safeList.begin();SocketClient* c = *i;safeList.erase(i);command->runSocketCommand(c);c->decRef();}
}

继续看 FlushCommand::runSocketCommand,会遍历 LastLogTimes 里面的元素找到自己这个 SocketClient,然后会唤醒线程读日志,没找到则构建一个 LogTimeEntry 然后等待被唤起,最后执行 entry->startReader_Locked() 启动线程读日志

void FlushCommand::runSocketCommand(SocketClient* client) {LastLogTimes::iterator it = times.begin();while (it != times.end()) {entry = (*it);if (entry->mClient == client) {......entry->triggerReader_Locked();if (entry->runningReader_Locked()) {LogTimeEntry::unlock();return;}entry->incRef_Locked();break;}it++;}if (it == times.end()) {// Create LogTimeEntry in notifyNewLog() ?if (mTail == (unsigned long)-1) {LogTimeEntry::unlock();return;}entry = new LogTimeEntry(mReader, client, mNonBlock, mTail, mLogMask,mPid, mStart, mTimeout);times.push_front(entry);}client->incRef();// release client and entry reference counts once doneentry->startReader_Locked();
}

接着看 LogTimeEntry::startReader_Locked,会调用 LogTimeEntry::threadStart 启动读日志线程

void LogTimeEntry::startReader_Locked(void) {pthread_attr_t attr;threadRunning = true;if (!pthread_attr_init(&attr)) {if (!pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED)) {if (!pthread_create(&mThread, &attr, LogTimeEntry::threadStart,this)) {pthread_attr_destroy(&attr);return;}}pthread_attr_destroy(&attr);}threadRunning = false;if (mClient) {mClient->decRef();}decRef_Locked();
}

设置线程名为 “logd.reader.per”,然后读取 mTail 条日志,这里有两次调用,第一次先遍历一遍,再实际读取日志,其中 FilterFirstPass 和 FilterSecondPass 参数为执行成功的回调函数,FilterFirstPass 计算元素的个数,FilterSecondPass 会返回指定的元素

void* LogTimeEntry::threadStart(void* obj) {prctl(PR_SET_NAME, "logd.reader.per");LogTimeEntry* me = reinterpret_cast<LogTimeEntry*>(obj);......if (me->mTail) {logbuf.flushTo(client, start, nullptr, privileged, security,FilterFirstPass, me);me->leadingDropped = true;}start = logbuf.flushTo(client, start, me->mLastTid, privileged,security, FilterSecondPass, me);......}return nullptr;
}

跟着看 LogBuffer::flushTo,首先根据参数 start 找到开始迭代的地方,然后执行 FilterPass 回调得到指定的日志元素,最后调用 element->flushTo 将日志写入

log_time LogBuffer::flushTo(SocketClient* reader, const log_time& start,pid_t* lastTid, bool privileged, bool security,int (*filter)(const LogBufferElement* element,void* arg),void* arg) {......for (; it != mLogElements.end(); ++it) {LogBufferElement* element = *it;......if (filter) {int ret = (*filter)(element, arg);......curr = element->flushTo(reader, this, privileged, sameTid);......return curr;
}

最后看 LogBufferElement::flushTo,构造 logger_entry_v4 结构体对象,然后放到 iovec 数组中,最后通过 reader->sendDatav 写入 socket

log_time LogBufferElement::flushTo(SocketClient* reader, LogBuffer* parent,bool privileged, bool lastSame) {struct logger_entry_v4 entry;memset(&entry, 0, sizeof(struct logger_entry_v4));struct iovec iovec[2];iovec[0].iov_base = &entry;iovec[0].iov_len = entry.hdr_size;char* buffer = NULL;if (mDropped) {entry.len = populateDroppedMessage(buffer, parent, lastSame);if (!entry.len) return mRealTime;iovec[1].iov_base = buffer;} else {entry.len = mMsgLen;iovec[1].iov_base = mMsg;}iovec[1].iov_len = entry.len;log_time retval = reader->sendDatav(iovec, 1 + (entry.len != 0))? FLUSH_ERROR: mRealTime;if (buffer) free(buffer);return retval;
}

小结:

(logd)main.cpp

  • new LogReader()

    • new SocketListener()

      • SocketListener::init()
  • LogReader->startListener()
    • SocketListener::startListener()

      • SocketListener::threadStart()

        • SocketListener::runListener()

          • LogReader::onDataAvailable()

            • FlushCommand::runSocketCommand()

              • new LogTimeEntry()
              • LogTimeEntry::startReader_Locked()
                • LogTimeEntry::threadStart()

                  • LogBuffer::flushTo()
                  • LogBufferElement::flushTo
          • LogReader::notifyNewLog()
            • SocketListener::runOnEachSocket

              • FlushCommand::runSocketCommand

5、log 流程小结

===========================================================Java 层:`android.utils.Log` -> `println_native` ->JNI 层:`android_util_Log_println_native` ->-----------------------------------------------------------C/C++ 层:`__android_log_print` -> `__android_log_write`===========================================================liblog.so: `__android_log_buf_write` -> `write_to_log` -> `__write_to_log_init` -> `write_to_log` -> `__write_to_log_daemon`===========================================================libd process: LogBuffer:LogBufferElement、LogTimeEntryLogListener:SocketListener、SocketClientLogReader:SocketListener、FlushCommand===========================================================

6、logcat 逻辑

logcat 是 Android 提供的一个日志打印工具,主要包含打印、过滤、清除日志等功能,运行在一个独立的进程里,不是每次 ps 都能找到这个进程

看一下 logcatd.rc,这个守护进程为 late_start 且默认情况下 disabled,即此服务默认不启动,而且每次启动都会把 pid 写到 /dev/cpuset/system-background/tasks 文件中,oom_score_adjust 设置为 -600 不容易被 LMK 杀死

service logcatd /system/bin/logcatd -L -b ${logd.logpersistd.buffer:-all} -v threadtime -v usec -v printable -D -f /data/misc/logd/logcat -r 1024 -n ${logd.logpersistd.size:-256} --id=${ro.build.id}class late_startdisabled# logd for write to /data/misc/logd, log group for read from log daemonuser logdgroup logwritepid /dev/cpuset/system-background/tasksoom_score_adjust -600

此服务可以在 root 权限下运行 start logcatd 开启,再找下这个进程就有了

logd          7012     1    9252   1880 __skb_recv_datagram 77a9f72f80 S logcatd
6.1、 logcat 逻辑

logcatd 的启动从 logcat_main.cpp 开始,流程非常清晰:调用 create_android_logcat 初始化 android_logcat_context 实例,然后运行 android_logcat_run_command 接收参数执行指定命令,android_logcat_destroy 结束此进程,具体实现都在 logcat.cpp 中

int main(int argc, char** argv, char** envp) {android_logcat_context ctx = create_android_logcat();if (!ctx) return -1;signal(SIGPIPE, exit);int retval = android_logcat_run_command(ctx, -1, -1, argc, argv, envp);int ret = android_logcat_destroy(&ctx);if (!ret) ret = retval;return ret;
}

先看 create_android_logcat,首先初始化一个 android_logcat_context_internal 上下文指针,为其分配内存,初始化部分变量的值,然后返回。

这个 android_logcat_context_internal 结构体与 logcat 实例相关联,里面的对象都是日志打印相关

android_logcat_context create_android_logcat() {android_logcat_context_internal* context;context = (android_logcat_context_internal*)calloc(1, sizeof(android_logcat_context_internal));if (!context) return nullptr;context->fds[0] = -1;context->fds[1] = -1;context->output_fd = -1;context->error_fd = -1;context->maxRotatedLogs = DEFAULT_MAX_ROTATED_LOGS;context->argv_hold.clear();context->args.clear();context->envp_hold.clear();context->envs.clear();return (android_logcat_context)context;
}

相应的,android_logcat_destroy 方法完成的工作就是销毁前面初始化的上下文对象,处理相关变量

继续看 android_logcat_run_command,这里会接收命令行参数,也很简单,还是把参数全设置到上面的 android_logcat_context_internal 结构体中,然后会去执行 __logcat(context)

int android_logcat_run_command(android_logcat_context ctx,int output, int error,int argc, char* const* argv,char* const* envp) {android_logcat_context_internal* context = ctx;context->output_fd = output;context->error_fd = error;context->argc = argc;context->argv = argv;context->envp = envp;context->stop = false;context->thread_stopped = false;return __logcat(context);
}

__logcat(context) 这个函数包含处理命令的核心代码,有接近一千行,流程比较清晰:

首先初始化一些变量和参数,然后在一个循环中完成“解析”工作,其中主要逻辑是调用 getopt_long_r 获取命令行参数,保存到 getopt_context 结构体中

static int __logcat(android_logcat_context_internal* context) {......for (;;) {......ret = getopt_long_r(argc, argv, ":cdDhLt:T:gG:sQf:r:n:v:b:BSpP:m:e:",long_options, &option_index, &optctx);......}......
}

然后根据返回的不同结果进不同的分支走不同的处理参数,如命令行参数 “-s” 会设置默认过滤器为 silent,会调用 android_log_addFilterRule 解析传入的参数,修改系统优先级设置,进而在后面的日志输出过程实现过滤,其他的 case 也类似,都是基于解析的参数配置到前面初始化的变量中

             switch (ret) {......case 's':// default to all silentandroid_log_addFilterRule(context->logformat, "*:s");break;......}

接下来是使用这些变量来执行逻辑了

这里是检查 context->devices,这是一个 log_device_t 结构的链表,如果链表为空,就放入 main/system/crash,也即默认缓冲区,类似的还有设置过滤条件、打印格式、黑白名单等

static int __logcat(android_logcat_context_internal* context) {......    if (!context->devices) {dev = context->devices = new log_device_t("main", false);context->devCount = 1;if (android_name_to_log_id("system") == LOG_ID_SYSTEM) {dev = dev->next = new log_device_t("system", false);context->devCount++;}if (android_name_to_log_id("crash") == LOG_ID_CRASH) {dev = dev->next = new log_device_t("crash", false);context->devCount++;}}......
}

接下来在一个 while 循环里,只要 context->stop 不为 True 且没有设置最大打印量 maxCount 或者设置了但是还没到预设的值,就会循环去调用 android_logger_list_read 读取日志信息,放到结构体 log_msg 中,最后调用 printBinary 或者 processBuffer 去打印日志的内容。

static int __logcat(android_logcat_context_internal* context) {......    while (!context->stop && (!context->maxCount || (context->printCount < context->maxCount))) {struct log_msg log_msg;int ret = android_logger_list_read(logger_list, &log_msg);......if (context->printBinary) {printBinary(context, &log_msg);} else {processBuffer(context, dev, &log_msg);}}......
}
6.2、读日志逻辑

这里的 android_logger_list_read 是 liblog 目录下的源文件,也即流程又会走到 liblog.so,包括其他清除日志(android_logger_clear)、获取缓冲区大小(android_logger_get_log_size)的方法最后也是调用到了 liblog.so 中的 logger_read.c 文件中,在此略过。

首先看下日志读取过程,主要做了这么几件事:将 logger_list 强转为 android_log_logger_list 结构对象 logger_list_internal,然后构造 android_log_transport_context 对象 transp 并通过 init_transport_contextnode_to_item 完成初始化,最后会调用 android_transport_read 执行真正的读日志

/* Read from the selected logs */
LIBLOG_ABI_PUBLIC int android_logger_list_read(struct logger_list* logger_list, struct log_msg* log_msg) {struct android_log_transport_context* transp;struct android_log_logger_list* logger_list_internal = (struct android_log_logger_list*)logger_list;int ret = init_transport_context(logger_list_internal);/* at least one transport */transp = node_to_item(logger_list_internal->transport.next, struct android_log_transport_context, node);....../* if only one, no need to copy into transport_context and merge-sort */return android_transport_read(logger_list_internal, transp, log_msg);
}

android_transport_read 则进一步调用 *transp->transport 对象的 read 方法

/* Validate log_msg packet, read function has already been null checked */
static int android_transport_read(struct android_log_logger_list* logger_list,struct android_log_transport_context* transp,struct log_msg* log_msg) {int ret = (*transp->transport->read)(logger_list, transp, log_msg);......
}

具体看下上面涉及到的数据结构:

首先是 android_logger_list_read 的参数 logger_list,是 logger_list 结构,没有找到在哪儿定义;

接下来是 android_log_logger_list,logger_list 一进来就被强转的类型,定义在 liblog/logger.h 中;

struct android_log_logger_list {struct listnode node;struct listnode logger;struct listnode transport;int mode;unsigned int tail;log_time start;pid_t pid;
};

然后是 android_log_transport_context 结构,还是定义在 liblog/logger.h 中,它的父节点就是上面的 android_log_logger_list,它的初始化过程也是借助 logger_list 完成的。此外,里面还定义了两个后面会用到的结构体 transport 和 logMsg

struct android_log_transport_context {struct listnode node;union android_log_context context; /* zero init per-transport context */struct android_log_logger_list* parent;struct android_log_transport_read* transport;unsigned logMask;      /* mask of requested log buffers */int ret;               /* return value associated with following data */struct log_msg logMsg; /* peek at upcoming data, valid if logMsg.len != 0 */
};

继续看 android_log_transport_read,它本质就是一个节点,但是内部定义了很多函数指针,如 readcleargetSize 等【这里就和上面 liblog.so 逻辑中“写日志”部分相对应】

struct android_log_transport_read {struct listnode node;const char* name; /* human name to describe the transport *//* Does not cause resources to be taken */int (*available)(log_id_t logId);void (*close)(struct android_log_logger_list* logger_list, struct android_log_transport_context* transp);int (*read)(struct android_log_logger_list* logger_list, struct android_log_transport_context* transp, struct log_msg* log_msg);......};

最后是 log_msg,定义了一个 entry 和 buf 的 union,然后如果是 C++,则重载了一些符号操作

struct log_msg {union {unsigned char buf[LOGGER_ENTRY_MAX_LEN + 1];struct logger_entry_v4 entry;struct logger_entry_v4 entry_v4;struct logger_entry_v3 entry_v3;struct logger_entry_v2 entry_v2;struct logger_entry entry_v1;} __attribute__((aligned(4)));......
};

继续看 android_log_transport_read,init_transport_context 初始化过程也会调用 __android_log_config_read 定义不同的结构来读日志,包含 logdLoggerRead、pmsgLoggerRead

LIBLOG_HIDDEN void __android_log_config_read() {if (__android_log_transport & LOGGER_LOCAL) {extern struct android_log_transport_read localLoggerRead;__android_log_add_transport(&__android_log_transport_read, &localLoggerRead);}#if (FAKE_LOG_DEVICE == 0)if ((__android_log_transport == LOGGER_DEFAULT) || (__android_log_transport & LOGGER_LOGD)) {extern struct android_log_transport_read logdLoggerRead;extern struct android_log_transport_read pmsgLoggerRead;__android_log_add_transport(&__android_log_transport_read, &logdLoggerRead);__android_log_add_transport(&__android_log_persist_read, &pmsgLoggerRead);}
#endif
}

还是看 logdLoggerRead,它的 read 方法对应的就是 logdRead 方法

LIBLOG_HIDDEN struct android_log_transport_read logdLoggerRead = {.node = { &logdLoggerRead.node, &logdLoggerRead.node },.name = "logd",.available = logdAvailable,.version = logdVersion,.read = logdRead,......
};

在 logdRead 中,首先调用 logdOpen 创建 socket_local_client 客户端去连接 “/dev/socket/logdr”,然后初始化 log_msg 结构,最后调用 recv 从 socket 中接收日志并写入 log_msg 中

同理,pmsgLoggerWrite 打开的是 “/sys/fs/pstore/pmsg-ramoops-0” 的 socket

所以,liblog.so 的另一个主要工作就是读取日志

6.3、打日志逻辑

上一节说到 logcat 读完日志会放到 log_msg 结构中,然后调用 printBinary/processBuffer 来打印

printBinary 逻辑很简单,就是调用 write 写到指定文件中,也有可能是直接输出到屏幕上

void printBinary(android_logcat_context_internal* context, struct log_msg* buf) {size_t size = buf->len();TEMP_FAILURE_RETRY(write(context->output_fd, buf, size));
}

processBuffer 里面首先会判断 dev->binary,如果为 True,则说明日志是二进制格式,会调用 android_log_processBinaryLogBuffer 将二进制格式的日志记录转化为 ASCII 形式,否则调用 android_log_processLogBuffer 将日志转换为 AndroidLogEntry 格式,结果也放到 AndroidLogEntry 类型的对象 entry 中,接下来调用 android_log_shouldPrintLine 则通过一系列过滤、正则条件判断此条日志是否需要输出,匹配通过就调用 android_log_printLogLine 打印日志

static void processBuffer(android_logcat_context_internal* context,log_device_t* dev, struct log_msg* buf) {AndroidLogEntry entry;if (dev->binary) {if (!context->eventTagMap && !context->hasOpenedEventTagMap) {context->eventTagMap = android_openEventTagMap(nullptr);context->hasOpenedEventTagMap = true;}err = android_log_processBinaryLogBuffer(&buf->entry_v1, &entry, context->eventTagMap, binaryMsgBuf, sizeof(binaryMsgBuf));} else {err = android_log_processLogBuffer(&buf->entry_v1, &entry);}if (android_log_shouldPrintLine(context->logformat, std::string(entry.tag, entry.tagLen).c_str(), entry.priority)) {bool match = regexOk(context, entry);context->printCount += match;if (match || context->printItAnyways) {android_log_printLogLine(context->logformat, context->output_fd, &entry);}}
}

Android_8.1 Log 系统源码分析相关推荐

  1. Android 8.0系统源码分析--Camera processCaptureResult结果回传源码分析

    相机,从上到下概览一下,真是太大了,上面的APP->Framework->CameraServer->CameraHAL,HAL进程中Pipeline.接各种算法的Node.再往下的 ...

  2. Kbuild系统源码分析(四)—./scripts/Makefile.build

    版权声明:本文为CSDN博主「ashimida@」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明. 原文链接:https://blog.csdn.net/lidan1 ...

  3. 抖音seo搜索排名,源码开发部署/seo排名系统源码分析。

    前言:抖音seo搜索排名,源码开发部署/seo排名系统源码分析.抖音seo源码搭建部分代码 抖音seo是什么?其实seo它是搜索引擎优化,不仅这样,抖音里也有搜索引擎优化,抖音seo其实就是优化抖音的 ...

  4. Android 8.0系统源码分析--开篇

    个人分类: Android框架总结Android源码解析android framework 版权声明:本文为博主原创文章,未经博主允许不得转载. https://blog.csdn.net/sinat ...

  5. (连载)Android系统源码分析--Android系统启动流程之Linux内核

    > **这是一个连载的博文系列,我将持续为大家提供尽可能透彻的Android源码分析 [github连载地址](https://github.com/foxleezh/AOSP/issues/3 ...

  6. 同城外卖跑腿系统源码分析

    外卖订餐已经成为很多"社畜"日常不可分割的一部分,足不出户,只需要一部电子设备即可在线订餐,并且可提供的选择非常多样化,与传统的电话订餐外卖模式相比也更便捷的多. 因此,同城外卖跑 ...

  7. Android系统源码分析--Activity的finish过程

    上一篇我们分析了Activity的启动流程,由于代码量很大,还是没有分析的很详细,但是基本流程都出来了,更详细的东西还是要去看源码.这一章来分析Activity的finish过程,分析一下finish ...

  8. android 系统源码分析

    获得Android源码后,我们来分析源码结构.源码的全部工程分为如下三个部分. ①Core Project:核心工程部分,这是建立Android系统的基础,保存在根目录的各个文件夹中. ②Extern ...

  9. 抖音seo搜索是什么?源码部署-seo排名系统源码分析

    抖音seo源码,抖音矩阵,seo系统,抖音搜索排名 思路:抖音SEO优化源码,搜索排名系统,技术理论分析,抖音矩阵,抖音seo系统. 我们都知道系统工具,系统会自定义出来各行业的核心关键词,要想我们自 ...

最新文章

  1. 在分页状态下删除纪录的问题
  2. 上海交通大学2006年各学院本科生平均薪酬统计
  3. SQL执行效率提升几万倍的操作详解!
  4. 天津农学院计算机考研专业,2021年天津农学院计算机与信息工程学院考研专业目录_研究生考试范围 - 学途吧...
  5. (25)二分频verilog与VHDL编码(学无止境)
  6. 【Jenkins】Jenkins : Mac中Jenkins的停止和启动
  7. 哪些数据类型能使用切片操作python_Python学习之路「16」非数字数据类型-切片-slice...
  8. hadoop交流群261039241
  9. matlab生成点的坐标,根据点的发展坐标,将点的轨迹画出来
  10. 【Altium Designer】PCB如何生成Gerber文件
  11. 使用EXCEL4J读取EXCEL以及坑
  12. perf_event 事件类型与分类
  13. 使用PhotoShop制作公章
  14. 白马非马的面向对象分析
  15. ps后期处理实用技巧2
  16. 视频断点续传+java视频
  17. 标准化思想及组装式架构在后端BFF中的实践
  18. C语言10以内加法口诀表,10以内加减法口诀表可打印.doc
  19. xx通用管理系统(超级好看的界面)javaswing+mybatis
  20. IMU预积分--详细推导过程

热门文章

  1. 王者荣耀服务器能不能注销,王者荣耀游戏账号能永久注销吗 永久删除后还能恢复吗...
  2. 节奏感:出色平面设计的必备要素
  3. 新手入门需要知道的Flutter基础
  4. SystemVerilog中随机的constrain语法
  5. 如何在 React Component 之外获取 Redux Store
  6. Flink电商实时数仓项目05-数据可视化
  7. 软件开发投标技术力量_开发者社区的力量
  8. java基于Springboot+vue的校园二手闲置商品交易平台系统 element
  9. 2006求奇数的乘积c++
  10. Android 心形图片心形ImageView、带边框的的心形图片和圆形图片