多文件多线程断点下载

最近因为一些原因,需要用到多文件多线程断点下载文件,所以四处查找资料然后做了一个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();
            }
        }
    }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容