Android okhttp+rxjava實現多檔案下載和斷點續傳
轉載地址:http://blog.csdn.net/bit_kaki/article/details/72566020?locationNum=3&fps=1
先說下我的需求。我的需求是PC端先進行更新資料的管理,然後移動端登入時候會自動訪問服務,傳入mac值,獲取需更新資料的資訊。如下圖所示:
從服務返回到的是json格式的字串,我解析後獲得一個list<bean>,bean的結構為:
public class OfflineDataBean { private String dataId; private String dataName; private String organizationName; private String mac; private int dataType; private String dataAddtime; private String dataUpdatetime; private String dataPath; private String dataStatus; private String remark; ... }
接下來就是將這個list展示在一個RecyclerView裡。在這裡我首先將RecyclerView的Adapter和Holder進行了一次封裝:
public abstract class BaseRecyclerAdapter<T> extends RecyclerView.Adapter<RecyclerViewHolder> { //list集合 protected final List<T> mData; protected final Context mContext; //上下文 protected LayoutInflater mInflater; //點選item監聽 private OnItemClickListener mClickListener; //長按item監聽 private OnItemLongClickListener mLongClickListener; /** * 構造方法 * * @param ctx * @param list */ public BaseRecyclerAdapter(Context ctx, List<T> list) { mData = (list != null) ? list : new ArrayList<T>(); mContext = ctx; mInflater = LayoutInflater.from(ctx); } public void clear() { this.mData.clear(); } /** * 方法中主要是引入xml佈局檔案,並且給item點選事件和item長按事件賦值 * * @param parent * @param viewType * @return */ @Override public RecyclerViewHolder onCreateViewHolder(ViewGroup parent, final int viewType) { final RecyclerViewHolder holder = new RecyclerViewHolder(mContext, mInflater.inflate(getItemLayoutId(viewType), parent, false)); if (mClickListener != null) { holder.itemView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { mClickListener.onItemClick(holder.itemView, holder.getPosition()); } }); } if (mLongClickListener != null) { holder.itemView.setOnLongClickListener(new View.OnLongClickListener() { @Override public boolean onLongClick(View v) { mLongClickListener.onItemLongClick(holder.itemView, holder.getPosition()); return true; } }); } return holder; } /** * onBindViewHolder這個方法主要是給子項賦值資料的 * * @param holder * @param position */ @Override public void onBindViewHolder(RecyclerViewHolder holder, int position) { bindData(holder, position, mData.get(position)); } @Override public int getItemCount() { return mData.size(); } /** * add方法是新增item方法 * * @param pos * @param item */ public void add(int pos, T item) { mData.add(pos, item); notifyItemInserted(pos); } /** * delete方法是刪除item方法 * * @param pos */ public void delete(int pos) { mData.remove(pos); notifyItemRemoved(pos); } /** * item點選事件set方法 * * @param listener */ public void setOnItemClickListener(OnItemClickListener listener) { mClickListener = listener; } /** * item長安事件set方法 * * @param listener */ public void setOnItemLongClickListener(OnItemLongClickListener listener) { mLongClickListener = listener; } /** * item中xml佈局檔案方法 * * @param viewType * @return */ abstract public int getItemLayoutId(int viewType); /** * 賦值資料方法 * * @param holder * @param position * @param item */ abstract public void bindData(RecyclerViewHolder holder, int position, T item); /** * item點選事件介面 */ public interface OnItemClickListener { public void onItemClick(View itemView, int pos); } /** * item長按事件介面 */ public interface OnItemLongClickListener { public void onItemLongClick(View itemView, int pos); } }
public class RecyclerViewHolder extends RecyclerView.ViewHolder { /** * 集合類,layout裡包含的View,以view的id作為key,value是view物件 */ private SparseArray<View> mViews; /** * 上下文物件 */ private Context mContext; /** * 構造方法 * * @param ctx * @param itemView */ public RecyclerViewHolder(Context ctx, View itemView) { super(itemView); mContext = ctx; mViews = new SparseArray<View>(); } /** * 存放xml頁面方法 * * @param viewId * @param <T> * @return */ private <T extends View> T findViewById(int viewId) { View view = mViews.get(viewId); if (view == null) { view = itemView.findViewById(viewId); mViews.put(viewId, view); } return (T) view; } public View getView(int viewId) { return findViewById(viewId); } /** * 存放文字的id * * @param viewId * @return */ public TextView getTextView(int viewId) { return (TextView) getView(viewId); } /** * 存放button的id * * @param viewId * @return */ public Button getButton(int viewId) { return (Button) getView(viewId); } /** * 存放圖片的id * * @param viewId * @return */ public ImageView getImageView(int viewId) { return (ImageView) getView(viewId); } public LinearLayout getLinearLayout(int viewId) { return (LinearLayout) getView(viewId); } public ProgressBar getProgressBar(int viewId) { return (ProgressBar) getView(viewId); } /** * 存放圖片按鈕的id * * @param viewId * @return */ public ImageButton getImageButton(int viewId) { return (ImageButton) getView(viewId); } /** * 存放輸入框的id * * @param viewId * @return */ public EditText getEditText(int viewId) { return (EditText) getView(viewId); } /** * 存放文字xml中的id並且可以賦值資料的方法 * * @param viewId * @param value * @return */ public RecyclerViewHolder setText(int viewId, String value) { TextView view = findViewById(viewId); view.setText(value); return null; } /** * 存放圖片xml中的id並且可以賦值資料的方法 * * @param viewId * @param resId * @return */ public RecyclerViewHolder setBackground(int viewId, int resId) { View view = findViewById(viewId); view.setBackgroundColor(resId); return null; } /** * 存放點選事件監聽 * * @param viewId * @param listener * @return */ public RecyclerViewHolder setClickListener(int viewId, View.OnClickListener listener) { View view = findViewById(viewId); view.setOnClickListener(listener); return null; } }
然後RecyclerView裡的item佈局檔案為:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:orientation="horizontal" android:paddingLeft="10dp" android:layout_height="60dp"> <TextView android:id="@+id/tv_name" android:layout_width="100dp" android:layout_height="wrap_content" android:textSize="16sp"/> <ProgressBar android:id="@+id/main_progress" android:layout_width="0dp" android:layout_weight="1" android:layout_height="match_parent" style="@style/Widget.AppCompat.ProgressBar.Horizontal" /> <TextView android:id="@+id/tv_percent" android:layout_marginLeft="10dp" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="00" android:textSize="18sp" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="/100" android:textSize="18sp"/> <Button android:id="@+id/btn_down" android:layout_marginLeft="10dp" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="開始下載"/> </LinearLayout>
開始佈局RecyclerView,思路為點選開始下載按鈕,開始下載檔案,再次點選暫停下載並可以續傳下載。下載完畢後提示下載完畢。
baseRecyclerAdapterOfflineData=new BaseRecyclerAdapter<OfflineDataBean>(this,offlineDataBeenList) { @Override public int getItemLayoutId(int viewType) { return R.layout.item_offlinedata; } @Override public void bindData(RecyclerViewHolder holder, int position, OfflineDataBean item) { TextView tvName=holder.getTextView(R.id.tv_name); TextView tvpercent=holder.getTextView(R.id.tv_percent); Button btnDown=holder.getButton(R.id.btn_down); ProgressBar progressBar=holder.getProgressBar(R.id.main_progress); tvName.setText(offlineDataBeenList.get(position).getDataName()); btnDown.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if(btnDown.getText().equals("開始下載")||btnDown.getText().equals("繼續下載")) { DownloadManager.getInstance().download(offlineDataBeenList.get(position).getDataPath(), new DownLoadObserver() { @Override public void onNext(DownloadInfo value) { super.onNext(value); tvpercent.setText(String.valueOf((int)(((double)value.getProgress()/(double)value.getTotal())*100.00))); progressBar.setMax((int) value.getTotal()); progressBar.setProgress((int) value.getProgress()); btnDown.setText("暫停下載"); } @Override public void onComplete() { if (downloadInfo != null) { btnDown.setText("下載結束"); } } }); }else if(btnDown.getText().toString().equals("暫停下載")) { DownloadManager.getInstance().cancel(offlineDataBeenList.get(position).getDataPath()); btnDown.setText("開始下載"); } } }); } }; rvDownload.setAdapter(baseRecyclerAdapterOfflineData); rvDownload.setLayoutManager(new LinearLayoutManager(this)); rvDownload.setItemAnimator(new DefaultItemAnimator());
重點還是在DownloadManager類,再次感謝下丰神。這個類裡最重要的是download方法,如下所示:
public void download(String url, DownLoadObserver downLoadObserver) { Observable.just(url) .filter(s -> !downCalls.containsKey(s))//call的map已經有了,就證明正在下載,則這次不下載 .flatMap(s -> Observable.just(createDownInfo(s))) .map(this::getRealFileName)//檢測本地資料夾,生成新的檔名 .flatMap(downloadInfo -> Observable.create(new DownloadSubscribe(downloadInfo)))//下載 .observeOn(AndroidSchedulers.mainThread())//在主執行緒回撥 .subscribeOn(Schedulers.io())//在子執行緒執行 .subscribe(downLoadObserver);//新增觀察者 }
其中url是檔案下載地址,downloadObserver是用來回撥的介面,監聽下載情況。簡要說明下這個rxjava的方法,從上往下每行的意思分別是:
傳入url引數;
判斷是否正在這個url下載檔案,如果存在,則這次不下載(防止多次點選同一個下載按鈕);
獲取並傳入下載資訊;
檢測本地檔案(檔案是否存在,如果存在已下載多少);
根據下載資訊建立下載的觀察者方法;
設定在主執行緒回撥;
觀察者方法在子執行緒執行;
新增觀察者方法,開始執行。
附上完整程式碼:
public class DownloadManager { private static final AtomicReference<DownloadManager> INSTANCE = new AtomicReference<>(); private HashMap<String, Call> downCalls;//用來存放各個下載的請求 private OkHttpClient mClient;//OKHttpClient; //獲得一個單例類 public static DownloadManager getInstance() { for (; ; ) { DownloadManager current = INSTANCE.get(); if (current != null) { return current; } current = new DownloadManager(); if (INSTANCE.compareAndSet(null, current)) { return current; } } } private DownloadManager() { downCalls = new HashMap<>(); mClient = new OkHttpClient.Builder().build(); } /** * 開始下載 * * @param url 下載請求的網址 * @param downLoadObserver 用來回撥的介面 */ public void download(String url, DownLoadObserver downLoadObserver) { Observable.just(url) .filter(s -> !downCalls.containsKey(s))//call的map已經有了,就證明正在下載,則這次不下載 .flatMap(s -> Observable.just(createDownInfo(s))) .map(this::getRealFileName)//檢測本地資料夾,生成新的檔名 .flatMap(downloadInfo -> Observable.create(new DownloadSubscribe(downloadInfo)))//下載 .observeOn(AndroidSchedulers.mainThread())//在主執行緒回撥 .subscribeOn(Schedulers.io())//在子執行緒執行 .subscribe(downLoadObserver);//新增觀察者 } public void cancel(String url) { Call call = downCalls.get(url); if (call != null) { call.cancel();//取消 } downCalls.remove(url); } /** * 建立DownInfo * * @param url 請求網址 * @return DownInfo */ private DownloadInfo createDownInfo(String url) { DownloadInfo downloadInfo = new DownloadInfo(url); long contentLength = getContentLength(url);//獲得檔案大小 downloadInfo.setTotal(contentLength); String fileName = url.substring(url.lastIndexOf("/")); downloadInfo.setFileName(fileName); return downloadInfo; } private DownloadInfo getRealFileName(DownloadInfo downloadInfo) { String fileName = downloadInfo.getFileName(); long downloadLength = 0, contentLength = downloadInfo.getTotal(); File file = new File(MyApp.sContext.getFilesDir(), fileName); if (file.exists()) { //找到了檔案,代表已經下載過,則獲取其長度 downloadLength = file.length(); } //之前下載過,需要重新來一個檔案 int i = 1; while (downloadLength >= contentLength) { int dotIndex = fileName.lastIndexOf("."); String fileNameOther; if (dotIndex == -1) { fileNameOther = fileName + "(" + i + ")"; } else { fileNameOther = fileName.substring(0, dotIndex) + "(" + i + ")" + fileName.substring(dotIndex); } File newFile = new File(MyApp.sContext.getFilesDir(), fileNameOther); file = newFile; downloadLength = newFile.length(); i++; } //設定改變過的檔名/大小 downloadInfo.setProgress(downloadLength); downloadInfo.setFileName(file.getName()); return downloadInfo; } private class DownloadSubscribe implements ObservableOnSubscribe<DownloadInfo> { private DownloadInfo downloadInfo; public DownloadSubscribe(DownloadInfo downloadInfo) { this.downloadInfo = downloadInfo; } @Override public void subscribe(ObservableEmitter<DownloadInfo> e) throws Exception { String url = downloadInfo.getUrl(); long downloadLength = downloadInfo.getProgress();//已經下載好的長度 long contentLength = downloadInfo.getTotal();//檔案的總長度 //初始進度資訊 e.onNext(downloadInfo); Request request = new Request.Builder() //確定下載的範圍,新增此頭,則伺服器就可以跳過已經下載好的部分 .addHeader("RANGE", "bytes=" + downloadLength + "-" + contentLength) .url(url) .build(); Call call = mClient.newCall(request); downCalls.put(url, call);//把這個新增到call裡,方便取消 Response response = call.execute(); File file = new File(MyApp.sContext.getFilesDir(), downloadInfo.getFileName()); InputStream is = null; FileOutputStream fileOutputStream = null; try { is = response.body().byteStream(); fileOutputStream = new FileOutputStream(file, true); byte[] buffer = new byte[2048];//緩衝陣列2kB int len; while ((len = is.read(buffer)) != -1) { fileOutputStream.write(buffer, 0, len); downloadLength += len; downloadInfo.setProgress(downloadLength); e.onNext(downloadInfo); } fileOutputStream.flush(); downCalls.remove(url); } finally { //關閉IO流 IOUtil.closeAll(is, fileOutputStream); } e.onComplete();//完成 } } /** * 獲取下載長度 * * @param downloadUrl * @return */ private long getContentLength(String downloadUrl) { Request request = new Request.Builder() .url(downloadUrl) .build(); try { Response response = mClient.newCall(request).execute(); if (response != null && response.isSuccessful()) { long contentLength = response.body().contentLength(); response.close(); return contentLength == 0 ? DownloadInfo.TOTAL_ERROR : contentLength; } } catch (IOException e) { e.printStackTrace(); } return DownloadInfo.TOTAL_ERROR; } }
其他地方不用多說,最核心一句程式碼是:
.addHeader("RANGE", "bytes=" + downloadLength + "-" + contentLength)通過這行程式碼確定下載的範圍,從已下載的地方下載到結束。前面在建立被觀察者時候執行的兩個方法createDownInfo和getRealFileName就是為了分別獲取總長度和已下載長度。引用丰神博文原話來說就是:
當要斷點續傳的話必須新增這個頭,讓輸入流跳過多少位元組的形式是不行的,所以我們要想能成功的新增這條資訊那麼就必須對這個url請求2次,一次拿到總長度,來方便判斷本地是否有下載一半的資料,第二次才開始真正的讀流進行網路請求,我還想了一種思路,當檔案沒有下載完成的時候新增一個自定義的字尾,當下載完成再把這個字尾取消了,應該就不需要請求兩次了
對應的下載資訊DownloadInfo為:
public class DownloadInfo { public static final long TOTAL_ERROR = -1;//獲取進度失敗 private String url; private long total; private long progress; private String fileName; public DownloadInfo(String url) { this.url = url; } public String getUrl() { return url; } public String getFileName() { return fileName; } public void setFileName(String fileName) { this.fileName = fileName; } public long getTotal() { return total; } public void setTotal(long total) { this.total = total; } public long getProgress() { return progress; } public void setProgress(long progress) { this.progress = progress; } }
回撥介面為:
public abstract class DownLoadObserver implements Observer<DownloadInfo> { protected Disposable d;//可以用於取消註冊的監聽者 protected DownloadInfo downloadInfo; @Override public void onSubscribe(Disposable d) { this.d = d; } @Override public void onNext(DownloadInfo downloadInfo) { this.downloadInfo = downloadInfo; } @Override public void onError(Throwable e) { e.printStackTrace(); } }
主要程式碼已經貼上,讓我們來看看效果為:
可以斷點續傳,可以監聽到實時下載情況,可以同時多個下傳。
需求達成。
.addHeader("RANGE", "bytes=" + downloadLength + "-" + contentLength)
相關文章
- PHP實現檔案下載斷點續傳詳解PHP斷點
- Android下載檔案(一)下載進度&斷點續傳Android斷點
- 使用curl斷點續傳下載檔案斷點
- Winform檔案下載之斷點續傳ORM斷點
- 利用HTTP協議實現檔案下載的多執行緒斷點續傳HTTP協議執行緒斷點
- Java實現檔案斷點續傳Java斷點
- C# 檔案下載之斷點續傳C#斷點
- php 支援斷點續傳的檔案下載類PHP斷點
- requests如何友好地請求下載大檔案?requests實現分段下載、斷點續傳斷點
- JAVA實現大檔案分片上傳斷點續傳Java斷點
- 檔案下載之斷點續傳(客戶端與服務端的實現)斷點客戶端服務端
- Linux如何實現斷點續傳檔案功能?Linux斷點
- Node.js實現大檔案斷點續傳Node.js斷點
- 用Go語言實現多協程檔案上傳,斷點續傳,你如何實現?Go斷點
- PHP大檔案下載(方式1不支援斷點續傳)PHP斷點
- 使用OkHttp實現下載的進度監聽和斷點續傳HTTP斷點
- VUE-多檔案斷點續傳、秒傳、分片上傳Vue斷點
- Android 多執行緒下載,斷點續傳,執行緒池Android執行緒斷點
- 無外掛實現大檔案分片上傳,斷點續傳斷點
- Android 中 Service+Notification 斷點續傳下載Android斷點
- Android斷點續傳下載器JarvisDownloaderAndroid斷點JAR
- PHP大檔案下載(方式2支援斷點續傳)PHP斷點
- Android OkHttp+RxJava 史上最優雅的實現檔案上傳/下載進度的監聽AndroidHTTPRxJava
- android典型程式碼系列(二十)------多執行緒下載、斷點續傳Android執行緒斷點
- 使用webuploader元件實現大檔案分片上傳,斷點續傳Web元件斷點
- iOS大檔案斷點續傳iOS斷點
- iOS 開發之 NSURLSession 下載和斷點續傳iOSSession斷點
- HTTP檔案斷點續傳的原理HTTP斷點
- Android 斷點續傳Android斷點
- scp實現斷點續傳---rsync斷點
- OkHttp使用+檔案的上傳+斷點續傳HTTP斷點
- 前端實現檔案下載和拖拽上傳前端
- android多執行緒斷點續傳薦Android執行緒斷點
- C# FTP上傳下載(支援斷點續傳)C#FTP斷點
- C# 上傳下載ftp(支援斷點續傳)C#FTP斷點
- 大檔案上傳、斷點續傳、秒傳、beego、vue斷點GoVue
- 1. 大檔案上傳如何斷點續傳斷點
- 用Java實現斷點續傳(HTTP)Java斷點HTTP