多文件多线程断点下载

作者: 一只胖胖胖胖猿 | 来源:发表于2017-08-28 17:07 被阅读75次

    最近因为一些原因,需要用到多文件多线程断点下载文件,所以四处查找资料然后做了一个Demo. 本项目主要参考的是简书宝塔上的猫-《Android实战:多线程多文件断点续传下载+通知栏控制》
    本项目GitHub地址:https://github.com/JonyZeng/JonyDownload
    对于多线程下载文件,我们应该首先需要了解单线程下载文件的原理,多线程下载就是把文件分为几份,每一份由一个线程去下载。然后将每一个线程单独下载的文件保存在一起就实现了多线程下载。
    断点下载相对于理解比较轻松,每一次暂停线程的时候,将当前下载的进度保存,下次继续从保存的进度进行下载。
    而多文件多线程下载的原理是基于多线程单文件下载的基础上,首先确定多文件同时下载需要多少个线程,然后再确定每一个文件多线程同时下载的线程数。
    本项目是采用MVP模式进行架构的,因为本人对MVP也是刚了解,所以选择它来进行架构。

    image.png
    1. 首先在布局文件中进行主界面activity_main的布局。
    <?xml version="1.0" encoding="utf-8"?>
    <android.support.constraint.ConstraintLayout
       xmlns:android="http://schemas.android.com/apk/res/android"
       xmlns:app="http://schemas.android.com/apk/res-auto"
       xmlns:tools="http://schemas.android.com/tools"
       android:layout_width="match_parent"
       android:layout_height="match_parent"
       tools:context="com.example.jonyz.jonydownload.MainActivity">
    
       <ListView
           android:layout_width="match_parent"
           android:layout_height="match_parent"
           android:id="@+id/LV_down"
         >
    
       </ListView>
    
    </android.support.constraint.ConstraintLayout>
    

    然后设置listView条目的布局item.xml

    <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            >
            <TextView
                android:id="@+id/Tv_fileName"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:hint="下载的文件名"
                />
    
            <ProgressBar
                android:id="@+id/Pb_down"
                style="?android:attr/progressBarStyleHorizontal"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_below="@+id/file_textview" />
        </LinearLayout>
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            >
            <Button
                android:id="@+id/Btn_start"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_below="@+id/progressBar2"
                android:layout_toLeftOf="@+id/stop_button"
                android:text="开始" />
    
            <Button
                android:id="@+id/Btn_stop"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignParentRight="true"
                android:layout_below="@+id/progressBar2"
                android:text="暂停" />
        </LinearLayout>
    
    

    2.布局完成之后,按照国际惯例,我们需要给ListView设置适配器,并优化。由于我们listView是需要显示下载文件的信息。所以我们需要创建一个参数类型是FileBean的list。 这里简单的贴一些FileBean(需要序列化)的参数代码

    public class FileBean implements Serializable {
        public String fileName;
        public Integer fileSize;
        public Integer filePause;//下载暂停位置
        public Integer DownSize; //finished
        public Integer id;
        public String Url;
        /**
         *
         * @param fileName 文件名
         * @param fileSize 文件大小
         * @param downSize 文件下载
         * @param id       文件ID
         * @param url       文件下载地址
         */
        public FileBean(Integer id,String fileName, Integer fileSize, Integer downSize,  String url) {
            this.fileName = fileName;
            this.fileSize = fileSize;
            DownSize = downSize;
            this.id = id;
            Url = url;
        }
    

    之后就到了最精彩的地方了。对getView的优化和对控件的赋值。

    /**
         * 进行数据和View的适配
         *
         * @param i
         * @param view
         * @param viewGroup
         * @return
         */
        @Override
        public View getView(int i, View view, ViewGroup viewGroup) {
            final FileBean fileBean=list.get(i);
            if (view==null){
                view=inflater.inflate(R.layout.item,null);
                viewHolder = new MyViewHolder();
                viewHolder.mTvfileName=(TextView)view.findViewById(R.id.Tv_fileName);
                viewHolder.mBarDown=(ProgressBar)view.findViewById(R.id.Pb_down);
                viewHolder.mBtnstart=(Button)view.findViewById(R.id.Btn_start);
                viewHolder.mBtnstop=(Button) view.findViewById(R.id.Btn_stop);
                viewHolder.mTvfileName.setText(list.get(i).getFileName());
                viewHolder.mBarDown.setProgress(fileBean.getDownSize());
                viewHolder.mBtnstart.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View view) {
                        //// TODO: 2017/8/23 开始下载
                        presenter = new DownloadPresenter();
                        presenter.startDownload(fileBean,context);
                        Log.d(TAG, "onClick:开始");
                        Toast.makeText(context, "点击了开始", Toast.LENGTH_SHORT).show();
    
                    }
                });
                viewHolder.mBtnstop.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View view) {
                        //// TODO: 2017/8/23 暂停下载
                        presenter.stopDownload(fileBean,context);
                        Log.d(TAG, "onClick:暂停");
                        Toast.makeText(context, "点击了暂停", Toast.LENGTH_SHORT).show();
                    }
                });
                view.setTag(viewHolder);
            }else {
                viewHolder= (MyViewHolder) view.getTag();
            }
    
            viewHolder.mBarDown.setProgress(fileBean.getDownSize());
            return view;
    
        }
    
    

    根据ListView的优化需求,需要创建一个静态的ViewHolder

    /**
         * 静态的View ,避免重复加载
         */
        static class MyViewHolder {
            TextView mTvfileName;
            ProgressBar mBarDown;
            Button mBtnstart;
            Button mBtnstop;
        }
    

    两个按钮的点击事件的逻辑代码是通过MVP的实现的。所以需要创建一个Contrast类设置接口,在model层实现代码的逻辑实现。

    image.png


    最终在model层里面开启服务。通过intent传递值过去

    private static final String TAG = DownloadModel.class.getSimpleName();
        private Intent intent;
    
        @Override
        public void startDownload(FileBean fileBean, Context context) {
            //开始下载
            intent = new Intent(context,DownloadService.class);
            intent.setAction(Config.ACTION_START);
            intent.putExtra("fileBean",fileBean);
            Log.d(TAG, "startDownload: 开启下载的服务");
            context.startService(intent);
    
        }
    
        @Override
        public void stopDownload(FileBean fileBean, Context context) {
            //暂停下载
            Intent intent = new Intent(context, DownloadService.class);
            intent.setAction(Config.ACTION_STOP);
            intent.putExtra("fileBean", fileBean);
            Log.d(TAG, "stopDownload: 停止下载服务");
            context.startService(intent);
        }
    
    1. 多线程断点下载服务。创建一个类继承Service,在AndroidMainfest.xml中声明。重写onStartCommand方法,在里面根据不同的intent值进行不同的操作。在开始下载的时候,需要通过线程池开启一个线程,所以我们需要自定义一个类继承Thread,在里面实现下载的代码。
      自定义线程类所需要的参数
    private URL url;
            private int responseCode;
            private int length = 0;   //判断长度
            private RandomAccessFile randomAccessFile;
            private FileBean fileBean = null;
    

    在重写的run方法中,进行对当前点击下载文件的进行获取和设置下载文件的路径。

    url = new URL(fileBean.getUrl());
                    connection = (HttpURLConnection) url.openConnection();
                    connection.setConnectTimeout(2000);
                    connection.setRequestMethod("GET");
                    Log.i(TAG, "run:获取网络请求");
                    responseCode = connection.getResponseCode();
                    if (responseCode == HttpURLConnection.HTTP_OK) {
                        length = connection.getContentLength();
                    }else{
                        Toast.makeText(DownloadService.this, "请检查网络情况", Toast.LENGTH_SHORT).show();
                    }
                    if (length <= 0) {//说明下载文件不存在
                        return;
                    }
                    //文件存在,开始下载
                    //判断文件路径是否存在
                    File dir = new File(Config.DownloadPath);
                    if (!dir.exists()) {
                        dir.mkdir();
                    }
    

    通过RandomAccessFile在内存中根据当前文件大小进行占位。

     File file = new File(dir, fileBean.getFileName());
                    randomAccessFile = new RandomAccessFile(file, "rwd");//随机访问,随时读写
                    randomAccessFile.setLength(length);
                    fileBean.setDownSize(length);   //设置文件的大小
    

    将通过handler将fileBean传递。

    Handler handler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                super.handleMessage(msg);
            switch (msg.what){
                case Config.MSG_INIT://开始下载
                    //调用DownloadTask的download下载
                    fileBean= (FileBean) msg.obj;
                    DownloadTask task = new DownloadTask(DownloadService.this, fileBean, 3);
                    task.download();
                    taskMap.put(fileBean.getId(),task);
                    Intent intent = new Intent(Config.ACTION_START);
                    intent.putExtra("fileBean",fileBean);
                    sendBroadcast(intent);
            }
            }
        };
    

    创建一个下载任务类DownloadTask,在里面执行下载任务。
    DownloadTask所需要的一些参数

        private static final String TAG = DownloadTask.class.getSimpleName();
        public static ExecutorService cachedThreadPool = Executors.newCachedThreadPool();  //创建一个线程池,每次开启线程通过线程池调用。
        public List<UrlBean> urlBeanList;  //一个UrlBean的list集合。
        private FileBean fileBean;    //下载文件的信息类
        public DBUtil dbUtil;          //数据库操作工具类
        private ThreadUtil threadUtil;    //下载线程的信息类
        public int threadCount=1;  
        public Context context;
        private List<DownloadList> downloadLists=null;
        private UrlBean urlBean;
        public boolean isonPause=false;//判断是否暂停
    

    创建一个数据库线程下载帮助类,里面实现线程对数据库的增删查改。查询数据库线程的方法

     /**
         * 查询数据库线程
         * @param url
         * @return
         */
        public synchronized List<UrlBean>queryThread(String url){
            Log.i(TAG, "queryThread: url: " + url);
            readableDatabase=dbUtil.getReadableDatabase();
            List<UrlBean> list=new ArrayList<>();
            Cursor cursor=readableDatabase.query("download_info", null, "url = ?", new String[] { url }, null, null, null);
            while (cursor.moveToNext()){
                UrlBean urlBean= new UrlBean();
                urlBean.setId(cursor.getInt(cursor.getColumnIndex("thread_id")));
                urlBean.setUrl(cursor.getString(cursor.getColumnIndex("url")));
                urlBean.setStart(cursor.getInt(cursor.getColumnIndex("start")));
                urlBean.setEnd(cursor.getInt(cursor.getColumnIndex("end")));
                urlBean.setFinished(cursor.getInt(cursor.getColumnIndex("finished")));
                list.add(urlBean);
            }
    
            cursor.close();
            readableDatabase.close();
    
            //readableDatabase.query("urlBean",)
            return list;
        }
    

    创建一个download方法,方便其他地方调用。在这个方法里面实现查询文件的大小和开启多个线程,分块下载。

        public void download(){
            Log.i(TAG, "download:"+fileBean.getUrl());
            urlBeanList =threadUtil.queryThread(fileBean.getUrl());
            //获取到数据库里面查询的list
            if (urlBeanList.size()==0){ //数据库中不存在,第一次下载
                //获取文件大小
                int length=fileBean.getDownSize();
                //获取需要分的模块
                int block=length/threadCount;
    
                for (int i = 0; i < threadCount; i++) {
                    //确定开始下载的位置
                    int star=block*i;
                    //确定结束下载的位置
                    int end=(i+1)*block-1;
                    if (i==threadCount-1){  // //最后一个线程下载结束的位置。
                        end=length-1;
                    }
                    //开启线程
                    urlBean = new UrlBean(fileBean.getUrl(),i,star,end,0);
                    urlBeanList.add(urlBean);
                }
            }
            Log.d(TAG, "download:");
            //下载文件线程的内部类
            downloadLists = new ArrayList<>();
            for (UrlBean urlBean:urlBeanList) {
                DownloadList dowmload=new DownloadList(urlBean);
                DownloadTask.cachedThreadPool.execute(dowmload);
                downloadLists.add(dowmload);
                threadUtil.insertThread(urlBean);
            }
        }
    

    创建一个内部类,用于将下载下来的文件,写入到内存中去。
    部分参数

            private HttpURLConnection urlConnection=null;
            private RandomAccessFile accessFile=null;
            private InputStream inputStream=null;
    
            private File file;
            private Integer finished=0;
            private Intent intent;
    
            private UrlBean urlBean;
            private boolean isFinished=false;
    

    重写run 方法,在里面请求网络,并且将请求下来的数据通过RandomAccessFile写入到内存中去。

     @Override
            public void run() {//执行下载耗时操作
                //获取http对象
                try {
                    URL url= new URL(fileBean.getUrl());
                    try {
                        urlConnection = (HttpURLConnection) url.openConnection();
                        urlConnection.setConnectTimeout(2000);
                        urlConnection.setRequestMethod("GET");
                        //设置下载的头部
                        int start=urlBean.getStart()+urlBean.getFinished();
                        //设置下载结束的位置
                        urlConnection.setRequestProperty("Range", "bytes=" + start + "-" + urlBean.getEnd());
                        //新建文件对象
                        file = new File(Config.DownloadPath, fileBean.getFileName());
                        //随机访问读写对象
                        accessFile = new RandomAccessFile(file, "rwd");
                        accessFile.seek(start);
                        //刷新当前以及下载的大小
                        finished +=urlBean.getFinished();
                        intent = new Intent();
                        intent.setAction(Config.ACTION_UPDATE);
                        int respCode=urlConnection.getResponseCode();
                        if (respCode==HttpURLConnection.HTTP_PARTIAL){  //请求成功
                            //获取输入流对象
                            inputStream = urlConnection.getInputStream();
                            //设置一个byte数组,中转数据
                            byte[] bytes = new byte[1024];
                            int length=-1;
                            //定义UI刷新时间
                            long time=System.currentTimeMillis();
                            while ((length=inputStream.read(bytes))!=-1){
                                accessFile.write(bytes,0,length);
                                //实时获取下载进度,刷新UI
                                finished+=length;
                                urlBean.setFinished(urlBean.getFinished()+length);
                                if (System.currentTimeMillis()-time>500){
                                    time=System.currentTimeMillis();
                                    intent.putExtra("finished",finished);
                                    Log.d(TAG, "finished:"+finished);
                                    intent.putExtra("id",fileBean.getId());
                                    Log.d(TAG, "finished"+fileBean.getId());
                                    context.sendBroadcast(intent);
                                }
                                if (isonPause){
                                    threadUtil.updateThread(urlBean);
                                    return;
                                }
    
                            }
    
    
                        }
                        //当前线程是否下载完成
                        isFinished = true;
                        //判断所有线程是否下载完成
                        checkAllFinished();
    

    判断所有的线是否已经下载完成。

    private synchronized void checkAllFinished() {
                boolean allFinished=true;
                for (DownloadList down:downloadLists) {
                    if (!down.isFinished)
                    allFinished=false;
                    break;
                }
                if (allFinished==true){
                    Log.d(TAG, "checkAllFinished: 下载完成");
                    threadUtil.delThread(fileBean.getUrl());
                    intent=new Intent(Config.ACTION_FINISHED);
                    intent.putExtra("urlBean",urlBean);
                    context.sendBroadcast(intent);
                }
            }
    

    4.好了,终于到最后MainActivity里面的一些逻辑实现了。

    
        private static final String TAG = MainActivity.class.getSimpleName();
        ListView listView;
        private FileAdapter mAdapter;
        private UrlBean urlBean;
        private List<FileBean> list;
        private UIRecive mRecive;
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            init();
        }
    
        /**
         * 初始化
         */
        private void init() {
            initData();
            initView();
            mAdapter = new FileAdapter(this, list);
            listView.setAdapter(mAdapter);
            mRecive=new UIRecive();
            IntentFilter intentFilter = new IntentFilter();
            intentFilter.addAction(Config.ACTION_UPDATE);
            intentFilter.addAction(Config.ACTION_FINISHED);
            intentFilter.addAction(Config.ACTION_START);
            registerReceiver(mRecive, intentFilter);
        }
    
        /**
         * 初始化控件
         */
        private void initView() {
            listView = (ListView) findViewById(R.id.LV_down);
    
        }
    
        /**
         * 初始化数据
         */
        private void initData() {
            //文件类集合
            list = new ArrayList<>();
            list.add(new FileBean(0,getfileName(Config.URLONE),0,0,Config.URLONE)); //(文件ID,文件名,文件大小,已经下载大小,URL)
            list.add(new FileBean(1,getfileName(Config.URLTWO),0,0,Config.URLTWO)); //(文件ID,文件名,文件大小,已经下载大小,URL)
            list.add(new FileBean(2,getfileName(Config.URLTHREE),0,0,Config.URLTHREE)); //(文件ID,文件名,文件大小,已经下载大小,URL)
            list.add(new FileBean(3,getfileName(Config.URLFOUR),0,0,Config.URLFOUR)); //(文件ID,文件名,文件大小,已经下载大小,URL)
        }
    
        @Override
        public String getfileName(String url) {
            return url.substring(url.lastIndexOf("/") + 1);
        }
    
        @Override
        protected void onDestroy() {
            unregisterReceiver(mRecive);
            super.onDestroy();
        }
        /**
         * 刷新Ui
         */
        private class UIRecive extends BroadcastReceiver{
    
    
            @Override
            public void onReceive(Context context, Intent intent) {  //接收到传递过来的数据
                if (Config.ACTION_UPDATE.equals(intent.getAction())) {
                    // 更新进度条的时候
                    int finished = intent.getIntExtra("finished", 0);
                    Log.d(TAG, "onReceive:finsihed"+finished);
                    int id = intent.getIntExtra("id", 0);
                    mAdapter.updataProgress(id, finished);
    
                } else if (Config.ACTION_FINISHED.equals(intent.getAction())){
                    // 下载结束的时候
                    FileBean fileBean = (FileBean) intent.getSerializableExtra("fileBean");    
                    mAdapter.updataProgress(fileBean.getId(), 0);
                    Toast.makeText(MainActivity.this, list.get(fileBean.getId()).getFileName() + "下载完毕", Toast.LENGTH_SHORT).show();
                }
            }
        }
    

    相关文章

      网友评论

      本文标题:多文件多线程断点下载

      本文链接:https://www.haomeiwen.com/subject/sbhgdxtx.html