1. 程式人生 > >Retrofit下載檔案進度

Retrofit下載檔案進度

預設情況下,Retrofit在處理結果前會將整個Server Response讀進記憶體,這在JSON或者XML等Response上表現還算良好,但如果是一個非常大的檔案,就可能造成OutofMemory異常。因此我們在進行下載大檔案時需要使用@Streaming註解,使用@Streaming主要作用是把實時下載的位元組就立馬寫入磁碟,而不用把整個檔案讀入記憶體。

final ExecutorService executorService = Executors.newFixedThreadPool(1);
Retrofit.Builder retrofitBuilder = new Retrofit.Builder
() .addConverterFactory(GsonConverterFactory.create()) .callbackExecutor(executorService) android.os.NetworkOnMainThreadException .baseUrl("https://raw.githubusercontent.com/");

在Retrofit中callback回撥預設在主執行緒,使用@Streaming必須把callback回撥放在子執行緒中,這裡增加callbackExecutor(executorService) 表示把callback回撥放在核心執行緒為1的執行緒池中執行。

實現進度監聽首先需要建立一個監聽進度的回撥介面:

/**
 * progress表示當前已經下載的檔案大小
 * total表示檔案大小
 * speed表示下載速度
 * done表示是否下載完成
 * Created by 1013369768 on 2017/10/20.
 */
public interface ProgressListener {
    void onProgress(long progress,long total,long speed,boolean done);
}

重寫ResponseBody類的某些方法:

public class ProgressResponseBody
extends ResponseBody {
private final ResponseBody responseBody; private final ProgressListener progressListener; private BufferedSource bufferedSource; public ProgressResponseBody(ResponseBody responseBody, ProgressListener progressListener) { this.responseBody = responseBody; this.progressListener = progressListener; } @Nullable @Override public MediaType contentType() { return responseBody.contentType(); } @Override public long contentLength() { return responseBody.contentLength(); } @Override public BufferedSource source() { if (bufferedSource == null) { bufferedSource = Okio.buffer(source(responseBody.source())); } return bufferedSource; } 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.onProgress(totalBytesRead, responseBody.contentLength(),bytesRead,bytesRead == -1); return bytesRead; } }; } }

在ProgressResponseBody類中,把已經讀取的位元組數totalBytesRead,檔案的總大小responseBody.contentLength(),讀取的速度bytesRead,bytesRead==-1表示讀取到檔案結尾返回true,未讀取到檔案結尾返回false;得到這些數值後傳入到ProgressListener介面,所以這個ProgressListener介面是在子執行緒中執行的。

public class ProgressHelper {
    private static ProgressBean progressBean = new ProgressBean();
    private static ProgressHandler mProgressHandler;

    public static OkHttpClient.Builder addProgress(OkHttpClient.Builder builder){
        if (builder == null){
            builder = new OkHttpClient.Builder();
        }

        final ProgressListener progressListener = new ProgressListener() {
            //該方法在子執行緒中執行
            @Override
            public void onProgress(long progress, long total,long speed, boolean done) {
                if (mProgressHandler == null){
                    return;
                }

                progressBean.setBytesRead(progress);
                progressBean.setContentLength(total);
                progressBean.setSpeed(speed);
                progressBean.setDone(done);
                mProgressHandler.sendMessage(progressBean);

            }
        };

        //新增攔截器,自定義ResponseBody,新增下載進度
        builder.networkInterceptors().add(new Interceptor() {
            @Override
            public okhttp3.Response intercept(Chain chain) throws IOException {
                okhttp3.Response originalResponse = chain.proceed(chain.request());
                return originalResponse.newBuilder().body(
                        new ProgressResponseBody(originalResponse.body(), progressListener))
                        .build();

            }
        });
        return builder;
    }

    public static void setProgressHandler(ProgressHandler progressHandler){
        mProgressHandler = progressHandler;
    }
}

我們通過為OkhttpClient新增一個攔截器來使用我們自定義的ProgressResponseBody。並且在ProgressHelper類中把long progress, long total,long speed, boolean done引數儲存在progressBean類中。

public class ProgressBean {
    private long bytesRead;
    private long contentLength;
    private long speed;
    private boolean done;

    public long getSpeed() {
        return speed;
    }

    public void setSpeed(long speed) {
        this.speed = speed;
    }

    public long getBytesRead() {
        return bytesRead;
    }

    public void setBytesRead(long bytesRead) {
        this.bytesRead = bytesRead;
    }

    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;
    }
}

由於onProgress方法回撥在子執行緒中,所以需要使用handler進行更新UI

public abstract class ProgressHandler {
    private static final int DOWNLOAD_PROGRESS = 1;
    protected abstract void onProgress(long progress, long total,long speed, boolean done);

    private final Handler mHandler = new UIHandler(Looper.getMainLooper(),this);

    protected void sendMessage(ProgressBean progressBean){
        mHandler.obtainMessage(DOWNLOAD_PROGRESS,progressBean).sendToTarget();
    }

    static class UIHandler extends Handler{
        private final WeakReference<ProgressHandler> mProgressHandler;

        public UIHandler(Looper looper,ProgressHandler progressHandler) {
            super(looper);
            mProgressHandler = new WeakReference<ProgressHandler>(progressHandler);
        }

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what){
                case DOWNLOAD_PROGRESS:
                    ProgressHandler progressHandler = mProgressHandler.get();
                    if(progressHandler!=null) {
                        ProgressBean progressBean = (ProgressBean)msg.obj;
                        progressHandler.onProgress(progressBean.getBytesRead(),progressBean.getContentLength(),progressBean.getSpeed(),progressBean.isDone());
                    }
                    break;
                default:
                    break;
            }
        }
    }
}

涉及到訊息機制就涉及到Handler類,在Handler的子類中維護一個弱引用指向外部類(用到了static防止記憶體洩露,但是需要呼叫外部類的一個非靜態函式,所以將外部類引用直接由建構函式傳入,在內部通過呼叫該引用的方法去實現),然後將主執行緒的Looper傳入,呼叫父類建構函式,在handleMessage函式中回撥我們的抽象方法。