零, 前言
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)
你看完上下两篇, 基本上就能够实现这样的效果了: 回过头来看一看, 也并非难到无法承受的地步, 多想想, 思路贯通之后还是很好理解的.
后记: 捷文规范
1. 本文成长记录及勘误表
项目源码 | 日期 | 备注 |
---|---|---|
V0.1-- 无 | 2018-11-13 | Android 原生下载(下篇)多文件下载 + 多线程下载 https://www.jianshu.com/p/e8d8bba01801 |
2. 更多关于我
笔名 | 微信 | 爱好 | |
---|---|---|---|
张风捷特烈 | 1981462002 | zdl1994328 | 语言 |
我的 github https://github.com/toly1994328 | 我的简书 https://www.jianshu.com/u/e4e52c116681 | 我的 CSDN https://blog.csdn.net/qq_30447263 | 个人网站 http://www.toly1994.com |
3. 声明
1---- 本文由张风捷特烈原创, 转载请注明
2---- 欢迎广大编程爱好者共同交流
3---- 个人能力有限, 如有不正之处欢迎大家批评指证, 必定虚心改正
4---- 看到这里, 我在此感谢你的喜欢与支持
来源: https://juejin.im/post/5beb6e25e51d4575f023ca1f