繼承的缺點
如果要給一個類擴展功能應該怎么做?
繼承與組合:
復用代碼是進行程序設計的一個重要原因,組合和繼承被委以重任,其中繼承更是面向對象的基石之一,但相比于組合,繼承其實有諸多缺點。組合只要持有另一個類的對象,就可以使用它暴露的所有功能,同時也隱藏了具體的實現(黑盒復用
);組合之間的關系是動態的,在運行才確定;組合有助于保持每個類被封裝,并被集中在單個任務上(單一原則
)。而然,類繼承允許我們根據自己的實現來覆蓋重寫父類的實現細節,父類的實現對于子類是可見的(白盒復用
);繼承是在編譯時刻靜態定義的,即是靜態復用,在編譯后子類已經確定了;繼承中父類定義了子類的部分實現,而子類中又會重寫這些實現,修改父類的實現,這是一種破壞了父類的封裝性的表現。總之組合相比繼承更具靈活性。即便如此,我們有不得不使用繼承的理由:
向上轉型,復用接口
如果用繼承來擴展功能會遇到上面所說的諸多問題,對父類的方法做了修改的話,則子類的方法必須做出相應的修改。所以說子類與父類是一種高耦合,并且因為子類是靜態的,當擴展的功能是多種情況的組合的話,你必須枚舉出所有的情況為它們定義子類。比如咖啡店里有四種咖啡:
現在還可以給咖啡添加額外的四種調料
Milk,Chocolate,Icecream,Whip如果為每一種咖啡和調料的組合編寫子類將有64種情況,顯然這種類型體系臃腫是無法接受的!
那么有什么方法可以即保留向上轉型的繼承結構,又避免繼承帶來的問題呢 ?
裝飾者優化繼承結構
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中,它同樣也是個接口:
BufferedSource
的具體實現是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();
}
}
}