【安卓开发系列 -- APP 开源框架】IO 框架 OKIO

【1】OKIO的整体框架

【1.1】OKIO的整体框架图示

【1.2】OKIO的优势

CPU和内存消耗低,OKIO采用了segment机制进行内存共享和复用,尽可能少的申请内存,从而降低了GC的频率(过于频繁的GC会给应用程序带来性能问题);
使用方便,OKIO提供了ByteString来处理不变的byte序列,并在内存上做了优化,不管是从byte[]到String或是从String到byte[],操作便利,同时提供了如hex字符,base64等工具;提供了Buffer处理可变byte序列,可以根据使用情况自动增长,在使用过程中也不用去关心position等位置的处理;
N合一(OKIO精简了输入输出流的类个数),对于Java的原生IO,InputStream/OutputStream,若需要读取数据(如读取一个整数,一个布尔,或是一个浮点),需要用DataInputStream来包装;若作为缓存来使用,则需要使用BufferedOutputStream包装;OKIO提供的BufferedSink/BufferedSource具备以上功能,从而不需要再串联上一系列的装饰类;
提供了一系列的方便工具,如GZip的透明处理,对数据计算md5、sha1等的支持,使得数据校验方便;

详见,深入理解okio的优化思想,Android学习笔记——Okio;

【2】OKIO使用方法示例

【2.1】OKIO示例代码

File inFile = new File(Environment.getExternalStorageDirectory() + "/" + "input.txt");
File outFile = new File(Environment.getExternalStorageDirectory() + "/" + "output.txt");
String name = "xiaoming";
int age = 11;
try {// 向文件写入数据Okio.buffer(Okio.sink(inFile)).writeUtf8(name).writeInt(age).close();// 拷贝文件Okio.buffer(Okio.sink(inFile)).writeAll(Okio.buffer(Okio.source(outFile)));// 从文件读取数据System.out.println(Okio.buffer(Okio.source(outFile)).readString(StandardCharsets.UTF_8));
} catch (FileNotFoundException e) {e.printStackTrace();
} catch (IOException ex) {ex.printStackTrace();
}

【2.2】OKIO读写流程图示

【3】OKIO源码分析

【3.1】Source/Sink接口

类继承结构图

Source接口源码

