okhttp3下載文件檢測進度與斷點續(xù)傳

之前有用過retrofit來做下載的功能,雖然retrofit基于okhttp,但是這還是有點不同。
我是在做更新功能的時候用到這個,具體的操作可能不會說太多,因為網(wǎng)上能找到很多基本的操作,我就說下一些流程和BUG,不管是okhttp還是retrofit都適用。

一.下載文件

1.下載操作

下載文件其實我感覺并不像上傳那么復雜,就按照拉取文本文件一樣弄就行。
這是我普通的用okhttp的get請求

        Request request = new Request.Builder()
                .url(murl)
                .build();
        Call call = okHttpClient.newCall(request);
        call.enqueue(callback);

下載文件的操作其實差不多

public Call download(String url, final DownloadListener downloadListener, final long startsPoint, Callback callback){
        Request request = new Request.Builder()
                .url(url)
                .header("RANGE", "bytes=" + startsPoint + "-")//斷點續(xù)傳
                .build();

        // 重寫ResponseBody監(jiān)聽請求
        Interceptor interceptor = new Interceptor() {
            @Override
            public Response intercept(Chain chain) throws IOException {
                Response originalResponse = chain.proceed(chain.request());
                return originalResponse.newBuilder()
                        .body(new DownloadResponseBody(originalResponse, startsPoint, downloadListener))
                        .build();
            }
        };
        
        OkHttpClient.Builder dlOkhttp = new OkHttpClient.Builder()
                .addNetworkInterceptor(interceptor);
        // 繞開證書
        try {
            setSSL(dlOkhttp);
        } catch (Exception e) {
            e.printStackTrace();
        }
        
        // 發(fā)起請求
        Call call = dlOkhttp.build().newCall(request);
        call.enqueue(callback);
        return call;
    }

注釋講得也比較請求,要重寫ResponseBody是因為要監(jiān)聽下載進度,網(wǎng)上有很多人的寫法是在onResponse的回調中讀寫字節(jié)到本地時監(jiān)聽進度,我建議是重寫ResponseBody來監(jiān)聽下載進度,因為好像寫在onResponse會有什么問題我忘記了,就算沒問題,自定義ResponseBody也會顯得更靈活。

2.自定義的ResponseBody
public class DownloadResponseBody extends ResponseBody{

    private Response originalResponse;
    private DownloadListener downloadListener;
    private long oldPoint = 0;

    public DownloadResponseBody(Response originalResponse, long startsPoint, DownloadListener downloadListener){
        this.originalResponse = originalResponse;
        this.downloadListener = downloadListener;
        this.oldPoint = startsPoint;
    }

    @Override
    public MediaType contentType() {
        return originalResponse.body().contentType();
    }

    @Override
    public long contentLength() {
        return originalResponse.body().contentLength();
    }

    @Override
    public BufferedSource source() {
        return Okio.buffer(new ForwardingSource(originalResponse.body().source()) {
            private long bytesReaded = 0;
            @Override
            public long read(Buffer sink, long byteCount) throws IOException {
                long bytesRead = super.read(sink, byteCount);
                bytesReaded += bytesRead == -1 ? 0 : bytesRead;
                if (downloadListener != null) {
                    downloadListener.loading((int) ((bytesReaded+oldPoint)/(1024)));
                }
                return bytesRead;
            }
        });
    }

}

主要就是要重寫這個source方法來實現(xiàn)監(jiān)聽,代碼也不難,其實不用多說什么。

3.定義請求中的回調

最好是用一個接口來定義在下載過程中的行為,接口的好處不用多說

public interface DownloadListener {

    /**
     *  開始下載
     */
    void start(long max);
    /**
     *  正在下載
     */
    void loading(int progress);
    /**
     *  下載完成
     */
    void complete(String path);
    /**
     *  請求失敗
     */
    void fail(int code, String message);
    /**
     *  下載過程中失敗
     */
    void loadfail(String message);
}

我這里定義了兩種失敗,主要是這邊要根據(jù)請求網(wǎng)絡的失敗和讀寫過程的失敗寫不同的邏輯,如果沒有特定的需求,這里只定義一個失敗的回調也是可以的。

二.下載的文件保存到本地

okhttp中是寫在onResponse方法中進行io操作,retrofit可以寫在onNext中

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                long length = response.body().contentLength();
                if (length == 0){
                    // 說明文件已經(jīng)下載完,直接跳轉安裝就好
                    downloadListener.complete(String.valueOf(getFile().getAbsoluteFile()));
                    return;
                }
                downloadListener.start(length+startsPoint);
                // 保存文件到本地
                InputStream is = null;
                RandomAccessFile randomAccessFile = null;
                BufferedInputStream bis = null;

                byte[] buff = new byte[2048];
                int len = 0;
                try {
                    is = response.body().byteStream();
                    bis  =new BufferedInputStream(is);

                    File file = getFile();
                    // 隨機訪問文件,可以指定斷點續(xù)傳的起始位置
                    randomAccessFile =  new RandomAccessFile(file, "rwd");
                    randomAccessFile.seek (startsPoint);
                    while ((len = bis.read(buff)) != -1) {
                        randomAccessFile.write(buff, 0, len);
                    }

                    // 下載完成
                    downloadListener.complete(String.valueOf(file.getAbsoluteFile()));
                } catch (Exception e) {
                    e.printStackTrace();
                    downloadListener.loadfail(e.getMessage());
                } finally {
                    try {
                        if (is != null) {
                            is.close();
                        }
                        if (bis != null){
                            bis.close();
                        }
                        if (randomAccessFile != null) {
                            randomAccessFile.close();
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }

            }
        });

    private File getFile() {
        String root = Environment.getExternalStorageDirectory().getPath();
        File file = new File(root,"updateDemo.apk");
        return file;
    }

    private long getFileStart(){
        String root = Environment.getExternalStorageDirectory().getPath();
        File file = new File(root,"updateDemo.apk");
        return file.length();
    }

