okhttp3下载文件检测进度与断点续传
之前有用過retrofit來做下載的功能,雖然retrofit基于okhttp,但是這還是有點不同。
我是在做更新功能的時候用到這個,具體的操作可能不會說太多,因為網上能找到很多基本的操作,我就說下一些流程和BUG,不管是okhttp還是retrofit都適用。
一.下載文件
1.下載操作
下載文件其實我感覺并不像上傳那么復雜,就按照拉取文本文件一樣弄就行。
這是我普通的用okhttp的get請求
下載文件的操作其實差不多
public Call download(String url, final DownloadListener downloadListener, final long startsPoint, Callback callback){Request request = new Request.Builder().url(url).header("RANGE", "bytes=" + startsPoint + "-")//斷點續傳.build();// 重寫ResponseBody監聽請求Interceptor interceptor = new Interceptor() {@Overridepublic 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();}// 發起請求Call call = dlOkhttp.build().newCall(request);call.enqueue(callback);return call;}注釋講得也比較請求,要重寫ResponseBody是因為要監聽下載進度,網上有很多人的寫法是在onResponse的回調中讀寫字節到本地時監聽進度,我建議是重寫ResponseBody來監聽下載進度,因為好像寫在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;}@Overridepublic MediaType contentType() {return originalResponse.body().contentType();}@Overridepublic long contentLength() {return originalResponse.body().contentLength();}@Overridepublic BufferedSource source() {return Okio.buffer(new ForwardingSource(originalResponse.body().source()) {private long bytesReaded = 0;@Overridepublic 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方法來實現監聽,代碼也不難,其實不用多說什么。
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); }我這里定義了兩種失敗,主要是這邊要根據請求網絡的失敗和讀寫過程的失敗寫不同的邏輯,如果沒有特定的需求,這里只定義一個失敗的回調也是可以的。
二.下載的文件保存到本地
okhttp中是寫在onResponse方法中進行io操作,retrofit可以寫在onNext中
@Overridepublic void onResponse(Call call, Response response) throws IOException {long length = response.body().contentLength();if (length == 0){// 說明文件已經下載完,直接跳轉安裝就好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();// 隨機訪問文件,可以指定斷點續傳的起始位置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,說明在我門本地已經下載好文件了,這里就不用下載了,直接跳轉到安裝。這個主要是對斷點續傳的一個判斷,你想想,如果我都已經下載完文件了,那我有什么必要再去開啟io流。
這里還要注意一下,獲取文件長度用file.length()而不用fis.available()是因為網上有個朋友測試過用fis.available()如果數據過大的話會出問題。
downloadListener.start(length+startsPoint);是我在開始讀寫前要先給ProgressBar設最大值。
讀寫時用到RandomAccessFile,這個主要是能隨時讀寫,做的就是斷點續傳的操作。其實斷點續傳主要就三句代碼
后面就沒有什么了,就是普通的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 安裝失敗的操作}}配置在清單中
<providertools:replace="android:authorities"android:name="android.support.v4.content.FileProvider"android:authorities="com.example.kylin.mindabs.fileProvider"android:grantUriPermissions="true"android:exported="false"><meta-datatools: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>還有就是很多人網上寫的跳轉是用startActivity,我建議用startActivityForResult,這樣可以拿到安裝頁面解析時的回到,方便之后做解析失敗之類的。
@Overridepublic void onActivityResult(int requestCode, int resultCode, Intent data) {super.onActivityResult(requestCode, resultCode, data);if(requestCode == 0) {......}}我這里之前調試的時候打印成功和失敗的回調
(1)成功
看到resultCode為0,data為空。注意,就算你不安裝應用點取消,只要是能識別這個包出來,都是算解析成功,那么什么時候是失敗的呢,比如安裝包損壞這些才會回調失敗的情況。
(2)失敗
我自己模擬了一個失敗的情況,這種情況它會直接提示你安裝包解析失敗。
四.開發中遇到的問題
1.斷點續傳的細節
在使用
.header("RANGE", "bytes=" + startsPoint + "-")之后,其實就是從文件的startsPoint 字節開始去下載,startsPoint 是我獲取的當前本地文件的大小,這本來是沒啥問題的,但是在apk已經下載完成的情況下startsPoint 就是整個文件的長度,按理說這里這樣操作應該會讓response.body().contentLength()等于0,而實際上我在調試的時候沒有崩潰,但是response.body().contentLength()莫名其妙的等于229,我認真看日記才發現請求時報416錯誤,就是越界了。為了解決這個文件,我在拿到斷點的時候會做一個-1的操作
final long startsPoint = getFileStart() > 0 ? getFileStart()-1 : getFileStart();2.流程問題
其實更新的知識點就那兩三個,但是對于流程來說需要嚴謹些,說得直白些,盡量有條能走通的路,所以我的代碼里大量加了try-catch
3.請求放在service
請求為什么要放在service中呢,其實是一個生命周期的問題,如果請求是在activity中發起的,關閉activity之后其實activity還不會結束,他會被請求影響生命周期,所以需要再service中請求。而如果你不是在activity中請求的話,比如你在彈框中請求,只要在彈框消失的時候取消請求就行,這種情況放不放在service里面做請求我覺得就無所謂了。
總結
以上是生活随笔為你收集整理的okhttp3下载文件检测进度与断点续传的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Confluence 6 启用远程 AP
- 下一篇: redux-form(V7.4.2)笔记