教你如何在 Android 使用多线程下载文件

教你如何在 Android 使用多线程下载文件

===============================================

前言

在 Android 日常开发中,我们会经常遇到下载文件需求,这里我们也可以用系统自带的 api `DownloadManager` 来解决这个问题,当然我们也可以自己来写。在这里我将教大家如何在 Android 使用多线程下载文件。

实现原理

  1. 获取目标文件的文件大小
  2. 根据线程的个数以及文件大小来分配每个线程下载文件的大小


    如:文件大小:9M 线程个数:3,那么每条线程下载的大小为 3M。


    在这里给出计算公式:blockSize=totalSize%countThread==0?totalSize/countThread:totalSize/countThread+1 ----blockSize 为每个线程下载的大小 totalSize 文件大小 countThread 线程个数
    3.开启线程下载(这里要处理比较多的事)

具体实现

1.获取文件的大小

这一步比较简单我直接给出代码:

	 URL url = null;
    HttpURLConnection http = null;
    try {
        url = new URL(this.apk_url);
        http = (HttpURLConnection) url
                .openConnection();
        http.setConnectTimeout(5 * 1000);
        http.setReadTimeout(5 * 1000);
        http.setRequestMethod("GET");
        if (http.getResponseCode() == 200) {
            this.filesize = http.getContentLength();//文件大小
        } else {
            this.filesize = -1;
        }
    } catch (Exception e) {
        e.printStackTrace();
        this.filesize = -1;
    } finally {
        http.disconnect();
    }

2.分配线程

既然要对各个线程分配对应的下载大小,我们就有必要知道各个线程对应的信息,那么我么先来定义 bean 类来表示这些信息

package com.h.kidbot.download;
public class DownLoadInfo {
	private int threadid;//线程id
	private long startpos;//下载的起始位置
	private long endpos;//下载的结束位置
	private long block;//每条下载的大小
	private long downpos;//该条线程已经下载的大小
	private String downloadurl;//下载地址

	public int getThreadid() {
    	return threadid;
	}

	public void setThreadid(int threadid) {
    	this.threadid = threadid;
	}

	public long getStartpos() {
    	return startpos;
	}

	public void setStartpos(long startpos) {
   		this.startpos = startpos;
	}

	public long getEndpos() {
    	return endpos;
	}	

	public void setEndpos(long endpos) {
    	this.endpos = endpos;
	}

	public long getBlock() {
    	return block;
	}

	public void setBlock(long block) {
    	this.block = block;
	}

	public long getDownpos() {
    	return downpos;
	}

	public void setDownpos(long downpos) {
    	this.downpos = downpos;
	}

	public String getDownloadurl() {
    	return downloadurl;
	}

	public void setDownloadurl(String downloadurl) {
    	this.downloadurl = downloadurl;
	}
}

定义好了这个类我们就可以根据刚才获取的文件大小来分配单个线程文件下载的大小了,为了方便起见呢 我就设置一条线程吧!

 for (int i = 0; i < this.threadcount; i++) {
        DownLoadInfo info = new DownLoadInfo();
        long startpos = 0, endpos = 0;
        if (i == this.threadcount - 1) {
            startpos = i * block;
            endpos = this.filesize - 1;
        } else {
            startpos = i * block;
            endpos = (i + 1) * block - 1;
        }
        info.setBlock(block);
        info.setDownpos(0);
        info.setStartpos(startpos);
        info.setEndpos(endpos);
        info.setDownloadurl(this.apk_url);
        info.setThreadid(i);
        DownDbUtils.insert(this.context, info);
        infos.add(info);
        info = null;
  }

得到每条线程对应的数据之后,我们就可以开启线程啦!下面的做法我和一般的不一样 因为我没有用到 RandomAccessFile 这个类,而是直接用 File + FileOutputStream 这两个类来实现的,原因呢 我发现 RandomAccessFile 这个类的性能非常的差,非常的差,非常的差!重要的是说三遍!因为这原因,我在我司的平板是下载文件的速度很慢很慢!都要哭了!


我哭了

当然之后我了解了一下 可以用 RandomAccessFile+ nio 来提升文件的写入速度!!


好啦!现在开始介绍下载类啦

public class DownLoadThread extends Thread {
 	private String apkurl;//下载地址
	private long startpos;//起始地址
	private long endpos;//结束地址
	private long downpos;//已经下载的大小
	private String apkpath;//保存地址
	private long block;//每块大小
	private int threadid;//线程ID
	private boolean finish = false; // 是否已经下载完成
 	private boolean error = false; // 是否出错
	private Context context;
	private DownLoader loader;
	private int downstate;//下载状态
	public static final int PAUSE = 2;//暂停
	public static final int RUNNING = 1;//正在下载
	public static final int STOP = 0;//停止