因為我們之前在請求時寫了


這里先獲取報文的長度,如果長度為0,說明在我門本地已經(jīng)下載好文件了,這里就不用下載了,直接跳轉到安裝。這個主要是對斷點續(xù)傳的一個判斷,你想想,如果我都已經(jīng)下載完文件了,那我有什么必要再去開啟io流。
這里還要注意一下,獲取文件長度用file.length()而不用fis.available()是因為網(wǎng)上有個朋友測試過用fis.available()如果數(shù)據(jù)過大的話會出問題。

downloadListener.start(length+startsPoint);是我在開始讀寫前要先給ProgressBar設最大值。
讀寫時用到RandomAccessFile,這個主要是能隨時讀寫,做的就是斷點續(xù)傳的操作。其實斷點續(xù)傳主要就三句代碼

.header("RANGE", "bytes=" + startsPoint + "-")
 randomAccessFile =  new RandomAccessFile(file, "rwd");
 randomAccessFile.seek (startsPoint);

后面就沒有什么了,就是普通的io操作。

三.安裝應用

下載完成后跳轉到安裝頁面,需要做一個7.0的判斷

    private void installApk(String path){
        try {
            Intent intent = new Intent(Intent.ACTION_VIEW);
            File file = new File(path);
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
                String authority = "com.example.kylin.mindabs" + ".fileProvider";
                Uri fileUri = FileProvider.getUriForFile(getActivity().getApplicationContext(), authority, file);
                intent.setDataAndType(fileUri, "application/vnd.android.package-archive");
            } else {
                intent.setDataAndType(Uri.fromFile(file),"application/vnd.android.package-archive");
                intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            }
            this.startActivityForResult(intent, 0);
        }catch (Exception e){
            // todo 安裝失敗的操作
        }

    }

配置在清單中

       <provider
            tools:replace="android:authorities"
            android:name="android.support.v4.content.FileProvider"
            android:authorities="com.example.kylin.mindabs.fileProvider"
            android:grantUriPermissions="true"
            android:exported="false">
            <meta-data
                tools:replace="android:resource"
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_paths" />
        </provider>

定義一個xml文件

<paths>
    <external-path path="Android/data/com.example.kylin.mindabs/" name="files_root" />
    <external-path path="." name="external_storage_root" />
</paths>

還有就是很多人網(wǎng)上寫的跳轉是用startActivity,我建議用startActivityForResult,這樣可以拿到安裝頁面解析時的回到,方便之后做解析失敗之類的。

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if(requestCode == 0) {
           ......
        }
    }

我這里之前調試的時候打印成功和失敗的回調

(1)成功



看到resultCode為0,data為空。注意,就算你不安裝應用點取消,只要是能識別這個包出來,都是算解析成功,那么什么時候是失敗的呢,比如安裝包損壞這些才會回調失敗的情況。
(2)失敗



我自己模擬了一個失敗的情況,這種情況它會直接提示你安裝包解析失敗。

四.開發(fā)中遇到的問題

1.斷點續(xù)傳的細節(jié)

在使用

.header("RANGE", "bytes=" + startsPoint + "-")

之后,其實就是從文件的startsPoint 字節(jié)開始去下載,startsPoint 是我獲取的當前本地文件的大小,這本來是沒啥問題的,但是在apk已經(jīng)下載完成的情況下startsPoint 就是整個文件的長度,按理說這里這樣操作應該會讓response.body().contentLength()等于0,而實際上我在調試的時候沒有崩潰,但是response.body().contentLength()莫名其妙的等于229,我認真看日記才發(fā)現(xiàn)請求時報416錯誤,就是越界了。為了解決這個文件,我在拿到斷點的時候會做一個-1的操作

final long startsPoint = getFileStart() > 0 ? getFileStart()-1 : getFileStart();
2.流程問題

其實更新的知識點就那兩三個,但是對于流程來說需要嚴謹些,說得直白些,盡量有條能走通的路,所以我的代碼里大量加了try-catch

3.請求放在service

請求為什么要放在service中呢,其實是一個生命周期的問題,如果請求是在activity中發(fā)起的,關閉activity之后其實activity還不會結束,他會被請求影響生命周期,所以需要再service中請求。而如果你不是在activity中請求的話,比如你在彈框中請求,只要在彈框消失的時候取消請求就行,這種情況放不放在service里面做請求我覺得就無所謂了。

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容