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

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

/**
 * 響應體進度回調接口,比如用於文件下載中
 * User:lizhangqu([email protected])
 * Date:2015-09-02
 * Time: 17:16
 */
public interface ProgressResponseListener {
    void onResponseProgress(long bytesRead, long contentLength, boolean done);
}

然後會使用到該接口

/**
 * 包裝的響體,處理進度
 * User:lizhangqu([email protected])
 * Date:2015-09-02
 * Time: 17:18
 */
public class ProgressResponseBody extends ResponseBody {
    //實際的待包裝響應體
    private final ResponseBody responseBody;
    //進度回調接口
    private final ProgressResponseListener progressListener;
    //包裝完成的BufferedSource
    private BufferedSource bufferedSource;

    /**
     * 構造函數,賦值
     * @param responseBody 待包裝的響應體
     * @param progressListener 回調接口
     */
    public ProgressResponseBody(ResponseBody responseBody, ProgressResponseListener progressListener) {
        this.responseBody = responseBody;
        this.progressListener = progressListener;
    }


    /**
     * 重寫調用實際的響應體的contentType
     * @return MediaType
     */
    @Override public MediaType contentType() {
        return responseBody.contentType();
    }

    /**
     * 重寫調用實際的響應體的contentLength
     * @return contentLength
     * @throws IOException 異常
     */
    @Override public long contentLength() throws IOException {
        return responseBody.contentLength();
    }

    /**
     * 重寫進行包裝source
     * @return BufferedSource
     * @throws IOException 異常
     */
    @Override public BufferedSource source() throws IOException {
        if (bufferedSource == null) {
            //包裝
            bufferedSource = Okio.buffer(source(responseBody.source()));
        }
        return bufferedSource;
    }

    /**
     * 讀取,回調進度接口
     * @param source Source
     * @return Source
     */
    private Source source(Source source) {

        return new ForwardingSource(source) {
            //當前讀取字節數
            long totalBytesRead = 0L;
            @Override public long read(Buffer sink, long byteCount) throws IOException {
                long bytesRead = super.read(sink, byteCount);
                //增加當前讀取的字節數,如果讀取完成了bytesRead會返回-1
                totalBytesRead += bytesRead != -1 ? bytesRead : 0;
                //回調,如果contentLength()不知道長度,會返回-1
                progressListener.onResponseProgress(totalBytesRead, responseBody.contentLength(), bytesRead == -1);
                return bytesRead;
            }
        };
    }
}

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

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

/**
 * 請求體進度回調接口,比如用於文件上傳中
 * User:lizhangqu([email protected])
 * Date:2015-09-02
 * Time: 17:16
 */
public interface ProgressRequestListener {
    void onRequestProgress(long bytesWritten, long contentLength, boolean done);
}

RequestBody的子類實現類比ResponseBody ,基本上復制一下稍加修改即可使用。


/**
 * 包裝的請求體,處理進度
 * User:lizhangqu([email protected])
 * Date:2015-09-02
 * Time: 17:15
 */
public  class ProgressRequestBody extends RequestBody {
    //實際的待包裝請求體
    private final RequestBody requestBody;
    //進度回調接口
    private final ProgressRequestListener progressListener;
    //包裝完成的BufferedSink
    private BufferedSink bufferedSink;

    /**
     * 構造函數,賦值
     * @param requestBody 待包裝的請求體
     * @param progressListener 回調接口
     */
    public ProgressRequestBody(RequestBody requestBody, ProgressRequestListener progressListener) {
        this.requestBody = requestBody;
        this.progressListener = progressListener;
    }

    /**
     * 重寫調用實際的響應體的contentType
     * @return MediaType
     */
    @Override
    public MediaType contentType() {
        return requestBody.contentType();
    }

    /**
     * 重寫調用實際的響應體的contentLength
     * @return contentLength
     * @throws IOException 異常
     */
    @Override
    public long contentLength() throws IOException {
        return requestBody.contentLength();
    }

