都看OkHttp的源码,Okio的源码你看过吗?

RealBufferedSource:BufferedSource实现类,读操作都是由该类来完成,可以通过Okio工厂获取
RealBufferedSink:BufferedSink实现类,写操作都是由该类来完成,可以通过Okio工厂获取
Okio:okio的工厂来用来获取buffer实现类和流的实现类
Segment:Buffer中存储数据的载体,内部通过一个8K的byte数组存储数据
SegmentPool:管理Segment创建和回收,最多存储64K的数据也就是8个Segment,当池中存在Segment的时候会复用减少对象的创建

以读写文件为例,读是先文件内容读到buffer中,然后再从buffer中获取数据,写是先写数据到buffer中,然后将将buffer中的数据写入文件,而buffe中数据是通过一个个Segment存储的,Segment则是通过SegmentPool创建和回收,每个类各司其职。

/ 读写流程源码 /

读流程

File file = new File(Environment.getExternalStorageState(), “test”);
try {
BufferedSource source = Okio.buffer(Okio.source(file));
byte[] array = new byte[1024];
source.read(array);
source.close();
} catch (Exception e) {
e.printStackTrace();
}

根据上面的例子我们分几段查看源码:

Okio.source()
Okio.buffer()
BufferedSource.read()

Okio.source()

//Okio#source
public static Source source(File file) throws FileNotFoundException {
if (file == null) throw new IllegalArgumentException(“file == null”);
return source(new FileInputStream(file));
}

可以看到由于我们传入的是file所以内部实际是通过FileInputStream读文件。

public static Source source(InputStream in) {
return source(in, new Timeout());
}
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 tail = sink.writableSegment(1);//从buffer中拿到一个可写的Segment
int maxToCopy = (int) Math.min(byteCount, Segment.SIZE - tail.limit);//byteCount是要读取的数据总量,Segment.SIZE - tail.limit是Segment可以装的数据量,取最小值
int bytesRead = in.read(tail.data, tail.limit, maxToCopy);//然后通过FileInputStream将文件数据读到segment的data中
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();
}
@Override
public Timeout timeout() {
return timeout;
}
@Override
public String toString() {
return “source(” + in + “)”;
}
};
}

上面这段就是创建了一个Source实现类,read的话则是先获取buffer中可写的segment,然后用FileInputStream将文件的数据读到segment中。对于Segment和Buffer在下面会说到

Okio.Buffer()

//Okio#buffer
public static BufferedSource buffer(Source source) {
return new RealBufferedSource(source);
}

由于我们传入的是Source所以会创建一个RealBufferedSource实例,Okio也都是通过它来做读操作,我们先来看下这个类。

final class RealBufferedSource implements BufferedSource {

public final Buffer buffer = new Buffer();//buffer对象
public final Source source;//Source实现类
RealBufferedSource(Source source) {
if (source == null) throw new NullPointerException(“source == null”);
this.source = source;
}
@Override
public int readInt() throws IOException {
require(4);//最终通过source#read()将数据读到Buffer
return buffer.readInt();//再从Buffer读取数据
}
@Override
public int read(byte[] sink, int offset, int byteCount) throws IOException {
checkOffsetAndCount(sink.length, offset, byteCount);
if (buffer.size == 0) {
long read = source.read(buffer, Segment.SIZE);//先将数据读到Buffer
if (read == -1) return -1;
}
int toRead = (int) Math.min(byteCount, buffer.size);
return buffer.read(sink, offset, toRead);//再从Buffer读取数据
}
@Override
public String readString(Charset charset) throws IOException {
if (charset == null) throw new IllegalArgumentException(“charset == null”);
buffer.writeAll(source);//最终通过source#read()将数据读到Buffer
return buffer.readString(charset);//再从Buffer读取数据
}
//后面省略了很多个read方法
}

可以看到它先将Okio.buffer(Okio.source(file))传入的Source实现类通过source成员变量存储,然后创建了一个Buffer对象用作缓冲区,而read方法都是一个流程通过source#read()方法将数据写入缓冲区,然后再从缓冲区中读取数据。接下来我们在看下缓冲区Buffer对象。

public final class Buffer implements BufferedSource, BufferedSink, Cloneable, ByteChannel {
@Nullable Segment head;//缓存数据的载体
long size;//当前缓冲区的数据量单位字节
public Buffer() {
}
//下面省略很多读写缓冲区的操作
}

Buffer是负责管理缓冲区的对象,内部则是通过一个双向环状链表Segment存储数据,然后我们再来看下Segment是如何存储数据的呢?

