Android中的信息输出:System.out和Log的源码分析与对比(System.out篇)

    想必大家在编写Android程序的时候必然绕不来输出信息,对于熟悉Java的程序员来讲,他们更喜欢使用System.out来输出信息,而Android开发经验更丰富的程序员则更倾向于使用Log,毕竟在调试的时候Android环境本身就会输出大量信息,如果不用Log的话输出信息可能很快就会被淹没,Log则是可以使用官方的或自定义的过滤器来将其他无用信息剔除。那么今天我们就来对比一下System.out和Log在实现上有什么不同。

    首先我们来看一下System.out,进入System类,可以看到如下代码

    /**
     * Default input stream.
     */
    public static final InputStream in;

    /**
     * Default output stream.
     */
    public static final PrintStream out;

    /**
     * Default error output stream.
     */
    public static final PrintStream err;

    static {
        err = new PrintStream(new FileOutputStream(FileDescriptor.err));
        out = new PrintStream(new FileOutputStream(FileDescriptor.out));
        in = new BufferedInputStream(new FileInputStream(FileDescriptor.in));
        unchangeableSystemProperties = initUnchangeableSystemProperties();
        systemProperties = createSystemProperties();
        lineSeparator = System.getProperty("line.separator");
    }
    这里我只截下了跟输入输出及其初始化有关的代码,可以看到,我们所熟悉的out和err都是PrintStream的对象,而in则是InputStream的对象。在static块中的初始化代码将out初始化为包装了FileOutputStream的PrintStream对象,可能会有人问FileDescriptor.out是什么东西,熟悉linux编程的朋友都知道,linux的系统调用中,write承包了大部分输出,而界定输出对象的就是文件描述符(标准输出的描述符是1),则是linux内核与程序员之间的调用接口协议,下面我们来看看FileDescriptor这个类

/**
 * Wraps a Unix file descriptor. It's possible to get the file descriptor used by some
 * classes (such as {@link FileInputStream}, {@link FileOutputStream},
 * and {@link RandomAccessFile}), and then create new streams that point to the same
 * file descriptor.
 */
public final class FileDescriptor {

    /**
     * Corresponds to {@code stdin}.
     */
    public static final FileDescriptor in = new FileDescriptor();

    /**
     * Corresponds to {@code stdout}.
     */
    public static final FileDescriptor out = new FileDescriptor();

    /**
     * Corresponds to {@code stderr}.
     */
    public static final FileDescriptor err = new FileDescriptor();

    /**
     * The Unix file descriptor backing this FileDescriptor.
     * A value of -1 indicates that this FileDescriptor is invalid.
     */
    private int descriptor = -1;

    static {
        in.descriptor = STDIN_FILENO;
        out.descriptor = STDOUT_FILENO;
        err.descriptor = STDERR_FILENO;
    }
    ...
}
    首先看最上面的注释,因为我们的PrintStream,InputStream要用到文件描述符,所以必须在Java语言层上包装一个文件描述符,接下来的代码就是在类中实例化三个自身的类,并设置其文件描述符,这里的常量STDOUT_FILENO是来自android.system.OsConstants这个包里的,我们再来看一看OsConstants

    private static native void initConstants();

    // A hack to avoid these constants being inlined by javac...
    private static int placeholder() { return 0; }
    // ...because we want to initialize them at runtime.
    static {
        initConstants();
    }

    这个类中包含了跟操作系统相关的变量和信息,如错误码和出错信息等。而常量的初始化就在native方法initConstants中完成(natove方法是指不在Java语言中实现,而是调用本地的二进制码,并通过jni接口协议来完成数据通信,以后有机会再讲吧)而对应的本地码的源代码文件为android_system_Osconstants.cpp,对应的代码为

    initConstant(env, c, "STDERR_FILENO", STDERR_FILENO);
    initConstant(env, c, "STDIN_FILENO", STDIN_FILENO);
    initConstant(env, c, "STDOUT_FILENO", STDOUT_FILENO);
    因为对应函数的代码太多,而且都是初始化操作系统常量,所以我就只把有关代码贴上。(其实就是把Linux系统的各种宏定义搬到Java中去)。这个函数的代码是

