1. 概览

  经过上一章的分析,现在也是时候讨论下logd的初始化了,虽然 logd 在代码量上来说并不大,但是还是分模块进行分析比较合适。所以这里就不贴整体代码了,这部分代码也被包含在AOSP t 的代码中,有兴趣的读者可以自己下载查看。

2. 屏蔽 SIGPIPE 信号

  下面先看看这部分代码长什么样

mainsignal(SIGPIPE, SIG_IGN);

  对于 signal 可以查看 man 手册,

NAMEsignal - ANSI C signal handlingSYNOPSIS#include <signal.h>typedef void (*sighandler_t)(int);sighandler_t signal(int signum, sighandler_t handler);DESCRIPTIONThe  behavior  of signal() varies across UNIX versions, and has also varied historically across different versions of Linux.  Avoid its use: use sigaction(2) instead.  See Portability below.signal() sets the disposition of the signal signum to handler, which is either SIG_IGN, SIG_DFL, or the address of  a  programmer-defined  function  (a"signal handler").If the signal signum is delivered to the process, then one of the following happens:*  If the disposition is set to SIG_IGN, then the signal is ignored.*  If the disposition is set to SIG_DFL, then the default action associated with the signal (see signal(7)) occurs.*  If  the  disposition  is set to a function, then first either the disposition is reset to SIG_DFL, or the signal is blocked (see Portability below),and then handler is called with argument signum.  If invocation of the handler caused the signal to be blocked, then the signal  is  unblocked  uponreturn from the handler.The signals SIGKILL and SIGSTOP cannot be caught or ignored.RETURN VALUEsignal() returns the previous value of the signal handler, or SIG_ERR on error.  In the event of an error, errno is set to indicate the cause.

   signal 的作用就是为调用进程注册一个对于信号量的处理函数,例如在logd中这句代码的含义则是,logd 在接收到信号 SIGPIPE 时调用函数 SIG_IGN 进行处理。而 SIG_IGN 则是什么也不做,即忽略了该信号。对于 SIGPIPE 信号,系统默认的操作则是让该进程异常退出。
  那么系统是在什么场景下会发送 SIGPIPE 到进程中?描述如下

当往一个写端关闭的管道或socket连接中连续写入数据时会引发SIGPIPE信号,引发SIGPIPE信号的写操作将设置errno为EPIPE。在TCP通信中,当通信的双方中的一方close一个连接时,若另一方接着发数据,根据TCP协议的规定,会收到一个RST响应报文,若再往这个服务器发送数据时,系统会发出一个SIGPIPE信号给进程,告诉进程这个连接已经断开了,不能再写入数据。

  因此 logd 作为一个socket的服务端,以上场景一样会发生在它身上,例如一个 logcat 何时退出是不会告知 logd 的。所以在 logd 中是需要忽略掉信号 SIGPIPE 的。

3. log 时间戳时区设置

  log中的时间戳非常的重要,时间戳代表着何时这部分log被打印,这一般也意味着此时程序想告诉开发人员它想表达的事项。因此 logd 为了统一,直接设置时区为UTC了。

mainsetenv("TZ", "UTC", 1);

  下面是 Google 对其的解释

    logd is written under the assumption that the timezone is UTC. If TZ is not set, persist.sys.timezone is looked up in some time utilitylibc functions, including mktime. It confuses the logd time handling, so here explicitly set TZ to UTC, which overrides the property.

4. logd 内部 log 的输出位置设置

  Andorid中在 logd 之后启动的组件可以将 log 传递给 logd 去处理,但是对于 logd 来说就没这么好运了。但是对于 logd 也是需要开发调试的,因此它的 log 也是很重要的。下面则是用于设置 logd 内部log的输出