    /**
     * 重寫進行寫入
     * @param sink BufferedSink
     * @throws IOException 異常
     */
    @Override
    public void writeTo(BufferedSink sink) throws IOException {
        if (bufferedSink == null) {
            //包裝
            bufferedSink = Okio.buffer(sink(sink));
        }
        //寫入
        requestBody.writeTo(bufferedSink);
        //必須調用flush,否則最後一部分數據可能不會被寫入
        bufferedSink.flush();

    }

    /**
     * 寫入,回調進度接口
     * @param sink Sink
     * @return Sink
     */
    private Sink sink(Sink sink) {
        return new ForwardingSink(sink) {
            //當前寫入字節數
            long bytesWritten = 0L;
            //總字節長度,避免多次調用contentLength()方法
            long contentLength = 0L;

            @Override
            public void write(Buffer source, long byteCount) throws IOException {
                super.write(source, byteCount);
                if (contentLength == 0) {
                    //獲得contentLength的值,後續不再調用
                    contentLength = contentLength();
                }
                //增加當前寫入的字節數
                bytesWritten += byteCount;
                //回調
                progressListener.onRequestProgress(bytesWritten, contentLength, bytesWritten == contentLength);
            }
        };
    }
}

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

還有一個重要的問題就是

如何進行使用呢?

如何進行使用呢?

如何進行使用呢?

重要的事要說三遍。

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

/**
 * 進度回調輔助類
 * User:lizhangqu([email protected])
 * Date:2015-09-02
 * Time: 17:33
 */
public class ProgressHelper {
    /**
     * 包裝OkHttpClient,用於下載文件的回調
     * @param client 待包裝的OkHttpClient
     * @param progressListener 進度回調接口
     * @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());
                //包裝響應體並返回
                return originalResponse.newBuilder()
                        .body(new ProgressResponseBody(originalResponse.body(), progressListener))
                        .build();
            }
        });
        return clone;
    }

    /**
     * 包裝請求體用於上傳文件的回調
     * @param requestBody 請求體RequestBody
     * @param progressRequestListener 進度回調接口
     * @return 包裝後的進度回調請求體
     */
    public static ProgressRequestBody addProgressRequestListener(RequestBody requestBody,ProgressRequestListener progressRequestListener){
        //包裝請求體
        return new ProgressRequestBody(requestBody,progressRequestListener);
    }
}

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

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


/**
 * UI進度回調實體類
 * User:lizhangqu([email protected])
 * Date:2015-09-02
 * Time: 22:39
 */
public class ProgressModel implements Serializable {
    //當前讀取字節長度
    private long currentBytes;
    //總字節長度
    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 +
                '}';
    }
}

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


/**
 * 請求體回調實現類,用於UI層回調
 * User:lizhangqu([email protected])
 * Date:2015-09-02
 * Time: 22:34
 */
public abstract class UIProgressRequestListener implements ProgressRequestListener {
    private static final int REQUEST_UPDATE = 0x01;

    //處理UI層的Handler子類
    private static class UIHandler extends Handler {
        //弱引用
        private final WeakReference mUIProgressRequestListenerWeakReference;

        public UIHandler(Looper looper, UIProgressRequestListener uiProgressRequestListener) {
            super(looper);
            mUIProgressRequestListenerWeakReference = new WeakReference(uiProgressRequestListener);
        }

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case REQUEST_UPDATE:
                    UIProgressRequestListener uiProgressRequestListener = mUIProgressRequestListenerWeakReference.get();
                    if (uiProgressRequestListener != null) {
                        //獲得進度實體類
                        ProgressModel progressModel = (ProgressModel) msg.obj;
                        //回調抽象方法
                        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發送進度消息
        Message message = Message.obtain();
        message.obj = new ProgressModel(bytesRead, contentLength, done);
        message.what = REQUEST_UPDATE;
        mHandler.sendMessage(message);
    }