/*** Supplies a stream of bytes. Use this interface to read data from wherever* it's located: from the network, storage, or a buffer in memory. ** 提供字节流,使用此接口可以从任何位置读取数据(网络、存储器或内存中的缓冲区)*/
public interface Source extends Closeable {/*** Removes at least 1, and up to {@code byteCount} bytes from this and appends* them to {@code sink}. Returns the number of bytes read, or -1 if this* source is exhausted.** 移除1至byteCount个字节并将这些字节追加到接收器(sink)* 返回读取的字节数,若source读取完毕则返回-1;*/long read(Buffer sink, long byteCount) throws IOException;/** Returns the timeout for this source. */// 返回source的timeout值Timeout timeout();/*** Closes this source and releases the resources held by this source. It is an* error to read a closed source. It is safe to close a source more than once.** 关闭此源并释放此源持有的资源* 读取关闭的源将产生错误* 源可以关闭多次*/@Override void close() throws IOException;
}/*** A source that keeps a buffer internally so that callers can do small reads without a performance* penalty. It also allows clients to read ahead, buffering as much as necessary before consuming* input.** 一种在内部保留缓冲区的源,从而调用者可以在不影响性能的情况下进行小的读取* 还允许客户端进行预读,在使用输入之前尽可能多地进行缓冲*/
public interface BufferedSource extends Source, ReadableByteChannel {/** Returns this source's internal buffer. */// 返回该source的内部bufferBuffer buffer();/*** Returns true if there are no more bytes in this source. This will block until there are bytes* to read or the source is definitely exhausted.** 如果此源中没有更多的字节则返回true* 该方法将阻塞直到有字节可读取或源绝对耗尽*/boolean exhausted() throws IOException;/*** Returns when the buffer contains at least {@code byteCount} bytes. Throws an* {@link java.io.EOFException} if the source is exhausted before the required bytes can be read.** 当缓冲区至少包含byteCount个字节时返回* 若在读取所需字节之前源已用尽则抛出java.io.EOFException异常*/void require(long byteCount) throws IOException;/*** Returns true when the buffer contains at least {@code byteCount} bytes, expanding it as* necessary. Returns false if the source is exhausted before the requested bytes can be read.** 当缓冲区至少包含byteCount个字节时返回true,并根据需要对其进行扩展* 如果在读取请求的字节之前源已用尽则返回false*/boolean request(long byteCount) throws IOException;/** Removes a byte from this source and returns it. */// 从源移除一个字节并返回该字节byte readByte() throws IOException;/*** Removes two bytes from this source and returns a big-endian short. <pre>{@code* 从源移除两个字节并返回一个大端的short变量*/short readShort() throws IOException;/*** Removes two bytes from this source and returns a little-endian short. <pre>{@code* 从源移除两个字节并返回一个小端的short变量*/short readShortLe() throws IOException;int readInt() throws IOException;int readIntLe() throws IOException;long readLong() throws IOException;long readLongLe() throws IOException;/*** Reads a long from this source in signed decimal form (i.e., as a string in base 10 with* optional leading '-')* 以带符号的十进制的形式从源中读取long变量*/long readDecimalLong() throws IOException;/*** Reads a long form this source in hexadecimal form (i.e., as a string in base 16). This will* iterate until a non-hexadecimal character is found. <pre>{@code* 以十六进制形式从源中读取long变量* 该方法将迭代直到找到非十六进制字符*/long readHexadecimalUnsignedLong() throws IOException;/*** Reads and discards {@code byteCount} bytes from this source. Throws an* {@link java.io.EOFException} if the source is exhausted before the* requested bytes can be skipped.* 从该源读取并丢弃byteCount字节* 如果在可以跳过请求的字节之前源已用尽则抛出java.io.EOFException*/void skip(long byteCount) throws IOException;/** Removes all bytes bytes from this and returns them as a byte string. */// 从源中删除所有字节字节并作为字节字符串返回ByteString readByteString() throws IOException;/** Removes {@code byteCount} bytes from this and returns them as a byte string. */// 从源中删除byteCount个字节字节并作为字节字符串返回ByteString readByteString(long byteCount) throws IOException;/*** Finds the first string in {@code options} that is a prefix of this buffer, consumes it from* this buffer, and returns its index. If no byte string in {@code options} is a prefix of this* buffer this returns -1 and no bytes are consumed.* 在options中查找作为此缓冲区前缀的第一个字符串,从该缓冲区消费它并返回其索引*/int select(Options options) throws IOException;/** Removes all bytes from this and returns them as a byte array. */// 从源中删除所有字节并作为字节数组返回byte[] readByteArray() throws IOException;/** Removes {@code byteCount} bytes from this and returns them as a byte array. */// 从中删除byteCount个字节并作为字节数组返回byte[] readByteArray(long byteCount) throws IOException;/*** Removes up to {@code sink.length} bytes from this and copies them into {@code sink}. Returns* the number of bytes read, or -1 if this source is exhausted.* 删除最多sink.length个字节并复制到sink*/int read(byte[] sink) throws IOException;/*** Removes exactly {@code sink.length} bytes from this and copies them into {@code sink}. Throws* an {@link java.io.EOFException} if the requested number of bytes cannot be read.* 完全删除sink.length个字节并将其复制到sink* 如果无法读取需要的字节数则抛出java.io.EOFException异常*/void readFully(byte[] sink) throws IOException;/*** Removes up to {@code byteCount} bytes from this and copies them into {@code sink} at {@code* offset}. Returns the number of bytes read, or -1 if this source is exhausted.* 从中删除最多byteCount个字节并复制到sink的offset处*/int read(byte[] sink, int offset, int byteCount) throws IOException;/*** Removes exactly {@code byteCount} bytes from this and appends them to {@code sink}. Throws an* {@link java.io.EOFException} if the requested number of bytes cannot be read.* 删除byteCount个字节并附加到sink* 如果无法读取需要的字节数则抛出java.io.EOFException异常*/void readFully(Buffer sink, long byteCount) throws IOException;/*** Removes all bytes from this and appends them to {@code sink}. Returns the total number of bytes* written to {@code sink} which will be 0 if this is exhausted.* 从中删除所有字节并附加到sink*/long readAll(Sink sink) throws IOException;/*** Removes all bytes from this, decodes them as UTF-8, and returns the string. Returns the empty* string if this source is empty.* 从中删除所有字节解码为UTF-8并返回字符串;如果此源为空则返回空字符串;*/String readUtf8() throws IOException;/*** Removes {@code byteCount} bytes from this, decodes them as UTF-8, and returns the string.* 从中删除byteCount个字节解码为UTF-8并返回字符串*/String readUtf8(long byteCount) throws IOException;/*** Removes and returns characters up to but not including the next line break. A line break is* either {@code "\n"} or {@code "\r\n"}; these characters are not included in the result.* 删除并返回下一个换行符但不包括下一个换行符*/@Nullable String readUtf8Line() throws IOException;/*** Removes and returns characters up to but not including the next line break. A line break is* either {@code "\n"} or {@code "\r\n"}; these characters are not included in the result.* 删除并返回下一个换行符但不包括下一个换行符*/String readUtf8LineStrict() throws IOException;String readUtf8LineStrict(long limit) throws IOException;/*** Removes and returns a single UTF-8 code point, reading between 1 and 4 bytes as necessary.* 移除并传回单一的UTF-8码位,必要时读取1至4个位元组*/int readUtf8CodePoint() throws IOException;/** Removes all bytes from this, decodes them as {@code charset}, and returns the string. */// 从中删除所有字节解码为charset并返回字符串String readString(Charset charset) throws IOException;String readString(long byteCount, Charset charset) throws IOException;long indexOf(byte b) throws IOException;/*** Returns the index of the first {@code b} in the buffer at or after {@code fromIndex}. This* expands the buffer as necessary until {@code b} is found. This reads an unbounded number of* bytes into the buffer. Returns -1 if the stream is exhausted before the requested byte is* found.* 返回缓冲区中位于fromindex处或之后的第一个b的索引*/long indexOf(byte b, long fromIndex) throws IOException;long indexOf(byte b, long fromIndex, long toIndex) throws IOException;long indexOf(ByteString bytes) throws IOException;/*** Returns the index of the first match for {@code bytes} in the buffer at or after {@code* fromIndex}. This expands the buffer as necessary until {@code bytes} is found. This reads an* unbounded number of bytes into the buffer. Returns -1 if the stream is exhausted before the* requested bytes are found.* 返回缓冲区中bytes在codefromindex处或之后的第一个匹配项的索引*/long indexOf(ByteString bytes, long fromIndex) throws IOException;long indexOfElement(ByteString targetBytes) throws IOException;/*** Returns the first index in this buffer that is at or after {@code fromIndex} and that contains* any of the bytes in {@code targetBytes}. This expands the buffer as necessary until a target* byte is found. This reads an unbounded number of bytes into the buffer. Returns -1 if the* stream is exhausted before the requested byte is found.* 返回此缓冲区中位于{@codefromindex}处或之后且包含{@code targetBytes}中任何字节的第一个索引*/long indexOfElement(ByteString targetBytes, long fromIndex) throws IOException;/*** Returns true if the bytes at {@code offset} in this source equal {@code bytes}. This expands* the buffer as necessary until a byte does not match, all bytes are matched, or if the stream* is exhausted before enough bytes could determine a match.* 如果此源中offset处的字节等于bytes则返回true*/boolean rangeEquals(long offset, ByteString bytes) throws IOException;/*** Returns true if {@code byteCount} bytes at {@code offset} in this source equal {@code bytes}* at {@code bytesOffset}. This expands the buffer as necessary until a byte does not match, all* bytes are matched, or if the stream is exhausted before enough bytes could determine a match.*/boolean rangeEquals(long offset, ByteString bytes, int bytesOffset, int byteCount)throws IOException;/** Returns an input stream that reads from this source. */// 返回从source中读取的输入流InputStream inputStream();
}

Sink接口源码

