多线程下载文件

作者:夏至 转载请保留这段申明
http://blog.csdn.net/u011418943/article/details/56675652

项目下载地址:
https://git.oschina.net/zhengshaorui/MtlThreadDownloadFile.git

上一章,我们实现了单线程的下载,这一章,我们实现一个简单的多线程下载,不先加断线续传,下一章再加断点续传。
上一章连接:http://blog.csdn.net/u011418943/article/details/56674086

首先,文件在下载中,我们常用的是用单线程下载,这样的好处在于好控制,能够监控这个文件的下载进度等等。缺点在于,没有完全利用cpu的利用率,而且如果是大文件,下载的速度较慢。所以,我们可以通过多线程的方式,去下载文件。
实现原理是什么呢?就是把一个文件给切分几块来下载。比如一个11M的文件,我们把它分成5个部分来下载;那么它的计算公式就为
blocksize = 11%5 == 0? 11/5:11%5+1;

//每一个线程要下载的大小
 blocksize = filesize%threadcount == 0? filesize/threadcount : filesize/threadcount+1

图片摘自网络,如下:
这里写图片描述

理解了原理,我们来实现一个小demo,下载一个南方周末的apk,连为:http://images.infzm.com/mobile/infzmreader.apk
首先,先想一下,我们需要传递给线程下载的参数有哪些,无非就是 url,apk下载的路径,文件名字,和线程个数,当然可以根据需求,加你想要的参数。
首先权限先加:

<!--获取网络权限-->
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
    <!--在SDCard中创建与删除文件权限  -->
    <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>
    <!-- 往SDCard写入数据权限 -->
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <!--  从SDCard读取数据权限 -->
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
再写我们的实体类,跟上一章一样
public class FilesInfo implements Serializable {
    private String fileUrl;
    private String fileName;
    private String fileDir;
    private int threadcount;
    public FilesInfo(String fileUrl, int threadcount, String fileDir, String fileName) {
        this.fileUrl = fileUrl;
        this.threadcount = threadcount;
        this.fileDir = fileDir;
        this.fileName = fileName;
    }
    public int getThreadcount() {
        return threadcount;
    }
    public void setThreadcount(int threadcount) {
        this.threadcount = threadcount;
    }
    public int getPriority() {
        return priority;
    }
    public String getFileUrl() {
        return fileUrl;
    }
    public void setFileUrl(String fileUrl) {
        this.fileUrl = fileUrl;
    }
    public String getFileName() {
        return fileName;
    }
    public void setFileName(String fileName) {
        this.fileName = fileName;
    }
    public String getFileDir() {
        return fileDir;
    }
    public void setFileDir(String fileDir) {
        this.fileDir = fileDir;
    }
}

这里,我用到了Serializable 这个属性来传递参数,毕竟我们是通过启动service 来启动线程的,用这个传递可以少去代码臃肿。然后在 onclick 那里,启动我们的服务:

public void download(View view){
       FilesInfo filesInfo = new FilesInfo(apkUrl,5,fileDir,"南方周末");
        Intent downloadservice = new Intent(MainActivity.this,DownloadManager.class);
        downloadservice.putExtra("fileinfo",filesInfo);
        startService(downloadservice);
    }

接着,我们在onStartCommand 这里获取我们的数据,然后开启我们的线程。

public int onStartCommand(Intent intent, int flags, int startId) {
        FilesInfo mFilesInfo = (FilesInfo) intent.getSerializableExtra("fileinfo");
        startdownload(mFilesInfo.getFileUrl(),mFilesInfo.getFileDir(),mFilesInfo.getFileName(),
                mFilesInfo.getThreadcount(),mFilesInfo.getPriority());
        return super.onStartCommand(intent, flags, startId);
    }

其中 startdownload 如下:

private void startdownload(String apkUrl, String fileDir, String filename, int threadcount
            , int priority) {
        File dir  = new File(fileDir);
        if (!dir.exists()){
            dir.mkdir();
        }
        String filepath =  filename;
            downloadManager downloadManager = new    downloadManager(apkUrl,dir,filepath,threadcount,priority);
        downloadManger.start();
    }

downloadManager 是我们下载的实现方法,代码如下:

class downloadManager extends  Thread{
        String apkurl,filepath;
        int threadcount,priority;
        int blocksize; //每个线程需要下载的区域
        File dir;
        public downloadManager(String apkUrl,File dir, String filepath, int threadcount, int priority) {
            this.apkurl = apkUrl;
            this.filepath = filepath;
            this.threadcount = threadcount;
            this.priority = priority;
            this.dir = dir;
        }
        @Override
        public void run() {
            HttpURLConnection con = null;
            try {
                DownloadTask[]  mDownloadTasks = new DownloadTask[threadcount];
                URL url = new URL(this.apkurl);
                con = (HttpURLConnection) url.openConnection();
                con.setRequestMethod("GET");
                con.setReadTimeout(5000);
                con.setConnectTimeout(5000);
                int filesize = con.getContentLength(); //获取文件总长度
                if (filesize <= 0){
                    Log.d(TAG, "文件获取失败");
                    return;
                }
                //每一个线程要下载的大小
                blocksize = filesize%threadcount == 0? filesize/threadcount : filesize/threadcount+1;
                Log.d(TAG, "filesize: "+filesize+" blocksize: "+blocksize);
                File file = new File(this.filepath);
                for (int i = 0; i < mDownloadTasks.length; i++) { //根据线程数,开启线程
                    mDownloadTasks[i] = new DownloadTask(url,dir,filepath,blocksize,
                            i,threadcount,filesize);

                      mDownloadTasks[i].start();
                }
                boolean isfinished = false;
                int filelength = 0;
                while(!isfinished){  //这里用来判断是否下载完成,不加去掉也行
                    isfinished = true;
                    filelength = 0;
                    for (int i = 0; i < mDownloadTasks.length; i++) {
                        if (!mDownloadTasks[i].isCompleted()){
                            isfinished = false;
                        }else{
                            filelength += mDownloadTasks[i].getDownloadLength();
                        }
                    }
                }
                Log.d(TAG, "finished: "+filelength+" "+filesize);
            } catch (Exception e) {
                e.printStackTrace();
            }
            super.run();
        }
    }