static void initConstant(JNIEnv* env, jclass c, const char* fieldName, int value) {
    jfieldID field = env->GetStaticFieldID(c, fieldName, "I");
    env->SetStaticIntField(c, field, value);
}
    参数中的jclass其实就是一个typedef,实际是一个指向Java类的指针(在jni.h中有)其他的像jint,jdouble,jchar都是typedef。这个函数实际上就是将Java类中相应的域置成传进来的参数,首先得到域的位置,然后把相应的值赋给该域。有了这个函数上面的代码就好解释了,其实就是讲Linux中相应的文件描述符赋给Java域(分别是2,0,1)。随后返回Java虚拟机中。
    下面我们回到PrintStream里面

    public void print(char[] chars) {
        print(new String(chars, 0, chars.length));
    }

    /**
     * Prints the string representation of the char {@code c}.
     */
    public void print(char c) {
        print(String.valueOf(c));
    }

    /**
     * Prints the string representation of the double {@code d}.
     */
    public void print(double d) {
        print(String.valueOf(d));
    }

    /**
     * Prints the string representation of the float {@code f}.
     */
    public void print(float f) {
        print(String.valueOf(f));
    }

    /**
     * Prints the string representation of the int {@code i}.
     */
    public void print(int i) {
        print(String.valueOf(i));
    }

    /**
     * Prints the string representation of the long {@code l}.
     */
    public void print(long l) {
        print(String.valueOf(l));
    }

    /**
     * Prints the string representation of the Object {@code o}, or {@code "null"}.
     */
    public void print(Object o) {
        print(String.valueOf(o));
    }

    /**
     * Prints a string to the target stream. The string is converted to an array
     * of bytes using the encoding chosen during the construction of this
     * stream. The bytes are then written to the target stream with
     * {@code write(int)}.
     *
     * 

If an I/O error occurs, this stream's error state is set to {@code true}. * * @param str * the string to print to the target stream. * @see #write(int) */ public synchronized void print(String str) { if (out == null) { setError(); return; } if (str == null) { print("null"); return; } try { if (encoding == null) { write(str.getBytes()); } else { write(str.getBytes(encoding)); } } catch (IOException e) { setError(); } } /** * Prints the string representation of the boolean {@code b}. */ public void print(boolean b) { print(String.valueOf(b)); }

    这就是大家平时调用的最多的打印函数(println一样,只是在后面加了一个newline函数),其实最后都会导向print(String str)这个函数,而这个函数会调用write函数。而write函数如下

    /**
     * Writes {@code count} bytes from {@code buffer} starting at {@code offset}
     * to the target stream. If autoFlush is set, this stream gets flushed after
     * writing the buffer.
     *
     * 

This stream's error flag is set to {@code true} if this stream is closed * or an I/O error occurs. * * @param buffer * the buffer to be written. * @param offset * the index of the first byte in {@code buffer} to write. * @param length * the number of bytes in {@code buffer} to write. * @throws IndexOutOfBoundsException * if {@code offset < 0} or {@code count < 0}, or if {@code * offset + count} is bigger than the length of {@code buffer}. * @see #flush() */ @Override public void write(byte[] buffer, int offset, int length) { Arrays.checkOffsetAndCount(buffer.length, offset, length); synchronized (this) { if (out == null) { setError(); return; } try { out.write(buffer, offset, length); if (autoFlush) { flush(); } } catch (IOException e) { setError(); } } } /** * Writes one byte to the target stream. Only the least significant byte of * the integer {@code oneByte} is written. This stream is flushed if * {@code oneByte} is equal to the character {@code '\n'} and this stream is * set to autoFlush. *

* This stream's error flag is set to {@code true} if it is closed or an I/O * error occurs. * * @param oneByte * the byte to be written */ @Override public synchronized void write(int oneByte) { if (out == null) { setError(); return; } try { out.write(oneByte);//这里需要注意 int b = oneByte & 0xFF; // 0x0A is ASCII newline, 0x15 is EBCDIC newline. boolean isNewline = b == 0x0A || b == 0x15; if (autoFlush && isNewline) { flush(); } } catch (IOException e) { setError(); } }

    函数本身没什么难懂的地方,我就不解释了,唯一需要关注的地方是我打上注释的那一行out.write(oneByte)这里才是真正的输出,out是其父类OutputStream的一个同类型变量,他被导向IoBrige类中的Os的write。Os是一个Interface,真正实现的是Posix.java类,下面我们来看看真正的调用

    public int write(FileDescriptor fd, ByteBuffer buffer) throws ErrnoException, InterruptedIOException {
        final int bytesWritten;
        final int position = buffer.position();
        if (buffer.isDirect()) {
            bytesWritten = writeBytes(fd, buffer, position, buffer.remaining());
        } else {
            bytesWritten = writeBytes(fd, NioUtils.unsafeArray(buffer), NioUtils.unsafeArrayOffset(buffer) + position, buffer.remaining());
        }

        maybeUpdateBufferPosition(buffer, position, bytesWritten);
        return bytesWritten;
    }
    public int write(FileDescriptor fd, byte[] bytes, int byteOffset, int byteCount) throws ErrnoException, InterruptedIOException {
        // This indirection isn't strictly necessary, but ensures that our public interface is type safe.
        return writeBytes(fd, bytes, byteOffset, byteCount);
    }
    private native int writeBytes(FileDescriptor fd, Object buffer, int offset, int byteCount) throws ErrnoException, InterruptedIOException;


    这里可以看到真正的native方法是writeByte,其他的代码只是做一些检查和格式化的处理,接下来来到C++层的libcore_io_Posix.cpp

static jint Posix_writeBytes(JNIEnv* env, jobject, jobject javaFd, jbyteArray javaBytes, jint byteOffset, jint byteCount) {
    ScopedBytesRO bytes(env, javaBytes);
    if (bytes.get() == NULL) {
        return -1;
    }
    return IO_FAILURE_RETRY(env, ssize_t, write, javaFd, bytes.get() + byteOffset, byteCount);
}
    这里没什么好说的,只是初始化一个环境,IO_FAILURE_RETRY这是一个宏定义

#define IO_FAILURE_RETRY(jni_env, return_type, syscall_name, java_fd, ...) ({ \
    return_type _rc = -1; \
    do { \
        bool _wasSignaled; \
        int _syscallErrno; \
        { \
            int _fd = jniGetFDFromFileDescriptor(jni_env, java_fd); \
            AsynchronousCloseMonitor _monitor(_fd); \
            _rc = syscall_name(_fd, __VA_ARGS__); \
            _syscallErrno = errno; \
            _wasSignaled = _monitor.wasSignaled(); \
        } \
        if (_wasSignaled) { \
            jniThrowException(jni_env, "java/io/InterruptedIOException", # syscall_name " interrupted"); \
            _rc = -1; \
            break; \
        } \
        if (_rc == -1 && _syscallErrno != EINTR) { \
            /* TODO: with a format string we could show the arguments too, like strace(1). */ \
            throwErrnoException(jni_env, # syscall_name); \
            break; \
        } \
    } while (_rc == -1); /* && _syscallErrno == EINTR && !_wasSignaled */ \
    _rc; })
    这里的代码简单讲就是在C/C++中调用系统调用,并检查出错信息,以jni的方式处理数据并适时抛出异常。而上一个函数中的write参数也是一个宏定义,即系统调用名。到这里我们就能够确定System.out的输出其实就是Linux中的write系统调用,中间通过jni和C++代码封装了数据。中间的缓冲区几乎全部放在Java层中(包括数据格式等),而接口的c++代码只是负责把JNI格式的数据转成本地数据然后调用系统调用再返回。到此为止整个过程就结束了。
    顺便多说几句,为什么我们都喜欢用System.out来输出但是几乎不会System.in来输入而是用一个或多个对象来包装System.in,其实我们来看看InputStream的代码就知道

    /**
     * Equivalent to {@code read(buffer, 0, buffer.length)}.
     */
    public int read(byte[] buffer) throws IOException {
        return read(buffer, 0, buffer.length);
    }

    /**
     * Reads up to {@code byteCount} bytes from this stream and stores them in
     * the byte array {@code buffer} starting at {@code byteOffset}.
     * Returns the number of bytes actually read or -1 if the end of the stream
     * has been reached.
     *
     * @throws IndexOutOfBoundsException
     *   if {@code byteOffset < 0 || byteCount < 0 || byteOffset + byteCount > buffer.length}.
     * @throws IOException
     *             if the stream is closed or another IOException occurs.
     */
    public int read(byte[] buffer, int byteOffset, int byteCount) throws IOException {
        Arrays.checkOffsetAndCount(buffer.length, byteOffset, byteCount);
        for (int i = 0; i < byteCount; ++i) {
            int c;
            try {
                if ((c = read()) == -1) {
                    return i == 0 ? -1 : i;
                }
            } catch (IOException e) {
                if (i != 0) {
                    return i;
                }
                throw e;
            }
            buffer[byteOffset + i] = (byte) c;
        }
        return byteCount;
    }
    在System.in的InputStream对象中,只能以字节流的方式来读取数据,说白了我们要自己做格式转换,所以我们才会用Scanner或是BufferReader来包装这个流。

    再多嘴一句,其实Java中的文件流类的结构就是我们设计模式中的装饰者(decorator)模式,通过包装使得基本的流具备更强大的功能,上面有一句out.write也是一样,在自己的函数中调用的是装饰者的同名函数,使得Java文件流对象可以动态的增加功能而不影响原来的类。虽然我们平时这么用但是很少人意思到这是装饰者。




你可能感兴趣的:(Android中的信息输出:System.out和Log的源码分析与对比(System.out篇))