当前位置: 代码迷 >> Android >> Android-多线程断点下载详解及源码下载(3)
  详细解决方案

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

热度:72   发布时间:2016-04-28 01:12:42.0
Android-多线程断点下载详解及源码下载(三)

本项目完成的功能类似与迅雷等下载工具所实现的功能——实现多线程断点下载。
主要设计的技术有:
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-多线程断点下载详解及源码下载(四)

源码下载(服务器端和客户端代码)

  相关解决方案