android::base::InitLogging(argv, [](android::base::LogId log_id, android::base::LogSeverity severity,const char* tag, const char* file, unsigned int line, const char* message) {if (tag && strcmp(tag, "logd") != 0) {//code 1auto prefixed_message = android::base::StringPrintf("%s: %s", tag, message);android::base::KernelLogger(log_id, severity, "logd", file, line,prefixed_message.c_str());} else {//code 2android::base::KernelLogger(log_id, severity, "logd", file, line, message);}});

  logd 的输出又分两种,如果 log 的 tag 为 logd 那么就使用 code 2 进行打印,否则使用 code 1 的格式。 code 1 的方式多了 log 来源的 tag,但还是由 logd 打印所以 log 过滤时还是用 logd 标签。最终这部分 log 会被打印到 kernel log 中去,即可以使用 dmesg 来获取。

5. logd buffer

   buffer 顾名思义,用来存放需要 logd 处理的 log 信息。在 logd 中,我们是可以通过设置属性值 logd.buffer_type 来决定使用不同的buffer的。

//system/logging/logd/README.property
logd.buffer_type           string (empty) Set the log buffer type.  Current choices are 'simple','chatty', or 'serialized'.  Defaults to 'chatty' if empty.

  从上面可知,目前支持 3 中buffer类型simple、chatty 和 serialized。但是属性 logd.buffer_type 为空的情况在 AndroidT 中可不是 chatty 而是 serialized

//file:system\logging\logd\main.cpp
mainstd::string buffer_type = GetProperty("logd.buffer_type", "serialized");

  因此我们也就以 serialized 类型的 log buffer 作为例子进行讲解。

mainLogBuffer* log_buffer = nullptr;if (buffer_type == "chatty") {...} else if (buffer_type == "serialized") {log_buffer = new SerializedLogBuffer(&reader_list, &log_tags, &log_statistics);} else if (buffer_type == "simple") {...} else {LOG(FATAL) << "buffer_type must be one of 'chatty', 'serialized', or 'simple'";}

5.1 SerializedLogBuffer 类图

  类的结构对了解类的作用及工作原理还是至关重要的,下面给出 SerializedLogBuffer 对应的类图。

5.2 SerializedLogBuffer 初始化解析

  LogBuffer 仅仅是个接口类,并没有对应的构造方法。因此只要看看 SerializedLogBuffer 的构造方法就可以剖析初始化了。

log_buffer = new SerializedLogBuffer(&reader_list, &log_tags, &log_statistics);reader_list_(reader_list), tags_(tags), stats_(stats)Init();log_id_for_each(i) {//code 1if (!SetSize(i, GetBufferSizeFromProperties(i))) {SetSize(i, kLogBufferMinSize);}}//code 2for (const auto& reader_thread : reader_list_->running_reader_threads()) {reader_thread->TriggerReader();}

  代码很简单,说下设计意图吧。 reader_list 里面会维护所有读者信息,在buffer来了数据后就会通知 reader_list 里面所有的读者来读取 log 数据,至于各个读者施展什么手段 buffer 并不关心,使用 reader_list_ 隔离掉了。
  在logd初始化阶段,显然是没有读者的,所以 SerializedLogBuffer 的构造方法也就剩两个事
    1)记录传进来的 reader_list、log_tag等变量。
    2)设置各个类型buffer的大小,如system/main/radio等,log_id_for_each 定义如下

//system\logging\logd\LogStatistics.h
#define log_id_for_each(i) \for (log_id_t i = LOG_ID_MIN; (i) < LOG_ID_MAX; (i) = (log_id_t)((i) + 1))

    从这里就可以看出 i 就是LOG_ID,现在来看看 SetSize

//system\logging\logd\SerializedLogBuffer.cpp
SetSize(i, GetBufferSizeFromProperties(i))//system\logging\logd\LogSize.cppsize = GetBufferSizeFromProperties(i)//code 1max_size_[id] = size;//code 2// If this buffer has been compressed, we only consider its compressed size when accounting for// memory consumption for pruning.  This is since the uncompressed log is only by used by// readers, and thus not a representation of how much these logs cost to keep in memory.MaybePrune(id);//code 3

  buffer 大小的设置大致分成如下几个步骤
    1) 在Android T 中已经不支持灵活的通过属性来配置对应各个buffer的大小了,描述如下

    We've been seeing timeouts from logcat in bugreports for years, but the
rate has gone way up lately. The suspicion is that this is because we
have a lot of dogfooders who still have custom (large) log sizes but
the new compressed logging is cramming way more in. The bugreports I've seen
have had 1,000,000+ lines, so taking 10s to collect that much logging seems
plausible. Of course, it's also possible that logcat is timing out because
the log is being *spammed* as it's being read. But temporarily disabling
custom log sizes like this should help us confirm (or deny) whether the
problem really is this simple.

  只根据设备是不是低内存设备来决定 各个buffer的大小

//Xsystem\logging\logd\LogSize.h
static constexpr size_t kDefaultLogBufferSize = 256 * 1024;
static constexpr size_t kLogBufferMinSize = 64 * 1024;
//system\logging\logd\LogSize.cppif (android::base::GetBoolProperty("ro.config.low_ram", false)) {return kLogBufferMinSize;}return kDefaultLogBufferSize;

  如果是低内存设备那么就返回 kLogBufferMinSize 的值也就是 64KB,否则返回 kDefaultLogBufferSize 为 256KB。
     2) 在 SerializedLogBuffer 内部使用变量 max_size_ 记录下各个log buffer的能力大小。

