想必大家在编写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();
}
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虚拟机中。
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;
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格式的数据转成本地数据然后调用系统调用再返回。到此为止整个过程就结束了。
/**
* 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文件流对象可以动态的增加功能而不影响原来的类。虽然我们平时这么用但是很少人意思到这是装饰者。