您的位置:首页 > 移动开发 > Android开发

Android-多线程断点下载详解及源码下载(三)

2015-04-30 10:04 435 查看
本项目完成的功能类似与迅雷等下载工具所实现的功能——实现多线程断点下载。

主要设计的技术有:

1、android中主线程与非主线程通信机制。

2、多线程的编程和管理。

3、android网络编程

4、自己设计实现设计模式-监听器模式

5、Activity、Service、数据库编程

6、android文件系统

7、缓存

博文链接:

Android-多线程断点下载详解及源码下载(一)

Android-多线程断点下载详解及源码下载(二)

Android-多线程断点下载详解及源码下载(四)

本篇接着上篇开始详细讲述客户端代码的具体实现,详细讲述下载器的实现以及多线程的管理工作。

下载器-线程管理

下载器是指本项目中的MultiThreadManager类,该类可以看作是线程池的作用,启动多个线程,同时管理和维护多个线程。既然可以管理多个线程,必然设计多线程的通信、同步、异步的问题。

首先分析下载器的功能:

1、启动多个线程,本项目中的具体实现文件下载的类是DownTaskThread,也就是说MultiThreadManager类要new出来多个下载类,启动线程。

2、线程启动之后,需要不断获取已经下载的长度,并更新已经下载的长度值,则MultiThreadManager类中有这样几个方法,如下代码:

//获取已经下载的长度
    public int getDownedLen() {
        return downedLen;
    }
    //追加已经下载的长度
    public synchronized void appendSize(int len){
        this.downedLen += len;
        System.out.println("已经下载的长度="+this.downedLen);
    }


3、多个线程同时进行下载,那么每个线程下载的长度也需要维护,因为要实现断点下载,需要保存每个线程已经下载的长度,则有如下方法:

/**
     * 设置成synchronized同步!
     * 这是因为该项目中有多个线程进行该操作。
     * 设计线程同步问题,同时更改一个数据会造成混乱。
     * 所以此处必须设置成同步操作。
     * @param downedLen
     */
    public synchronized void setDownedLen(int threadId,long downedLen) {
        this.map.put(threadId, downedLen);
        System.out.println("线程"+threadId+"已下载长度="+downedLen+",map数量="+map.size());
        this.downDatabaseService.update(this.downPath, this.map);
    }


方法设置为synchronized是很好理解的,因为涉及多线程,同时更新一个数据,必然需要同步,不然乱套了!

4、既然是下载管理器,那么是有可能退出下载或者暂停下载的功能的,那么下载管理器可以有一个标记位,标记是下载还是暂停,则有如下方法:

//设置是否退出或者暂停
    public boolean isExist() {
        return isExist;
    }
    //获取是否退出或者暂停
    public void setExist(boolean isExist) {
        this.isExist = isExist;
    }


可能大家看到这个方法仅仅是个标记位,如何起到暂停下载的作用呢?其实是这样实现的,每个线程的run方法里面,循环读取输入流的方法中,每读取一次缓存区会判断该标记位是否已经设置为退出或者暂停,这样就可以实现暂停的功能了。

几个主要的功能是这四个方面,下载器的全部代码如下:

public class MultiThreadManager {
private int threadNum;//启动的线程数量
private String downPath;//下载路径
private int downedLen;//已下载的长度
private boolean isExist;//是否已经退出下载或者暂停
//通过该类完成数据库中信息的更新
private DownDatabaseService downDatabaseService;
private DownTaskThread[] downTaskThreads;//线程数组,即线程池
private long fileLen;//文件长度
private File saveDir;//保存路径
private String fileName;//文件名
@SuppressLint("UseSparseArrays")
private Map<Integer, Long> map =
new HashMap<Integer, Long>();//缓存已经下载的各个线程的长度
private long block;//每个线程下载块的大小
public MultiThreadManager(int threadNum,String downPath,
File saveDir,Context context){
this.threadNum = threadNum;
this.downPath = downPath;
downDatabaseService = new DownDatabaseService(context);
downTaskThreads = new DownTaskThread[threadNum];
fileLen = getDownLoaderFileLen(downPath);
this.saveDir = new File(saveDir,this.fileName);
this.block = (fileLen%threadNum==0)?(fileLen/threadNum):(fileLen/threadNum+1);
System.out.println("文件块的大小block="+block);
}

//获取已经下载的长度
public int getDownedLen() {
return downedLen;
}

/** * 设置成synchronized同步! * 这是因为该项目中有多个线程进行该操作。 * 设计线程同步问题,同时更改一个数据会造成混乱。 * 所以此处必须设置成同步操作。 * @param downedLen */ public synchronized void setDownedLen(int threadId,long downedLen) { this.map.put(threadId, downedLen); System.out.println("线程"+threadId+"已下载长度="+downedLen+",map数量="+map.size()); this.downDatabaseService.update(this.downPath, this.map); }

//追加已经下载的长度
public synchronized void appendSize(int len){
this.downedLen += len;
System.out.println("已经下载的长度="+this.downedLen);
}

//设置是否退出或者暂停 public boolean isExist() { return isExist; } //获取是否退出或者暂停 public void setExist(boolean isExist) { this.isExist = isExist; }

//获取线程数量
public int getThreadNum() {
return threadNum;
}

public long getFileLen() {
return fileLen;
}

/**
* 获取下载的文件的长度
* @param url
* @return
*/
private int getDownLoaderFileLen(String url){
int len = 0;
try {
URL path = new URL(url);
HttpURLConnection httpURLConnection = (HttpURLConnection) path.openConnection();
httpURLConnection.setDoOutput(true);
httpURLConnection.setDoInput(true);
httpURLConnection.setConnectTimeout(5*1000);
httpURLConnection.setUseCaches(true);
httpURLConnection.setRequestMethod("GET");
//设置客户端可接受的媒体类型
httpURLConnection.setRequestProperty("Accept", "image/gif,image/jpeg," +
"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,*/*");
//设置客户端语言
httpURLConnection.setRequestProperty("Accept-Language", "zh-CN");
//设置请求来源,便于服务器进行来源统计
httpURLConnection.setRequestProperty("Referer", url);
//设置客户端编码
httpURLConnection.setRequestProperty("Charset", "UTF-8");
//设置用户代理
httpURLConnection.setRequestProperty("User-Agent", "Mozilla/4.0(" +
"compatible; MSIE 8.0; Windows NT 5.2; Trident/4.0; " +
".NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.04506.30; " +
".NET CLR 3.0.4506.2152; .NET CLR 3.5.30729)");
//设置连接方式
httpURLConnection.setRequestProperty("Connetion", "Keep-Alive");
httpURLConnection.connect();
printResponseHeader(httpURLConnection);
if (httpURLConnection.getResponseCode() == 200) {
len = httpURLConnection.getContentLength();
this.fileName = getFileName(httpURLConnection);
if (len<=0) {
System.out.println("文件大小不知");
}
this.map = this.downDatabaseService
.getDownLoadedLen(this.downPath);
if (map.size()>0) {//说明已经有下载数据
System.out.println("已经有下载数据,map的数量为"+map.size());
}else {
System.out.println("无下载数据,map的数量为"+map.size());
}
if (map.size() == this.threadNum) {//如果已经下载的线程数据的数量和
//现有设置的线程数量相同则计算所有线程亿i纪念馆下载的总长度
for (int i = 0; i < this.threadNum; i++) {
//遍历每条线程,计算总下载长度
this.downedLen += this.map.get(i+1);
//通过线程threadId获取每条线程已经下载的长度
//这里的i+1是因为线程threadId从1开始
}
System.out.println("总已下载长度="+downedLen);
}

}else {
System.out.println("服务器响应错误。"+httpURLConnection.getResponseCode()
+httpURLConnection.getResponseMessage());
}
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("从服务器获取文件的长度="+len);
return len;
}

/**
* 该方法执行线程启动操作
* @param iDownProgressing
* @throws Exception
*/
public int downloader(IDownProgressing iDownProgressing) throws Exception{
RandomAccessFile randomAccessFile = new RandomAccessFile(this.saveDir, "rwd");
if (this.fileLen>0) {
randomAccessFile.setLength(this.fileLen);
}
//close之后就会即可把上面的设置信息提交。并且也必须调用close方法
randomAccessFile.close();
/**
* 如果已经保存的线程数和本次开启的线程数不一致
* 则使用新设置的线程数量重新进行下载
*/
if (this.threadNum != this.map.size()) {
//如果已经保存的线程数和本次开启的线程数不一致
System.out.println("map被清理");
this.map.clear();
for (int i = 0; i < this.threadNum; i++) {
map.put(i+1, 0l);//新开启的每一条线程设置为0
}
this.downedLen = 0;
}
for (int i = 0; i < this.threadNum; i++) {
if (this.map.get(i+1) < this.block && this.downedLen < this.fileLen) {
downTaskThreads[i] = new DownTaskThread
(this, this.downPath, this.block, this.saveDir,
this.map.get(i+1), i+1);
this.downTaskThreads[i].setPriority(Thread.MAX_PRIORITY);
downTaskThreads[i].start();
}else {
downTaskThreads[i] = null;
}
}
this.downDatabaseService.delete(this.downPath);
this.downDatabaseService.setData(this.downPath, this.map);
System.out.println("设置值之后map数量="+this.downDatabaseService.getDownLoadedLen(this.downPath).size());
boolean isFinished = false;
while (!isFinished) {
Thread.sleep(900);
isFinished = true;
for (int i = 0; i <this.threadNum; i++) {
if (this.downTaskThreads[i] != null &&
!this.downTaskThreads[i].isFinished()) {
isFinished = false;
//==-1说明下载失败
if (this.downTaskThreads[i].getDownedLen() == -1) {
this.downTaskThreads[i] =
new DownTaskThread(this, this.downPath,
this.block, saveDir, this.map.get(i+1), i+1);
this.downTaskThreads[i].setPriority(Thread.MAX_PRIORITY);
this.downTaskThreads[i].start();
}
}
}
//更新进度值,iDownProgressing 可以说明不显示进度值
if(iDownProgressing != null){
iDownProgressing.setDownLoaderNum(downedLen);
}
}
if (downedLen >= fileLen) {
//如果已下载完毕,则删除下载记录
downDatabaseService.delete(this.downPath);
}
return this.downedLen;
}

/**
* 打印网络请求响应头信息
* @param connection
*/
private void printResponseHeader(HttpURLConnection connection){
Map<String, List<String>> map = connection.getHeaderFields();
Set<Entry<String,List<String>>> set = map.entrySet();
System.out.println("获取的头字段:");
for (Entry<String, List<String>> entry:set) {
System.out.println(entry.getKey()+"=="+entry.getValue());
}
}

/**
* 获取文件名字
* @param connection
* @return String
*/
private String getFileName(HttpURLConnection connection){
String fileName = this.downPath.substring(this.downPath.lastIndexOf("/")+1);
if (fileName == null || fileName.trim().equals("")) {
fileName = UUID.randomUUID() + ".tmp";
//有网卡上的标识数字(每个网卡都有唯一的标识号)
//及CPU时钟的唯一数字生成的一个16字节的二进制数
//作为文件名
}
System.out.println("从服务器获取的文件名字="+fileName);
return fileName;
}
}


上面的代码中详细给出了注释,所以应该不难理解。

具体下载线程DownTaskThread类

具体下载线程DownTaskThread类作用就是获取服务器的输入流,读取文件,并写入对应的文件当中。同时通过引用下载器MultiThreadManager实现更新已经下载的文件长度、更新进度值等操作。具体代码如下:

public class DownTaskThread extends Thread {
    private String url;//下载路径-服务器路径
    private long startPos;//下载开始位置
    private File saveDir;//保存路径
    private long downedLen;//已下载长度
    private long block;//下载的长度块
    private int threadId;//线程ID值
    private MultiThreadManager multiThreadManager;//多线程管理类
    private boolean isFinished = false;
    public DownTaskThread(MultiThreadManager multiThreadManager,
            String url,long block,File saveDir,long downedLen,int threadId){
        this.url = url;
        this.saveDir = saveDir;
        this.downedLen = downedLen;
        this.threadId = threadId;
        this.block = block;
        this.multiThreadManager = multiThreadManager;
        this.startPos = block*(threadId-1) + downedLen;
        System.out.println("线程"+threadId+"起始位置="+startPos);
    }

