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

栏目: Android · 发布时间: 7年前

内容简介:1.上篇实现了单线程的单文件下载,本篇将讲述多个文件的多线程下载,2.本篇将用到上篇之外的技术:多线程、线程池(简)、RecyclerView、数据库多线程访问下的注意点、volatile AtomicLong(简)

1.上篇实现了单线程的单文件下载,本篇将讲述多个文件的多线程下载, 在此之前希望你先弄懂上篇

2.本篇将用到上篇之外的技术:

多线程、线程池(简)、RecyclerView、数据库多线程访问下的注意点、volatile AtomicLong(简)

最终静态的效果

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

最终动态的效果

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

一、分析一下多线程下载单个文件的原理:

1.线程分工方式

大家都知道,一个文件是很多的字节组成的,字节又是由二进制的位组成,如果把一个字节当成一块砖。
那下载就像把服务器的砖头搬到手机里,然后摆在一个文件里摆好,搬完了,文件满了,任务就完成了
然后文件是电影就能播,是图片就能看,app就能安装。

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

一个字节的丢失就可能导致一个文件的损坏,可想而知要多个人一起干活必须分工明确

不然一块砖搬错了,整个文件就报废了,下面看一下线程怎么分工,拿3个线程下载1000字节来说:

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

2.多线程下载的流程图

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

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

二、代码实现:

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内部类,方便使用其中的变量

还有就是由于是多线程,每个执行的快慢不定,判断结束的标识必须三个线程都结束才代表下载结束

  • 三个线程共同工作
Android原生下载(下篇)多文件下载+多线程下载
  • 暂停时数据库情况
Android原生下载(下篇)多文件下载+多线程下载
/**
 * 作者:张风捷特烈<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原生下载(下篇)多文件下载+多线程下载

以上所述就是小编给大家介绍的《Android原生下载(下篇)多文件下载+多线程下载》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Pro Git

Pro Git

Scott Chacon / Apress / 2009-8-27 / USD 34.99

Git is the version control system developed by Linus Torvalds for Linux kernel development. It took the open source world by storm since its inception in 2005, and is used by small development shops a......一起来看看 《Pro Git》 这本书的介绍吧!

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具