我们已经在上一篇文章中看完了System.out的全部实现,这次我们来看一看Log的实现。
我们知道,Log不仅可以将运行时的状况完整的记录下来,还可以根据程序员的一些预处理来做过滤,使程序员可以很快定位信息,是一个非常方便的工具。但是大多数Android开发人员的认识停留在了应用层,今天我们来看看Log是怎么实现的。
这里我先明说,Log实际上是基于底层的Linux系统的一个叫做Logger的设备驱动程序来实现的。一般来讲Log在系统中的类型分为五种,分别为main,system,radio,event,crash。对应的设备驱动文件为/dev/log_main、/dev/log_system、/dev/log_radio、/dev/log_event、/dev/log_crash。
我们最常使用的是main这类,main就是用来给应用程序开发,调试,维护使用的。而system则是系统级的,记录系统运行时的情况,因为系统比应用程序重要,所以把system和main分开记,这样也能够更好的区别信息。radio适合无线通信相关的,这个数据量巨大所以专门分开放。event则是系统诊断用的,一般应用开发不用。crash则是系统崩溃信息,一般不用。搞清楚之后我们就可以开始看代码了
private Log() {
}
public static int v(String tag, String msg) {
return println_native(LOG_ID_MAIN, VERBOSE, tag, msg);
}
public static int v(String tag, String msg, Throwable tr) {
return println_native(LOG_ID_MAIN, VERBOSE, tag, msg + '\n' + getStackTraceString(tr));
}
public static int d(String tag, String msg) {
return println_native(LOG_ID_MAIN, DEBUG, tag, msg);
}
public static int d(String tag, String msg, Throwable tr) {
return println_native(LOG_ID_MAIN, DEBUG, tag, msg + '\n' + getStackTraceString(tr));
}
public static int i(String tag, String msg) {
return println_native(LOG_ID_MAIN, INFO, tag, msg);
}
public static int i(String tag, String msg, Throwable tr) {
return println_native(LOG_ID_MAIN, INFO, tag, msg + '\n' + getStackTraceString(tr));
}
public static int w(String tag, String msg) {
return println_native(LOG_ID_MAIN, WARN, tag, msg);
}
public static int w(String tag, String msg, Throwable tr) {
return println_native(LOG_ID_MAIN, WARN, tag, msg + '\n' + getStackTraceString(tr));
}
/**
* Checks to see whether or not a log for the specified tag is loggable at the specified level.
*
* The default level of any tag is set to INFO. This means that any level above and including
* INFO will be logged. Before you make any calls to a logging method you should check to see
* if your tag should be logged. You can change the default level by setting a system property:
* 'setprop log.tag.<YOUR_LOG_TAG> <LEVEL>'
* Where level is either VERBOSE, DEBUG, INFO, WARN, ERROR, ASSERT, or SUPPRESS. SUPPRESS will
* turn off all logging for your tag. You can also create a local.prop file that with the
* following in it:
* 'log.tag.<YOUR_LOG_TAG>=<LEVEL>'
* and place that in /data/local.prop.
*
* @param tag The tag to check.
* @param level The level to check.
* @return Whether or not that this is allowed to be logged.
* @throws IllegalArgumentException is thrown if the tag.length() > 23.
*/
public static native boolean isLoggable(String tag, int level);
public static int w(String tag, Throwable tr) {
return println_native(LOG_ID_MAIN, WARN, tag, getStackTraceString(tr));
}
public static int e(String tag, String msg) {
return println_native(LOG_ID_MAIN, ERROR, tag, msg);
}
public static int e(String tag, String msg, Throwable tr) {
return println_native(LOG_ID_MAIN, ERROR, tag, msg + '\n' + getStackTraceString(tr));
}
/** @hide */ public static native int println_native(int bufID,
int priority, String tag, String msg);
上面的大部分static方法相信大家一定不陌生,没错,这就是大家平时输出信息时最常用的方法,LOG_ID_MAIN就是指明这是main的信息。这里大家唯一陌生的是两个native方法,没错,Android就是通过这两个方法向系统发出调用Logger设备驱动的请求。下面我们重点关注println_native方法。这个方法的jni实现在Android_util_Log.cpp这个文件里
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 (msgObj == NULL) {
jniThrowNullPointerException(env, "println needs a message");
return -1;
}
if (bufID < 0 || bufID >= LOG_ID_MAX) {
jniThrowNullPointerException(env, "bad bufID");
return -1;
}
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;
}
这个函数中唯一需要关注的就是__android_log_buf_write这个被调用的函数,其他的都是一些对jni参数的检查,如果了解jni或对这块感兴趣的可以试着读读看。接写来的代码在logd_write.c这个文件中
static int __write_to_log_init(log_id_t, struct iovec *vec, size_t nr);
static int (*write_to_log)(log_id_t, struct iovec *vec, size_t nr) = __write_to_log_init;
...
int __android_log_buf_write(int bufID, int prio, const char *tag, const char *msg)
{
struct iovec vec[3];
char tmp_tag[32];
if (!tag)
tag = "";
/* XXX: This needs to go! */
if ((bufID != LOG_ID_RADIO) &&
(!strcmp(tag, "HTC_RIL") ||
!strncmp(tag, "RIL", 3) || /* Any log tag with "RIL" as the prefix */
!strncmp(tag, "IMS", 3) || /* Any log tag with "IMS" as the prefix */
!strcmp(tag, "AT") ||
!strcmp(tag, "GSM") ||
!strcmp(tag, "STK") ||
!strcmp(tag, "CDMA") ||
!strcmp(tag, "PHONE") ||
!strcmp(tag, "SMS"))) {
bufID = LOG_ID_RADIO;
/* Inform third party apps/ril/radio.. to use Rlog or RLOG */
snprintf(tmp_tag, sizeof(tmp_tag), "use-Rlog/RLOG-%s", tag);
tag = tmp_tag;
}
vec[0].iov_base = (unsigned char *) &prio;
vec[0].iov_len = 1;
vec[1].iov_base = (void *) tag;
vec[1].iov_len = strlen(tag) + 1;
vec[2].iov_base = (void *) msg;
vec[2].iov_len = strlen(msg) + 1;
return write_to_log(bufID, vec, 3);
}
这里的条件判断我们暂时不需要关心,iovec结构体如果有Linux系统编程经验的朋友应该有所耳闻,但这都不是今天的重点,重点在最后的函数write_to_log,在最上面初始时它被指向__write_to_log_init(后面会改变指向)
static int __write_to_log_init(log_id_t log_id, struct iovec *vec, size_t nr)
{
#ifdef HAVE_PTHREADS
pthread_mutex_lock(&log_init_lock);
#endif
if (write_to_log == __write_to_log_init) {
int ret;
ret = __write_to_log_initialize();
if (ret < 0) {
#ifdef HAVE_PTHREADS
pthread_mutex_unlock(&log_init_lock);
#endif
return ret;
}
write_to_log = __write_to_log_kernel;
}
#ifdef HAVE_PTHREADS
pthread_mutex_unlock(&log_init_lock);
#endif
return write_to_log(log_id, vec, nr);
}
这就是上述的函数,我们跳过pthread线程互斥锁相关的代码,开始有一个if判断上述的函数指针是不是指向init(就是上面那个初始化的),所以第一次调用这个函数时肯定会进入if判断内,可以看到,在里面有调用了一个initialize的函数,下面我会把这个函数代码附上并配合接下来的代码讲解,这里我们先继续看下去。如果ret大于等于0的话,write_to_log指针会被指向__write_to_log_kernel函数,而最后调用的该函数指针就是指向那个函数的(从此以后就是调用那个函数了)。
static int (*redirectOpen)(const char *pathName, int flags) = NULL;
static int (*redirectClose)(int fd) = NULL;
static ssize_t (*redirectWritev)(int fd, const struct iovec* vector, int count)
= NULL;
static int log_fds[(int)LOG_ID_MAX] = { -1, -1, -1, -1, -1 };
...
#if FAKE_LOG_DEVICE
static const char *LOG_NAME[LOG_ID_MAX] = {
[LOG_ID_MAIN] = "main",
[LOG_ID_RADIO] = "radio",
[LOG_ID_EVENTS] = "events",
[LOG_ID_SYSTEM] = "system",
[LOG_ID_CRASH] = "crash"
};
const char *android_log_id_to_name(log_id_t log_id)
{
if (log_id >= LOG_ID_MAX) {
log_id = LOG_ID_MAIN;
}
return LOG_NAME[log_id];
}
#endif
static int __write_to_log_initialize()
{
int i, ret = 0;
#if FAKE_LOG_DEVICE
for (i = 0; i < LOG_ID_MAX; i++) {
char buf[sizeof("/dev/log_system")];
snprintf(buf, sizeof(buf), "/dev/log_%s", android_log_id_to_name(i));
log_fds[i] = fakeLogOpen(buf, O_WRONLY);
}
#else
if (logd_fd >= 0) {
i = logd_fd;
logd_fd = -1;
close(i);
}
i = socket(PF_UNIX, SOCK_DGRAM | SOCK_CLOEXEC, 0);
if (i < 0) {
ret = -errno;
write_to_log = __write_to_log_null;
} else if (fcntl(i, F_SETFL, O_NONBLOCK) < 0) {
ret = -errno;
close(i);
i = -1;
write_to_log = __write_to_log_null;
} else {
struct sockaddr_un un;
memset(&un, 0, sizeof(struct sockaddr_un));
un.sun_family = AF_UNIX;
strcpy(un.sun_path, "/dev/socket/logdw");
if (connect(i, (struct sockaddr *)&un, sizeof(struct sockaddr_un)) < 0) {
ret = -errno;
close(i);
i = -1;
}
}
logd_fd = i;
#endif
return ret;
}
int fakeLogOpen(const char *pathName, int flags)
{
if (redirectOpen == NULL) {
setRedirects();
}
return redirectOpen(pathName, flags);
}
int fakeLogClose(int fd)
{
/* Assume that open() was called first. */
return redirectClose(fd);
}
ssize_t fakeLogWritev(int fd, const struct iovec* vector, int count)
{
/* Assume that open() was called first. */
return redirectWritev(fd, vector, count);
}
我们从后缀initialize的函数开始看起,因为我们用的是虚拟设备来记录Log所以我们只要看FAKE_LOG_DEVICE那块就可以了,可以看到这里的代码再循环调用android_log_id_to_name和fakeLogOpen这两个函数,从最上面我们能找到android_log_id_to_name这个函数的定义,函数是在定义了FAKE_LOG_DEVICE的情况下,分别返回日志类型的一张表(上述对应的五个类型)。之后的fakeLogOpen函数则是在调用事先判断redirectOpen这个指针是否为空,从最上面的代码我们看到,一开始redirectOpen(Close和Writev同理)都是NULL,所以会进入if判断中,调用的setRedirects函数我们现在来看一下:
static void setRedirects()
{
const char *ws;
/* Wrapsim sets this environment variable on children that it's
* created using its LD_PRELOAD wrapper.
*/
ws = getenv("ANDROID_WRAPSIM");
if (ws != NULL && strcmp(ws, "1") == 0) {
/* We're running inside wrapsim, so we can just write to the device. */
redirectOpen = (int (*)(const char *pathName, int flags))open;
redirectClose = close;
redirectWritev = writev;
} else {
/* There's no device to delegate to; handle the logging ourselves. */
redirectOpen = logOpen;
redirectClose = logClose;
redirectWritev = logWritev;
}
}
static int logClose(int fd)
{
deleteFakeFd(fd);
return 0;
}
/*
* Open a log output device and return a fake fd.
*/
static int logOpen(const char* pathName, int flags __unused)
{
LogState *logState;
int fd = -1;
lock();
logState = createLogState();
if (logState != NULL) {
configureInitialState(pathName, logState);
fd = logState->fakeFd;
} else {
errno = ENFILE;
}
unlock();
return fd;
}
这段代码我们就不细读了,可以看出是初始化redirect的,在setRedirects中,如果存在真实设备,就把开关和写函数指针置为对应的设备驱动函数,如果没有的话,就把函数指针置为一个假的设备驱动函数,返回一个假的文件描述符(环境代码和配置初始化我们就不读了,这根Linux系统的环境有关)。到这里,redirectOpen就设置好了,回到最上面的循环中,log_fds这个数组就已经记录了所有类型的Log的文件描述符了,接下来我们所有的写操作都会用到对应的文件描述符和调用对应的函数。至此初始化完成。
因为篇幅原因我们就不看写日志的代码了,如果各位有兴趣可以自己去读一下。System.out和Log的解析就此完成,前者使用的是write系统调用来对控制台的设备驱动文件写,后者则是对Logger设备文件写,这里的写函数是完全不一样的(这里需要一些Linux设备驱动程序的经验,不细讲)。本质上讲,Log更适合Android的程序调试,为什么呢,因为Android系统本身就有很多的信息输出,而且对信息的过滤和分类,查找都是非常重要的,所以在Android开发中大家应该多加利用Log的优势,做到事半功倍。