/*** Receives a stream of bytes. Use this interface to write data wherever it's* needed: to the network, storage, or a buffer in memory. Sinks may be layered* to transform received data, such as to compress, encrypt, throttle, or add* protocol framing.** 接收字节流* 使用此接口可以在需要的地方写入数据(网络、存储器或内存中的缓冲区)* 接收器可以分层以转换接收的数据,例如压缩、加密、调节或添加协议帧*/
public interface Sink extends Closeable, Flushable {/** Removes {@code byteCount} bytes from {@code source} and appends them to this. */// 从source移除byteCount个字节并追加到sink中void write(Buffer source, long byteCount) throws IOException;/** Pushes all buffered bytes to their final destination. */// 将所有缓冲字节推送到最终目的地@Override void flush() throws IOException;/** Returns the timeout for this sink. */Timeout timeout();/*** Pushes all buffered bytes to their final destination and releases the* resources held by this sink. It is an error to write a closed sink. It is* safe to close a sink more than once.** 将所有缓冲字节推送到最终目的地并释放此接收器保留的资源* 写已经关闭的sink将产生错误* 可以多次关闭一个sink*/@Override void close() throws IOException;
}/*** A sink that keeps a buffer internally so that callers can do small writes* without a performance penalty.** 一种在内部保留缓冲区的接收器,从而调用者可以在不影响性能的情况下进行小的写入*/
public interface BufferedSink extends Sink, WritableByteChannel {/** Returns this sink's internal buffer. */// 返回该sink的内部bufferBuffer buffer();// 写方法BufferedSink write(ByteString byteString) throws IOException;/*** 写入一个完整的字节数组*/BufferedSink write(byte[] source) throws IOException;/*** 从字节数组源source的offset开始写入byteCount个字节*/BufferedSink write(byte[] source, int offset, int byteCount) throws IOException;/*** 移除source中的所有字节并追加到sink中* 返回读取的字节数*/long writeAll(Source source) throws IOException;// 移除source中的byteCount个字节并追加到sink中BufferedSink write(Source source, long byteCount) throws IOException;/*** 对string进行UTF-8编码并追加到sink中*/BufferedSink writeUtf8(String string) throws IOException;/*** 对string字符串beginIndex到endIndex的字符进行UTF-8编码并追加到sink中*/BufferedSink writeUtf8(String string, int beginIndex, int endIndex) throws IOException;/** Encodes {@code codePoint} in UTF-8 and writes it to this sink. */// 对int型数据进行UTF-8编码并追加到sink中BufferedSink writeUtf8CodePoint(int codePoint) throws IOException;/** Encodes {@code string} in {@code charset} and writes it to this sink. */// 对字符串进行指定类型的编码并追加到sink中BufferedSink writeString(String string, Charset charset) throws IOException;// 对字符串的beginIndex到endIndex进行指定类型的编码并追加到sink中BufferedSink writeString(String string, int beginIndex, int endIndex, Charset charset)throws IOException;/** Writes a byte to this sink. */// 写入一个字节到sink中BufferedSink writeByte(int b) throws IOException;// 使用两个字节向sink中写入大端short变量BufferedSink writeShort(int s) throws IOException;// 使用两个字节向sink中写入小端short变量BufferedSink writeShortLe(int s) throws IOException;BufferedSink writeInt(int i) throws IOException;BufferedSink writeIntLe(int i) throws IOException;BufferedSink writeLong(long v) throws IOException;BufferedSink writeLongLe(long v) throws IOException;/*** Writes a long to this sink in signed decimal form (i.e., as a string in base 10). <pre>{@code* 以带符号的十进制的形式向sink写入long变量*/BufferedSink writeDecimalLong(long v) throws IOException;/*** Writes a long to this sink in hexadecimal form* 以十六进制形式向sink写入long变量*/BufferedSink writeHexadecimalUnsignedLong(long v) throws IOException;/*** Writes all buffered data to the underlying sink, if one exists. Then that sink is recursively* flushed which pushes data as far as possible towards its ultimate destination. Typically that* destination is a network socket or file.** 将所有缓冲数据写入基础接收器* 然后递归地刷新该接收器,从而将数据尽可能地推向最终目的地* 通常该目的地址是网络套接字或文件*/@Override void flush() throws IOException;/*** Writes all buffered data to the underlying sink, if one exists. Like {@link #flush}, but* weaker. Call this before this buffered sink goes out of scope so that its data can reach its* destination. <pre>{@code** 将所有缓冲数据写入基础接收器* 类似于flush方法但比较弱* 在该缓冲接收器超出作用域之前调用它,以便其数据可以到达目的地*/BufferedSink emit() throws IOException;/*** Writes complete segments to the underlying sink, if one exists. Like {@link #flush}, but* weaker. Use this to limit the memory held in the buffer to a single segment. Typically* application code will not need to call this: it is only necessary when application code writes* directly to this {@linkplain #buffer() sink's buffer}. <pre>{@code** 将完整段写入基础接收器* 类似于flush方法但比较弱* 使用该方法可将缓冲区中的内存限制为单个段* 通常,应用程序代码不需要调用这个函数:*    只有当应用程序代码直接写入该sink的buffer时才需要调用该方法*/BufferedSink emitCompleteSegments() throws IOException;/** Returns an output stream that writes to this sink. */// 返回写入此接收器的输出流OutputStream outputStream();
}

【3.2】Buffer类

类继承结构图示

Buffer类原理图示

Buffer源码分析

public final class Okio {// Returns a new source that buffers reads from {@code source}// 返回新的source,该源可以缓存从其中读取的数据public static BufferedSource buffer(Source source) {// RealBufferedSource类内部包含Buffer实例buffer// RealBufferedSource类中的read相关方法都转发给该Buffer实例buffer对应的read方法处理return new RealBufferedSource(source);}/** Returns a source that reads from {@code file}. */// 根据传入的File实例构建一个Source实例public static Source source(File file) throws FileNotFoundException {if (file == null) throw new IllegalArgumentException("file == null");return source(new FileInputStream(file));}/** Returns a source that reads from {@code in}. */// 根据InputStream实例构建一个Source实例public static Source source(InputStream in) {return source(in, new Timeout());}// 创建一个Source实例(带有超时)并实现接口中的方法private static Source source(final InputStream in, final Timeout timeout) {if (in == null) throw new IllegalArgumentException("in == null");if (timeout == null) throw new IllegalArgumentException("timeout == null");return new Source() {@Override public long read(Buffer sink, long byteCount) throws IOException {// 检查入参的有效性if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount);if (byteCount == 0) return 0;try {// 超时检查timeout.throwIfReached();// 获取一个可以写的Segment/*** Returns a tail segment that we can write at least minimumCapacity bytes to, creating it if necessary.* 返回tail指向的至少可以写入minimumCapacity容量字节Segment并且在必要时创建该Segment*/Segment tail = sink.writableSegment(1);int maxToCopy = (int) Math.min(byteCount, Segment.SIZE - tail.limit);int bytesRead = in.read(tail.data, tail.limit, maxToCopy);if (bytesRead == -1) return -1;tail.limit += bytesRead;sink.size += bytesRead;return bytesRead;} catch (AssertionError e) {if (isAndroidGetsocknameError(e)) throw new IOException(e);throw e;}}// 关闭输出流@Override public void close() throws IOException {in.close();}// 返回Timeout实例@Override public Timeout timeout() {return timeout;}// 对象的toString方法覆写@Override public String toString() {return "source(" + in + ")";}};}
}
public final class Okio {// Returns a new sink that buffers writes to {@code sink}// 返回新的sink,该sink可以缓存待写入其中的数据public static BufferedSink buffer(Sink sink) {// RealBufferedSource类内部包含Buffer实例buffer// RealBufferedSource类中的write相关方法都转发给该Buffer实例buffer对应的write方法处理 return new RealBufferedSink(sink);}/** Returns a sink that writes to {@code file}. */// 根据传入的File实例构建一个Sink实例public static Sink sink(File file) throws FileNotFoundException {if (file == null) throw new IllegalArgumentException("file == null");return sink(new FileOutputStream(file));}/** Returns a sink that writes to {@code out}. */// 根据OutputStream实例构建一个Sink实例public static Sink sink(OutputStream out) {return sink(out, new Timeout());}// 创建一个Sink实例(带有超时)并实现接口中的方法private static Sink sink(final OutputStream out, final Timeout timeout) {// 检查入参的有效性if (out == null) throw new IllegalArgumentException("out == null");if (timeout == null) throw new IllegalArgumentException("timeout == null");// 创建一个匿名Sink对象return new Sink() {// 覆写write方法@Override public void write(Buffer source, long byteCount) throws IOException {// 检查Buffer的有效性checkOffsetAndCount(source.size, 0, byteCount);// 循环读取数据while (byteCount > 0) {// 超时检查timeout.throwIfReached();Segment head = source.head;int toCopy = (int) Math.min(byteCount, head.limit - head.pos);out.write(head.data, head.pos, toCopy);head.pos += toCopy;byteCount -= toCopy;source.size -= toCopy;// 当一个Segment读取完毕则将该Segment放入SegmentPool中if (head.pos == head.limit) {// 源缓冲source双向Segment列表中弹出一个Segmentsource.head = head.pop();// SegmentPool回收弹出的SegmentSegmentPool.recycle(head);}}}// 刷新输出流@Override public void flush() throws IOException {out.flush();}// 关闭输出流@Override public void close() throws IOException {out.close();}// 返回Timeout实例@Override public Timeout timeout() {return timeout;}// 对象的toString方法覆写@Override public String toString() {return "sink(" + out + ")";}};}
}