final class Segment {
static final int SIZE = 8192;//最大存储8K数据
final byte[] data;//存储数据数组
int pos;//当前segment数据已经读取到哪了,接下来从pos位置开始读
int limit;//当前segment数据写到哪了,接下来从limit开始写
boolean owner;//当前segment是否允许追加数据,也就是能不能多次写入
Segment next;
Segment prev;

Segment() {
this.data = new byte[SIZE];
this.owner = true;//默认可以多次写入
this.shared = false;
}

可以看到segment内部有一个8k的byte数组来存储数据,pos记录的是segment可读数据位置,(pos-1)到0是已经读过的数据,limit是segment可写数据的位置,limit到Segment.SIZE是剩余可写数据量,pos到limit是还未读取的数据。

ok,那我们回到本例中的source.read(array)。

@Override
public int read(byte[] sink) throws IOException {
return read(sink, 0, sink.length);
}

@Override
public int read(byte[] sink, int offset, int byteCount) throws IOException {
checkOffsetAndCount(sink.length, offset, byteCount);//数组相关检查
if (buffer.size == 0) {//如果缓冲区是空的
long read = source.read(buffer, Segment.SIZE);//将source中的数据读取到buffer中
if (read == -1) return -1;
}
int toRead = (int) Math.min(byteCount, buffer.size);
return buffer.read(sink, offset, toRead);//将数据写入sink也就是byte数组
}

对于source#read我们在上面已经看过就是取出Buffer中的Segment,然后将数据写入。不过细节当时我们没说现在来看下。

//source#read()
@Override
public long read(Buffer sink, long byteCount) throws IOException {
try {
Segment tail = sink.writableSegment(1);//从buffer中拿到一个可写的Segment
int maxToCopy = (int) Math.min(byteCount, Segment.SIZE - tail.limit);//byteCount是要读取的数据总量,Segment.SIZE - tail.limit是Segment可写数据量,取最小值
int bytesRead = in.read(tail.data, tail.limit, maxToCopy);//然后通过FileInputStream将文件数据读到segment的data数组中
if (bytesRead == -1) return -1;
tail.limit += bytesRead;//修改segment可写位置
sink.size += bytesRead;//修改buffer中数据总量
return bytesRead;
} catch (AssertionError e) {
if (isAndroidGetsocknameError(e)) throw new IOException(e);
throw e;
}
}

buffer#writableSegment(1)如何获取Segment的?

Segment writableSegment(int minimumCapacity) {
if (minimumCapacity < 1 || minimumCapacity > Segment.SIZE) throw new IllegalArgumentException();
if (head == null) {//如果buffer中没有Segment
head = SegmentPool.take(); //从SegmentPool中获取
return head.next = head.prev = head;//维护Buffer内部的Segment双向环状链表
}

Segment tail = head.prev;//拿到最后一个Segment
if (tail.limit + minimumCapacity > Segment.SIZE || !tail.owner) {//如果最后一个Segment可写位置limit+需要的最小容量>Segment.SIZE || 该Segment不支持追加写入
tail = tail.push(SegmentPool.take()); // 添加一个新的Segment到尾部
}
return tail;
}

如果Buffer中没有Segment则直接从SegmentPool获取,如果有则获取链表尾部也就是最新的Segment,判断数据是否存的下,存不下的话从SegmentPool获取一个新的插到链表尾部。接下来看下SegmentPool#take如何创建Segment的。

final class SegmentPool {
static final long MAX_SIZE = 64 * 1024; // 64 KiB.最大容量64K
static @Nullable Segment next;//通过一个单链表存储回收的Segment
static long byteCount;//当前SegmentPool容量
private SegmentPool() {
}
static Segment take() {
synchronized (SegmentPool.class) {
if (next != null) {//如果当前有回收的Segment
Segment result = next;
next = result.next;
result.next = null;
byteCount -= Segment.SIZE;
return result;
}
}
return new Segment(); //否则直接创建
}

static void recycle(Segment segment) {
if (segment.next != null || segment.prev != null) throw new IllegalArgumentException();
if (segment.shared) return; //当前Segment如果有共享数据不能回收(这个后面说)
synchronized (SegmentPool.class) {
if (byteCount + Segment.SIZE > MAX_SIZE) return; //当前SegmentPool满了的话则不能回收
byteCount += Segment.SIZE;//容量增加
segment.next = next;//加到链表中
segment.pos = segment.limit = 0;//可写和可读位置都清零
next = segment;
}
}

SegmentPool#take就是看当前的池子中有缓存的Segment的么,有直接使用,没有则创建一个。ok在回到最前面RealBufferedSource#read。

@Override
public int read(byte[] sink, int offset, int byteCount) throws IOException {
checkOffsetAndCount(sink.length, offset, byteCount);
if (buffer.size == 0) {
long read = source.read(buffer, Segment.SIZE);//先将数据读到Buffer
if (read == -1) return -1;
}
int toRead = (int) Math.min(byteCount, buffer.size);
return buffer.read(sink, offset, toRead);//再从Buffer读取数据
}

第一块source#read(buffer, Segment.SIZE)已经梳理了一遍就是通过FileInputStream将数据读到Buffer的Segment中,然后再来buffer#read将数据读到byte数组中。

@Override
public int read(byte[] sink, int offset, int byteCount) {
checkOffsetAndCount(sink.length, offset, byteCount);
Segment s = head;//拿到第一个Segment
if (s == null) return -1;
int toCopy = Math.min(byteCount, s.limit - s.pos);//判断要读取的数据量,取byteCount和当前segment可读的数据量s.limit - s.pos
System.arraycopy(s.data, s.pos, sink, offset, toCopy);//将数据拷贝到数组中
s.pos += toCopy;//移动Segment已经读过的数据指针pos
size -= toCopy;//当前Buffer容量减去读过数据量
if (s.pos == s.limit) {//如果当前Segment已经读完
head = s.pop();//从链表中脱离
SegmentPool.recycle(s);//SegmentPool尝试回收
}
return toCopy;
}

将Buffer中Segment数据拷贝到数组中,如果Segment数据已经读完则从Buffer链表中脱离,SegmentPool尝试回收。ok,那读流程就讲完了,我们回顾下大体流程:

BufferedSource source = Okio.buffer(Okio.source(file));
byte[] array = new byte[1024];
source.read(array);
source.close();

Okio.Source():是创建了一个Source实现类,提供read的能力,需要传入一个Buffer来获取数据,数据读取是通过FileInputStream来读取的,写入到Buffer的Segment中。
Okio.buffer():则创建了一个Buffer实现类RealBufferedSource,内部维护了一个环形双向链表Segment,是我们真正用来读取数据的对象。
BufferedSource.read():读取数据操作,流程是先将数据读到buffer中,然后再从buffer中读取数据。

写流程

写流程是读流程反过来,先将数据写到buffer,然后在从buffer写到文件中,大体跟前面差不多我们快速说一下。

byte[] array = new byte[1024];
BufferedSink sink = Okio.buffer(Okio.sink(file));
sink.write(array);
sink.close();

Okio.sink()

先看Okio#sink(file)。

public static Sink sink(File file) throws FileNotFoundException {
if (file == null) throw new IllegalArgumentException(“file == null”);
return sink(new FileOutputStream(file));//创建FileOutputStream
}

public static Sink sink(OutputStream out) {
return sink(out, new Timeout());
}

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”);
return new Sink() {
@Override
public void write(Buffer source, long byteCount) throws IOException {
checkOffsetAndCount(source.size, 0, byteCount);
while (byteCount > 0) {//遍历将Buffer中数据都写入到文件中
timeout.throwIfReached();
Segment head = source.head;//获取buffer中的segment
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;
if (head.pos == head.limit) {
source.head = head.pop();//segment脱链
SegmentPool.recycle(head);//回收
}
}
}
@Override
public void flush() throws IOException {
out.flush();
}
@Override
public void close() throws IOException {
out.close();
}
@Override
public Timeout timeout() {
return timeout;
}
@Override
public String toString() {
return “sink(” + out + “)”;
}
};

内部通过FileOutputStream进行io操作,通过一个while循环将Buffer中Segment数据统统写入到文件中。

Okio.buffer()

public static BufferedSink buffer(Sink sink) {
return new RealBufferedSink(sink);
}

创建一个RealBufferedSink。

final class RealBufferedSink implements BufferedSink {
public final Buffer buffer = new Buffer();//创建缓冲区
public final Sink sink;//真正的io操作对象
boolean closed;
RealBufferedSink(Sink sink) {
if (sink == null) throw new NullPointerException(“sink == null”);
this.sink = sink;
}
@Override
public BufferedSink write(byte[] source) throws IOException {
if (closed) throw new IllegalStateException(“closed”);
buffer.write(source);//写流程第一步是将数据写到buffer
return emitCompleteSegments();//然后将Buffer数据写到文件中
}
@Override
public BufferedSink emitCompleteSegments() throws IOException {
if (closed) throw new IllegalStateException(“closed”);
long byteCount = buffer.completeSegmentByteCount();
if (byteCount > 0) sink.write(buffer, byteCount);//然后将Buffer数据写到文件中
return this;
}
}

内部创建了一个Buffer作为缓存区,并将Sink通过成员变量存储起来,写数据则是先将数据写到buffer中,在由Buffer通过FileOutputStream写到文件中。

接下来看看 buffer#write(source)是如何将数据写入Buffer的。

@Override
public Buffer write(byte[] source) {
if (source == null) throw new IllegalArgumentException(“source == null”);
return write(source, 0, source.length);
}
@Override
public Buffer write(byte[] source, int offset, int byteCount) {
if (source == null) throw new IllegalArgumentException(“source == null”);
checkOffsetAndCount(source.length, offset, byteCount);
int limit = offset + byteCount;//byte数组需要写到的最后一位下标
while (offset < limit) {
Segment tail = writableSegment(1);//获取可写的Segment
int toCopy = Math.min(limit - offset, Segment.SIZE - tail.limit);
System.arraycopy(source, offset, tail.data, tail.limit, toCopy);//数据拷贝到Segment中
offset += toCopy;
tail.limit += toCopy;
}
size += byteCount;
return this;
}

循环的获取一个可写的Segment将数据写到当中,直到byte数组中写完。然后就是RealBufferedSink#emitCompleteSegments将Buffer数据写到文件中。

@Override
public BufferedSink emitCompleteSegments() throws IOException {
if (closed) throw new IllegalStateException(“closed”);
long byteCount = buffer.completeSegmentByteCount();//获取写完的Segment中byte数
if (byteCount > 0) sink.write(buffer, byteCount);//然后将Buffer数据写到文件中
return this;
}

buffer#completeSegmentByteCount()。

public long completeSegmentByteCount() {
long result = size;
if (result == 0) return 0;
// Omit the tail if it’s still writable.
Segment tail = head.prev;
if (tail.limit < Segment.SIZE && tail.owner) {//最后一个Segment如果没装满则暂不写入文件
result -= tail.limit - tail.pos;
}
return result;
}

获取写满的Segment字节数。

/ Buffer精华操作 /

除了前面看到的对缓冲区的优化接下来看看Okio对于输入流流到输出流的优化。

try {
BufferedSource source = Okio.buffer(Okio.source(file));
BufferedSink sink = Okio.buffer(Okio.sink(file));
sink.writeAll(source);
sink.close();
source.close();
} catch (Exception e) {
e.printStackTrace();
}

先看sink#writeAll()。

@Override
public long writeAll(Source source) throws IOException {
if (source == null) throw new IllegalArgumentException(“source == null”);
long totalBytesRead = 0;
for (long readCount; (readCount = source.read(buffer, Segment.SIZE)) != -1; ) {//for循环将数据读到输出流的buffer中
totalBytesRead += readCount;
emitCompleteSegments();//在把数据从buffer写入文件
}
return totalBytesRead;
}

调用了source.read()将数据读到BufferedSink的buffer中。

@Override
public long read(Buffer sink, long byteCount) {
if (sink == null) throw new IllegalArgumentException(“sink == null”);
if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount);
if (size == 0) return -1L;
if (byteCount > size) byteCount = size;
sink.write(this, byteCount);
return byteCount;
}