6. kernel log 处理器 – klogd

6.1 klogd 功能选择

  从 logcat 工具也是能看出,它是支持 kernel log 的显示的。klogd也就是 logd 实现 kernel 显示的关键

erd8535:/ # logcat -h
Usage: logcat [options] [filterspecs]General options:-b, --buffer=<buffer>       Request alternate ring buffer(s):main system radio events crash default allAdditionally, 'kernel' for userdebug and eng builds, and'security' for Device Owner installations.

  Additionally 这个副词也说明了 kernel log 在 logd 中的地位有些不一样,有点像孤儿、送的、没人要的。实际上 kernel log 的打印确实和 logd 中 main/system 这些 log 不一样。logd 是通过读取节点 /proc/kmsg 来获取 kernel log 的。这里值得注意的是 /proc/kmsg 是不支持多读者的,即一个读者读走内容后,下一个读者就获取不到 log 信息了。所以例如在使用 logcat -b kernel 后,不要再类似手动 cat 这个节点了。下面就来看看实现吧。

main//code 1static const char dev_kmsg[] = "/dev/kmsg";int fdDmesg = android_get_control_file(dev_kmsg);//code 2bool klogd = GetBoolPropertyEngSvelteDefault("ro.logd.kernel");if (klogd) {//code 3SetProperty("ro.logd.kernel", "true");static const char proc_kmsg[] = "/proc/kmsg";fdPmesg = android_get_control_file(proc_kmsg);}

  code 1 用于获取 kmsg 节点的 fd,它在init启动lod得时候就被以仅写的权限打开了

//system\logging\logd\logd.rc
service logd /system/bin/logdfile /dev/kmsg w

  用于确认是否开启 klogd,即 logd 是否支持 kernel log 的处理。下面来看看 code 2 的代码实现

//system\logging\logd\main.cpp
bool klogd = GetBoolPropertyEngSvelteDefault("ro.logd.kernel");bool default_value =GetBoolProperty("ro.debuggable", false) && !GetBoolProperty("ro.config.low_ram", false);return GetBoolProperty(name, default_value);

  ro.debuggable 只有在 eng 和 userdebug 两个版本中为 1,下面是 Android 团队的官方解释,供参考。android build-variants
  同样的 ro.config.low_ram 在低内存设备上为false。
  综上,只要满足任意一个条件,logd 对 kernel log 的功能默认就是关闭的,毕竟 kernel log 一般包含系统关键信息。
    a) 低内存设备
    b) user 版本的Android 系统
  当然用户可以明确通过属性 ro.logd.kernel 来开启 klog 的功能。