【3.3】Segment/SegmentPool

// A segment of a buffer
// 缓冲区中的一个分片
final class Segment {/** The size of all segments in bytes. */// 分片中的总字节数static final int SIZE = 8192;/** Segments will be shared when doing so avoids {@code arraycopy()} of this many bytes. */// static final int SHARE_MINIMUM = 1024;// 存储数据final byte[] data;/** The next byte of application data byte to read in this segment. */// 下一次读取开始的位置int pos;/** The first byte of available data ready to be written to. */// 写入的开始位置int limit;/** True if other segments or byte strings use the same byte array. */// 若其他段或字节字符串使用相同的字节数组则取值为trueboolean shared;/** True if this segment owns the byte array and can append to it, extending {@code limit}. */// 如果这个段拥有字节数组并且可以附加到该字节数组则取值为true// 标记data是否仅当前Segment独有boolean owner;/** Next segment in a linked or circularly-linked list. */// 循环双向链表的下一个节点Segment next;/** Previous segment in a circularly-linked list. */// 循环双向链表的前一个节点Segment prev;// Segment的构造方法// SegmentPool的take方法中调用Segment() {this.data = new byte[SIZE];this.owner = true;this.shared = false;}// Segment的构造方法// Segment类的sharedCopy/unsharedCopy以及SegmentedByteString类的write方法中调用Segment(byte[] data, int pos, int limit, boolean shared, boolean owner) {this.data = data;this.pos = pos;this.limit = limit;this.shared = shared;this.owner = owner;}/*** Returns a new segment that shares the underlying byte array with this. Adjusting pos and limit* are safe but writes are forbidden. This also marks the current segment as shared, which* prevents it from being pooled.** 返回与该段共享字节数组的新段* 调整位置和限制是安全的但禁止写入* 将当前段标记为共享段并阻止该段被池化*/Segment sharedCopy() {shared = true;return new Segment(data, pos, limit, true, false);}/** Returns a new segment that its own private copy of the underlying byte array. */// 返回一个新的段,该段拥有自己独有的底层字节数组Segment unsharedCopy() {return new Segment(data.clone(), pos, limit, false, true);}/*** Removes this segment of a circularly-linked list and returns its successor.* Returns null if the list is now empty.* 将当前段从循环链接列表中删除并返回其后续项* 如果当前列表为空则返回null*/public @Nullable Segment pop() {Segment result = next != this ? next : null;prev.next = next;next.prev = prev;next = null;prev = null;return result;}/*** Appends {@code segment} after this segment in the circularly-linked list.* Returns the pushed segment.* 将入参segment追加到循环链接列表中的当前段之后* 返回加入的段*/public Segment push(Segment segment) {segment.prev = this;segment.next = next;next.prev = segment;next = segment;return segment;}/*** Splits this head of a circularly-linked list into two segments. The first* segment contains the data in {@code [pos..pos+byteCount)}. The second* segment contains the data in {@code [pos+byteCount..limit)}. This can be* useful when moving partial segments from one buffer to another.* 将循环链表的头拆分为两段* 第一段包含{@code[pos..pos+byteCount)}中的数据* 第二段包含{@code [pos+byteCount..limit)}中的数据* 在将部分段从一个缓冲区移动到另一个缓冲区时非常有用*/public Segment split(int byteCount) {if (byteCount <= 0 || byteCount > limit - pos) throw new IllegalArgumentException();Segment prefix;// We have two competing performance goals://  - Avoid copying data. We accomplish this by sharing segments.//  - Avoid short shared segments. These are bad for performance because they are readonly and//    may lead to long chains of short segments.// To balance these goals we only share segments when the copy will be large.//// 性能处理// - 通过共享段避免拷贝数据// - 避免短的共享段,过短的段对性能不利,因为这些段是只读的并且这些短段会导致过长的链表// 在副本比较大的时候共享段以平衡上述的目标if (byteCount >= SHARE_MINIMUM) {prefix = sharedCopy();} else {prefix = SegmentPool.take();System.arraycopy(data, pos, prefix.data, 0, byteCount);}prefix.limit = prefix.pos + byteCount;pos += byteCount;prev.push(prefix);return prefix;}/*** Call this when the tail and its predecessor may both be less than half* full. This will copy data so that segments can be recycled.* 将当前Segment结点和prev前驱结点合并成一个Segment并统一合并到prev* 然后当前Segment结点从双向链表移除并添加到SegmentPool复用;*    合并的前提是,2个Segment的字节总和不超过8K,合并后可能会移动pos、limit*/public void compact() {if (prev == this) throw new IllegalStateException();if (!prev.owner) return; // Cannot compact: prev isn't writable.int byteCount = limit - pos;int availableByteCount = SIZE - prev.limit + (prev.shared ? 0 : prev.pos);if (byteCount > availableByteCount) return; // Cannot compact: not enough writable space.writeTo(prev, byteCount);// 将当前段从循环链接列表中删除并返回其后续项pop();// 回收当前Segment到SegmentPool中SegmentPool.recycle(this);}/** Moves {@code byteCount} bytes from this segment to {@code sink}. */// 从当前节点移动byteCount个字节到{@code sink}中public void writeTo(Segment sink, int byteCount) {if (!sink.owner) throw new IllegalArgumentException();// sink中可写的空间不足if (sink.limit + byteCount > SIZE) {// We can't fit byteCount bytes at the sink's current position. Shift sink first.// 判断sink是否只读if (sink.shared) throw new IllegalArgumentException();// 判断sink中的剩余空间是否满足写入的字节数if (sink.limit + byteCount - sink.pos > SIZE) throw new IllegalArgumentException();// sink中的数据前移System.arraycopy(sink.data, sink.pos, sink.data, 0, sink.limit - sink.pos);sink.limit -= sink.pos;sink.pos = 0;}// 将当前Segment中的数据拷贝到sink中System.arraycopy(data, pos, sink.data, sink.limit, byteCount);sink.limit += byteCount;pos += byteCount;}
}
/*** A collection of unused segments, necessary to avoid GC churn and zero-fill.* This pool is a thread-safe static singleton.* 未使用段的集合,注意避免GC混乱和零填充* 此池是线程安全的静态单例*/
final class SegmentPool {/** The maximum number of bytes to pool. */// 池的最大字节数static final long MAX_SIZE = 64 * 1024; // 64 KiB./** Singly-linked list of segments. */// Segment的单向链列表static @Nullable Segment next;/** Total bytes in this pool. */// 此池中的总字节数static long byteCount;// SegmentPool的构造方法private SegmentPool() {}// 从池中获取一个Segment对象static Segment take() {synchronized (SegmentPool.class) {if (next != null) {Segment result = next;next = result.next;result.next = null;byteCount -= Segment.SIZE;return result;}}// 返回一个Segment实例,不要在持有锁的时候将Segment置零return new Segment(); // Pool is empty. Don't zero-fill while holding a lock.}// 将Segment状态初始化并回收到池中static void recycle(Segment segment) {if (segment.next != null || segment.prev != null) throw new IllegalArgumentException();// shared标记的Segment不能被回收if (segment.shared) return; // This segment cannot be recycled.synchronized (SegmentPool.class) {if (byteCount + Segment.SIZE > MAX_SIZE) return; // Pool is full.byteCount += Segment.SIZE;segment.next = next;segment.pos = segment.limit = 0;next = segment;}}
}