调用输出流buffer.write()将数据写入,前方高能重点来了。

@Override
public void write(Buffer source, long byteCount) {
if (source == null) throw new IllegalArgumentException(“source == null”);
if (source == this) throw new IllegalArgumentException(“source == this”);
checkOffsetAndCount(source.size, 0, byteCount);
while (byteCount > 0) {
if (byteCount < (source.head.limit - source.head.pos)) {//如果只需要当前Segment部分数据,或者说读到最后一个Segment了
Segment tail = head != null ? head.prev : null;
if (tail != null && tail.owner
&& (byteCount + tail.limit - (tail.shared ? 0 : tail.pos) <= Segment.SIZE)) {//如果要写的数据byteCount+limit+(如果当前不是共享Segment,则可以覆盖前面已读数据所以+pos,如果是共享Segment则不能覆盖已读数据所以为0)<= Segment.SIZE,证明写Buffer的Segment装得下
source.head.writeTo(tail, (int) byteCount);//把读buffer中Segment数据写到Segment中
source.size -= byteCount;
size += byteCount;
return;//return
} else {//如果装不下则分割读buffer的Segment为两个,将我们需要数据的部分放在头部
source.head = source.head.split((int) byteCount);
}
}
//下面就是把读buffer中segment从链表取出,拼接到写buffer的segment链表中
Segment segmentToMove = source.head;
long movedByteCount = segmentToMove.limit - segmentToMove.pos;
source.head = segmentToMove.pop();//从读buffer中断链
if (head == null) {
head = segmentToMove;
head.next = head.prev = head;
} else {
Segment tail = head.prev;
tail = tail.push(segmentToMove);//拼接到写buffer的链表中
tail.compact();//尝试压缩Segment数据,移除多余Segment对象
}
source.size -= movedByteCount;
size += movedByteCount;
byteCount -= movedByteCount;
}
}