6.2 klog – LogKlog 的初始化

  最终 klogd 如果被设置为 true,那么 logd 就支持处理 kernel log 的特性。LogKlog 类就是被用于 logd 处理kernel log的。下面先来看看它的类图

  可见它的父类就是 SocketListener,对于它的讲解见 《AndroidT(13) Log 系统 – SocketListener 帮助类详解(六)》 章节。下面先来看看它的初始化

//system\logging\logd\main.cpp
main...kl = new LogKlog(log_buffer/*buf*/, fdDmesg/*fdWrite*/, fdPmesg/*fdRead*/, al != nullptr/*auditd*/, &log_statisticsstats/**/);SocketListener(fdRead, false),//code 1logbuf(buf),...//code 2static const char klogd_message[] = "%s%s%" PRIu64 "\n";char buffer[strlen(priority_message) + strlen(klogdStr) +strlen(klogd_message) + 20];snprintf(buffer, sizeof(buffer), klogd_message, priority_message, klogdStr,signature.nsec());write(fdWrite, buffer, strlen(buffer));

  值得注意的是,LogKlog 中使用 SocketListener 来监听文件句柄(fdPmesg 为 /proc/kmsg ),而非 socket的连接i请求,因为/proc/kmsg 并不是 socket 句柄,如果有客户端想获取kernel log的话,那也是通过 logr 的。所以此处 SocketListener 传入的 listen 为 false。
  code 2 用于设置当前进程所读取到的kernel log的格式,下面是对应的格式

//system\logging\logd\LogKlog.cpp
static const char klogdStr[] = "logd.klogd: ";"priority_message""klogdStr""signature(CLOCK_MONOTONIC)"

  的拆解过程如下,最终就是去掉"<44>"后缀’\0’的内容

//bionic\libc\include\syslog.h
#define LOG_SYSLOG   (5<<3)
#define LOG_INFO 6#define KMSG_PRIORITY(LOG_INFO) \'<', '0' + ((5<<3) | 6) / 10,'0' + (10 1000b | 1100b) / 10,'0' + 44 / 10,'4','0' + ((5<<3) | (6)) % 10,'4','>'static const char priority_message[] = { KMSG_PRIORITY(LOG_INFO), '\0' };{'<','4','4','>'}

6.3 LogKlog 的监听启动

  从 LogKlog 的类图可知它的父类为 SocketListener ,所以启动监听则是通过它的 startListener 接口实现,启动成功后新线程会被创建用于监听所给句柄是否有数据可读,并且调用子类也就是 LogKlog 中的 onDataAvailable 方法进行处理。

//system\logging\logd\main.cpp
bool LogKlog::onDataAvailable(SocketClient* cli){prctl(PR_SET_NAME, "logd.klogd");for (;;) {read(cli->getSocket(), buffer + len, sizeof(buffer) - 1 - len);}
}

  对于 LogKlog 是如何处理kmsg的log即它重写的 onDataAvailable 实现细节放到后面章节讲解,本章只关注 logd 的初始化。

7. logd 监听听注册器 – logdr

   logdr 这个 socket 节点用于客户端获取 logd 中所管理的 log,例如典型的 logcat 工具就是通过和该 socket 节点进行通信从而获取 log的。

7.1 LogReader 的初始化

  logd 中则是使用类 LogReader 来实现上面提到的部分功能的。实际上 LogReader 并不参与数据的传输,它只负责处理客户端连接 logdr 的请求。下面先看看它的类图

   从类图可知它的结构很简单,就一个父类 SocketListener,下面就看看它的构造

mainLogReader* reader = new LogReader(log_buffer, &reader_list);//code 1SocketListener(getLogSocket(), true)//code 2log_buffer_(logbuf)//code 3reader_list_(reader_list)

  code 1,又见老朋友 SocketListener 了,不过这里的 mListen LogKlog 中的不一样,前者是支持作为服务端功能的,即可以处理客户端发过来的连接请求。这里的客户端也就是对 logd 所管理的 log 感兴趣的,例如前面提到过的 logcat。对于 SocketListener 的初始化,这里就不再赘述了,请参考相关章节的解析。