【3.4】Timeout机制

【3.4.1】Timeout类

// Timeout 类
public class Timeout {/*** An empty timeout that neither tracks nor detects timeouts. Use this when* timeouts aren't necessary, such as in implementations whose operations* do not block.* 空的Timeout实例,该实例既不跟踪也不检测超时* 通常在不需要超时时使用,比如非阻塞的操作*/public static final Timeout NONE = new Timeout() {@Override public Timeout timeout(long timeout, TimeUnit unit) {return this;}@Override public Timeout deadlineNanoTime(long deadlineNanoTime) {return this;}@Override public void throwIfReached() throws IOException {}};/*** True if {@code deadlineNanoTime} is defined. There is no equivalent to null* or 0 for {@link System#nanoTime}.* 如果定义了{@code deadlineNanoTime}则为True*/private boolean hasDeadline;// 截止时间点private long deadlineNanoTime;// timeout等待的时间private long timeoutNanos;public Timeout() {}/*** Wait at most {@code timeout} time before aborting an operation. Using a* per-operation timeout means that as long as forward progress is being made,* no sequence of operations will fail.* 在中止操作之前最多等待{@code timeout}* 对每个操作设置超时意味着,只要正在进行前向处理,任何操作序列都不会失败*/public Timeout timeout(long timeout, TimeUnit unit) {if (timeout < 0) throw new IllegalArgumentException("timeout < 0: " + timeout);if (unit == null) throw new IllegalArgumentException("unit == null");this.timeoutNanos = unit.toNanos(timeout);return this;}/** Returns the timeout in nanoseconds, or {@code 0} for no timeout. */// 返回timeout时间大小public long timeoutNanos() {return timeoutNanos;}/** Returns true if a deadline is enabled. */// 返回是否使能了deadlinepublic boolean hasDeadline() {return hasDeadline;}/*** Returns the {@linkplain System#nanoTime() nano time} when the deadline will* be reached.* 当到达deadline时间点时返回deadline的值*/public long deadlineNanoTime() {if (!hasDeadline) throw new IllegalStateException("No deadline");return deadlineNanoTime;}/*** Sets the {@linkplain System#nanoTime() nano time} when the deadline will be* reached. All operations must complete before this time. Use a deadline to* set a maximum bound on the time spent on a sequence of operations.* 设置deadline* 在deadline之前所有的操作必须完成* 使用deadline设置操作序列所花费时间的最大限制*/public Timeout deadlineNanoTime(long deadlineNanoTime) {this.hasDeadline = true;this.deadlineNanoTime = deadlineNanoTime;return this;}/** Set a deadline of now plus {@code duration} time. */// 设置deadline,按时间单位转换为纳秒public final Timeout deadline(long duration, TimeUnit unit) {if (duration <= 0) throw new IllegalArgumentException("duration <= 0: " + duration);if (unit == null) throw new IllegalArgumentException("unit == null");return deadlineNanoTime(System.nanoTime() + unit.toNanos(duration));}/** Clears the timeout. Operating system timeouts may still apply. */// 清除timeout,从而操作可以继续执行public Timeout clearTimeout() {this.timeoutNanos = 0;return this;}/** Clears the deadline. */// 关闭deadlinepublic Timeout clearDeadline() {this.hasDeadline = false;return this;}/*** Throws an {@link InterruptedIOException} if the deadline has been reached or if the current* thread has been interrupted. This method doesn't detect timeouts; that should be implemented to* asynchronously abort an in-progress operation.* 当deadline到达并且当前线程中断后抛出InterruptedIOException异常* 此方法不检测超时,应实现超时以异步中止正在进行的操作*/public void throwIfReached() throws IOException {if (Thread.interrupted()) {throw new InterruptedIOException("thread interrupted");}if (hasDeadline && deadlineNanoTime - System.nanoTime() <= 0) {throw new InterruptedIOException("deadline reached");}}/*** Waits on {@code monitor} until it is notified. Throws {@link InterruptedIOException} if either* the thread is interrupted or if this timeout elapses before {@code monitor} is notified. The* caller must be synchronized on {@code monitor}.* 在{@code monitor}等待直到接收到通知* 在线程中断或在{@code monitor}通知之前超时抛出InterruptedIOException异常* 调用者必须在{@code monitor}上同步*/public final void waitUntilNotified(Object monitor) throws InterruptedIOException {try {boolean hasDeadline = hasDeadline();long timeoutNanos = timeoutNanos();if (!hasDeadline && timeoutNanos == 0L) {monitor.wait(); // There is no timeout: wait forever.return;}// Compute how long we'll wait.// 计算等待的时间long waitNanos;long start = System.nanoTime();if (hasDeadline && timeoutNanos != 0) {long deadlineNanos = deadlineNanoTime() - start;waitNanos = Math.min(timeoutNanos, deadlineNanos);} else if (hasDeadline) {waitNanos = deadlineNanoTime() - start;} else {waitNanos = timeoutNanos;}// Attempt to wait that long. This will break out early if the monitor is notified.long elapsedNanos = 0L;if (waitNanos > 0L) {long waitMillis = waitNanos / 1000000L;// 线程在监视器上等待一定的时间monitor.wait(waitMillis, (int) (waitNanos - waitMillis * 1000000L));elapsedNanos = System.nanoTime() - start;}// Throw if the timeout elapsed before the monitor was notified.// 若timeout在监视器的通知到达之前超过则抛出InterruptedIOException异常if (elapsedNanos >= waitNanos) {throw new InterruptedIOException("timeout");}} catch (InterruptedException e) {throw new InterruptedIOException("interrupted");}}
}

