前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Android原生下载(下篇)多文件下载+多线程下载

Android原生下载(下篇)多文件下载+多线程下载

作者头像
张风捷特烈
发布2018-12-12 15:29:48
15.5K1
发布2018-12-12 15:29:48
举报
零、前言

1.上篇实现了单线程的单文件下载,本篇将讲述多文件的多线程下载,在此之前希望你先弄懂上篇 2.本篇将用到上篇之外的技术: 多线程、线程池(简)、RecyclerView、数据库多线程访问下的注意点、volatile AtomicLong(简)

最终静态的效果

最终效果.png

最终动态的效果

动态效果图.gif


一、分析一下多线程下载单个文件的原理:
1.线程分工方式
代码语言:javascript
复制
大家都知道,一个文件是很多的字节组成的,字节又是由二进制的位组成,如果把一个字节当成一块砖。
那下载就像把服务器的砖头搬到手机里,然后摆在一个文件里摆好,搬完了,文件满了,任务就完成了
然后文件是电影就能播,是图片就能看,app就能安装。

对于下载一个文件,上篇讲的单线程下载相当于一个人一块一块地搬。
而本篇的多线程则是雇几个人来搬,可想而知效率是更高的。
那我开一千个线程岂不是秒下?如果你要搬1000块砖,找1000个人,效率固然高,
但人家也不是白干活,相对于3个人搬,你要多付333倍的工资,也就是开线程要消耗的,适量即可。

一个字节的丢失就可能导致一个文件的损坏,可想而知要多个人一起干活必须分工明确 不然一块砖搬错了,整个文件就报废了,下面看一下线程怎么分工,拿3个线程下载1000字节来说:

多线程下载分析.png

2.多线程下载的流程图

整体架构和单线程的下载类似,最大的改变的是:

代码语言:javascript
复制
由于多线程需要管理,使用一个DownLoadTask来管理一个文件的所有下载线程,其中封装了下载和暂停逻辑。  
在DownLoadTask#download方法里,如果数据库没有信息,则进行线程的任务分配及线程信息的创建,并插入数据库。
DownLoadThread作为DownLoadTask的内部类,方便使用。最后在download方法一一创建DownLoadThread并开启,
将DownLoadThread存入集合管理,在DownLoadTask#pause方法里,将集合中的线程全部关闭即可

多线程下载流程图.png


二、代码实现:
1.RecyclerView的使用:

用RecyclerView将单个条目便成一个列表界面

1).增加URL常量
代码语言:javascript
复制
    //掘金下载地址
    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).初始化数据
代码语言:javascript
复制
/**
 * 初始化数据
 *
 * @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里

代码语言:javascript
复制
/**
 * 作者:张风捷特烈<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中
代码语言:javascript
复制
mAdapter = new RVAdapter(this, fileBeans);
mIdRvPage.setAdapter(mAdapter);
mIdRvPage.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false));

2.DownLoadTask的分析:

DownLoadTask最重要的在于:管理一个文件下载的所有线程,download是暴漏出的下载方法。pause停止。 比如开三个线程,该类的mDownLoadThreads就将线程存到集合里,以便使用 DownLoadThread 和上篇核心逻辑基本一至,这里作为DownLoadTask内部类,方便使用其中的变量 还有就是由于是多线程,每个执行的快慢不定,判断结束的标识必须三个线程都结束才代表下载结束

  • 三个线程共同工作

三个线程共同工作.png

  • 暂停时数据库情况

暂停时数据库情况.png

代码语言:javascript
复制
/**
 * 作者:张风捷特烈<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来存储

代码语言:javascript
复制
/**
 * 作者:张风捷特烈<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刷新视图

代码语言:javascript
复制
/**
 * 作者:张风捷特烈<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对象不同,这里使用单例模式

代码语言:javascript
复制
    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

避免多个线程修改数据库产生冲突

代码语言:javascript
复制
 public synchronized void insertThread(ThreadBean threadBean)
 public synchronized void deleteThread(String url)
 public synchronized void updateThread(String url, int threadId, long loadedLen)

你看完上下两篇,基本上就能够实现这样的效果了: 回过头来看一看,也并非难到无法承受的地步,多想想,思路贯通之后还是很好理解的。

动态效果图.gif


本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2018.11.13 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 零、前言
    • 最终静态的效果
      • 最终动态的效果
      • 一、分析一下多线程下载单个文件的原理:
        • 1.线程分工方式
          • 2.多线程下载的流程图
          • 二、代码实现:
            • 1.RecyclerView的使用:
              • 1).增加URL常量
            • 2).初始化数据
              • 3).RecyclerView适配器
              • 4).设置适配器:MainActivity中
            • 2.DownLoadTask的分析:
              • 3.Service 的修改
                • 4.广播的处理:
                • 三、数据库的多线程操作注意点:
                  • 1.DownLoadDBHelper的单例
                    • 2.在变动数据库的方法上加同步:db.DownLoadDaoImpl
                    相关产品与服务
                    数据库
                    云数据库为企业提供了完善的关系型数据库、非关系型数据库、分析型数据库和数据库生态工具。您可以通过产品选择和组合搭建,轻松实现高可靠、高可用性、高性能等数据库需求。云数据库服务也可大幅减少您的运维工作量,更专注于业务发展,让企业一站式享受数据上云及分布式架构的技术红利!
                    领券
                    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档