接下来我们梳理下上面的流程:

先判断当前是不是读到最后一个Segment了,如果是在判断写buffer中最后一个Segment写不写的下,写的下的话就写入然后return结束了,否则把要读的那个Segment拆分成两段,将我们需要数据的部分放在头部
如果第一个判断中没return,证明有整段的Segment数据需要拷贝,为了提高效率则直接将读buffer中Segment脱链,直接接到写buffer中提高效率,然后尝试压缩Segment链表的数据,移除多余的Segment

流程说了,接下来我们看上面没分析的几个方法

source.head.writeTo(tail, (int) byteCount)
source.head.split((int) byteCount)
tail.compact()

先看source.head.writeTo(tail, (int) byteCount)。

public void writeTo(Segment sink, int byteCount) {
if (!sink.owner) throw new IllegalArgumentException();
if (sink.limit + byteCount > SIZE) {//如果limit+byteCount>Size即装不下所以需要把数据往前移覆盖已读数据
if (sink.shared) throw new IllegalArgumentException();//当前Segment是共享数组的则报错,至于shared属性在split方法的时候会说
if (sink.limit + byteCount - sink.pos > SIZE) throw new IllegalArgumentException();
System.arraycopy(sink.data, sink.pos, sink.data, 0, sink.limit - sink.pos);//将数据向前移覆盖已读数据腾出空间
sink.limit -= sink.pos;
sink.pos = 0;
}
System.arraycopy(data, pos, sink.data, sink.limit, byteCount);//数据写入
sink.limit += byteCount;
pos += byteCount;
}