【3.4.2】AsyncTimeout类

// This timeout uses a background thread to take action exactly when the timeout occurs
// 此超时使用后台线程在超时发生时执行操作
public class AsyncTimeout extends Timeout {/*** Don't write more than 64 KiB of data at a time, give or take a segment. Otherwise slow* connections may suffer timeouts even when they're making (slow) progress. Without this, writing* a single 1 MiB buffer may never succeed on a sufficiently slow connection.* 一次写的数据不要超过64kb* 否则慢连接可能会出现超时,即使正在进行(缓慢)处理* 如果没有这一点则在一个足够慢的连接上写一个1MB缓冲区可能永远不会成功*/private static final int TIMEOUT_WRITE_SIZE = 64 * 1024;/** Duration for the watchdog thread to be idle before it shuts itself down. */// 看门狗线程在关闭自身之前处于空闲状态的持续时间private static final long IDLE_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(60);private static final long IDLE_TIMEOUT_NANOS = TimeUnit.MILLISECONDS.toNanos(IDLE_TIMEOUT_MILLIS);/*** The watchdog thread processes a linked list of pending timeouts, sorted in the order to be* triggered. This class synchronizes on AsyncTimeout.class. This lock guards the queue.* 看门狗线程处理挂起超时的单向链表,timeout项按触发的顺序排序*/static @Nullable AsyncTimeout head;/** True if this node is currently in the queue. */// 若当前节点在队列中则值为trueprivate boolean inQueue;/** The next node in the linked list. */// 指向单向链表中的下一个节点private @Nullable AsyncTimeout next;/** If scheduled, this is the time that the watchdog should time this out. */// 超时时间private long timeoutAt;// 把当前AsyncTimeout对象加入节点并加入链表public final void enter() {// 检查参数if (inQueue) throw new IllegalStateException("Unbalanced enter/exit");long timeoutNanos = timeoutNanos();boolean hasDeadline = hasDeadline();if (timeoutNanos == 0 && !hasDeadline) {// 在没有deadline和timeout的情况下直接返回return; // No timeout and no deadline? Don't bother with the queue.}inQueue = true;// 节点入队并开启watchdog线程监视timeout链表scheduleTimeout(this, timeoutNanos, hasDeadline);}private static synchronized void scheduleTimeout(AsyncTimeout node, long timeoutNanos, boolean hasDeadline) {// Start the watchdog thread and create the head node when the first timeout is scheduled.// 若timeout链表为空则新建timeout链表并开始Watchdog线程if (head == null) {head = new AsyncTimeout();new Watchdog().start();}long now = System.nanoTime();// 确定当前节点的timeout时间点if (timeoutNanos != 0 && hasDeadline) {// Compute the earliest event; either timeout or deadline. Because nanoTime can wrap around,// Math.min() is undefined for absolute values, but meaningful for relative ones.node.timeoutAt = now + Math.min(timeoutNanos, node.deadlineNanoTime() - now);} else if (timeoutNanos != 0) {node.timeoutAt = now + timeoutNanos;} else if (hasDeadline) {node.timeoutAt = node.deadlineNanoTime();} else {throw new AssertionError();}// Insert the node in sorted order.// 计算到达timeout时间点剩余的时间long remainingNanos = node.remainingNanos(now);// 遍历timeout链表按照到达timeout的时间点的剩余时间排序for (AsyncTimeout prev = head; true; prev = prev.next) {if (prev.next == null || remainingNanos < prev.next.remainingNanos(now)) {node.next = prev.next;prev.next = node;if (prev == head) {// 插入节点之后唤醒watchdog线程AsyncTimeout.class.notify(); // Wake up the watchdog when inserting at the front.}break;}}}/** Returns true if the timeout occurred. */// 从链表中移除节点,若timeout发生则返回truepublic final boolean exit() {// 节点没有加入timeout链表则直接返回if (!inQueue) return false;inQueue = false;return cancelScheduledTimeout(this);}/** Returns true if the timeout occurred. */// 从链表中移除节点private static synchronized boolean cancelScheduledTimeout(AsyncTimeout node) {// Remove the node from the linked list.// 从链表中移除指定的节点for (AsyncTimeout prev = head; prev != null; prev = prev.next) {if (prev.next == node) {prev.next = node.next;node.next = null;return false;}}// The node wasn't found in the linked list: it must have timed out!// 若节点不在链表中则表明发生了timeoutreturn true;}/*** 返回到达timeout时间点剩余的时间,该值可以为负,此时timeout立即发生*/private long remainingNanos(long now) {return timeoutAt - now;}/*** 由watchdog线程在timeout发生时调用,处理timeout逻辑*/protected void timedOut() {}.../*** Throws an IOException if {@code throwOnTimeout} is {@code true} and a timeout occurred.* 如果{@code throwOnTimeout}为{@code true}并且发生超时则抛出IOException*/final void exit(boolean throwOnTimeout) throws IOException {boolean timedOut = exit();if (timedOut && throwOnTimeout) throw newTimeoutException(null);}/*** Returns either {@code cause} or an IOException that's caused by {@code cause} if a timeout occurred.* 如果发生超时,则返回{@code cause}或由{@code cause}引起的IOException*/final IOException exit(IOException cause) throws IOException {if (!exit()) return cause;return newTimeoutException(cause);}/*** Returns an {@link IOException} to represent a timeout.* 返回一个IOException异常表示timeout*/protected IOException newTimeoutException(@Nullable IOException cause) {InterruptedIOException e = new InterruptedIOException("timeout");if (cause != null) {e.initCause(cause);}return e;}// Watchdog守护线程private static final class Watchdog extends Thread {Watchdog() {super("Okio Watchdog");setDaemon(true);}public void run() {while (true) {try {AsyncTimeout timedOut;synchronized (AsyncTimeout.class) {// awaitTimeout方法// 移除并返回位于链表顶部的节点,如有必要则等待其超时// 如果在启动时列表的头部没有节点并且在等待{@code IDLE_TIMEOUT_NANOS}之后仍然没有节点则返回{@link}head}// 如果在等待时插入了新节点则返回null// 否则,将返回正在等待且已删除的节点timedOut = awaitTimeout();// Didn't find a node to interrupt. Try again.// 返回null则表明链表中插入了新节点if (timedOut == null) continue;// The queue is completely empty. Let this thread exit and let another watchdog thread// get created on the next call to scheduleTimeout().// 返回head表明链表为空则退出当前Watchdog守护线程// 另一个Watchdog守护线程会在下一次调用scheduleTimeout()创建并启动if (timedOut == head) {head = null;return;}}// Close the timed out node.// 处理发生timeout的节点的timedOut()方法以实现特定的业务逻辑timedOut.timedOut();} catch (InterruptedException ignored) {}}}}/*** Removes and returns the node at the head of the list, waiting for it to time out if necessary.* 移除并返回位于链表顶部的节点,如有必要则等待其超时* 如果在启动时列表的头部没有节点并且在等待{@code IDLE_TIMEOUT_NANOS}之后仍然没有节点则返回{@link}head}* 如果在等待时插入了新节点则返回null* 否则,将返回正在等待且已删除的节点*/static @Nullable AsyncTimeout awaitTimeout() throws InterruptedException {// Get the next eligible node.// 获取链表中的下一个节点AsyncTimeout node = head.next;// The queue is empty. Wait until either something is enqueued or the idle timeout elapses.// 若链表为空则等到新节点入队或timeout到达if (node == null) {long startNanos = System.nanoTime();AsyncTimeout.class.wait(IDLE_TIMEOUT_MILLIS);return head.next == null && (System.nanoTime() - startNanos) >= IDLE_TIMEOUT_NANOS? head  // The idle timeout elapsed.: null; // The situation has changed.}long waitNanos = node.remainingNanos(System.nanoTime());// 链表的首节点没有timeout则等待// The head of the queue hasn't timed out yet. Await that.if (waitNanos > 0) {// Waiting is made complicated by the fact that we work in nanoseconds,// but the API wants (millis, nanos) in two arguments.long waitMillis = waitNanos / 1000000L;waitNanos -= (waitMillis * 1000000L);AsyncTimeout.class.wait(waitMillis, (int) waitNanos);return null;}// The head of the queue has timed out. Remove it.// 链表的首节点到达了timeout则首节点出队head.next = node.next;node.next = null;return node;}
}