	public DownLoadThread(Context context, String apkurl, long startpos, long endpos, long 			downpos, String apkpath, long block, int threadid, DownLoader loader) {
    	this.context = context;
    	this.apkurl = apkurl;
    	this.startpos = startpos;
    	this.endpos = endpos;
    	this.downpos = downpos;
    	this.apkpath = apkpath;
    	this.block = block;
    	this.threadid = threadid;
    	this.loader = loader;
    	this.downstate = RUNNING;
	}

	public DownLoadThread() {
	}

	@Override
	public void run() {
    	File file=null;
    	FileOutputStream fout=null;
    	InputStream in = null;
    	if (downpos < block) {
        	try {
            	URL url = new URL(apkurl);
            	HttpURLConnection http = (HttpURLConnection) url
                    .openConnection();
            	http.setConnectTimeout(5 * 1000);
            	http.setReadTimeout(5 * 1000);
            	http.setRequestMethod("GET");
            	http.setRequestProperty(
                   "Accept",
                    "image/gif, image/jpeg, image/pjpeg, image/pjpeg, application/x-	shockwave-flash, application/xaml+xml, application/vnd.ms-xpsdocument, application/x-ms-xbap, 	application/x-ms-application, application/vnd.ms-excel, application/vnd.ms-powerpoint, 	application/msword, */*");
            	http.setRequestProperty("Accept-Language", "zh-CN");
            	http.setRequestProperty("Referer", url.toString());
            	http.setRequestProperty("Charset", "UTF-8");
            	http.setRequestProperty("Connection", "Keep-Alive");
            	long startPos = startpos + downpos;
            	long endPos = endpos;
            http.setRequestProperty("Range", "bytes=" + startPos + "-");// 设置获取实体数据的范围
            file=new File(apkpath);
            if (file.length()>0){
                fout=new FileOutputStream(file,true);
            }else{
                fout=new FileOutputStream(file);
            }
            byte[] bytes = new byte[2048];
            int len = 0;
            in = http.getInputStream();

            LogUtils.e("开始");
            while ((len = in.read(bytes, 0, bytes.length)) != -1) {
                if (PAUSE == this.downstate || STOP == this.downstate) {
                    DownDbUtils.update(this.context, this.threadid, this.apkurl, downpos);
                    break;
                }
                fout.write(bytes,0,len);
                downpos += len;//已下载的大小
                this.loader.setDownlength(len);
            }
            DownDbUtils.update(this.context, this.threadid, this.apkurl, downpos);
            if (!DeviceUtils.isNet(context)) {
                this.finish = false;
                this.downstate=PAUSE;
            } else {
                this.finish = true;
            }
      		 } catch (Exception e) {
           		DownDbUtils.update(this.context, this.threadid, this.apkurl, downpos);
            	LogUtils.e(e.toString());
            	e.printStackTrace();
            	downpos = -1;
            	this.error = true;
            	this.finish = false;
        } finally {
            closeIO(in,fout);
        }
    }
}
//得到每条线程已经下载的大小

public long getDownpos() {
    return downpos;
}

public int getDownstate() {
    return downstate;
}

public void setDownstate(int downstate) {
    this.downstate = downstate;
}

//是否下载完成
public boolean isFinish() {
    return finish;
}

public void setFinish(boolean finish) {
    this.finish = finish;
}

//是否下载出错
public boolean isError() {
    return error;
}

	public void setError(boolean error) {
	    this.error = error;
	}

	public void setDownpos(long downpos) {
	    this.downpos = downpos;
	}

//关闭流
 public static void closeIO(Closeable... closeables) {
    if (null == closeables || closeables.length <= 0) {
        return;
    }
    for (Closeable cb : closeables) {
        try {
            if (null == cb) {
                continue;
            }
            cb.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
}

这里大家得了解下 http 中这个 Range 这个字段的含义:用户请求头中,指定第一个字节的位置和最后一个字节的位置,如(Range:200-300)! 这样就可以指定下载文件的位置了呢!到了这里核心的部分已经说完了!

其他

上面已经把核心的都说完了,其实还有其他的可以说呢:

  1. 实现断点下载(保存下载的长度,用数据库或者文件保存都可以)
  2. 多线程下载的管理 (需要读者实现管理器了)
  3. 可以把下载这个模块放到另外一个进程中,这样可以是主进程更加的流畅。当然这涉及到了进程见通信的问题啦
  4. 一般下载的时候都会有下载进度条,这里要注意下更新的频率的问题,在 listview 中更新太快会造成页面卡顿的哦!
原文地址:https://www.cnblogs.com/likeandroid/p/5539634.html