7.2 LogReader 的启动

  从 LogReader 的类图可知它的父类为 SocketListener ,所以启动监听则是通过它的 startListener 接口实现,对于客户端的连接请求处理在 SocketListener 章节中也有详细的说明。对于 mListen 为true的情况,SocketListener 的子类只要在 onDataAvailable 中处理已经连接上的客户端的数据即可。

bool LogReader::onDataAvailable(SocketClient* cli) {//code 1prctl(PR_SET_NAME, "logd.reader");//code 2int len = read(cli->getSocket(), buffer, sizeof(buffer) - 1);...//code 3auto entry = std::make_unique<LogReaderThread>(log_buffer_, reader_list_,std::move(socket_log_writer), nonBlock, tail,logMask, pid, start, sequence, deadline);//code 4reader_list_->AddPendingThread(std::move(entry));
}

  code 1,之前提到过 onDataAvailable 是跑在新建立的线程的,所以此处设置下线程名,方便区分。
  code 2,既然是客户端来了数据,那么总得先把数据读出来。
  code 3,在 LogReader 使用类 LogReaderThread 来管理每一个读者,实际上其内部也会创建一个线程进行log数据的传输。下面先给出它的类图供欣赏。

  code 4,此处将被 LogReaderThread 封装的新的 log reader 加到 reader list中去,以便统一管理。
  至于细节,会放到对应章节中去,此处就不赘述了。

8. log 接收器 – logdw

  logd 进程相当于一个 log 收集器,因此需要被显示的 log 最终也会被发送到 logd 进程来。logdw socket节点就是用来接受来自系统各个进程的log的。在 logd 中使用 LogListener 来实现这块功能。它就简单很多了,因为只要处理 socket 本身的数据即可。

8.1 logdw 的初始化 – LogListener

  下面来看看它的初始化

LogListener::LogListener(LogBuffer* buf)socket_(GetLogSocket())logbuf_(buf)

  打开并记录下 logdw socket,然后记录下buf共后面来 log 数据后的存储及处理,这个 buffer 实际上就是 SerializedLogBuffer 的一个实例。

8.2 logdw 的监听启动

  虽然没有使用 startListener ,但是启动的接口名却保持了一致

//system\logging\logd\main.cpp
mainLogListener* swl = new LogListener(log_buffer);swl->StartListener()auto thread = std::thread(&LogListener::ThreadFunction, this);thread.detach();return true;
}

  可见此处又会启动一个新的线程来处理,它的处理方法为 LogListener::ThreadFunction , 入参为 LogListener 的当前实例。最终 ThreadFunction 会被调用

//system\logging\logd\LogListener.cpp
ThreadFunctionprctl(PR_SET_NAME, "logd.writer");while (true) {HandleData();}

9. logd 的控制器 – logd

   logd 节点用于接收对 logd 本身的控制请求,例如获取/设置 logd 中维护 buffer 的大小。在 logd 中使用类 CommandListener 来实现该功能,下面是对应的类图。

9.1 CommandListener 类图

  它的顶层基类还是老朋友 SocketListener,邻近父类为 FrameworkListener,所以在看初始化的时候不能拉下这两个基类对应的构造方法了。

9.2 CommandListener 的初始化

//system\logging\logd\main.cpp
mainCommandListener* cl = new CommandListener(log_buffer/*buf*/, &log_tags/*tags*/, &prune_list/*prune*/, &log_statistics/*stats*/);//code 1FrameworkListener(getLogSocket()/*int sock*/)//system\logging\logd\CommandListener.cpp//code 1-1SocketListener(sock, true)//code 1-2init(nullptr, false);//code 2registerCmd(new ClearCmd(this));...registerCmd(new ExitCmd(this));

  code 1 部分为 CommandListener 父类的构造, code 1-1 用于监听 logd 这个socket 句柄,并且它是支持作为server的,即会处理客户端的连接请求的,这也意味这 FrameworkListener 要处理的内容是客户端连接上后通过新建立的 socket 句柄发送而来的。code 1-2 仅仅是变量初始化,没什么好说的。
  code 2 以及后面的都是一样的含义,用于注册命令,最终 CommandListener 会根据客户端要求执行的命令来调用对应的命令类。下面就看看 code 2 中的例子。