【3.4.3】具有Timeout机制的Sink与Source

public class AsyncTimeout extends Timeout {.../*** Returns a new sink that delegates to {@code sink}, using this to implement timeouts. This works* best if {@link #timedOut} is overridden to interrupt {@code sink}'s current operation.* 返回具有超时功能的Sink实例*/public final Sink sink(final Sink sink) {return new Sink() {@Override public void write(Buffer source, long byteCount) throws IOException {checkOffsetAndCount(source.size, 0, byteCount);while (byteCount > 0L) {// Count how many bytes to write. This loop guarantees we split on a segment boundary.long toWrite = 0L;for (Segment s = source.head; toWrite < TIMEOUT_WRITE_SIZE; s = s.next) {int segmentSize = s.limit - s.pos;toWrite += segmentSize;if (toWrite >= byteCount) {toWrite = byteCount;break;}}// Emit one write. Only this section is subject to the timeout.boolean throwOnTimeout = false;enter();try {sink.write(source, toWrite);byteCount -= toWrite;throwOnTimeout = true;} catch (IOException e) {throw exit(e);} finally {exit(throwOnTimeout);}}}@Override public void flush() throws IOException {boolean throwOnTimeout = false;enter();try {sink.flush();throwOnTimeout = true;} catch (IOException e) {throw exit(e);} finally {exit(throwOnTimeout);}}@Override public void close() throws IOException {boolean throwOnTimeout = false;enter();try {sink.close();throwOnTimeout = true;} catch (IOException e) {throw exit(e);} finally {exit(throwOnTimeout);}}@Override public Timeout timeout() {return AsyncTimeout.this;}@Override public String toString() {return "AsyncTimeout.sink(" + sink + ")";}};}/*** Returns a new source that delegates to {@code source}, using this to implement timeouts. This* works best if {@link #timedOut} is overridden to interrupt {@code sink}'s current operation.* 返回具有超时功能的Source实例*/public final Source source(final Source source) {return new Source() {@Override public long read(Buffer sink, long byteCount) throws IOException {boolean throwOnTimeout = false;enter();try {long result = source.read(sink, byteCount);throwOnTimeout = true;return result;} catch (IOException e) {throw exit(e);} finally {exit(throwOnTimeout);}}@Override public void close() throws IOException {boolean throwOnTimeout = false;try {source.close();throwOnTimeout = true;} catch (IOException e) {throw exit(e);} finally {exit(throwOnTimeout);}}@Override public Timeout timeout() {return AsyncTimeout.this;}@Override public String toString() {return "AsyncTimeout.source(" + source + ")";}};}...}

【3.5】生产者与消费者模型(Pipe)

public final class Pipe {// Pipe的最大容量final long maxBufferSize;// Buffer实例用于转发write/read方法final Buffer buffer = new Buffer();// 标记Sink是否关闭boolean sinkClosed;// 标记Source是否关闭boolean sourceClosed;// PipeSink实例,PipeSink为内部类private final Sink sink = new PipeSink();// PipeSource实例,PipeSource为内部类private final Source source = new PipeSource();// Pipe类的构造方法public Pipe(long maxBufferSize) {if (maxBufferSize < 1L) {throw new IllegalArgumentException("maxBufferSize < 1: " + maxBufferSize);}this.maxBufferSize = maxBufferSize;}// 返回PipeSource实例public Source source() {return source;}// 返回PipeSink实例public Sink sink() {return sink;}// PipeSink类实现了Sink接口并覆写了其中的方法final class PipeSink implements Sink {// 内部包含了Timeout实例final Timeout timeout = new Timeout();@Override public void write(Buffer source, long byteCount) throws IOException {synchronized (buffer) {if (sinkClosed) throw new IllegalStateException("closed");while (byteCount > 0) {if (sourceClosed) throw new IOException("source is closed");long bufferSpaceAvailable = maxBufferSize - buffer.size();if (bufferSpaceAvailable == 0) {// buffer中没有剩余空间等待消费者消费timeout.waitUntilNotified(buffer); // Wait until the source drains the buffer.continue;}long bytesToWrite = Math.min(bufferSpaceAvailable, byteCount);// 使用Buffer实例写入数据buffer.write(source, bytesToWrite);byteCount -= bytesToWrite;// 通知source有新的数据了buffer.notifyAll(); // Notify the source that it can resume reading.}}}@Override public void flush() throws IOException {synchronized (buffer) {if (sinkClosed) throw new IllegalStateException("closed");if (sourceClosed && buffer.size() > 0) throw new IOException("source is closed");}}@Override public void close() throws IOException {synchronized (buffer) {if (sinkClosed) return;if (sourceClosed && buffer.size() > 0) throw new IOException("source is closed");sinkClosed = true;// 通知source,此时没有新加入的数据了buffer.notifyAll(); // Notify the source that no more bytes are coming.}}@Override public Timeout timeout() {return timeout;}}// PipeSource类实现了Source接口并覆写了其中的方法final class PipeSource implements Source {final Timeout timeout = new Timeout();@Override public long read(Buffer sink, long byteCount) throws IOException {synchronized (buffer) {if (sourceClosed) throw new IllegalStateException("closed");while (buffer.size() == 0) {if (sinkClosed) return -1L;// Pipe中没有数据则等待生产者写入timeout.waitUntilNotified(buffer); // Wait until the sink fills the buffer.}long result = buffer.read(sink, byteCount);// 通知sink可以写入数据buffer.notifyAll(); // Notify the sink that it can resume writing.return result;}}@Override public void close() throws IOException {synchronized (buffer) {sourceClosed = true;// 通知sink,此时不再读取数据buffer.notifyAll(); // Notify the sink that no more bytes are desired.}}@Override public Timeout timeout() {return timeout;}}
}

参考致谢
本博客为博主的学习实践总结,并参考了众多博主的博文,在此表示感谢,博主若有不足之处,请批评指正。

【1】深入理解okio的优化思想

【2】Android学习笔记——Okio

【3】Okio精简高效的IO库

【4】Okio源码分析

【5】拆轮子系列:拆 Okio

【6】Okio 1.9简单入门

【安卓开发系列 -- APP 开源框架】IO 框架 OKIO相关推荐

