内容简介:1.上篇实现了单线程的单文件下载,本篇将讲述多个文件的多线程下载,2.本篇将用到上篇之外的技术:多线程、线程池(简)、RecyclerView、数据库多线程访问下的注意点、volatile AtomicLong(简)
1.上篇实现了单线程的单文件下载,本篇将讲述多个文件的多线程下载, 在此之前希望你先弄懂上篇
2.本篇将用到上篇之外的技术:
多线程、线程池(简)、RecyclerView、数据库多线程访问下的注意点、volatile AtomicLong(简)
最终静态的效果
最终动态的效果
一、分析一下多线程下载单个文件的原理:
1.线程分工方式
大家都知道,一个文件是很多的字节组成的,字节又是由二进制的位组成,如果把一个字节当成一块砖。 那下载就像把服务器的砖头搬到手机里,然后摆在一个文件里摆好,搬完了,文件满了,任务就完成了 然后文件是电影就能播,是图片就能看,app就能安装。 对于下载一个文件,上篇讲的单线程下载相当于一个人一块一块地搬。 而本篇的多线程则是雇几个人来搬,可想而知效率是更高的。 那我开一千个线程岂不是秒下?如果你要搬1000块砖,找1000个人,效率固然高, 但人家也不是白干活,相对于3个人搬,你要多付333倍的工资,也就是开线程要消耗的,适量即可。 复制代码
一个字节的丢失就可能导致一个文件的损坏,可想而知要多个人一起干活必须分工明确
不然一块砖搬错了,整个文件就报废了,下面看一下线程怎么分工,拿3个线程下载1000字节来说:
2.多线程下载的流程图
整体架构和单线程的下载类似,最大的改变的是:
由于多线程需要管理,使用一个DownLoadTask来管理一个文件的所有下载线程,其中封装了下载和暂停逻辑。 在DownLoadTask#download方法里,如果数据库没有信息,则进行线程的任务分配及线程信息的创建,并插入数据库。 DownLoadThread作为DownLoadTask的内部类,方便使用。最后在download方法一一创建DownLoadThread并开启, 将DownLoadThread存入集合管理,在DownLoadTask#pause方法里,将集合中的线程全部关闭即可 复制代码
二、代码实现:
1.RecyclerView的使用:
用RecyclerView将单个条目便成一个列表界面
1).增加URL常量
//掘金下载地址 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"; 复制代码
2).初始化数据
/** * 初始化数据 * * @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; } 复制代码
3).RecyclerView适配器
上篇在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); } } 复制代码
4).设置适配器:MainActivity中
mAdapter = new RVAdapter(this, fileBeans); mIdRvPage.setAdapter(mAdapter); mIdRvPage.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)); 复制代码
2.DownLoadTask的分析:
DownLoadTask最重要的在于:管理一个文件下载的所有线程,download是暴漏出的下载方法。pause停止。
比如开三个线程,该类的mDownLoadThreads就将线程存到集合里,以便使用
DownLoadThread 和上篇核心逻辑基本一至,这里作为DownLoadTask内部类,方便使用其中的变量
还有就是由于是多线程,每个执行的快慢不定,判断结束的标识必须三个线程都结束才代表下载结束
- 三个线程共同工作
- 暂停时数据库情况
/** * 作者:张风捷特烈<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 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<>(); } /** * 下载逻辑 */ 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);//开始下载时将该线程加入集合 } } 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.下载的核心逻辑 Intent intent = new Intent(Cons.ACTION_UPDATE);//更新进度的广播intent mLoadedLen.set(mLoadedLen.get() + mThreadBean.getLoadedLen()); //206-----部分内容和范围请求 不要200写顺手了... if (conn.getResponseCode() == 206) { //读取数据 is = conn.getInputStream(); byte[] buf = new byte[1024 * 4]; int len = 0; long time = System.currentTimeMillis(); while ((len = is.read(buf)) != -1) { //写入文件 raf.write(buf, 0, len); //发送广播给Activity,通知进度 mLoadedLen.set(mLoadedLen.get() + len);//累加整个文件的完成进度 //累加每个线程完成的进度 mThreadBean.setLoadedLen(mThreadBean.getLoadedLen() + len); if (System.currentTimeMillis() - time > 1500) {//减少UI的渲染速度 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); time = System.currentTimeMillis(); } //暂停保存进度到数据库 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) { //下载完成,删除线程信息 mDao.deleteThread(mThreadBean.getUrl()); //通知下载结束 Intent intent = new Intent(); intent.setAction(Cons.ACTION_FINISH);//加完成的Action intent.putExtra(Cons.SEND_FILE_BEAN, mFileBean); mContext.sendBroadcast(intent); } } } } 复制代码
3.Service 的修改
稍微不同的就是一个下载任务变成了多个下载任务,这里使用安卓特有的SparseArray来存储
/** * 作者:张风捷特烈<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; } } 复制代码
4.广播的处理:
这里多了一个下载完成的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()); } } } 复制代码
三、数据库的多线程操作注意点:
1.DownLoadDBHelper的单例
为了避免不同线程拿到的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; } 复制代码
2.在变动数据库的方法上加同步:db.DownLoadDaoImpl
避免多个线程修改数据库产生冲突
public synchronized void insertThread(ThreadBean threadBean) public synchronized void deleteThread(String url) public synchronized void updateThread(String url, int threadId, long loadedLen) 复制代码
你看完上下两篇,基本上就能够实现这样的效果了: 回过头来看一看,也并非难到无法承受的地步,多想想,思路贯通之后还是很好理解的。
以上所述就是小编给大家介绍的《Android原生下载(下篇)多文件下载+多线程下载》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- iOS原生级别后台下载详解
- Tiercel 2!完美支持原生级别后台下载
- 在生产环境中,阿里云如何构建高性能云原生容器网络?(含 PPT 下载)
- 小说下载器 ebookdownloader v1.7.5 发布:添加新下载源
- 前端培训-初级阶段-场景实战(2019-06-06)-下载文件&下载进度
- Windows 10 解决无法完整下载安装语言包(日语输入法无法下载使用)
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。