9.2.1 ClearCmd 类的由来

//system\logging\logd\CommandListener.h
#define LogCmd(name, command_string)                                \class name##Cmd : public FrameworkCommand {                     \public:                                                       \explicit name##Cmd(CommandListener* parent)                 \: FrameworkCommand(#command_string), parent_(parent) {} \virtual ~name##Cmd() {}                                     \int runCommand(SocketClient* c, int argc, char** argv);     \\private:                                                      \LogBuffer* buf() const { return parent_->buf_; }            \LogTags* tags() const { return parent_->tags_; }            \PruneList* prune() const { return parent_->prune_; }        \LogStatistics* stats() const { return parent_->stats_; }    \CommandListener* parent_;                                   \}LogCmd(Clear, clear);

  还是相当的简单的,通过宏 LogCmd 就可以创建出一个类,下面是预编译后加了换行符的类定义

LogCmd(Clear/*name*/, clear/*command_string*/);
#define LogCmd(name, command_string)                              class ClearCmd : public FrameworkCommand {                    public:                                                     explicit ClearCmd(CommandListener* parent)                : FrameworkCommand(clear), parent_(parent) {} virtual ~ClearCmd() {}                                    int runCommand(SocketClient* c, int argc, char** argv);   private:                                                    LogBuffer* buf() const { return parent_->buf_; }          LogTags* tags() const { return parent_->tags_; }          PruneList* prune() const { return parent_->prune_; }      LogStatistics* stats() const { return parent_->stats_; }  CommandListener* parent_;                                 };

  ClearCmd 类的实现,其实只要实现 runCommand 就可以了

//system\logging\logd\CommandListener.cpp
int CommandListener::ClearCmd::runCommand(SocketClient* cli, int argc, char** argv) {...return LogIdCommand(cli, argc, argv, [&](log_id_t id) {cli->sendMsg(buf()->Clear(id, uid) ? "success" : "busy");});
}

9.2.2 LogIdCommand

  其中 LogIdCommand 是一个模板方法,只要传入的处理方法不一致,那就是发生了函数重载,所以是可以支持多命令复用的

//system\logging\logd\CommandListener.cpp
template <typename F>
static int LogIdCommand(SocketClient* cli, int argc, char** argv, F&& function) {setname();function(static_cast<log_id_t>(log_id));return 0;
}

  可见最终还是调用传入的函数来处理对应的命令。

9.3 CommandListener 的启动

  CommandListener 也是调用 startListener 启动的,它继承自它的顶层基类 SocketListener

mainCommandListener* cl = new CommandListener(log_buffer, &log_tags, &prune_list, &log_statistics);cl->startListener()// Notify that others can now interact with logdSetProperty("logd.ready", "true");

10.总结

  至此整个 logd 至此也准备完成了,其中的各种 unix domain socket 节点都可以使用了,例如客户端写 log 内容到 logdw,客户端同 logdr 注册log监听。

