RxRetrofit - 終極封裝 - 深入淺出 & 斷點續傳

wzgiceman發表於2016-12-25

背景

斷點續傳下載一直是移動開發中必不可少的一項重要的技術,同樣的RxJavaRetrofit的結合讓這個技術解決起來更加的靈活,我們完全可以封裝一個適合自的下載框架,簡單而且安全!

效果

RxRetrofit - 終極封裝 - 深入淺出 & 斷點續傳


實現

下載和之前的http請求可以相互獨立,所以我們單獨給download建立一個工程moudel處理

資料庫

預設專案採用的GreenDao資料庫管理資料,新版GreenDao自動生成的炒作類的方式,所以如果缺失xxxDao檔案,檢查是否關聯正確的路徑

根目錄build.gradle

classpath 'org.greenrobot:greendao-gradle-plugin:+'複製程式碼

依賴:

apply plugin: 'org.greenrobot.greendao'
            xxxxxxxxx
compile 'org.greenrobot:greendao:3.2.0'複製程式碼

greenDAO-原始碼

如果需要替換自己的資料庫框架字需要修改DbDownUtil檔案即可

傳送門-DbDownUtil

1.建立service介面

和以前一樣,先寫介面
注意:Streaming是判斷是否寫入記憶體的標示,如果小檔案可以考慮不寫,一般情況必須寫;下載地址需要通過@url動態指定(不適固定的),@head標籤是指定下載的起始位置(斷點續傳的位置)

 /*斷點續傳下載介面*/
    @Streaming/*大檔案需要加入這個判斷,防止下載過程中寫入到記憶體中*/
    @GET
    Observable<ResponseBody> download(@Header("RANGE") String start, @Url String url);複製程式碼

2.複寫ResponseBody

和之前的上傳封裝一樣,下載更加的需要進度,所以我們同樣覆蓋ResponseBody類,寫入進度監聽回撥

/**
 * 自定義進度的body
 * @author wzg
 */
public class DownloadResponseBody extends ResponseBody {
    private ResponseBody responseBody;
    private DownloadProgressListener progressListener;
    private BufferedSource bufferedSource;

    public DownloadResponseBody(ResponseBody responseBody, DownloadProgressListener progressListener) {
        this.responseBody = responseBody;
        this.progressListener = progressListener;
    }

    @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);
                // read() returns the number of bytes read, or -1 if this source is exhausted.
                totalBytesRead += bytesRead != -1 ? bytesRead : 0;
                if (null != progressListener) {
                    progressListener.update(totalBytesRead, responseBody.contentLength(), bytesRead == -1);
                }
                return bytesRead;
            }
        };
    }
}複製程式碼

3.自定義進度回撥介面

/**
 * 成功回撥處理
 * Created by WZG on 2016/10/20.
 */
public interface DownloadProgressListener {
    /**
     * 下載進度
     * @param read
     * @param count
     * @param done
     */
    void update(long read, long count, boolean done);
}複製程式碼

4.複寫Interceptor

複寫Interceptor,可以將我們的監聽回撥通過okhttp的client方法addInterceptor自動載入我們的監聽回撥和ResponseBody

/**
 * 成功回撥處理
 * Created by WZG on 2016/10/20.
 */
public class DownloadInterceptor implements Interceptor {

    private DownloadProgressListener listener;

    public DownloadInterceptor(DownloadProgressListener listener) {
        this.listener = listener;
    }

    @Override
    public Response intercept(Chain chain) throws IOException {
        Response originalResponse = chain.proceed(chain.request());

        return originalResponse.newBuilder()
                .body(new DownloadResponseBody(originalResponse.body(), listener))
                .build();
    }
}複製程式碼

5.封裝請求downinfo資料

這個類中的資料可自由擴充套件,使用者自己選擇需要保持到資料庫中的資料,可以自由選擇需要資料庫第三方框架,demo採用greenDao框架儲存資料

public class DownInfo {
    /*儲存位置*/
    private String savePath;
    /*下載url*/
    private String url;
    /*基礎url*/
    private String baseUrl;
    /*檔案總長度*/
    private long countLength;
    /*下載長度*/
    private long readLength;
    /*下載唯一的HttpService*/
    private HttpService service;
    /*回撥監聽*/
    private HttpProgressOnNextListener listener;
    /*超時設定*/
    private  int DEFAULT_TIMEOUT = 6;
    /*下載狀態*/
    private DownState state;
    }複製程式碼

