学习基于OkHttp的网络框架(一)Okio详解
继承的缺点
如果要给一个类扩展功能应该怎么做?
继承与组合:
复用代码是进行程序设计的一个重要原因,组合和继承被委以重任,其中继承更是面向对象的基石之一,但相比于组合,继承其实有诸多缺点。组合只要持有另一个类的对象,就可以使用它暴露的所有功能,同时也隐藏了具体的实现(黑盒复用
);组合之间的关系是动态的,在运行才确定;组合有助于保持每个类被封装,并被集中在单个任务上(单一原则
)。而然,类继承允许我们根据自己的实现来覆盖重写父类的实现细节,父类的实现对于子类是可见的(白盒复用
);继承是在编译时刻静态定义的,即是静态复用,在编译后子类已经确定了;继承中父类定义了子类的部分实现,而子类中又会重写这些实现,修改父类的实现,这是一种破坏了父类的封装性的表现。总之组合相比继承更具灵活性。即便如此,我们有不得不使用继承的理由:
向上转型,复用接口
如果用继承来扩展功能会遇到上面所说的诸多问题,对父类的方法做了修改的话,则子类的方法必须做出相应的修改。所以说子类与父类是一种高耦合,并且因为子类是静态的,当扩展的功能是多种情况的组合的话,你必须枚举出所有的情况为它们定义子类。比如咖啡店里有四种咖啡:
现在还可以给咖啡添加额外的四种调料
Milk,Chocolate,Icecream,Whip如果为每一种咖啡和调料的组合编写子类将有64种情况,显然这种类型体系臃肿是无法接受的!
那么有什么方法可以即保留向上转型的继承结构,又避免继承带来的问题呢 ?
装饰者优化继承结构
Decorator Pattern
ConcreteComponent
和Component
是原有的继承结构,相比于直接在ConcreteComponent
上开刀来扩展功能,我们重新定义了一个Decorator
类,Decorator
用组合的方式持有一个Component
对象,同时继承Component
这样就实现了保留向上转型的继承结构的同时,拥有组合的优点:
- 通过动态的方式来扩展一个对象的功能
- 通过装饰类的排列组合,可以创造恒多不同行为的组合
- 装饰类
Decorator
和构建类ConcreteComponent
可以独立变化
OKio原理分析
好了,终于进入正题了。和Java的io流相同,Okio的整体设计也是装饰者模式,一层层的拼接流(Stream)正是在使用使用装饰者在装饰的过程。
- Okio封装了java.io,java.nio的功能使用起来更方便
- Okio优化了缓存,使io操作更高效
Source和Sink流程
Source
和Sink
类似于InputStream
和OutputStream
,是io操作的顶级接口类,Source
和Sink
中只定义了三个方法:
public interface Source extends Closeable {
/**
* 定义基础的read操作,该方法将字节写入Buffer
*/
long read(Buffer sink, long byteCount) throws IOException;
/** Returns the timeout for this source. */
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;
}
Sink
的结构是相同的,就不废话了。那么Source和Sink的具体实现在哪里呢?Okio
类提供了静态的方法生产Sink和Source,这个方法也比较简单,将InputStream
中的数据写入到Buffer
的Segment
中,Buffer
和Segment
是Okio对io流操作进行优化的关键类,后面在详细讨论,先把读写操作的流程走完。
private static Source source(final InputStream in, final Timeout timeout) {
//.....
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);
int maxToCopy = (int) Math.min(byteCount, Segment.SIZE - tail.limit);
//写入Segment
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;
}
}
//.....
}
不同的读取操作定义在BufferedSource中,它同样也是个接口:
BufferedSourceBufferedSource
的具体实现是RealBufferedSource,可以看到RealBufferedSource
其实是个装饰类,内部管理Source
对象来扩展Source
的功能,同时拥有Source
读取数据时用到的Buffer
对象。
final class RealBufferedSource implements BufferedSource {
public final Buffer buffer = new Buffer();
public final Source source;
boolean closed;
//....
@Override public long read(Buffer sink, long byteCount) throws IOException {
if (sink == null) throw new IllegalArgumentException("sink == null");
if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount);
if (closed) throw new IllegalStateException("closed");
//先将数据读到buffer中
if (buffer.size == 0) {
//source是被装饰的对象
long read = source.read(buffer, Segment.SIZE);
if (read == -1) return -1;
}
long toRead = Math.min(byteCount, buffer.size);
return buffer.read(sink, toRead);
}
//...
}
小结一下:
Source
对象每次read,Sink
对象每次write都需要一个Buffer
对象,Buffer
管理者循环双向链表Segment,每次读写数据都先保存在segment
中进行缓冲,BufferedSource
和BufferedSink
进行读写操作时都是间接调用Buffer
对Segment
的操作来完成的,整个过程层层嵌套还是有点绕的。
InputStream--Source--BufferedSource--Buffer--segment--Buffer--Sink--BufferedSink--OutputStream
为什么Okio更高效
在buffer
的注释中说明了Okio的高效性:
- 采用了segment的机制进行内存共享和复用,避免了copy数组;
- 根据需要动态分配内存大小;
- 避免了数组创建时的zero-fill,同时降低GC的频率。
Segment和SegmentPool:
Segment是一个循环双向列表,内部维护者固定长度的byte[]数组:
static final int SIZE = 8192;
/** Segments 用分享的方式避免复制数组 */
static final int SHARE_MINIMUM = 1024;
final byte[] data;
/** data[]中第一个可读的位置*/
int pos;
/** data[]中第一个可写的位置 */
int limit;
/**与其它Segment共享 */
boolean shared;
boolean owner;
Segment next;
Segment prev;
/**
* 将当前segment从链表中移除
*/
public Segment pop() {
//....
}
/**
* 将一个segment插入到当前segment后
*/
public Segment push(Segment segment) {
//....
}
SegmentPool是一个Segment池,由一个单向链表构成。该池负责Segment的回收和闲置Segment的管理,也就是说Buffer使用的Segment是从Segment单向链表中取出的,这样有效的避免了GC频率。
/** 总容量 */
static final long MAX_SIZE = 64 * 1024; // 64 KiB.
/**用Segment实现的单向链表,next是表头*/
static Segment next;
/** Total bytes in this pool. */
static long byteCount;
//回收闲置的segment,插在链表头部
static void recycle(Segment segment) {
if (segment.next != null || segment.prev != null) throw new IllegalArgumentException();
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;
}
}
//从链表头部取出一个
static Segment take() {
synchronized (SegmentPool.class) {
if (next != null) {
Segment result = next;
next = result.next;
result.next = null;
byteCount -= Segment.SIZE;
return result;
}
}
return new Segment(); // Pool is empty. Don't zero-fill while holding a lock.
}
Segment中还有两个特殊的方法split()
和compact()
,split()
根据当前的Segment产生一个新的Segment,新的Segment与原来的Segment共用同一个data[]数组,但是改变了读写的标记位pos
和limit
,从原来的
[pos..limit]拆分为[pos..pos+byteCount]和[pos+byteCount..limit],从而避免了复制数组带来的性能消耗。前一个和自身的数据量都不足一半时,compact()
会对segement进行压缩,把自身的数据写入到前一Segment中,然后将自身进行回收,使Segment的利用更高效!
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 = new Segment(this);
} else {
prefix = SegmentPool.take();
System.arraycopy(data, pos, prefix.data, 0, byteCount);
}
prefix.limit = prefix.pos + byteCount;
pos += byteCount;
prev.push(prefix);
return prefix;
}
Okio实战
Okio封装了io操作底层操纵字节的细节,使用起来更简单了。但一般来说高度的封装意味着无法定制,比如说在网络应用中经常要监听文件的上传下载进度,显然Okio默认是没有这个功能的,应该怎么扩展呢?别忘了,装饰者模式。实际上Okio已经提供了Decorator
类:ForwardingSink
,ForwardingSource
,只要继承这两个类就可以自己定制功能了。
class CountingSink extends ForwardingSink{
private long bytesWritten = 0;
private Listen listen;
private File file;
private long totalLength;
public CountingSink(Sink delegate,File file,Listen listen) {
super(delegate);
this.listen = listen;
this.file = file;
totalLength = contentLength();
}
public long contentLength(){
if (file != null) {
long length = file.length();
Log.d("abc : length :", length + "");
return length;
}else {
return 0;
}
}
@Override
public void write(Buffer source, long byteCount) throws IOException {
super.write(source, byteCount);
bytesWritten += byteCount;
listen.onProgress(bytesWritten, totalLength);
}
interface Listen{
void onProgress(long bytesWritten, long contentLength);
}
}
我们复制一首歌做测试:
File fileSrc = new File(Environment.getExternalStorageDirectory() + "/000szh", "pain.mp3");
File fileCopy = new File(Environment.getExternalStorageDirectory() + "/000szh","pain3.mp3");
CountingSink.Listen listen = new CountingSink.Listen() {
@Override
public void onProgress(long bytesWritten, long contentLength) {
long total = contentLength;
float pos = bytesWritten *1.0f / total;
}
};
BufferedSink bufferedSink = null;
Source source = null;
try {
//包装sink
Sink sink= Okio.sink(fileCopy);
CountingSink countingSink = new CountingSink(sink, fileSrc,listen);
bufferedSink = Okio.buffer(countingSink);
source = Okio.source(fileSrc);
bufferedSink.writeAll(source);
bufferedSink.flush();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
closeAll(bufferedSink, source);
} catch (IOException e) {
e.printStackTrace();
}
}
}