可以看到Segment#writeTo()是将数据写入传入的Segment中,如果sink写不下的话则会将已读数据覆盖腾出空间在写入。

再来source.head.split((int) byteCount)。

public Segment split(int byteCount) {
if (byteCount <= 0 || byteCount > limit - pos) throw new IllegalArgumentException();
Segment prefix;
if (byteCount >= SHARE_MINIMUM) {//如果要分割的数据量大于1024即1K则共享数组而不是数据拷贝
prefix = sharedCopy();//共享数组
} else {//数据量小于1K
prefix = SegmentPool.take();//创建一个新的Segment
System.arraycopy(data, pos, prefix.data, 0, byteCount);//拷贝数据
}
prefix.limit = prefix.pos + byteCount;
pos += byteCount;
prev.push(prefix);//插入分割出的Segment
return prefix;
}
Segment sharedCopy() {
shared = true;
return new Segment(data, pos, limit, true, false);//共享同一个data数组
}

split则是先判断要分割的数据量大于1K么,如果大于则使用共享数组的方式创建Segment减少拷贝,否则创建一个新的Segment通过数组拷贝的方式将数据传入。而前面判断的shared就是这里赋值的,使用数组共享方式创建的Segment,是不能为了写入数据将数据前移覆盖已读数据腾出位置,因为持有的是数组引用会影响到别的Segment。最后tail.compact():

public void compact() {
if (prev == this) throw new IllegalStateException();
if (!prev.owner) return; // Cannot compact: prev isn’t writable.
int byteCount = limit - pos;//计算当前Segment数据量
int availableByteCount = SIZE - prev.limit + (prev.shared ? 0 : prev.pos);
if (byteCount > availableByteCount) return; //判断前一个Segment装得下么,装不下return
writeTo(prev, byteCount);//将当前Segment数据写到前一个Segment中
pop();//将当前Segment脱链
SegmentPool.recycle(this);//回收
}

判断当前Segment中数据能放到前一个Segment么,如果可以则将数据写入,移除当前Segment。这里总结下Buffer的精髓操作。