6.DownState狀態封裝

很簡單,和大多數封裝框架一樣

public enum  DownState {
    START,
    DOWN,
    PAUSE,
    STOP,
    ERROR,
    FINISH,
}複製程式碼

7.請求HttpProgressOnNextListener回撥封裝類

注意:這裡和DownloadProgressListener不同,這裡是下載這個過程中的監聽回撥,DownloadProgressListener只是進度的監聽
通過抽象類,可以自由選擇需要覆蓋的類,不需要完全覆蓋!更加靈活

/**
 * 下載過程中的回撥處理
 * Created by WZG on 2016/10/20.
 */
public abstract class HttpProgressOnNextListener<T> {
    /**
     * 成功後回撥方法
     * @param t
     */
    public abstract void onNext(T t);

    /**
     * 開始下載
     */
    public abstract void onStart();

    /**
     * 完成下載
     */
    public abstract void onComplete();


    /**
     * 下載進度
     * @param readLength
     * @param countLength
     */
    public abstract void updateProgress(long readLength, long countLength);

    /**
     * 失敗或者錯誤方法
     * 主動呼叫,更加靈活
     * @param e
     */
     public  void onError(Throwable e){

     }

    /**
     * 暫停下載
     */
    public void onPuase(){

    }

    /**
     * 停止下載銷燬
     */
    public void onStop(){

    }
}複製程式碼

8.封裝回撥Subscriber

準備的工作做完,需要將回撥和傳入回撥的資訊統一封裝到sub中,統一判斷;和封裝二的原理一樣,我們通過自定義Subscriber來提前處理返回的資料,讓使用者字需要關係成功和失敗以及向關心的資料,避免重複多餘的程式碼出現在處理類中

  • sub需要繼承DownloadProgressListener,和自帶的回撥一起組成我們需要的回撥結果

  • 傳入DownInfo資料,通過回撥設定DownInfo的不同狀態,儲存狀態

  • 通過RxAndroid將進度回撥指定到主執行緒中(如果不需要進度最好去掉該處理避免主執行緒處理負擔)

  • update進度回撥在斷點續傳使用時,需要手動判斷斷點後載入的長度,因為指定斷點下載長度下載後總長度=(物理長度-起始下載長度)


/**
 * 用於在Http請求開始時,自動顯示一個ProgressDialog
 * 在Http請求結束是,關閉ProgressDialog
 * 呼叫者自己對請求資料進行處理
 * Created by WZG on 2016/7/16.
 */
public class ProgressDownSubscriber<T> extends Subscriber<T> implements DownloadProgressListener {
    //弱引用結果回撥
    private WeakReference<HttpProgressOnNextListener> mSubscriberOnNextListener;
    /*下載資料*/
    private DownInfo downInfo;


    public ProgressDownSubscriber(DownInfo downInfo) {
        this.mSubscriberOnNextListener = new WeakReference<>(downInfo.getListener());
        this.downInfo=downInfo;
    }

    /**
     * 訂閱開始時呼叫
     * 顯示ProgressDialog
     */
    @Override
    public void onStart() {
        if(mSubscriberOnNextListener.get()!=null){
            mSubscriberOnNextListener.get().onStart();
        }
        downInfo.setState(DownState.START);
    }

    /**
     * 完成,隱藏ProgressDialog
     */
    @Override
    public void onCompleted() {
        if(mSubscriberOnNextListener.get()!=null){
            mSubscriberOnNextListener.get().onComplete();
        }
        downInfo.setState(DownState.FINISH);
    }

    /**
     * 對錯誤進行統一處理
     * 隱藏ProgressDialog
     *
     * @param e
     */
    @Override
    public void onError(Throwable e) {
        /*停止下載*/
        HttpDownManager.getInstance().stopDown(downInfo);
        if(mSubscriberOnNextListener.get()!=null){
            mSubscriberOnNextListener.get().onError(e);
        }
        downInfo.setState(DownState.ERROR);
    }

    /**
     * 將onNext方法中的返回結果交給Activity或Fragment自己處理
     *
     * @param t 建立Subscriber時的泛型型別
     */
    @Override
    public void onNext(T t) {
        if (mSubscriberOnNextListener.get() != null) {
            mSubscriberOnNextListener.get().onNext(t);
        }
    }

