Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android OkHttp 文件上傳和下載

Android OkHttp 文件上傳和下載

編輯:關於Android編程

相信大家對OkHttp也是相當的熟悉了,畢竟是Square的東西,對於其種種優點,這裡也不再敘說。優秀是優秀,但是畢竟優秀的東西給我們封裝了太多,那麼問題來了,我們使用OkHttp作為我們的網絡層,簡單地進行GET/POST請求是毫無問題。近日看了產品的設計稿,毛估估會有文件的上傳與下載的需求,如果使用OkHttp作為網絡層進行封裝,你會驚訝的發現,簡直封裝的太“完美”了。如果現在有這麼一個需求,要求對文件進行上傳或下載,但是在上傳或者下載前,你需要給用戶一個友好的提示,在上傳或者下載中,你需要將進度展示給用戶,下載或者完成後提示用戶下載完成。

但是呢,找啊找,你會發現基本上找不到OkHttp的這種用法,百度是找不到,但是你別忘記了還有谷歌,谷歌一搜,Stackoverflow就全出來了,甚至github上的issue都出來了,可見並不是我們遇到了這麼一個問題,還要許許多多的人遇到了這個問題,粗粗看了幾個回答,感覺有幾個還是比較靠譜的。為了日後的重用,我將其封裝為一個OkHttp的擴展庫,暫時取名為CoreProgress。

要實現進度的監聽,需要使用到OkHttp的依賴包Okio裡的兩個類,一個是Source,一個是Sink,至於Okio的東西,這裡也不多說。

 

首先我們實現文件下載的進度監聽。OkHttp給我們的只是一個回調,裡面有Response返回結果,我們需要繼承一個類,對結果進行監聽,這個類就是ResponseBody,但是如何將它設置到OkHttp中去呢,答案是攔截器。攔截器的部分後面再敘述,這裡先實現ResponseBody的子類ProgressResponseBody。

要監聽進度,我們必然需要一個監聽器,也就是一個接口,在其實現類中完成回調內容的處理,該接口聲明如下。

1 2 3 4 5 6 7 8 9 /** *響應體進度回調接口,比如用於文件下載中 *Date:2015-09-02 *Time:17:16 */ publicinterfaceProgressResponseListener{ voidonResponseProgress(longbytesRead,longcontentLength,booleandone); }

然後會使用到該接口

 

