1.上篇實現了單線程的單文件下載,本篇將講述多個文件的多線程下載,在此以前但願你先弄懂上篇
2.本篇將用到上篇以外的技術:
多線程、線程池(簡)、RecyclerView、數據庫多線程訪問下的注意點、volatile AtomicLong(簡)android
你們都知道,一個文件是不少的字節組成的,字節又是由二進制的位組成,若是把一個字節當成一塊磚。
那下載就像把服務器的磚頭搬到手機裏,而後擺在一個文件裏擺好,搬完了,文件滿了,任務就完成了
而後文件是電影就能播,是圖片就能看,app就能安裝。
對於下載一個文件,上篇講的單線程下載至關於一我的一塊一塊地搬。
而本篇的多線程則是僱幾我的來搬,可想而知效率是更高的。
那我開一千個線程豈不是秒下?若是你要搬1000塊磚,找1000我的,效率當然高,
但人家也不是白乾活,相對於3我的搬,你要多付333倍的工資,也就是開線程要消耗的,適量便可。
複製代碼
一個字節的丟失就可能致使一個文件的損壞,可想而知要多我的一塊兒幹活必須分工明確
否則一塊磚搬錯了,整個文件就報廢了,下面看一下線程怎麼分工,拿3個線程下載1000字節來講:git
總體架構和單線程的下載相似,最大的改變的是:github
因爲多線程須要管理,使用一個DownLoadTask來管理一個文件的全部下載線程,其中封裝了下載和暫停邏輯。
在DownLoadTask#download方法裏,若是數據庫沒有信息,則進行線程的任務分配及線程信息的建立,並插入數據庫。
DownLoadThread做爲DownLoadTask的內部類,方便使用。最後在download方法一一建立DownLoadThread並開啓,
將DownLoadThread存入集合管理,在DownLoadTask#pause方法裏,將集合中的線程所有關閉便可
複製代碼
用RecyclerView將單個條目便成一個列表界面web
//掘金下載地址
public static final String URL_JUEJIN = "https://imtt.dd.qq.com/16891/4611E43165D203CB6A52E65759FE7641.apk?fsname=com.daimajia.gold_5.6.2_196.apk&csr=1bbd";
//qq下載地址
public static final String URL_QQ = "https://qd.myapp.com/myapp/qqteam/Androidlite/qqlite_3.7.1.704_android_r110206_GuanWang_537057973_release_10000484.apk";
//有道雲筆記下載地址
public static final String URL_YOUDAO = "http://codown.youdao.com/note/youdaonote_android_6.3.5_youdaoweb.apk";
//微信下載地址
public static final String URL_WEIXIN = "http://gdown.baidu.com/data/wisegame/3d4de3ae1d2dc7d5/weixin_1360.apk";
//有道詞典下載地址
public static final String URL_YOUDAO_CIDIAN = "http://codown.youdao.com/dictmobile/youdaodict_android_youdaoweb.apk";
複製代碼
/**
* 初始化數據
*
* @return
*/
@NonNull
private ArrayList<FileBean> initData() {
FileBean juejin = new FileBean(0, Cons.URL_JUEJIN, "掘金.apk", 0, 0);
FileBean yunbiji = new FileBean(1, Cons.URL_YOUDAO, "有道雲筆記.apk", 0, 0);
FileBean qq = new FileBean(2, Cons.URL_QQ, "QQ.apk", 0, 0);
FileBean weiChat = new FileBean(3, Cons.URL_WEIXIN, "微信.apk", 0, 0);
FileBean cidian = new FileBean(4, Cons.URL_YOUDAO_CIDIAN, "有道詞典.apk", 0, 0);
ArrayList<FileBean> fileBeans = new ArrayList<>();
fileBeans.add(juejin);
fileBeans.add(yunbiji);
fileBeans.add(qq);
fileBeans.add(weiChat);
fileBeans.add(cidian);
return fileBeans;
}
複製代碼
上篇在Activity中的按鈕中實現的下載和暫停intent,這裏放在RVAdapter裏數據庫
/**
* 做者:張風捷特烈<br/>
* 時間:2018/11/13 0013:11:58<br/>
* 郵箱:1981462002@qq.com<br/>
* 說明:RecyclerView適配器
*/
public class RVAdapter extends RecyclerView.Adapter<MyViewHolder> {
private Context mContext;
private List<FileBean> mData;
public RVAdapter(Context context, List<FileBean> data) {
mContext = context;
mData = data;
}
@NonNull
@Override
public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(mContext).inflate(R.layout.item_pb, parent, false);
view.setOnClickListener(v -> {
//TODO 點擊條目
});
return new MyViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull MyViewHolder holder, int position) {
FileBean fileBean = mData.get(position);
holder.mBtnStart.setOnAlphaListener(v -> {
ToastUtil.showAtOnce(mContext, "開始下載: " + fileBean.getFileName());
Intent intent = new Intent(mContext, DownLoadService.class);
intent.setAction(Cons.ACTION_START);
intent.putExtra(Cons.SEND_FILE_BEAN, fileBean);//使用intent攜帶對象
mContext.startService(intent);//開啓服務--下載標示
});
holder.mBtnStop.setOnAlphaListener(v -> {
Intent intent = new Intent(mContext, DownLoadService.class);
intent.setAction(Cons.ACTION_STOP);
intent.putExtra(Cons.SEND_FILE_BEAN, fileBean);//使用intent攜帶對象
mContext.startService(intent);//啓動服務---中止標示
ToastUtil.showAtOnce(mContext, "中止下載: " + fileBean.getFileName());
});
holder.mTVFileName.setText(fileBean.getFileName());
holder.mPBH.setProgress((int) fileBean.getLoadedLen());
holder.mPBV.setProgress((int) fileBean.getLoadedLen());
}
@Override
public int getItemCount() {
return mData.size();
}
/**
* 更新進度
* @param id 待更新的文件id
* @param progress 進度數
*/
public void updateProgress(int id, int progress) {
mData.get(id).setLoadedLen(progress);
notifyDataSetChanged();//通知數據修改
}
}
/**
* ViewHolder
*/
class MyViewHolder extends RecyclerView.ViewHolder {
public ProgressBar mPBH;
public ProgressBar mPBV;
public AlphaImageView mBtnStart;
public AlphaImageView mBtnStop;
public TextView mTVFileName;
public MyViewHolder(View itemView) {
super(itemView);
mPBH = itemView.findViewById(R.id.id_pb_h);
mPBV = itemView.findViewById(R.id.id_pb_v);
mBtnStart = itemView.findViewById(R.id.id_btn_start);
mBtnStop = itemView.findViewById(R.id.id_btn_stop);
mTVFileName = itemView.findViewById(R.id.id_tv_file_name);
}
}
複製代碼
mAdapter = new RVAdapter(this, fileBeans);
mIdRvPage.setAdapter(mAdapter);
mIdRvPage.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false));
複製代碼
DownLoadTask最重要的在於:管理一個文件下載的全部線程,download是暴漏出的下載方法。pause中止。
好比開三個線程,該類的mDownLoadThreads就將線程存到集合裏,以便使用
DownLoadThread 和上篇核心邏輯基本一至,這裏做爲DownLoadTask內部類,方便使用其中的變量
還有就是因爲是多線程,每一個執行的快慢不定,判斷結束的標識必須三個線程都結束才表明下載結束
另外使用Timer定時器來發送進度,在DownLoadThread發送會致使幾個線程中的進度不統一,影響視覺編程
/**
* 做者:張風捷特烈<br/>
* 時間:2018/11/13 0013:15:21<br/>
* 郵箱:1981462002@qq.com<br/>
* 說明:下載一個文件的任務(mDownLoadThreads儲存該文件任務的全部線程)
*/
public class DownLoadTask {
private FileBean mFileBean;//下載文件的信息
private DownLoadDao mDao;//數據訪問接口
private Context mContext;//上下文
private int mThreadCount;//線程數量
public boolean isDownLoading;//是否正在下載
private Timer mTimer;//定時器
private List<DownLoadThread> mDownLoadThreads;//該文件全部線程的集合
//已下載的長度:共享變量----使用volatile和Atomic進行同步
private volatile AtomicLong mLoadedLen = new AtomicLong();
//使用線程池
public static ExecutorService sExe = Executors.newCachedThreadPool();
public DownLoadTask(FileBean fileBean, Context context, int threadCount) {
mFileBean = fileBean;
mContext = context;
mThreadCount = threadCount;
mDao = new DownLoadDaoImpl(context);
mDownLoadThreads = new ArrayList<>();
mTimer = new Timer();
}
/**
* 下載邏輯
*/
public void download() {
//從數據獲取線程信息
List<ThreadBean> threads = mDao.getThreads(mFileBean.getUrl());
if (threads.size() == 0) {//若是沒有線程信息,就新建線程信息
//------獲取每一個進程下載長度
long len = mFileBean.getLength() / mThreadCount;
for (int i = 0; i < mThreadCount; i++) {
//建立threadCount個線程信息
ThreadBean threadBean = null;
if (i != mThreadCount - 1) {
threadBean = new ThreadBean(
i, mFileBean.getUrl(), len * i, (i + 1) * len - 1, 0);
} else {
threadBean = new ThreadBean(
i, mFileBean.getUrl(), len * i, mFileBean.getLength(), 0);
}
//建立後添加到線程集合中
threads.add(threadBean);
//2.若是數據庫沒有此下載線程的信息,則向數據庫插入該線程信息
mDao.insertThread(threadBean);
}
}
//啓動多個線程
for (ThreadBean info : threads) {
DownLoadThread thread = new DownLoadThread(info);//建立下載線程
sExe.execute(thread);//開始線程
thread.isDownLoading = true;
isDownLoading = true;
mDownLoadThreads.add(thread);//開始下載時將該線程加入集合
}
mTimer.schedule(new TimerTask() {//啓動定時器發送廣播
@Override
public void run() {
Intent intent = new Intent(Cons.ACTION_UPDATE);//更新進度的廣播intent
mContext.sendBroadcast(intent);
intent.putExtra(Cons.SEND_LOADED_PROGRESS,
(int) (mLoadedLen.get() * 100 / mFileBean.getLength()));
intent.putExtra(Cons.SEND_FILE_ID, mFileBean.getId());
mContext.sendBroadcast(intent);
}
}, 1000, 1000);
}
public void pause() {
for (DownLoadThread downLoadThread : mDownLoadThreads) {
downLoadThread.isDownLoading = false;
isDownLoading = false;
}
}
/**
* 下載的核心線程類
*/
public class DownLoadThread extends Thread {
private ThreadBean mThreadBean;//下載線程的信息
public boolean isDownLoading;//是否在下載
public DownLoadThread(ThreadBean threadBean) {
mThreadBean = threadBean;
}
@Override
public void run() {
if (mThreadBean == null) {//1.下載線程的信息爲空,直接返回
return;
}
HttpURLConnection conn = null;
RandomAccessFile raf = null;
InputStream is = null;
try {
//3.鏈接線程的url
URL url = new URL(mThreadBean.getUrl());
conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(5000);
conn.setRequestMethod("GET");
//4.設置下載位置
long start = mThreadBean.getStart() + mThreadBean.getLoadedLen();//開始位置
//conn設置屬性,標記資源的位置(這是給服務器看的)
conn.setRequestProperty("Range", "bytes=" + start + "-" + mThreadBean.getEnd());
//5.尋找文件的寫入位置
File file = new File(Cons.DOWNLOAD_DIR, mFileBean.getFileName());
//建立隨機操做的文件流對象,可讀、寫、刪除
raf = new RandomAccessFile(file, "rwd");
raf.seek(start);//設置文件寫入位置
//6.下載的核心邏輯
mLoadedLen.set(mLoadedLen.get() + mThreadBean.getLoadedLen());
//206-----部份內容和範圍請求 不要200寫順手了...
if (conn.getResponseCode() == 206) {
//讀取數據
is = conn.getInputStream();
byte[] buf = new byte[1024 * 4];
int len = 0;
while ((len = is.read(buf)) != -1) {
//寫入文件
raf.write(buf, 0, len);
//發送廣播給Activity,通知進度
mLoadedLen.set(mLoadedLen.get() + len);//累加整個文件的完成進度
//累加每一個線程完成的進度
mThreadBean.setLoadedLen(mThreadBean.getLoadedLen() + len);
//暫停保存進度到數據庫
if (!this.isDownLoading) {
mDao.updateThread(mThreadBean.getUrl(), mThreadBean.getId(),
mThreadBean.getLoadedLen());
return;
}
}
}
//是否全部線程都已經下載完成
isDownLoading = false;
checkIsAllOK();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (conn != null) {
conn.disconnect();
}
try {
if (raf != null) {
raf.close();
}
if (is != null) {
is.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 檢查是否全部線程都已經完成了
*/
private synchronized void checkIsAllOK() {
boolean allFinished = true;
for (DownLoadThread downLoadThread : mDownLoadThreads) {
if (downLoadThread.isDownLoading) {
allFinished = false;
break;
}
}
if (allFinished) {
mTimer.cancel();//下載完成,取消定時器
//下載完成,刪除線程信息
mDao.deleteThread(mThreadBean.getUrl());
//通知下載結束
Intent intent = new Intent();
intent.setAction(Cons.ACTION_FINISH);//加完成的Action
intent.putExtra(Cons.SEND_FILE_BEAN, mFileBean);
mContext.sendBroadcast(intent);
}
}
}
}
複製代碼
稍微不一樣的就是一個下載任務變成了多個下載任務,這裏使用安卓特有的SparseArray來存儲bash
/**
* 做者:張風捷特烈<br/>
* 時間:2018/11/12 0012:12:23<br/>
* 郵箱:1981462002@qq.com<br/>
* 說明:下載的服務
*/
public class DownLoadService extends Service {
//因爲多文件,維護一個Task集合:使用SparseArray存儲int型的鍵---的鍵值對
private SparseArray<DownLoadTask> mTaskMap = new SparseArray<>();
/**
* 處理消息使用的Handler
*/
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case Cons.MSG_CREATE_FILE_OK:
FileBean fileBean = (FileBean) msg.obj;
//已在主線程,可更新UI
ToastUtil.showAtOnce(DownLoadService.this, "文件長度:" + fileBean.getLength());
DownLoadTask task = new DownLoadTask(fileBean, DownLoadService.this, 3);
task.download();
mTaskMap.put(fileBean.getId(), task);
break;
}
}
};
@Override//每次啓動服務會走此方法
public int onStartCommand(Intent intent, int flags, int startId) {
if (intent.getAction() != null) {
switch (intent.getAction()) {
case Cons.ACTION_START:
FileBean fileBean = (FileBean) intent.getSerializableExtra(Cons.SEND_FILE_BEAN);
DownLoadTask start = mTaskMap.get(fileBean.getId());
if (start != null) {
if (start.isDownLoading) {
return super.onStartCommand(intent, flags, startId);
}
}
DownLoadTask.sExe.execute(new LinkURLThread(fileBean, mHandler));
break;
case Cons.ACTION_STOP:
FileBean stopFile = (FileBean) intent.getSerializableExtra(Cons.SEND_FILE_BEAN);
//獲取中止的下載線程
DownLoadTask task = mTaskMap.get(stopFile.getId());
if (task != null) {
task.pause();
}
break;
}
}
return super.onStartCommand(intent, flags, startId);
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
}
複製代碼
這裏多了一個下載完成的Action,而且由MainActivity傳入進度條,改成mAdapter.updateProgress刷新視圖服務器
/**
* 做者:張風捷特烈<br/>
* 時間:2018/11/12 0012:16:05<br/>
* 郵箱:1981462002@qq.com<br/>
* 說明:更新ui的廣播接收者
*/
public class UpdateReceiver extends BroadcastReceiver {
private RVAdapter mAdapter;
public UpdateReceiver(RVAdapter adapter) {
mAdapter = adapter;
}
@Override
public void onReceive(Context context, Intent intent) {
if (Cons.ACTION_UPDATE.equals(intent.getAction())) {//進度更新
int loadedProgress = intent.getIntExtra(Cons.SEND_LOADED_PROGRESS, 0);
int id = intent.getIntExtra(Cons.SEND_FILE_ID, 0);
mAdapter.updateProgress(id, loadedProgress);
} else if (Cons.ACTION_FINISH.equals(intent.getAction())) {//下載結束
FileBean fileBean = (FileBean) intent.getSerializableExtra(Cons.SEND_FILE_BEAN);
mAdapter.updateProgress(fileBean.getId(), 0);
ToastUtil.showAtOnce(context, "文佳下載完成:" + fileBean.getFileName());
}
}
}
複製代碼
爲了不不一樣線程拿到的DownLoadDBHelper對象不一樣,這裏使用單例模式微信
private static DownLoadDBHelper sDownLoadDBHelper;
public static DownLoadDBHelper newInstance(Context context) {
if (sDownLoadDBHelper == null) {
synchronized (DownLoadDBHelper.class) {
if (sDownLoadDBHelper == null) {
sDownLoadDBHelper = new DownLoadDBHelper(context);
}
}
}
return sDownLoadDBHelper;
}
複製代碼
避免多個線程修改數據庫產生衝突多線程
public synchronized void insertThread(ThreadBean threadBean)
public synchronized void deleteThread(String url)
public synchronized void updateThread(String url, int threadId, long loadedLen)
複製代碼
你看完上下兩篇,基本上就可以實現這樣的效果了: 回過頭來看一看,也並不是難到沒法承受的地步,多想一想,思路貫通以後仍是很好理解的。
項目源碼 | 日期 | 備註 |
---|---|---|
V0.1--無 | 2018-11-13 | Android原生下載(下篇)多文件下載+多線程下載 |
筆名 | 微信 | 愛好 | |
---|---|---|---|
張風捷特烈 | 1981462002 | zdl1994328 | 語言 |
個人github | 個人簡書 | 個人CSDN | 我的網站 |
1----本文由張風捷特烈原創,轉載請註明 2----歡迎廣大編程愛好者共同交流 3----我的能力有限,若有不正之處歡迎你們批評指證,一定虛心改正 4----看到這裏,我在此感謝你的喜歡與支持