  1. 【安卓开发系列 -- APP】APP 开发基础技术整理

    [安卓开发系列 -- APP]APP 开发基础技术整理 [1]Android Studio APP 项目目录布局  [2]活动的生命周期 活动的状态 : 1. 运行状态,一个活动位于返回栈栈顶时,活动 ...

  2. 【安卓开发系列 -- APP 】APP 性能优化 -- 崩溃分析

    [安卓开发系列 -- APP ]APP 性能优化 -- 崩溃分析 [1]Native Crash 分析示例 [1.1]Linux 编译 breadpad 下载 breadpad 源码 git clon ...

  3. ABP开发框架前后端开发系列---(9)ABP框架的权限控制管理

    在前面两篇随笔<ABP开发框架前后端开发系列---(7)系统审计日志和登录日志的管理>和<ABP开发框架前后端开发系列---(8)ABP框架之Winform界面的开发过程>开始 ...

  4. 利用PHP语言开发手机app后台服务器的框架是什么?或者说开发流程是怎么样的?

    最近正在做一个手机APP的服务端API开发,虽然是基于Ruby on Rails的,做的也不太专业,不过大致相通,希望能够给你一些启发. 首先,如果是比较简单的手机APP,例如新闻客户端这样的 不会涉 ...

  5. 【安卓开发系列 -- 系统开发】搭建云手机容器环境 (基于 openvmi)

    [安卓开发系列 -- 系统开发]搭建云手机容器环境 (基于 openvmi) [1]编译安装 openvmi [1.1]安装相关依赖 apt install -y build-essential cm ...

  6. 【安卓开发系列 -- 开发环境】Unbuntu 下 Android 持续集成打包环境搭建 -- Jenkins 构建工具安装(gradle + git + android 工具)

    [安卓开发系列 -- 开发环境]Unbuntu 下 Android 持续集成打包环境搭建 -- Jenkins 构建工具安装(gradle + git + android 工具) [1]Unbuntu ...

  7. 20多个可以提高你安卓开发技能的开源app

    编辑推荐:稀土掘金,这是一个针对技术开发者的一个应用,你可以在掘金上获取最新最优质的技术干货,不仅仅是Android知识.前端.后端以至于产品和设计都有涉猎,想成为全栈工程师的朋友不要错过! 英文:2 ...

  8. ABP开发框架前后端开发系列---(8)ABP框架之Winform界面的开发过程

    在前面随笔介绍的<ABP开发框架前后端开发系列---(7)系统审计日志和登录日志的管理>里面,介绍了如何改进和完善审计日志和登录日志的应用服务端和Winform客户端,由于篇幅限制,没有进 ...

  9. 安卓开发(APP)之智能家电案例教程

    智能家电工具介绍与客户端开发: 此文章教程内容: 用安卓手机的APP客户端控制智能家电实现的小案例 本文章所需工具在文章末尾,请君自取,所用工具Android studio. 家居模拟器程序介绍 本程 ...

最新文章

  1. python目标检测与识别_Python 使用Opencv实现目标检测与识别的示例代码
  2. 企业级自动化运维方案设计及Saltstack、Ansible等5种工具比较分析--云平台技术栈08...
  3. 源恩教育计算机,源恩计算机二级
  4. 进程、线程、端口、服务间关系
  5. python3 测试函数的一个例子
  6. 封装JDBC事务操作,执行存储过程测试
  7. linux 命令博客,Linux命令(一)
  8. linux环境变量重复设置,请叫下环境变量重复设置的问题
  9. 烂泥:团购网站的购买流程
  10. 父亲去年喂猪挣了21万
  11. 6.16 实现音乐的背景播放功能 [原创iOS开发-Xcode教程]
  12. android投影到创维电视,创维电视怎么投屏?图文讲解安卓和苹果手机投屏到创维电视方法...
  13. (2022牛客多校五)H-Cutting Papers(签到)
  14. 鸿蒙 华为watch gt3手表hello world
  15. 微信小程序开发之——数据存储Storage
  16. cpu和gpu已过时,npu和apu的时代开始
  17. 中职计算机专业可以考什么大学,中职生可以考哪些大学?
  18. Android系统启动流程 -- bootloader
  19. IPFS存储一致性难题?IPFS-Cluster帮你解决
  20. iphone导出视频 无法连接到设备_手机资讯:怎么将iPhone微信小视频存储到本地

热门文章

  1. 【企业】如实利用 250 定律,服务好客户
  2. C++习题二:职工管理系统:
  3. 今天有个做测试的朋友跳槽涨薪20k,我惊呆了
  4. linux下各种文件大小限制
  5. JS判断字符串长度的几种方法(区分中文和英文)
  6. 【Kettle从零开始】第二弹之Kettle文件夹与界面介绍
  7. 东华大学计算机学院推免生面试难吗,东华大学接收推免生复试与录取办法
  8. 苏州工业园区2023年度重点产业紧缺人才需求目录
  9. Ant Design Pro V5精讲(实践篇一):自定义登录界面、主界面
  10. watchOS 4 教程(1):开始