/** *包裝的響體,處理進度 *Date:2015-09-02 *Time:17:18 */ publicclassProgressResponseBodyextendsResponseBody{ //實際的待包裝響應體 privatefinalResponseBodyresponseBody; //進度回調接口 privatefinalProgressResponseListenerprogressListener; //包裝完成的BufferedSource privateBufferedSourcebufferedSource; /** *構造函數,賦值 *@paramresponseBody待包裝的響應體 *@paramprogressListener回調接口 */ publicProgressResponseBody(ResponseBodyresponseBody,ProgressResponseListenerprogressListener){ this.responseBody=responseBody; this.progressListener=progressListener; } /** *重寫調用實際的響應體的contentType *@returnMediaType */ @OverridepublicMediaTypecontentType(){ returnresponseBody.contentType(); } /** *重寫調用實際的響應體的contentLength *@returncontentLength *@throwsIOException異常 */ @OverridepubliclongcontentLength()throwsIOException{ returnresponseBody.contentLength(); } /** *重寫進行包裝source *@returnBufferedSource *@throwsIOException異常 */ @OverridepublicBufferedSourcesource()throwsIOException{ if(bufferedSource==null){ //包裝 bufferedSource=Okio.buffer(source(responseBody.source())); } returnbufferedSource; } /** *讀取,回調進度接口 *@paramsourceSource *@returnSource */ privateSourcesource(Sourcesource){ returnnewForwardingSource(source){ //當前讀取字節數 longtotalBytesRead=0L; @Overridepubliclongread(Buffersink,longbyteCount)throwsIOException{ longbytesRead=super.read(sink,byteCount); //增加當前讀取的字節數,如果讀取完成了bytesRead會返回-1 totalBytesRead+=bytesRead!=-1?bytesRead:0; //回調,如果contentLength()不知道長度,會返回-1 progressListener.onResponseProgress(totalBytesRead,responseBody.contentLength(),bytesRead==-1); returnbytesRead; } }; } }

類似裝飾器,我們對原始的ResponseBody進行了一層包裝。並在其讀取數據的時候設置了回調,回調的接口由構造函數傳入,此外構造函數還傳入了原始的ResponseBody,當系統內部調用了ResponseBody的source方法的時候,返回的便是我們包裝後的Source。然後我們還重寫了幾個方法調用原始的ResponseBody對應的函數返回結果。

同理既然下載是這樣,那麼上傳也應該是這樣,我們乘熱打鐵完成上傳的部分,下載是繼承ResponseBody,上傳就是繼承RequestBody,同時也應該還有一個監聽器。

 

 

/** *請求體進度回調接口,比如用於文件上傳中 *Date:2015-09-02 *Time:17:16 */ publicinterfaceProgressRequestListener{ voidonRequestProgress(longbytesWritten,longcontentLength,booleandone); }
RequestBody的子類實現類比ResponseBody,基本上復制一下稍加修改即可使用。
/** *包裝的請求體,處理進度 *Date:2015-09-02 *Time:17:15 */ publicclassProgressRequestBodyextendsRequestBody{ //實際的待包裝請求體 privatefinalRequestBodyrequestBody; //進度回調接口 privatefinalProgressRequestListenerprogressListener; //包裝完成的BufferedSink privateBufferedSinkbufferedSink; /** *構造函數,賦值 *@paramrequestBody待包裝的請求體 *@paramprogressListener回調接口 */ publicProgressRequestBody(RequestBodyrequestBody,ProgressRequestListenerprogressListener){ this.requestBody=requestBody; this.progressListener=progressListener; } /** *重寫調用實際的響應體的contentType *@returnMediaType */ @Override publicMediaTypecontentType(){ returnrequestBody.contentType(); } /** *重寫調用實際的響應體的contentLength *@returncontentLength *@throwsIOException異常 */ @Override publiclongcontentLength()throwsIOException{ returnrequestBody.contentLength(); } /** *重寫進行寫入 *@paramsinkBufferedSink *@throwsIOException異常 */ @Override publicvoidwriteTo(BufferedSinksink)throwsIOException{ if(bufferedSink==null){ //包裝 bufferedSink=Okio.buffer(sink(sink)); } //寫入 requestBody.writeTo(bufferedSink); //必須調用flush,否則最後一部分數據可能不會被寫入 bufferedSink.flush(); } /** *寫入,回調進度接口 *@paramsinkSink *@returnSink */ privateSinksink(Sinksink){ returnnewForwardingSink(sink){ //當前寫入字節數 longbytesWritten=0L; //總字節長度,避免多次調用contentLength()方法 longcontentLength=0L; @Override publicvoidwrite(Buffersource,longbyteCount)throwsIOException{ super.write(source,byteCount); if(contentLength==0){ //獲得contentLength的值,後續不再調用 contentLength=contentLength(); } //增加當前寫入的字節數 bytesWritten+=byteCount; //回調 progressListener.onRequestProgress(bytesWritten,contentLength,bytesWritten==contentLength); } }; } }

內部維護了一個原始的RequestBody以及一個監聽器,同樣的也是由構造函數傳入。當然也是要重寫幾個函數調用原始的RequestBody對應的函數,文件的下載是read函數中進行監聽的設置,毫無疑問文件的上傳就是write函數了,我們在write函數中進行了類似的操作,並回調了接口中的函數。當系統內部調用了RequestBody的writeTo函數時,我們對BufferedSink進行了一層包裝,即設置了進度監聽,並返回了我們包裝的BufferedSink。於是乎,上傳於下載的進度監聽就完成了。

還有一個重要的問題就是

如何進行使用呢?

如何進行使用呢?

如何進行使用呢?

重要的事要說三遍。

我們還需要一個Helper類,對上傳或者下載進行監聽設置。文件的上傳其實很簡單,將我們的原始RequestBody和監聽器傳入,返回我們的包裝的ProgressRequestBody,使用包裝後的ProgressRequestBody進行請求即可,但是文件的下載呢,OkHttp給我們返回的是Response,我們如何將我們包裝的ProgressResponseBody設置進去呢,答案之前已經說過了,就是攔截器,具體見代碼吧。

/** *進度回調輔助類 *Date:2015-09-02 *Time:17:33 */ publicclassProgressHelper{ /** *包裝OkHttpClient,用於下載文件的回調 *@paramclient待包裝的OkHttpClient *@paramprogressListener進度回調接口 *@return包裝後的OkHttpClient,使用clone方法返回 */ publicstaticOkHttpClientaddProgressResponseListener(OkHttpClientclient,finalProgressResponseListenerprogressListener){ //克隆 OkHttpClientclone=client.clone(); //增加攔截器 clone.networkInterceptors().add(newInterceptor(){ @Override publicResponseintercept(Chainchain)throwsIOException{ //攔截 ResponseoriginalResponse=chain.proceed(chain.request()); //包裝響應體並返回 returnoriginalResponse.newBuilder() .body(newProgressResponseBody(originalResponse.body(),progressListener)) .build(); } }); returnclone; } /** *包裝請求體用於上傳文件的回調 *@paramrequestBody請求體RequestBody *@paramprogressRequestListener進度回調接口 *@return包裝後的進度回調請求體 */ publicstaticProgressRequestBodyaddProgressRequestListener(RequestBodyrequestBody,ProgressRequestListenerprogressRequestListener){ //包裝請求體 returnnewProgressRequestBody(requestBody,progressRequestListener); } }

對於文件下載的監聽器我們為了不影響原來的OkHttpClient 實例,我們調用clone方法進行了克隆,之後對克隆的方法設置了響應攔截,並返回該克隆的實例。而文件的上傳則十分簡單,直接包裝後返回即可。

但是你別忘記了,我們的目的是在UI層進行回調,而OkHttp的所有請求都不在UI層。於是我們還要實現我們寫的接口,進行UI操作的回調。由於涉及到消息機制,我們對之前的兩個接口回調傳的參數進行封裝,封裝為一個實體類便於傳遞。

/** *UI進度回調實體類 *Date:2015-09-02 *Time:22:39 */ publicclassProgressModelimplementsSerializable{ //當前讀取字節長度 privatelongcurrentBytes; //總字節長度 privatelongcontentLength; //是否讀取完成 privatebooleandone; publicProgressModel(longcurrentBytes,longcontentLength,booleandone){ this.currentBytes=currentBytes; this.contentLength=contentLength; this.done=done; } publiclonggetCurrentBytes(){ returncurrentBytes; } publicvoidsetCurrentBytes(longcurrentBytes){ this.currentBytes=currentBytes; } publiclonggetContentLength(){ returncontentLength; } publicvoidsetContentLength(longcontentLength){ this.contentLength=contentLength; } publicbooleanisDone(){ returndone; } publicvoidsetDone(booleandone){ this.done=done; } @Override publicStringtoString(){ return"ProgressModel{"+ "currentBytes="+currentBytes+ ",contentLength="+contentLength+ ",done="+done+ '}'; } }

再實現我們的UI回調接口,對於文件的上傳,我們需要實現的是ProgressRequestListener接口,文件的下載需要實現的是ProgressResponseListener接口,但是內部的邏輯處理是完全一樣的。我們使用抽象類,提供一個抽象方法,該抽象方法用於UI層回調的處理,由具體開發去實現。涉及到消息機制就涉及到Handler類,在Handler的子類中維護一個弱引用指向外部類(用到了static防止內存洩露,但是需要調用外部類的一個非靜態函數,所以將外部類引用直接由構造函數傳入,在內部通過調用該引用的方法去實現),然後將主線程的Looper傳入,調用父類構造函數。在onRequestProgress中發送進度更新的消息,在handleMessage函數中回調我們的抽象方法。我們只需要實現抽象方法,編寫對應的UI更新代碼即可。具體代碼如下。

/** *請求體回調實現類,用於UI層回調 *Date:2015-09-02 *Time:22:34 */ publicabstractclassUIProgressRequestListenerimplementsProgressRequestListener{ privatestaticfinalintREQUEST_UPDATE=0x01; //處理UI層的Handler子類 privatestaticclassUIHandlerextendsHandler{ //弱引用 privatefinalWeakReferencemUIProgressRequestListenerWeakReference; publicUIHandler(Looperlooper,UIProgressRequestListeneruiProgressRequestListener){ super(looper); mUIProgressRequestListenerWeakReference=newWeakReference(uiProgressRequestListener); } @Override publicvoidhandleMessage(Messagemsg){ switch(msg.what){ caseREQUEST_UPDATE: UIProgressRequestListeneruiProgressRequestListener=mUIProgressRequestListenerWeakReference.get(); if(uiProgressRequestListener!=null){ //獲得進度實體類 ProgressModelprogressModel=(ProgressModel)msg.obj; //回調抽象方法 uiProgressRequestListener.onUIRequestProgress(progressModel.getCurrentBytes(),progressModel.getContentLength(),progressModel.isDone()); } break; default: super.handleMessage(msg); break; } } } //主線程Handler privatefinalHandlermHandler=newUIHandler(Looper.getMainLooper(),this); @Override publicvoidonRequestProgress(longbytesRead,longcontentLength,booleandone){ //通過Handler發送進度消息 Messagemessage=Message.obtain(); message.obj=newProgressModel(bytesRead,contentLength,done); message.what=REQUEST_UPDATE; mHandler.sendMessage(message); } /** *UI層回調抽象方法 *@parambytesWrite當前寫入的字節長度 *@paramcontentLength總字節長度 *@paramdone是否寫入完成 */ publicabstractvoidonUIRequestProgress(longbytesWrite,longcontentLength,booleandone); }

另一個實現類代碼雷同,不做敘述。

/** *請求體回調實現類,用於UI層回調 *Date:2015-09-02 *Time:22:34 */ publicabstractclassUIProgressResponseListenerimplementsProgressResponseListener{ privatestaticfinalintRESPONSE_UPDATE=0x02; //處理UI層的Handler子類 privatestaticclassUIHandlerextendsHandler{ //弱引用 privatefinalWeakReferencemUIProgressResponseListenerWeakReference; publicUIHandler(Looperlooper,UIProgressResponseListeneruiProgressResponseListener){ super(looper); mUIProgressResponseListenerWeakReference=newWeakReference(uiProgressResponseListener); } @Override publicvoidhandleMessage(Messagemsg){ switch(msg.what){ caseRESPONSE_UPDATE: UIProgressResponseListeneruiProgressResponseListener=mUIProgressResponseListenerWeakReference.get(); if(uiProgressResponseListener!=null){ //獲得進度實體類 ProgressModelprogressModel=(ProgressModel)msg.obj; //回調抽象方法 uiProgressResponseListener.onUIResponseProgress(progressModel.getCurrentBytes(),progressModel.getContentLength(),progressModel.isDone()); } break; default: super.handleMessage(msg); break; } } } //主線程Handler privatefinalHandlermHandler=newUIHandler(Looper.getMainLooper(),this); @Override publicvoidonResponseProgress(longbytesRead,longcontentLength,booleandone){ //通過Handler發送進度消息 Messagemessage=Message.obtain(); message.obj=newProgressModel(bytesRead,contentLength,done); message.what=RESPONSE_UPDATE; mHandler.sendMessage(message); } /** *UI層回調抽象方法 *@parambytesRead當前讀取響應體字節長度 *@paramcontentLength總字節長度 *@paramdone是否讀取完成 */ publicabstractvoidonUIResponseProgress(longbytesRead,longcontentLength,booleandone); }

最簡單的一步就是在我們的程序中使用了。為了方便Android Studio用戶使用,我將其發布到了中央庫。加入如下依賴即可

  dependencies{ compile'cn.edu.zafu:coreprogress:0.0.1' }

注意OkHttp的依賴需要自己手動添加,這個庫中OkHttp只是編譯時依賴,並沒有打包進去。

 

這裡貼一個簡單的例子,布局文件,兩個進度條用於顯示進度

  "http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context=".MainActivity"> android:id="@+id/upload_progress" style="?android:attr/progressBarStyleHorizontal" android:layout_width="match_parent" android:layout_height="wrap_content" android:max="100" android:progress="0" /> android:id="@+id/upload" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Upload"/> android:id="@+id/download_progress" style="?android:attr/progressBarStyleHorizontal" android:layout_width="match_parent" android:layout_height="wrap_content" android:max="100" android:progress="0" /> android:id="@+id/download" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Download"/>

一個上傳操作,一個下載操作,分別提供了UI層與非UI層回調的示例。最終代碼中使用的監聽器都是UI層的,因為我們要更新進度條。

  privatevoiddownload(){ //這個是非ui線程回調,不可直接操作UI finalProgressResponseListenerprogressResponseListener=newProgressResponseListener(){ @Override publicvoidonResponseProgress(longbytesRead,longcontentLength,booleandone){ 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線程回調,可直接操作UI finalUIProgressResponseListeneruiProgressResponseListener=newUIProgressResponseListener(){ @Override publicvoidonUIResponseProgress(longbytesRead,longcontentLength,booleandone){ 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層回調 downloadProgeress.setProgress((int)((100*bytesRead)/contentLength)); //Toast.makeText(getApplicationContext(),bytesRead+""+contentLength+""+done,Toast.LENGTH_LONG).show(); } }; //構造請求 finalRequestrequest1=newRequest.Builder() .url("http://121.41.119.107:81/test/1.doc") .build(); //包裝Response使其支持進度回調 ProgressHelper.addProgressResponseListener(client,uiProgressResponseListener).newCall(request1).enqueue(newCallback(){ @Override publicvoidonFailure(Requestrequest,IOExceptione){ Log.e("TAG","error",e); } @Override publicvoidonResponse(Responseresponse)throwsIOException{ Log.e("TAG",response.body().string()); } }); } privatevoidupload(){ Filefile=newFile("/sdcard/1.doc"); //此文件必須在手機上存在,實際情況下請自行修改,這個目錄下的文件只是在我手機中存在。 //這個是非ui線程回調,不可直接操作UI finalProgressRequestListenerprogressListener=newProgressRequestListener(){ @Override publicvoidonRequestProgress(longbytesWrite,longcontentLength,booleandone){ 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線程回調,可直接操作UI finalUIProgressRequestListeneruiProgressRequestListener=newUIProgressRequestListener(){ @Override publicvoidonUIRequestProgress(longbytesWrite,longcontentLength,booleandone){ 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層回調 uploadProgress.setProgress((int)((100*bytesWrite)/contentLength)); //Toast.makeText(getApplicationContext(),bytesWrite+""+contentLength+""+done,Toast.LENGTH_LONG).show(); } }; //構造上傳請求,類似web表單 RequestBodyrequestBody=newMultipartBuilder().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(); //進行包裝,使其支持進度回調 finalRequestrequest=newRequest.Builder().url("http://121.41.119.107:81/test/result.php").post(ProgressHelper.addProgressRequestListener(requestBody,uiProgressRequestListener)).build(); //開始請求 client.newCall(request).enqueue(newCallback(){ @Override publicvoidonFailure(Requestrequest,IOExceptione){ Log.e("TAG","error",e); } @Override publicvoidonResponse(Responseresponse)throwsIOException{ Log.e("TAG",response.body().string()); } }); }

需要特別注意的一點是文件的下載中如果文件大小未知,contentLength會始終返回-1,對於已知文件大小的情況下,它返回的是實際文件大小。但是done只要在文件下載完成後才會返回true,可以使用兩者的結合進行判斷文件是否下載完成。

當然,我們進行進度的監聽,最終還是會回調內部的接口,如果請求成功會回調onResponse,請求失敗則回調onFailure,我們可以在onResponse中處理最終的結果。比如提示用戶上傳完成或者下載完成等等 。

還有一個細節需要注意就是連接的超時,讀取與寫入數據的超時時間的設置,在讀取或者寫入一些大文件的時候如果不設置這個參數可能會報異常,這裡就隨便設置了一下值,設得有點大,實際情況按需設置。

  //設置超時,不設置可能會報異常 privatevoidinitClient(){ client.setConnectTimeout(1000,TimeUnit.MINUTES); client.setReadTimeout(1000,TimeUnit.MINUTES); client.setWriteTimeout(1000,TimeUnit.MINUTES); }

文件的上傳需要服務器的配合,為了證明PHP是世界上最好的語言,服務器的操作使用PHP大法,其實就是簡單的輸出POST的內容和文件的信息,並將一個文件保存到了服務器當前目錄下的file文件夾下。

  //打印POST傳遞的參數 var_dump($_POST); //打印文件信息 var_dump($_FILES); //將文件保存到當前目錄下的file文件夾下 move_uploaded_file($_FILES["photo"]["tmp_name"],"./file/".$_FILES["photo"]["name"]); ?>

下面是運行結果,可以看到點擊了上傳或者下載後,進度條在不斷更新。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved