說在前面
? ? ? ?要實(shí)現(xiàn)進(jìn)度的監(jiān)聽譬巫,需要使用到OkHttp的依賴包Okio里的兩個(gè)類捣鲸,一個(gè)是Source,一個(gè)是Sink甸箱,至于Okio的東西育叁,這里也不多說,其實(shí)OKHttp底層的實(shí)現(xiàn)就是基于Socket芍殖,大家都知道Socket網(wǎng)絡(luò)編程豪嗽,差不多就是操作Stream,所以O(shè)KHttp所有的操作都是依賴OKio這個(gè)IO操作庫來做的豌骏,那么我們要實(shí)現(xiàn)一個(gè)下載監(jiān)聽龟梦,就應(yīng)該對(duì)連接中的Stream監(jiān)聽,在OKHttp中網(wǎng)絡(luò)請(qǐng)求基本都是RequestBody和ResponseBody這兩個(gè)類封裝了Stream的操作窃躲,所以下文就基于這兩個(gè)類來介紹了变秦。
? ? ? ?首先我們實(shí)現(xiàn)文件下載的進(jìn)度監(jiān)聽。OkHttp給我們的只是一個(gè)回調(diào)框舔,里面有Response返回結(jié)果蹦玫,我們需要繼承一個(gè)類,對(duì)結(jié)果進(jìn)行監(jiān)聽刘绣,這個(gè)類就是ResponseBody樱溉,但是如何將它設(shè)置到OkHttp中去呢,答案是攔截器纬凤。攔截器的部分后面再敘述福贞,這里先實(shí)現(xiàn)ResponseBody的子類ProgressResponseBody。
要監(jiān)聽進(jìn)度停士,我們必然需要一個(gè)監(jiān)聽器挖帘,也就是一個(gè)接口,在其實(shí)現(xiàn)類中完成回調(diào)內(nèi)容的處理恋技,該接口聲明如下:
/**
* 包裝的響體拇舀,處理進(jìn)度
*/
public interface ProgressResponseListener {
void onResponseProgress(long bytesRead, long contentLength,boolean done);
}
然后會(huì)使用到該接口:
/**
* 包裝的響體,處理進(jìn)度
*/
public class ProgressResponseBody extends ResponseBody {
//實(shí)際的待包裝響應(yīng)體
private final ResponseBody responseBody;
//進(jìn)度回調(diào)接口
private final ProgressResponseListener progressListener;
//包裝完成的BufferedSource
private BufferedSource bufferedSource;
/**
* 構(gòu)造函數(shù)蜻底,賦值
* @param responseBody 待包裝的響應(yīng)體
* @param progressListener 回調(diào)接口
*/
public ProgressResponseBody(ResponseBody responseBody, ProgressResponseListener progressListener) {
this.responseBody = responseBody;
this.progressListener = progressListener;
}
/**
* 重寫調(diào)用實(shí)際的響應(yīng)體的contentType
* @return MediaType
*/
@Override public MediaType contentType() {
return responseBody.contentType();
}
/**
* 重寫調(diào)用實(shí)際的響應(yīng)體的contentLength
* @return contentLength
* @throws IOException 異常
*/
@Override public long contentLength() throws IOException {
return responseBody.contentLength();
}
/**
* 重寫進(jìn)行包裝source
* @return BufferedSource
* @throws IOException 異常
*/
@Override public BufferedSource source() throws IOException {
if (bufferedSource == null) {
//包裝
Source source = source(responseBody.source())骄崩;
bufferedSource = Okio.buffer();
}
return bufferedSource;
}
/**
* 讀取,回調(diào)進(jìn)度接口
* @param source Source
* @return Source
*/
private Source source(Source source) {
return new ForwardingSource(source) {
//當(dāng)前讀取字節(jié)數(shù)
long totalBytesRead = 0L;
@Override public long read(Buffer sink, long byteCount) throws IOException {
long bytesRead = super.read(sink, byteCount);
//增加當(dāng)前讀取的字節(jié)數(shù)薄辅,如果讀取完成了bytesRead會(huì)返回-1
totalBytesRead += bytesRead != -1 ? bytesRead : 0;
//回調(diào)要拂,如果contentLength()不知道長度,會(huì)返回-1
progressListener.onResponseProgress(totalBytesRead, responseBody.contentLength(), bytesRead == -1);
return bytesRead;
}
};
}
}
? ? ? ?類似裝飾器站楚,我們對(duì)原始的ResponseBody 進(jìn)行了一層包裝脱惰。并在其讀取數(shù)據(jù)的時(shí)候設(shè)置了回調(diào),回調(diào)的接口由構(gòu)造函數(shù)傳入窿春,此外構(gòu)造函數(shù)還傳入了原始的ResponseBody拉一,當(dāng)系統(tǒng)內(nèi)部調(diào)用了ResponseBody 的source方法的時(shí)候采盒,返回的便是我們包裝后的Source。然后我們還重寫了幾個(gè)方法調(diào)用原始的ResponseBody對(duì)應(yīng)的函數(shù)返回結(jié)果舅踪。
? ? ? ?同理既然下載是這樣纽甘,那么上傳也應(yīng)該是這樣良蛮,我們乘熱打鐵完成上傳的部分抽碌,下載是繼承ResponseBody ,上傳就是繼承RequestBody决瞳,同時(shí)也應(yīng)該還有一個(gè)監(jiān)聽器货徙。
/**
* 請(qǐng)求體進(jìn)度回調(diào)接口,比如用于文件上傳中
*/
public interface ProgressRequestListener {
void onRequestProgress(long bytesWritten, long contentLength, boolean done);
}
RequestBody的子類實(shí)現(xiàn)類比ResponseBody 皮胡,基本上復(fù)制一下稍加修改即可使用:
/**
* 包裝的請(qǐng)求體痴颊,處理進(jìn)度
*/
public class ProgressRequestBody extends RequestBody {
//實(shí)際的待包裝請(qǐng)求體
private final RequestBody requestBody;
//進(jìn)度回調(diào)接口
private final ProgressRequestListener progressListener;
//包裝完成的BufferedSink
private BufferedSink bufferedSink;
/**
* 構(gòu)造函數(shù),賦值
* @param requestBody 待包裝的請(qǐng)求體
* @param progressListener 回調(diào)接口
*/
public ProgressRequestBody(RequestBody requestBody, ProgressRequestListener progressListener) {
this.requestBody = requestBody;
this.progressListener = progressListener;
}
/**
* 重寫調(diào)用實(shí)際的響應(yīng)體的contentType
* @return MediaType
*/
@Override
public MediaType contentType() {
return requestBody.contentType();
}
/**
* 重寫調(diào)用實(shí)際的響應(yīng)體的contentLength
* @return contentLength
* @throws IOException 異常
*/
@Override
public long contentLength() throws IOException {
return requestBody.contentLength();
}
/**
* 重寫進(jìn)行寫入
* @param sink BufferedSink
* @throws IOException 異常
*/
@Override
public void writeTo(BufferedSink sink) throws IOException {
if (bufferedSink == null) {
//包裝
Sink sk = sink(sink)屡贺;
bufferedSink = Okio.buffer(sk );
}
//寫入
requestBody.writeTo(bufferedSink);
//必須調(diào)用flush蠢棱,否則最后一部分?jǐn)?shù)據(jù)可能不會(huì)被寫入
bufferedSink.flush();
}
/**
* 寫入,回調(diào)進(jìn)度接口
* @param sink Sink
* @return Sink
*/
private Sink sink(Sink sink) {
return new ForwardingSink(sink) {
//當(dāng)前寫入字節(jié)數(shù)
long bytesWritten = 0L;
//總字節(jié)長度甩栈,避免多次調(diào)用contentLength()方法
long contentLength = 0L;
@Override
public void write(Buffer source, long byteCount) throws IOException {
super.write(source, byteCount);
if (contentLength == 0) {
//獲得contentLength的值泻仙,后續(xù)不再調(diào)用
contentLength = contentLength();
}
//增加當(dāng)前寫入的字節(jié)數(shù)
bytesWritten += byteCount;
//回調(diào)
progressListener.onRequestProgress(bytesWritten, contentLength, bytesWritten == contentLength);
}
};
}}
? ? ? ?內(nèi)部維護(hù)了一個(gè)原始的RequestBody 以及一個(gè)監(jiān)聽器,同樣的也是由構(gòu)造函數(shù)傳入量没。當(dāng)然也是要重寫幾個(gè)函數(shù)調(diào)用原始的RequestBody 對(duì)應(yīng)的函數(shù)玉转,文件的下載是read函數(shù)中進(jìn)行監(jiān)聽的設(shè)置,毫無疑問文件的上傳就是write函數(shù)了殴蹄,我們?cè)趙rite函數(shù)中進(jìn)行了類似的操作究抓,并回調(diào)了接口中的函數(shù)。當(dāng)系統(tǒng)內(nèi)部調(diào)用了RequestBody 的writeTo函數(shù)時(shí)袭灯,我們對(duì)BufferedSink 進(jìn)行了一層包裝刺下,即設(shè)置了進(jìn)度監(jiān)聽,并返回了我們包裝的BufferedSink 稽荧。于是乎怠李,上傳于下載的進(jìn)度監(jiān)聽就完成了。
? ? ? ?我們還需要一個(gè)Helper類蛤克,對(duì)上傳或者下載進(jìn)行監(jiān)聽設(shè)置捺癞。文件的上傳其實(shí)很簡單,將我們的原始RequestBody和監(jiān)聽器 傳入构挤,返回我們的包裝的ProgressRequestBody 髓介,使用包裝后的ProgressRequestBody 進(jìn)行請(qǐng)求即可,但是文件的下載呢筋现,OkHttp給我們返回的是Response唐础,我們?nèi)绾螌⑽覀儼b的ProgressResponseBody設(shè)置進(jìn)去呢箱歧,答案之前已經(jīng)說過了,就是攔截器一膨,具體見代碼吧呀邢。
/**
* 進(jìn)度回調(diào)輔助類
*/
public class ProgressHelper {
/**
* 包裝OkHttpClient,用于下載文件的回調(diào)
* @param client 待包裝的OkHttpClient
* @param progressListener 進(jìn)度回調(diào)接口
* @return 包裝后的OkHttpClient豹绪,使用clone方法返回
*/
public static OkHttpClient addProgressResponseListener(OkHttpClient client,final ProgressResponseListener progressListener){
//克隆
OkHttpClient clone = client.clone();
//增加攔截器
clone.networkInterceptors().add(new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
//攔截
Response originalResponse = chain.proceed(chain.request());
//包裝響應(yīng)體并返回
return originalResponse.newBuilder()
.body(new ProgressResponseBody(originalResponse.body(), progressListener))
.build();
}
});
return clone;
}
/**
* 包裝請(qǐng)求體用于上傳文件的回調(diào)
* @param requestBody 請(qǐng)求體RequestBody
* @param progressRequestListener 進(jìn)度回調(diào)接口
* @return 包裝后的進(jìn)度回調(diào)請(qǐng)求體
*/
public static ProgressRequestBody addProgressRequestListener(RequestBody requestBody,ProgressRequestListener progressRequestListener){
//包裝請(qǐng)求體
return new ProgressRequestBody(requestBody,progressRequestListener);
}}
? ? ? ?對(duì)于文件下載的監(jiān)聽器我們?yōu)榱瞬挥绊懺瓉淼腛kHttpClient 實(shí)例价淌,我們調(diào)用clone方法進(jìn)行了克隆,之后對(duì)克隆的方法設(shè)置了響應(yīng)攔截瞒津,并返回該克隆的實(shí)例蝉衣。而文件的上傳則十分簡單,直接包裝后返回即可巷蚪。
? ? ? ?但是你別忘記了病毡,我們的目的是在UI層進(jìn)行回調(diào),而OkHttp的所有請(qǐng)求都不在UI層屁柏。于是我們還要實(shí)現(xiàn)我們寫的接口啦膜,進(jìn)行UI操作的回調(diào)。由于涉及到消息機(jī)制淌喻,我們對(duì)之前的兩個(gè)接口回調(diào)傳的參數(shù)進(jìn)行封裝僧家,封裝為一個(gè)實(shí)體類便于傳遞。
/**
* UI進(jìn)度回調(diào)實(shí)體類
*/
public class ProgressModel implements Serializable {
//當(dāng)前讀取字節(jié)長度
private long currentBytes;
//總字節(jié)長度
private long contentLength;
//是否讀取完成
private boolean done;
public ProgressModel(long currentBytes, long contentLength, boolean done) {
this.currentBytes = currentBytes;
this.contentLength = contentLength;
this.done = done;
}
public long getCurrentBytes() {
return currentBytes;
}
public void setCurrentBytes(long currentBytes) {
this.currentBytes = currentBytes;
}
public long getContentLength() {
return contentLength;
}
public void setContentLength(long contentLength) {
this.contentLength = contentLength;
}
public boolean isDone() {
return done;
}
public void setDone(boolean done) {
this.done = done;
}
@Override
public String toString() {
return "ProgressModel{" +
"currentBytes=" + currentBytes +
", contentLength=" + contentLength +
", done=" + done +
'}';
}}
? ? ? ?再實(shí)現(xiàn)我們的UI回調(diào)接口似嗤,對(duì)于文件的上傳啸臀,我們需要實(shí)現(xiàn)的是ProgressRequestListener接口,文件的下載需要實(shí)現(xiàn)的是ProgressResponseListener接口烁落,但是內(nèi)部的邏輯處理是完全一樣的乘粒。我們使用抽象類,提供一個(gè)抽象方法伤塌,該抽象方法用于UI層回調(diào)的處理灯萍,由具體開發(fā)去實(shí)現(xiàn)。涉及到消息機(jī)制就涉及到Handler類每聪,在Handler的子類中維護(hù)一個(gè)弱引用指向外部類(用到了static防止內(nèi)存泄露旦棉,但是需要調(diào)用外部類的一個(gè)非靜態(tài)函數(shù),所以將外部類引用直接由構(gòu)造函數(shù)傳入药薯,在內(nèi)部通過調(diào)用該引用的方法去實(shí)現(xiàn))绑洛,然后將主線程的Looper傳入,調(diào)用父類構(gòu)造函數(shù)童本。在onRequestProgress中發(fā)送進(jìn)度更新的消息真屯,在handleMessage函數(shù)中回調(diào)我們的抽象方法。我們只需要實(shí)現(xiàn)抽象方法穷娱,編寫對(duì)應(yīng)的UI更新代碼即可绑蔫。具體代碼如下运沦。
/**
* 請(qǐng)求體回調(diào)實(shí)現(xiàn)類,用于UI層回調(diào)
*/
public abstract class UIProgressRequestListener implements ProgressRequestListener {
private static final int REQUEST_UPDATE = 0x01;
//處理UI層的Handler子類
private static class UIHandler extends Handler {
//弱引用
private final WeakReference<UIProgressRequestListener> mUIProgressRequestListenerWeakReference;
public UIHandler(Looper looper, UIProgressRequestListener uiProgressRequestListener) {
super(looper);
mUIProgressRequestListenerWeakReference = new WeakReference<UIProgressRequestListener>(uiProgressRequestListener);
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case REQUEST_UPDATE:
UIProgressRequestListener uiProgressRequestListener = mUIProgressRequestListenerWeakReference.get();
if (uiProgressRequestListener != null) {
//獲得進(jìn)度實(shí)體類
ProgressModel progressModel = (ProgressModel) msg.obj;
//回調(diào)抽象方法
uiProgressRequestListener.onUIRequestProgress(progressModel.getCurrentBytes(), progressModel.getContentLength(), progressModel.isDone());
}
break;
default:
super.handleMessage(msg);
break;
}
}
}
//主線程Handler
private final Handler mHandler = new UIHandler(Looper.getMainLooper(), this);
@Override
public void onRequestProgress(long bytesRead, long contentLength, boolean done) {
//通過Handler發(fā)送進(jìn)度消息
Message message = Message.obtain();
message.obj = new ProgressModel(bytesRead, contentLength, done);
message.what = REQUEST_UPDATE;
mHandler.sendMessage(message);
}
/**
* UI層回調(diào)抽象方法
* @param bytesWrite 當(dāng)前寫入的字節(jié)長度
* @param contentLength 總字節(jié)長度
* @param done 是否寫入完成
*/
public abstract void onUIRequestProgress(long bytesWrite, long contentLength, boolean done);}
另一個(gè)實(shí)現(xiàn)類代碼雷同配深,不做敘述携添。
/**
* 請(qǐng)求體回調(diào)實(shí)現(xiàn)類,用于UI層回調(diào)
*/
public abstract class UIProgressResponseListener implements ProgressResponseListener {
private static final int RESPONSE_UPDATE = 0x02;
//處理UI層的Handler子類
private static class UIHandler extends Handler {
//弱引用
private final WeakReference<UIProgressResponseListener> mUIProgressResponseListenerWeakReference;
public UIHandler(Looper looper, UIProgressResponseListener uiProgressResponseListener) {
super(looper);
mUIProgressResponseListenerWeakReference = new WeakReference<UIProgressResponseListener>(uiProgressResponseListener);
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case RESPONSE_UPDATE:
UIProgressResponseListener uiProgressResponseListener = mUIProgressResponseListenerWeakReference.get();
if (uiProgressResponseListener != null) {
//獲得進(jìn)度實(shí)體類
ProgressModel progressModel = (ProgressModel) msg.obj;
//回調(diào)抽象方法
uiProgressResponseListener.onUIResponseProgress(progressModel.getCurrentBytes(), progressModel.getContentLength(), progressModel.isDone());
}
break;
default:
super.handleMessage(msg);
break;
}
}
}
//主線程Handler
private final Handler mHandler = new UIHandler(Looper.getMainLooper(), this);
@Override
public void onResponseProgress(long bytesRead, long contentLength, boolean done) {
//通過Handler發(fā)送進(jìn)度消息
Message message = Message.obtain();
message.obj = new ProgressModel(bytesRead, contentLength, done);
message.what = RESPONSE_UPDATE;
mHandler.sendMessage(message);
}
/**
* UI層回調(diào)抽象方法
* @param bytesRead 當(dāng)前讀取響應(yīng)體字節(jié)長度
* @param contentLength 總字節(jié)長度
* @param done 是否讀取完成
*/
public abstract void onUIResponseProgress(long bytesRead, long contentLength, boolean done);}
? ? ? ?一個(gè)上傳操作篓叶,一個(gè)下載操作烈掠,分別提供了UI層與非UI層回調(diào)的示例。最終代碼中使用的監(jiān)聽器都是UI層的澜共,因?yàn)槲覀円逻M(jìn)度條向叉。
private void download() {
//這個(gè)是非ui線程回調(diào)锥腻,不可直接操作UI
final ProgressResponseListener progressResponseListener = new ProgressResponseListener() {
@Override
public void onResponseProgress(long bytesRead, long contentLength, boolean done) {
Log.e("TAG", "bytesRead:" + bytesRead);
Log.e("TAG", "contentLength:" + contentLength);
Log.e("TAG", "done:" + done);
if (contentLength != -1) {
//長度未知的情況下回返回-1
Log.e("TAG", (100 * bytesRead) / contentLength + "% done");
}
Log.e("TAG", "================================");
}
};
//這個(gè)是ui線程回調(diào)嗦董,可直接操作UI
final UIProgressResponseListener uiProgressResponseListener = new UIProgressResponseListener() {
@Override
public void onUIResponseProgress(long bytesRead, long contentLength, boolean done) {
Log.e("TAG", "bytesRead:" + bytesRead);
Log.e("TAG", "contentLength:" + contentLength);
Log.e("TAG", "done:" + done);
if (contentLength != -1) {
//長度未知的情況下回返回-1
Log.e("TAG", (100 * bytesRead) / contentLength + "% done");
}
Log.e("TAG", "================================");
//ui層回調(diào)
downloadProgeress.setProgress((int) ((100 * bytesRead) / contentLength));
//Toast.makeText(getApplicationContext(), bytesRead + " " + contentLength + " " + done, Toast.LENGTH_LONG).show();
}
};
//構(gòu)造請(qǐng)求
final Request request1 = new Request.Builder()
.url("http://121.41.119.107:81/test/1.doc")
.build();
//包裝Response使其支持進(jìn)度回調(diào)
ProgressHelper.addProgressResponseListener(client, uiProgressResponseListener).newCall(request1).enqueue(new Callback() {
@Override
public void onFailure(Request request, IOException e) {
Log.e("TAG", "error ", e);
}
@Override
public void onResponse(Response response) throws IOException {
Log.e("TAG", response.body().string());
}
});
}
private void upload() {
File file = new File("/sdcard/1.doc");
//此文件必須在手機(jī)上存在,實(shí)際情況下請(qǐng)自行修改瘦黑,這個(gè)目錄下的文件只是在我手機(jī)中存在京革。
//這個(gè)是非ui線程回調(diào),不可直接操作UI
final ProgressRequestListener progressListener = new ProgressRequestListener() {
@Override
public void onRequestProgress(long bytesWrite, long contentLength, boolean done) {
Log.e("TAG", "bytesWrite:" + bytesWrite);
Log.e("TAG", "contentLength" + contentLength);
Log.e("TAG", (100 * bytesWrite) / contentLength + " % done ");
Log.e("TAG", "done:" + done);
Log.e("TAG", "================================");
}
};
//這個(gè)是ui線程回調(diào)幸斥,可直接操作UI
final UIProgressRequestListener uiProgressRequestListener = new UIProgressRequestListener() {
@Override
public void onUIRequestProgress(long bytesWrite, long contentLength, boolean done) {
Log.e("TAG", "bytesWrite:" + bytesWrite);
Log.e("TAG", "contentLength" + contentLength);
Log.e("TAG", (100 * bytesWrite) / contentLength + " % done ");
Log.e("TAG", "done:" + done);
Log.e("TAG", "================================");
//ui層回調(diào)
uploadProgress.setProgress((int) ((100 * bytesWrite) / contentLength));
//Toast.makeText(getApplicationContext(), bytesWrite + " " + contentLength + " " + done, Toast.LENGTH_LONG).show();
}
};
//構(gòu)造上傳請(qǐng)求匹摇,類似web表單
RequestBody requestBody = new MultipartBuilder().type(MultipartBuilder.FORM)
.addFormDataPart("hello", "android")
.addFormDataPart("photo", file.getName(), RequestBody.create(null, file))
.addPart(Headers.of("Content-Disposition", "form-data; name=\"another\";filename=\"another.dex\""), RequestBody.create(MediaType.parse("application/octet-stream"), file))
.build();
//進(jìn)行包裝,使其支持進(jìn)度回調(diào)
final Request request = new Request.Builder().url("").post(ProgressHelper.addProgressRequestListener(requestBody, uiProgressRequestListener)).build();
//開始請(qǐng)求
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Request request, IOException e) {
Log.e("TAG", "error ", e);
}
@Override
public void onResponse(Response response) throws IOException {
Log.e("TAG", response.body().string());
}
});
}
? ? ? ?還有一個(gè)細(xì)節(jié)需要注意就是連接的超時(shí)甲葬,讀取與寫入數(shù)據(jù)的超時(shí)時(shí)間的設(shè)置廊勃,在讀取或者寫入一些大文件的時(shí)候如果不設(shè)置這個(gè)參數(shù)可能會(huì)報(bào)異常,這里就隨便設(shè)置了一下值经窖,設(shè)得有點(diǎn)大坡垫,實(shí)際情況按需設(shè)置。
//設(shè)置超時(shí)画侣,不設(shè)置可能會(huì)報(bào)異常
private void initClient() {
client.setConnectTimeout(1000, TimeUnit.MINUTES);
client.setReadTimeout(1000, TimeUnit.MINUTES);
client.setWriteTimeout(1000, TimeUnit.MINUTES);
}