1. 程式人生 > >Android okhttp+rxjava實現多檔案下載和斷點續傳

Android okhttp+rxjava實現多檔案下載和斷點續傳

         首先先感謝丰神,核心程式碼源於他的這篇微博http://blog.csdn.net/cfy137000/article/details/54838608,思路很棒。基於他的程式碼做了一些改動,實現我所需功能。

      先說下我的需求。我的需求是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)