AndroidT(13) Log 系统 -- logd 服务的初始化(七)相关推荐

  1. AndroidT(13) Log 系统 -- C plus plus 语言格式的LOG输出(二)

    1.概览   上一章提到的是在Android系统中,以C语言格式方式进行log输出.本章就来讲讲c++语言格式的. std::cout<<"This is a c++ log&q ...

  2. AndroidT(13) Log 系统 -- C 语言格式的LOG输出(一)

    1.概览   c语言中的printf我想大家都非常的熟悉了,他的基本格式如下 int printf(const char *format, ...);   前半部分 format 指向的字符串用于描述 ...

  3. android log.d 参数,Android log 机制 - logd 总览

    Android 早期版本使用的是一个 log 驱动,后来逐渐使用 logd 进程替代(具体哪个版本我就没有去探究了,至少在 Android 8.0 里,log 驱动已经被移除).原有 log 驱动负责 ...

  4. Android_8.1 Log 系统源码分析

    文章目录 0x01 [Android Log框架推荐](https://www.jianshu.com/p/64b63e51fd4c) 1. [logger](https://github.com/o ...

  5. Meego系统全面解析(初始化)

    http://blog.chinaunix.net/space.php?uid=20451980&do=blog&cuid=2320277 Meego系统全面解析   Meego从上电 ...

  6. Linux日志系统_syslog服务详解

    Linux日志系统_syslog服务详解 参考链接:https://blog.csdn.net/weixin_42569329/article/details/116609984 一台服务器的日志对系 ...

  7. 关于批量启动微服务的jar包_分布式任务抢占及系统监控服务 Radish

    分布式任务抢占及系统监控服务. 适用于中小微企业,将系统任务独立部署,统一管理.区别与传统的嵌入在系统中的任务, 可以很好的解耦任务服务. 具有以下优势: 方便灵活的配置系统和强大的容错重试以及报警机 ...

  8. 安卓系统蓝牙服务com.android.bluetooth的使能

    蓝牙系统服务层的使能流程分析 蓝牙服务层的使能基础是其初始化完成,也就是AdapterService通过onBind()将AdapterServiceBinder上报给bind该服务的调用者.我们现在 ...

  9. 2022-5-1-jjk网络验证系统开源--服务端

    2022-5-1-jjk网络验证系统开源 服务端代码 服务端相关代码 http协议 通讯 .版本 2.程序集 通讯.子程序 初始化服务端, 逻辑型全_通信句柄 = 全_Http服务端.创建 (假) . ...

最新文章

  1. 基于ArduinoLeonardo板子的BadUSB攻击实战
  2. java通过maven构建项目实现日志生成模拟(三)通过logback 打印日志
  3. android avd orientation support,Android AVD-无法旋转风景/人像
  4. 一个菜鸟怎样做好功能测试?
  5. Rancher 2.0集群与工作负载告警
  6. php intval0.57100,应用NuSoap构建新型的基于PHP的Web服务
  7. .NET在VS2008中生成DLL并调用
  8. 在Unity3D中使用Protobuf3
  9. 面试题3二维数组中的查找
  10. 一个小型的中文文本分类系统(项目链接文末)——《ML算法原理和实践》学习笔记
  11. Android入门项目(校园软件)
  12. 【热血传奇】 怪物添加(下)
  13. 织梦dede采集文章
  14. 算法工程师书籍推荐——典藏版
  15. Freebase Data Dump结构初探
  16. 郭德纲家训--话糙理不糙
  17. 敬业签手机版便签软件怎么绑定QQ或微信互联登录?
  18. 【DL】为什么需要深度学习:模组化、端到端学习(语音识别、图像处理情景)、类比逻辑电路
  19. workbook对象需要关闭_Excel VBA解读(92):Workbook对象的Open事件和BeforeClose事件
  20. 计算机网络按照延伸距离划分为,计算机应用基础填空题

热门文章

  1. QT For Android 图标制作
  2. python完美突破tls/ja3(大树乘凉版)
  3. 细菌或原核生物16S rRNA
  4. android版本5.0会咋样,Android5.0系统手机怎么样?安卓5.0手机最新体验
  5. 永恒之塔最新服务器2020,【永恒之塔5.8服务端】2020全新小结版一键端+GM方式内嵌+GM专用工具+详尽安裝构建教程...
  6. archlinux 2014.03.01 硬盘安装 win + grub4dos + arch
  7. 洛谷P1080 国王游戏 贪心+高精度
  8. 弱网环境搭建之 Linux tc iptables 详解
  9. Ubuntu 编译 哔哩哔哩 IJKPlayer so库,并支持RTSP
  10. GMS认证-Android VTS测试