多文件多线程断点下载

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

多文件多线程断点下载_第1张图片
image.png
  1. 首先在布局文件中进行主界面activity_main的布局。



   

   


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


        

        
    
    
        

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层实现代码的逻辑实现。

多文件多线程断点下载_第2张图片
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 urlBeanList;  //一个UrlBean的list集合。
    private FileBean fileBean;    //下载文件的信息类
    public DBUtil dbUtil;          //数据库操作工具类
    private ThreadUtil threadUtil;    //下载线程的信息类
    public int threadCount=1;  
    public Context context;
    private List downloadLists=null;
    private UrlBean urlBean;
    public boolean isonPause=false;//判断是否暂停

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

 /**
     * 查询数据库线程
     * @param url
     * @return
     */
    public synchronized ListqueryThread(String url){
        Log.i(TAG, "queryThread: url: " + url);
        readableDatabase=dbUtil.getReadableDatabase();
        List 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 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();
            }
        }
    }

你可能感兴趣的:(多文件多线程断点下载)