    /**
     * UI層回調抽象方法
     * @param bytesWrite 當前寫入的字節長度
     * @param contentLength 總字節長度
     * @param done 是否寫入完成
     */
    public abstract void onUIRequestProgress(long bytesWrite, long contentLength, boolean done);
}

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


/**
 * 請求體回調實現類,用於UI層回調
 * User:lizhangqu([email protected])
 * Date:2015-09-02
 * Time: 22:34
 */
public abstract class UIProgressResponseListener implements ProgressResponseListener {
    private static final int RESPONSE_UPDATE = 0x02;
    //處理UI層的Handler子類
    private static class UIHandler extends Handler {
        //弱引用
        private final WeakReference mUIProgressResponseListenerWeakReference;

        public UIHandler(Looper looper, UIProgressResponseListener uiProgressResponseListener) {
            super(looper);
            mUIProgressResponseListenerWeakReference = new WeakReference(uiProgressResponseListener);
        }

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case RESPONSE_UPDATE:
                    UIProgressResponseListener uiProgressResponseListener = mUIProgressResponseListenerWeakReference.get();
                    if (uiProgressResponseListener != null) {
                        //獲得進度實體類
                        ProgressModel progressModel = (ProgressModel) msg.obj;
                        //回調抽象方法
                        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發送進度消息
        Message message = Message.obtain();
        message.obj = new ProgressModel(bytesRead, contentLength, done);
        message.what = RESPONSE_UPDATE;
        mHandler.sendMessage(message);
    }

    /**
     * UI層回調抽象方法
     * @param bytesRead 當前讀取響應體字節長度
     * @param contentLength 總字節長度
     * @param done 是否讀取完成
     */
    public abstract void onUIResponseProgress(long bytesRead, long contentLength, boolean done);
}

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

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

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

至於代碼,也已經寄托在了github上,如果有bug,歡迎修正。https://github.com/lizhangqu/CoreProgress,具體使用方法github上也已經貼出來了。

如果依賴gradle不能下載的話可以直接從github上下代碼。

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



    

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

private void download() {
        //這個是非ui線程回調,不可直接操作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, ================================);
            }
        };


        //這個是ui線程回調,可直接操作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層回調
                downloadProgeress.setProgress((int) ((100 * bytesRead) / contentLength));
                //Toast.makeText(getApplicationContext(), bytesRead +   + contentLength +   + done, Toast.LENGTH_LONG).show();
            }
        };

        //構造請求
        final Request request1 = new Request.Builder()
                .url(http://121.41.119.107:81/test/1.doc)
                .build();

        //包裝Response使其支持進度回調
        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);
        //此文件必須在手機上存在,實際情況下請自行修改,這個目錄下的文件只是在我手機中存在。


        //這個是非ui線程回調,不可直接操作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, ================================);
            }
        };


        //這個是ui線程回調,可直接操作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層回調
                uploadProgress.setProgress((int) ((100 * bytesWrite) / contentLength));
                //Toast.makeText(getApplicationContext(), bytesWrite +   + contentLength +   + done, Toast.LENGTH_LONG).show();
            }
        };

        //構造上傳請求,類似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();

        //進行包裝,使其支持進度回調
        final Request request = new Request.Builder().url(http://121.41.119.107:81/test/result.php).post(ProgressHelper.addProgressRequestListener(requestBody, uiProgressRequestListener)).build();
        //開始請求
        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());
            }
        });

    }

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

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

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

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

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


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

這裡寫圖片描述

以及對應的日志輸出。<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxwPs7EvP61xMnPtKujrNPDyv2+3cu1u7A8L3A+DQo8cD48aW1nIGFsdD0="這裡寫圖片描述" src="/uploadfile/Collfiles/20150906/20150906083207122.png" title="\" />

文件的下載,同樣是數據

這裡寫圖片描述

如果這篇文章對你有用,那麼請點擊下方的頂支持一個吧。

 

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