其中,DownloadTask 为我们的下载类,代码如下:

public class DownloadTask extends Thread{
    private static final String TAG = "zsr";
    public DownloadTask(){
    }
    private boolean isCompleted = false;
    private URL connectUrl;
    private File dir;
    private int blocksize,threadid;
    private int downloadLength = 0;
    private int threadcount;
    private int filelength;
    private String filename;
    public DownloadTask(URL connectUrl, File dir, String filename,int blocksize, int threadid,
                        int threadcount,int filelength){
        this.connectUrl = connectUrl;
        this.dir = dir;
        this.blocksize = blocksize;
        this.threadid = threadid;
        this.threadcount = threadcount;
        this.filelength = filelength;
        this.filename = filename;
    }
    @Override
    public void run() {
        RandomAccessFile raf = null;
        BufferedInputStream bis = null;
        HttpURLConnection con = null;
        try {
            con = (HttpURLConnection) connectUrl.openConnection();
            con.setRequestMethod("GET");
            con.setReadTimeout(5000);
            con.setConnectTimeout(5000);
            int startpos = blocksize * threadid; //开始位置
            int endpos = blocksize * (threadid+1) -1; //结束位置
            if (threadid == (threadcount -1)) endpos = filelength;
            //设置下载位置
            con.setRequestProperty("Range", "bytes="+startpos+"-"+endpos);
            Log.d(TAG, "range: "+startpos+" "+endpos);
            //设置文件的写入位置
            File file = new File(dir,filename);
            raf = new RandomAccessFile(file,"rwd");
            raf.seek(startpos);
            //读数据
            bis = new BufferedInputStream(con.getInputStream());
            byte[] bytes = new byte[1024*4];
            int len = -1;
            while( (len = bis.read(bytes)) !=-1 ){
                raf.write(bytes,0,len);
                downloadLength += len;
            }
            isCompleted = true;
            Log.d(TAG, "finish: "+downloadLength);
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if (bis != null) try {
                bis.close();
                if (raf != null) raf.close();
             } catch (IOException e) {
                e.printStackTrace();
            }
            if (con != null) con.disconnect();
        }
        super.run();
    }
    public boolean isCompleted() {
        return isCompleted;
    }
    public int getDownloadLength(){
        return downloadLength;
    }
}

代码,没啥好解释的,跟一般的文件下载差不多,只不过这里是分块的形式,所以,random 属性那里,start 和end 要做一下判断:

    int startpos = blocksize * threadid; //开始位置
            int endpos = blocksize * (threadid+1) -1; //结束位置
            if (threadid == (threadcount -1)) endpos = filelength; //最后一个结尾,以文件的长度结尾

这里写图片描述

2、用线程池管理

在上面中,我们都是直接 start() 启动我们的线程,在多个线程的线程的时候,我们这种做法是很不对的,因为线程的开启和销毁,是需要时间的,这个时候,线程就会在内存一直占用着,这样的后果是你的内存一直在上升;假如你把这个回收了,过一会又要启动了,这个启动和销毁也是耗资源的。
这个时候,线程池的作用就出来了,顾名思义,线程池,就是线程组织的一个池塘,它由 Excutors 创建和管理。用线程池的好处如下:

  1. 在同事并发执行线程时,提高API的性能
  2. 减少线程的重复开启、销毁
  3. 减少内存消耗,控制线程数量

线程池,共有四种,分别如下:newCachedThreadPool() :

  • cacheThreadPool 缓存型池子,先查看池中有没有以前建立的线程,如果有,就reuse.如果没有,就建一个新的线程加入池中。能reuse的线程,必须是timeout IDLE内的池中线程,缺省timeout是60s,超过这个IDLE时长,线程实例将被终止及移出池。缓存型池子通常用于执行一些生存期很短的异步型任务 。
  • newFixedThreadPool() fixedThreadPool与cacheThreadPool差不多,也是能reuse就用,但不能随时建新的线程 其独特之处:任意时间点,最多只能有固定数目的活动线程存在,此时如果有新的线程要建立,只能放在另外的队列中等待,直到当前的线程中某个线程终止直接被移出池子。和cacheThreadPool不同:fixedThreadPool池线程数固定,但是0秒IDLE(无IDLE)。这也就意味着创建的线程会一直存在。所以fixedThreadPool多数针对一些很稳定很固定的正规并发线程,多用于服务器。
  • newScheduledThreadPool() 调度型线程池。这个池子里的线程可以按schedule依次delay执行,或周期执行 。0秒IDLE(无IDLE)。
  • SingleThreadExecutor 单例线程,任意时间池中只能有一个线程 。用的是和cache池和fixed池相同的底层池,但线程数目是1-1,0秒IDLE(无IDLE)。

由于,我们是用到一个下载器的作用,所以,我们这里采用 newFixedThreadPool 固定并发的线程池,那么怎么用?很简单,把线程的start() 方法,用 线程池的 excute 就行了。如:

private ExecutorService mExecutorService = Executors.newFixedThreadPool(5);//固定线程并发数量为5
// mDownloadTasks[i].start();
 mExecutorService.execute(mDownloadTasks[i]);

log 如下:
这里写图片描述

  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值