    @Override
    public void update(long read, long count, boolean done) {
        if(downInfo.getCountLength()>count){
            read=downInfo.getCountLength()-count+read;
        }else{
            downInfo.setCountLength(count);
        }
        downInfo.setReadLength(read);
        if (mSubscriberOnNextListener.get() != null) {
            /*接受進度訊息,造成UI阻塞,如果不需要顯示進度可去掉實現邏輯,減少壓力*/
            rx.Observable.just(read).observeOn(AndroidSchedulers.mainThread())
                    .subscribe(new Action1<Long>() {
                @Override
                public void call(Long aLong) {
                      /*如果暫停或者停止狀態延遲,不需要繼續傳送回撥,影響顯示*/
                    if(downInfo.getState()==DownState.PAUSE||downInfo.getState()==DownState.STOP)return;
                    downInfo.setState(DownState.DOWN);
                    mSubscriberOnNextListener.get().updateProgress(aLong,downInfo.getCountLength());
                }
            });
        }
    }

}複製程式碼

9.下載管理類封裝HttpDownManager

1. 單利獲取

 /**
     * 獲取單例
     * @return
     */
    public static HttpDownManager getInstance() {
        if (INSTANCE == null) {
            synchronized (HttpDownManager.class) {
                if (INSTANCE == null) {
                    INSTANCE = new HttpDownManager();
                }
            }
        }
        return INSTANCE;
    }複製程式碼

2. 因為單利所以需要記錄正在下載的資料和回到sub

 /*回撥sub佇列*/
    private HashMap<String,ProgressDownSubscriber> subMap;
    /*單利物件*/
    private volatile static HttpDownManager INSTANCE;

    private HttpDownManager(){
        downInfos=new HashSet<>();
        subMap=new HashMap<>();
    }複製程式碼

3.開始下載需要記錄下載的service避免每次都重複建立,然後請求sercie介面,得到ResponseBody資料後將資料流寫入到本地檔案中(6.0系統後需要提前申請許可權)

 /**
     * 開始下載
     */
    public void startDown(DownInfo info){
        /*正在下載不處理*/
        if(info==null||subMap.get(info.getUrl())!=null){
            return;
        }
        /*新增回撥處理類*/
        ProgressDownSubscriber subscriber=new ProgressDownSubscriber(info);
        /*記錄回撥sub*/
        subMap.put(info.getUrl(),subscriber);
        /*獲取service,多次請求公用一個sercie*/
        HttpService httpService;
        if(downInfos.contains(info)){
            httpService=info.getService();
        }else{
            DownloadInterceptor interceptor = new DownloadInterceptor(subscriber);
            OkHttpClient.Builder builder = new OkHttpClient.Builder();
            //手動建立一個OkHttpClient並設定超時時間
            builder.connectTimeout(info.getConnectionTime(), TimeUnit.SECONDS);
            builder.addInterceptor(interceptor);

            Retrofit retrofit = new Retrofit.Builder()
                    .client(builder.build())
                    .addConverterFactory(GsonConverterFactory.create())
                    .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
                    .baseUrl(info.getBaseUrl())
                    .build();
            httpService= retrofit.create(HttpService.class);
            info.setService(httpService);
        }
        /*得到rx物件-上一次下載的位置開始下載*/
        httpService.download("bytes=" + info.getReadLength() + "-",info.getUrl())
                /*指定執行緒*/
                .subscribeOn(Schedulers.io())
                .unsubscribeOn(Schedulers.io())
                   /*失敗後的retry配置*/
                .retryWhen(new RetryWhenNetworkException())
                /*讀取下載寫入檔案*/
                .map(new Func1<ResponseBody, DownInfo>() {
                    @Override
                    public DownInfo call(ResponseBody responseBody) {
                        try {
                            writeCache(responseBody,new File(info.getSavePath()),info);
                        } catch (IOException e) {
                            /*失敗丟擲異常*/
                            throw new HttpTimeException(e.getMessage());
                        }
                        return info;
                    }
                })
                /*回撥執行緒*/
                .observeOn(AndroidSchedulers.mainThread())
                /*資料回撥*/
                .subscribe(subscriber);

    }複製程式碼

4.寫入檔案