对于输入流流到输出流是将输入流Buffer的数据直接转移到输出流Buffer中,转移分为两种情况:

整段的Segment数据转移则是直接从输出Buffer中脱链然后插入输出Buffer中,直接修改指针效率非常高
非整段的Segment数据转移是判断输出Buffer最后一个Segment是否写的下,写的下的话是数组拷贝,写不下的话则将输入Segment一分为二,将要转移数据的Segment放第一个,然后按照1方式整段Segment转移到写buffer中

并且对于Segment分割的时候有做优化,当需要转移数据量小于1K的时候是通过数组拷贝的方式将数据写到新Segment中,大于1K的时候是共享同一个数组,只是修改pos和limit来控制读取区间。

/ Timeout超时处理 /

okio的超时分为两种

同步超时TimeOut
异步超时AsyncTimeout

同步超时TimeOut

同步超时比较简单前面看到过,以写为例:

public static Sink sink(OutputStream out) {
return sink(out, new Timeout());//创建同步超时对象
}
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”);
return new Sink() {
@Override
public void write(Buffer source, long byteCount) throws IOException {
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;
if (head.pos == head.limit) {

最后

其实Android开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。

当然我也为你们整理好了百度、阿里、腾讯、字节跳动等等互联网超级大厂的历年面试真题集锦。这也是我这些年来养成的习惯,一定要学会把好的东西,归纳整理,然后系统的消化吸收,这样才能极大的提高学习效率和成长进阶。碎片、零散化的东西,我觉得最没有价值的。就好比你给我一张扑克牌,我只会觉得它是一张废纸,但如果你给我一副扑克牌,它便有了它的价值。这和我们收集资料就要收集那些系统化的,是一个道理。

网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!
y;
byteCount -= toCopy;
source.size -= toCopy;
if (head.pos == head.limit) {

最后

其实Android开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。

当然我也为你们整理好了百度、阿里、腾讯、字节跳动等等互联网超级大厂的历年面试真题集锦。这也是我这些年来养成的习惯,一定要学会把好的东西,归纳整理,然后系统的消化吸收,这样才能极大的提高学习效率和成长进阶。碎片、零散化的东西,我觉得最没有价值的。就好比你给我一张扑克牌,我只会觉得它是一张废纸,但如果你给我一副扑克牌,它便有了它的价值。这和我们收集资料就要收集那些系统化的,是一个道理。

[外链图片转存中…(img-9GnmLVxS-1715419668158)]

网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!

  • 5
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
OkHttp是一个开源的HTTP客户端库,由Square公司开发,主要用于发送网络请求和处理HTTP响应。它可以用于Android和Java平台,并提供了简单易用的API,使得网络请求变得简单和高效。 OkHttp的版本可以分为两个方面:主要版本和支持库版本。 主要版本:OkHttp的主要版本指的是核心库的版本号,它包含了OkHttp的基本功能和特性。目前最新的主要版本是OkHttp3,它引入了许多新特性和优化,如支持HTTP/2协议、连接池管理、自动重定向和Gzip压缩等。 支持库版本:OkHttp的支持库版本是指与其他库或框架的集成版本号,它可以与其他库一起使用,以提供更高级的功能和扩展。支持库版本通常以主要版本号和次要版本号来表示。例如,OkHttp3.12.0是以主要版本号3和次要版本号12表示的支持库版本。支持库版本与主要版本保持兼容,因此可以使用OkHttp的较新版本与较旧版本进行集成。 Okio是一个用于处理I/O操作的库,也由Square公司开发。它提供了高效、灵活和易于使用的API,用于处理流和字节。Okio的版本同样分为主要版本和支持库版本。 主要版本:Okio的主要版本指的是核心库的版本号,它包含了Okio的基本功能和特性。当前的主要版本是Okio2,它在Okio的基础上做了一些改进和优化,提供了更好的性能和可靠性。 支持库版本:Okio的支持库版本是指与其他库或框架的集成版本号,用于提供更高级的功能和扩展。支持库版本通常以主要版本号和次要版本号来表示。例如,Okio2.9.0是以主要版本号2和次要版本号9表示的支持库版本。支持库版本与主要版本保持兼容,因此可以使用Okio的较新版本与较旧版本进行集成。 总结来说,OkHttpOkio都是Square公司开发的优秀库,用于简化网络请求和I/O操作。它们的版本分为主要版本和支持库版本,可以根据需求选择合适的版本来使用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值