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

本项目完成的功能类似与迅雷等下载工具所实现的功能——实现多线程断点下载。
主要设计的技术有:
1、android中主线程与非主线程通信机制。
2、多线程的编程和管理。
3、android网络编程
4、自己设计实现设计模式-监听器模式
5、Activity、Service、数据库编程
6、android文件系统
7、缓存

博文链接:
Android-多线程断点下载详解及源码下载(一)
Android-多线程断点下载详解及源码下载(二)
Android-多线程断点下载详解及源码下载(四)

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

  • 下载器-线程管理

下载器是指本项目中的MultiThreadManager类,该类可以看作是线程池的作用,启动多个线程,同时管理和维护多个线程。既然可以管理多个线程,必然设计多线程的通信、同步、异步的问题。
首先分析下载器的功能:
1、启动多个线程,本项目中的具体实现文件下载的类是DownTaskThread,也就是说MultiThreadManager类要new出来多个下载类,启动线程。
2、线程启动之后,需要不断获取已经下载的长度,并更新已经下载的长度值,则MultiThreadManager类中有这样几个方法,如下代码:

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

3、多个线程同时进行下载,那么每个线程下载的长度也需要维护,因为要实现断点下载,需要保存每个线程已经下载的长度,则有如下方法:
/**
     * 设置成synchronized同步!
     * 这是因为该项目中有多个线程进行该操作。
     * 设计线程同步问题,同时更改一个数据会造成混乱。
     * 所以此处必须设置成同步操作。
     * @param downedLen
     */publicsynchronizedvoidsetDownedLen(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、既然是下载管理器,那么是有可能退出下载或者暂停下载的功能的,那么下载管理器可以有一个标记位,标记是下载还是暂停,则有如下方法:

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

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

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

publicclassMultiThreadManager {privateint threadNum;//启动的线程数量private String downPath;//下载路径privateint downedLen;//已下载的长度privateboolean isExist;//是否已经退出下载或者暂停//通过该类完成数据库中信息的更新private DownDatabaseService downDatabaseService;
    private DownTaskThread[] downTaskThreads;//线程数组,即线程池privatelong fileLen;//文件长度private File saveDir;//保存路径private String fileName;//文件名@SuppressLint("UseSparseArrays")
    private Map<Integer, Long> map = 
            new HashMap<Integer, Long>();//缓存已经下载的各个线程的长度privatelong block;//每个线程下载块的大小publicMultiThreadManager(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);
    }

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

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

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

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

    //获取线程数量publicintgetThreadNum() {
        return threadNum;
    }

    publiclonggetFileLen() {
        return fileLen;
    }

    /**
     * 获取下载的文件的长度
     * @param url
     * @return
     */privateintgetDownLoaderFileLen(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 
     */publicintdownloader(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);
        }
        returnthis.downedLen;
    }

    /**
     * 打印网络请求响应头信息
     * @param connection
     */privatevoidprintResponseHeader(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实现更新已经下载的文件长度、更新进度值等操作。具体代码如下:

publicclassDownTaskThreadextendsThread {private String url;//下载路径-服务器路径privatelong startPos;//下载开始位置private File saveDir;//保存路径privatelong downedLen;//已下载长度privatelong block;//下载的长度块privateint threadId;//线程ID值private MultiThreadManager multiThreadManager;//多线程管理类privateboolean isFinished = false;
    publicDownTaskThread(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);
    }

    publicbooleanisFinished() {
        return isFinished;
    }

    publiclonggetDownedLen() {
        return downedLen;
    }

    @Overridepublicvoidrun() {
        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 = newbyte[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-多线程断点下载详解及源码下载(四)

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

For more complete information about compiler optimizations, see our Optimization Notice.