注意:一開始呼叫進度回撥是第一次寫入在進度回撥之前,所以需要判斷一次DownInfo是否獲取到下載總長度,沒有這選擇當前ResponseBody 讀取長度為總長度

    /**
     * 寫入檔案
     * @param file
     * @param info
     * @throws IOException
     */
    public void writeCache(ResponseBody responseBody,File file,DownInfo info) throws IOException{
        if (!file.getParentFile().exists())
            file.getParentFile().mkdirs();
        long allLength;
        if (info.getCountLength()==0){
            allLength=responseBody.contentLength();
        }else{
            allLength=info.getCountLength();
        }
            FileChannel channelOut = null;
            RandomAccessFile randomAccessFile = null;
            randomAccessFile = new RandomAccessFile(file, "rwd");
            channelOut = randomAccessFile.getChannel();
            MappedByteBuffer mappedBuffer = channelOut.map(FileChannel.MapMode.READ_WRITE,
                    info.getReadLength(),allLength-info.getReadLength());
            byte[] buffer = new byte[1024*8];
            int len;
            int record = 0;
            while ((len = responseBody.byteStream().read(buffer)) != -1) {
                mappedBuffer.put(buffer, 0, len);
                record += len;
            }
            responseBody.byteStream().close();
                if (channelOut != null) {
                    channelOut.close();
                }
                if (randomAccessFile != null) {
                    randomAccessFile.close();
                }
    }複製程式碼

5.停止下載

呼叫 subscriber.unsubscribe()解除監聽,然後remove記錄的下載資料和sub回撥,並且設定下載狀態(同步資料庫自己新增)

    /**
     * 停止下載
     */
    public void stopDown(DownInfo info){
        if(info==null)return;
        info.setState(DownState.STOP);
        info.getListener().onStop();
        if(subMap.containsKey(info.getUrl())) {
            ProgressDownSubscriber subscriber=subMap.get(info.getUrl());
            subscriber.unsubscribe();
            subMap.remove(info.getUrl());
        }
        /*儲存資料庫資訊和本地檔案*/
        db.save(info);
    }複製程式碼

6.暫停下載

原理和停止下載原理一樣

  /**
     * 暫停下載
     * @param info
     */
    public void pause(DownInfo info){
        if(info==null)return;
        info.setState(DownState.PAUSE);
        info.getListener().onPuase();
        if(subMap.containsKey(info.getUrl())){
            ProgressDownSubscriber subscriber=subMap.get(info.getUrl());
            subscriber.unsubscribe();
            subMap.remove(info.getUrl());
        }
        /*這裡需要講info資訊寫入到資料中,可自由擴充套件,用自己專案的資料庫*/
        db.update(info);
    }複製程式碼

7.暫停全部和停止全部下載任務

    /**
     * 停止全部下載
     */
    public void stopAllDown(){
        for (DownInfo downInfo : downInfos) {
            stopDown(downInfo);
        }
        subMap.clear();
        downInfos.clear();
    }

    /**
     * 暫停全部下載
     */
    public void pauseAll(){
        for (DownInfo downInfo : downInfos) {
            pause(downInfo);
        }
        subMap.clear();
        downInfos.clear();
    }複製程式碼

8.整合程式碼HttpDownManager

同樣使用了封裝二中的retry處理和執行時異常自定義處理封裝(不復述了)

傳送門-HttpDownManager

總結

到此我們的Rxjava+ReTrofit+okHttp深入淺出-封裝就基本完成了,已經可以完全勝任開發和學習的全部工作,如果後續再使用過程中有任何問題歡迎留言給我,會一直維護!

    1.Retrofit+Rxjava+okhttp基本使用方法
    2.統一處理請求資料格式
    3.統一的ProgressDialog和回撥Subscriber處理
    4.取消http請求
    5.預處理http請求
    6.返回資料的統一判斷
    7.失敗後的retry封裝處理
    8.RxLifecycle管理生命週期,防止洩露
    9.檔案上傳和檔案下載(支援多檔案斷點續傳)複製程式碼

終極封裝專欄

RxJava+Retrofit+OkHttp深入淺出-終極封裝專欄)


原始碼

傳送門-下載封裝原始碼

傳送門-全部封裝原始碼


建議

如果你對這套封裝有任何的問題和建議歡迎加入QQ群告訴我!

相關文章