    public boolean isFinished() {
        return isFinished;
    }

    public long getDownedLen() {
        return downedLen;
    }

    @Override
    public void run() {
        super.run();
        try {
            URL url = new URL(this.url);
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
            connection.setDoOutput(true);
            connection.setDoInput(true);
            long endPos = block*threadId-1;
            connection.setConnectTimeout(5*1000);
            connection.setRequestMethod("GET");
            //设置客户端可接受的媒体类型
            connection.setRequestProperty("Accept", "image/gif,image/jpeg," +
                    "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,*/*");
            //设置客户端语言
            connection.setRequestProperty("Accept-Language", "zh-CN");
            //设置请求来源,便于服务器进行来源统计
            connection.setRequestProperty("Referer", this.url);
            //设置客户端编码
            connection.setRequestProperty("Charset", "UTF-8");
            //设置用户代理
            connection.setRequestProperty("User-Agent", "Mozilla/4.0(" +
                    "compatible; MSIE 8.0; Windows NT 5.2; Trident/4.0; " +
                    ".NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.04506.30; " +
                    ".NET CLR 3.0.4506.2152; .NET CLR 3.5.30729)");
            //设置获取实体数据的范围,如果超过了实体数据的大小会自动返回实际的数据的大小
            connection.setRequestProperty("Range", "bytes="+this.startPos+"-"+endPos);
            //设置连接方式
            connection.setRequestProperty("Connection","Keep-Alive");
            connection.connect();
            InputStream inputStream = connection.getInputStream();
            byte[] buffer = new byte[1024];
            int len = 0;
            RandomAccessFile randomAccessFile = new RandomAccessFile(this.saveDir, "rwd");
            randomAccessFile.seek(this.startPos);
            while (!this.multiThreadManager.isExist() &&
                    (len = inputStream.read(buffer, 0, buffer.length))>0) {
                randomAccessFile.write(buffer,0,len);
                this.downedLen += len;
                this.multiThreadManager.setDownedLen(this.threadId, this.downedLen);
                this.multiThreadManager.appendSize(len);
            }
            randomAccessFile.close();
            inputStream.close();
            if (this.multiThreadManager.isExist()) {
                System.out.println("线程"+this.threadId+"已经被暂停");
            }else {
                System.out.println("线程"+this.threadId+"已经下载完成");
            }
            this.isFinished = true;
        } catch (Exception e) {
            e.printStackTrace();
            this.downedLen = -1;
            System.out.println("线程"+this.threadId+"出现异常");
        }
    }
}


下载线程类DownTaskThread有一点非常关键,就是这一行代码:

//设置获取实体数据的范围,如果超过了实体数据的大小会自动返回实际的数据的大小
            connection.setRequestProperty("Range", "bytes="+this.startPos+"-"+endPos);


这一行代码是进行断点下载的标准代码,获取实体的范围进行下载。

代价有可能可以使用别的方法实现,例如利用下载的代码实现:

。。。。。。。。。。。上面一样。。。。。。。。。。。。
//设置用户代理
            connection.setRequestProperty("User-Agent", "Mozilla/4.0(" +
                    "compatible; MSIE 8.0; Windows NT 5.2; Trident/4.0; " +
                    ".NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.04506.30; " +
                    ".NET CLR 3.0.4506.2152; .NET CLR 3.5.30729)");
            //设置连接方式
    connection.setRequestProperty("Connection","Keep-Alive");
            connection.connect();
            InputStream inputStream = connection.getInputStream();
            byte[] buffer = new byte[1024];
            int len = 0;
            RandomAccessFile randomAccessFile = new RandomAccessFile(this.saveDir, "rwd");
            inputStream.skip(this.startPos);//这行代码跳过指定的字节数开始读取数据
            randomAccessFile.seek(this.startPos);
            while (!this.multiThreadManager.isExist() &&
                    (len = inputStream.read(buffer, 0, buffer.length))>0) {
                randomAccessFile.write(buffer,0,len);
。。。。。。。。。下面一样。。。。。。。。。。。。。。。


这样的方法和上面的代码中的区别仅仅是这一行代码:

inputStream.skip(this.startPos);//这行代码跳过指定的字节数开始读取数据


目的是想利用inputStream跳过指定的字节数后在进行读取,但是想法是对的,没有错!但问题是inputStream的这个方法有问题,达不到想要的效果。

这个问题请参考博文:

Java.IO.InputStream.skip() 错误(跳过字节数和预想的不等)

该博文中详细讲述了这个方法的问题,以及解决办法。

篇幅有些长了,本篇就到此,如果有什么疑问,欢迎大家留言评论。下一篇完结,并进行总结。

博文链接:

Android-多线程断点下载详解及源码下载(一)

Android-多线程断点下载详解及源码下载(二)

Android-多线程断点下载详解及源码下载(四)

源码下载(服务器端和客户端代码)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: