Android笔记(五):利用多线程可断点下载远程文件(已解决文件名含有中文)

本文记录用多线程来下载文件(支持断点)

FileDownloadController类控制整个文件下载过程,外部通过实例化FileDownloadController对象设置下载过程监听并调用startDownload方法即可开始下载文件。

public class FileDownloadController {
    private static final int START = 0;
    private static final int PROCESSING = 1;    //正在下载实时数据传输 Message标志
    private static final int SUCCESS = 2;       //下载成功时的Message标志
    private static final int FAILURE = -1;      //下载失败时的Message标志


    private Context context;
    private DownloadProgressListener listener;
    private DownloadTask task;
    private Handler handler=new UIHandler();
    private class UIHandler extends Handler {
        /**
         * 需要在主线程中调用接口回调
         */
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case START:   //下载开始时
                    listener.onDownloadStart(msg.getData().getInt("file_size"));
                    break;
                case PROCESSING:        //下载时
                    //从消息中获取已经下载的数据长度
                    listener.onDownloading(msg.getData().getInt("size"));
                    break;
                case SUCCESS:
                    listener.onDownloadFinished();
                    break;
                case FAILURE:    //下载失败时
                    listener.onDownloadFailure((Exception) msg.getData().getSerializable("exception"));  //提示用户下载失败
                    break;
            }
        }
    }
    public FileDownloadController(Context context,DownloadProgressListener listener){
        this.context=context;
        this.listener=listener;
    }
    /**
     * 退出下载
     */
    public void stopDownload(){
        if(task!=null) task.exit(); //如果有下载对象时,退出下载
    }
    /**
     * 下载资源,生命下载执行者并开辟线程开始现在
     */
    public void startDownload(String downloadPath,File savePath){//此方法运行在主线程
        task = new DownloadTask(downloadPath, savePath); //实例化下载任务
        new Thread(task).start();   //开始下载
    }
    private final class DownloadTask implements Runnable{
        private String path;    //下载路径
        private File saveDir;   //下载到保存到的文件
        private FileDownloader loader;  //文件下载器(下载线程的容器)
        /**
         * 构造方法,实现变量初始化
         * @param path  下载路径
         * @param saveDir   下载要保存到的文件
         */
        public DownloadTask(String path, File saveDir) {
            this.path = path;
            this.saveDir = saveDir;
        }

        /**
         * 退出下载
         */
        public void exit(){
            if(loader!=null) loader.exit();//如果下载器存在的话则退出下载
        }

        /**
         * 下载线程的执行方法,会被系统自动调用
         */
        public void run() {
            try {
                loader = new FileDownloader(context, path, saveDir, 3);  //初始化下载
                loader.download(FileDownloadController.this);
            } catch (Exception e) {
                e.printStackTrace();
                Message msg = new Message(); //新建立一个Message对象
                msg.what = FAILURE;       //设置ID为-1;
                msg.getData().putSerializable("exception", e); //把文件下载的size设置进Message对象
                handler.sendMessage(msg);//通过handler发送消息到消息队列
            }
        }
    }
    public void onDownloadStart(int fileSize){
        Message msg = new Message(); //新建立一个Message对象
        msg.what = START;       //设置ID为0;
        msg.getData().putInt("file_size", fileSize); //把文件下载的size设置进Message对象
        handler.sendMessage(msg);//通过handler发送消息到消息队列
    }
    public void onDownloading(int size){
        Message msg = new Message(); //新建立一个Message对象
        msg.what = PROCESSING;       //设置ID为1;
        msg.getData().putInt("size", size); //把文件下载的size设置进Message对象
        handler.sendMessage(msg);//通过handler发送消息到消息队列
    }
    public void onDownloadFinished(){
        Message msg = new Message(); //新建立一个Message对象
        msg.what = SUCCESS;       //设置ID为2;
        handler.sendMessage(msg);//通过handler发送消息到消息队列
    }
}


FileDownloader类是文件下载器对象,主要用于获取下载文件的属性并初始化下载线程,操作数据库记录下载进度,回调FileDownloadController中的方法。

public class FileDownloader {
    private static final String TAG="FileDownloader";
    private static final int RESPONSE_OK=200;
    private Context context;
    private FileService fileService;
    private boolean exited;
    private int downloadedSize=0;
    private int fileSize=0;
    private DownloadThread[] threads;
    private File saveFile;
    private Map<Integer,Integer> data=new ConcurrentHashMap<>();
    private int block;
    private String downloadUrl;
    private URL url;
    public void exit(){
        this.exited=true;
    }
    public boolean getExited(){
        return this.exited;
    }
    protected synchronized void append(int size){
        downloadedSize+=size;
    }
    protected synchronized void update(int threadid,int pos){
        this.data.put(threadid,pos);
        this.fileService.update(this.downloadUrl,threadid,pos);
    }
    public FileDownloader(Context context,String downloadUrl,File fileSaveDir,int threadNum) {
        try {
            this.context = context;
            this.downloadUrl = downloadUrl;
            fileService = new FileService(this.context);

            url=new URL(encodeURL(downloadUrl));
            if (!fileSaveDir.exists()) fileSaveDir.mkdirs();
            this.threads = new DownloadThread[threadNum];
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            conn.setConnectTimeout(5 * 1000);
            conn.setRequestMethod("GET");
            conn.setRequestProperty("Accept", "*/*");    //设置客户端可以接受的媒体类型
            conn.setRequestProperty("Accept-Language", "zh-CN");
            conn.setRequestProperty("Referer", downloadUrl);
            conn.setRequestProperty("Charset", "UTF-8");
            conn.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)");
            conn.setRequestProperty("Connection", "Keep-Alive");
            conn.connect();
            printResponseHeader(conn);
            if (conn.getResponseCode() == RESPONSE_OK) {    //此处的请求会打开返回流并获取返回的状态码,用于检查是否请求成功,当返回码为200时执行下面的代码
                this.fileSize = conn.getContentLength();//根据响应获取文件大小
                if (this.fileSize <= 0)
                    throw new RuntimeException("Unknown file size ");    //当文件大小为小于等于零时抛出运行时异常

                String filename = getFileName(conn);//获取文件名称
                this.saveFile = new File(fileSaveDir, filename);//根据文件保存目录和文件名构建保存文件
                Map<Integer, Integer> logdata = fileService.getData(downloadUrl);//获取下载记录

                if (logdata.size() > 0) {//如果存在下载记录
                    for (Map.Entry<Integer, Integer> entry : logdata.entrySet())    //遍历集合中的数据
                        data.put(entry.getKey(), entry.getValue());//把各条线程已经下载的数据长度放入data中
                }

                if (this.data.size() == this.threads.length) {//如果已经下载的数据的线程数和现在设置的线程数相同时则计算所有线程已经下载的数据总长度
                    for (int i = 0; i < this.threads.length; i++) {    //遍历每条线程已经下载的数据
                        this.downloadedSize += this.data.get(i + 1);    //计算已经下载的数据之和
                    }
                    print("已经下载的长度" + this.downloadedSize + "个字节");    //打印出已经下载的数据总和
                }

                this.block = (this.fileSize % this.threads.length) == 0 ? this.fileSize / this.threads.length : this.fileSize / this.threads.length + 1;    //计算每条线程下载的数据长度
            } else {
                print("服务器响应错误:" + conn.getResponseCode() + conn.getResponseMessage());    //打印错误
                throw new RuntimeException("server response error ");    //抛出运行时服务器返回异常
            }
        } catch (Exception e) {
            print(e.toString());
            throw new RuntimeException("Can't connection this url");
        }
    }
    private String getFileName(HttpURLConnection conn) {
        String filename = downloadUrl.substring(downloadUrl.lastIndexOf('/') + 1);	//从下载路径的字符串中获取文件名称

        if(filename==null || "".equals(filename.trim())){//如果获取不到文件名称
            for (int i = 0;; i++) {	//无限循环遍历
                String mine = conn.getHeaderField(i);	//从返回的流中获取特定索引的头字段值
                if (mine == null) break;	//如果遍历到了返回头末尾这退出循环
                if("content-disposition".equals(conn.getHeaderFieldKey(i).toLowerCase())){	//获取content-disposition返回头字段,里面可能会包含文件名
                    Matcher m = Pattern.compile(".*filename=(.*)").matcher(mine.toLowerCase());	//使用正则表达式查询文件名
                    if(m.find()) return m.group(1);	//如果有符合正则表达规则的字符串
                }
            }
            filename = UUID.randomUUID()+ ".tmp";//由网卡上的标识数字(每个网卡都有唯一的标识号)以及 CPU 时钟的唯一数字生成的的一个 16 字节的二进制作为文件名
        }
        return filename;
    }

    /**
     *  开始下载文件
     * @param controller 监听下载数量的变化,如果不需要了解实时下载的数量,可以设置为null
     * @throws Exception
     */
    public void download(FileDownloadController controller) throws Exception{	//进行下载,并抛出异常给调用者,如果有异常的话
        controller.onDownloadStart(fileSize);
        try {
            RandomAccessFile randOut = new RandomAccessFile(saveFile, "rwd");	//The file is opened for reading and writing. Every change of the file's content must be written synchronously to the target device.
            if(fileSize>0) randOut.setLength(fileSize);	//设置文件的大小
            randOut.close();	//关闭该文件,使设置生效
            if(data.size() != threads.length){	//如果原先未曾下载或者原先的下载线程数与现在的线程数不一致
                data.clear();	//Removes all elements from this Map, leaving it empty.
                for (int i = 0; i < threads.length; i++) {	//遍历线程池
                    data.put(i+1, 0);//初始化每条线程已经下载的数据长度为0
                }
                this.downloadedSize = 0;	//设置已经下载的长度为0
            }
            for (int i = 0; i < this.threads.length; i++) {//开启线程进行下载
                int downloadedLength = this.data.get(i+1);	//通过特定的线程ID获取该线程已经下载的数据长度
                if(downloadedLength < this.block && this.downloadedSize < this.fileSize){//判断线程是否已经完成下载,否则继续下载
                    this.threads[i] = new DownloadThread(this, url, this.saveFile, this.block, this.data.get(i+1), i+1);	//初始化特定id的线程
                    this.threads[i].setPriority(7);	//设置线程的优先级,Thread.NORM_PRIORITY = 5 Thread.MIN_PRIORITY = 1 Thread.MAX_PRIORITY = 10
                    this.threads[i].start();	//启动线程
                }else{
                    this.threads[i] = null;	//表明在线程已经完成下载任务
                }
            }
            fileService.delete(this.downloadUrl);	//如果存在下载记录,删除它们,然后重新添加
            fileService.save(this.downloadUrl, this.data);	//把已经下载的实时数据写入数据库
            boolean notFinished = true;//下载未完成
            while (notFinished) {// 循环判断所有线程是否完成下载
                Thread.sleep(900);
                notFinished = false;//假定全部线程下载完成
                for (int i = 0; i < this.threads.length; i++){
                    if (this.threads[i] != null && !this.threads[i].isFinished()) {//如果发现线程未完成下载
                        notFinished = true;//设置标志为下载没有完成
                        if(this.threads[i].getDownloadedLength() == -1){//如果下载失败,再重新在已经下载的数据长度的基础上下载
                            this.threads[i] = new DownloadThread(this, url, saveFile, block, data.get(i+1), i+1);	//重新开辟下载线程
                            this.threads[i].setPriority(7);	//设置下载的优先级
                            this.threads[i].start();	//开始下载线程
                        }
                    }
                }
                controller.onDownloading(downloadedSize);//通知目前已经下载完成的数据长度
                if(downloadedSize == fileSize){
                    fileService.delete(downloadUrl);//下载完成删除记录
                    controller.onDownloadFinished();
                    break;
                }
            }

        } catch (Exception e) {
            print(e.toString());	//打印错误
            throw new Exception("File downloads error");	//抛出文件下载异常
        }
    }
    /**
     * 获取Http响应头字段
     * @param http	HttpURLConnection对象
     * @return	返回头字段的LinkedHashMap
     */
    private static Map<String, String> getHttpResponseHeader(HttpURLConnection http) {
        Map<String, String> header = new LinkedHashMap<>();	//使用LinkedHashMap保证写入和遍历的时候的顺序相同,而且允许空值存在
        for (int i = 0;; i++) {	//此处为无限循环,因为不知道头字段的数量
            String fieldValue = http.getHeaderField(i);	//getHeaderField(int n)用于返回 第n个头字段的值。

            if (fieldValue == null) break;	//如果第i个字段没有值了,则表明头字段部分已经循环完毕,此处使用Break退出循环
            header.put(http.getHeaderFieldKey(i), fieldValue);	//getHeaderFieldKey(int n)用于返回 第n个头字段的键。
        }
        return header;
    }
    /**
     * 打印Http头字段
     * @param http HttpURLConnection对象
     */
    private static void printResponseHeader(HttpURLConnection http){
        Map<String, String> header = getHttpResponseHeader(http);	//获取Http响应头字段
        for(Map.Entry<String, String> entry : header.entrySet()){	//使用For-Each循环的方式遍历获取的头字段的值,此时遍历的循序和输入的顺序相同
            String key = entry.getKey()!=null ? entry.getKey()+ ":" : "";	//当有键的时候这获取键,如果没有则为空字符串
            print(key+ entry.getValue());	//答应键和值的组合
        }
    }

    /**
     * 打印信息
     * @param msg	信息字符串
     */
    private static void print(String msg){
        Log.i(TAG, msg);	//使用LogCat的Information方式打印信息
    }

    /**
     * 解码带中文和空格的url
     * @param url
     * @return  解码后的字符串
     */
    private static String encodeURL(String url){
        String codeInfoUrl = "";
        for(int i = 0;i < url.length();i++){
            if(url.charAt(i)>= 19968 &&url.charAt(i)<=171941)
                try {
                    codeInfoUrl = codeInfoUrl + URLEncoder.encode(url.substring(i, i+1), "UTF-8");
                } catch (UnsupportedEncodingException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            else {
                codeInfoUrl = codeInfoUrl +url.substring(i, i+1);
            }
        }
        return codeInfoUrl.replaceAll(" ","%20");
    }
}
注意:

由于可能存在多个线程同时访问append和update方法,所以需要给这两个方法加上synchronized关键字;

在getFileName方法中使用的正则表达式 ".*filename=(.*)" ,其中.*为贪婪量词,代表尽可能多的字符,group(0)是指整个字符串,group(1)是指第一个括号内的内容,即文件名;

具体Http的响应头字段格式可参考文章 Android笔记(三):整理——利用http上传小文件

encodeURL方法是将URL中含有中文文字和空格的字符进行重新编码;反之,可用URLDecoder.decode(url,"utf-8")进行解码。


DownloadThread类为下载线程

public class DownloadThread extends Thread {
    private static final String TAG = "DownloadThread";	//定义TAG,方便日子的打印输出
    private File saveFile;	//下载的数据保存到的文件
    private URL downUrl;	//下载的URL
    private int block;	//每条线程下载的大小
    private int threadId = -1;	//初始化线程id设置
    private int downloadedLength;	//该线程已经下载的数据长度
    private boolean finished = false;	//该线程是否完成下载的标志
    private FileDownloader downloader;	//文件下载器

    public DownloadThread(FileDownloader downloader, URL downUrl, File saveFile, int block, int downloadedLength, int threadId) {
        this.downUrl = downUrl;
        this.saveFile = saveFile;
        this.block = block;
        this.downloader = downloader;
        this.threadId = threadId;
        this.downloadedLength = downloadedLength;
    }

    @Override
    public void run() {
        if(downloadedLength < block){//未下载完成
            try {
                HttpURLConnection http = (HttpURLConnection) downUrl.openConnection();	//开启HttpURLConnection连接
                http.setConnectTimeout(5 * 1000);	//设置连接超时时间为5秒钟
                http.setRequestMethod("GET");	//设置请求的方法为GET
                http.setRequestProperty("Accept", "*/*");	//设置客户端可以接受的返回数据类型
                http.setRequestProperty("Accept-Language", "zh-CN");	//设置客户端使用的语言为中文
                http.setRequestProperty("Referer", downUrl.toString()); 	//设置请求的来源,便于对访问来源进行统计
                http.setRequestProperty("Charset", "UTF-8");	//设置通信编码为UTF-8
                int startPos = block * (threadId - 1) + downloadedLength;//开始位置
                int endPos = block * threadId -1;//结束位置
                http.setRequestProperty("Range", "bytes=" + startPos + "-"+ endPos);//设置获取实体数据的范围,如果超过了实体数据的大小会自动返回实际的数据大小
                http.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)");	//客户端用户代理
                http.setRequestProperty("Connection", "Keep-Alive");	//使用长连接

                InputStream inStream = http.getInputStream();	//获取远程连接的输入流
                byte[] buffer = new byte[1024];	//设置本地数据缓存的大小为1M
                int offset = 0;	//设置每次读取的数据量
                print("Thread " + this.threadId + " starts to download from position "+ startPos);	//打印该线程开始下载的位置
                RandomAccessFile threadFile = new RandomAccessFile(this.saveFile, "rwd");	//If the file does not already exist then an attempt will be made to create it and it require that every update to the file's content be written synchronously to the underlying storage device.
                threadFile.seek(startPos);	//文件指针指向开始下载的位置
                while (!downloader.getExited() && (offset = inStream.read(buffer, 0, 1024)) != -1) {	//但用户没有要求停止下载,同时没有到达请求数据的末尾时候会一直循环读取数据
                    threadFile.write(buffer, 0, offset);	//直接把数据写到文件中
                    downloadedLength += offset;	//把新下载的已经写到文件中的数据加入到下载长度中
                    downloader.update(this.threadId, downloadedLength);	//把该线程已经下载的数据长度更新到数据库和内存哈希表中
                    downloader.append(offset);	//把新下载的数据长度加入到已经下载的数据总长度中
                }//该线程下载数据完毕或者下载被用户停止
                threadFile.close();	//Closes this random access file stream and releases any system resources associated with the stream.
                inStream.close();	//Concrete implementations of this class should free any resources during close
                if(downloader.getExited())
                {
                    print("Thread " + this.threadId + " has been paused");
                }
                else
                {
                    print("Thread " + this.threadId + " download finish");
                }

                this.finished = true;	//设置完成标志为true,无论是下载完成还是用户主动中断下载
            } catch (Exception e) {	//出现异常
                this.downloadedLength = -1;	//设置该线程已经下载的长度为-1
                print("Thread "+ this.threadId+ ":"+ e);	//打印出异常信息
            }
        }
    }
    /**
     * 打印信息
     * @param msg	信息
     */
    private static void print(String msg){
        Log.i(TAG, msg);	//使用Logcat的Information方式打印信息
    }

    /**
     * 下载是否完成
     * @return
     */
    public boolean isFinished() {
        return finished;
    }

    /**
     * 已经下载的内容大小
     * @return 如果返回值为-1,代表下载失败
     */
    public long getDownloadedLength() {
        return downloadedLength;
    }
}


FileService类封装了对数据库的操作,主要用于将下载进度记录到数据库,从而实现断点下载功能
public class FileService {
    private DBOpenHelper openHelper;
    public  FileService(Context context){
        openHelper=new DBOpenHelper(context);
    }
    public Map<Integer,Integer> getData(String path){
        SQLiteDatabase db=openHelper.getReadableDatabase();
        Cursor cursor=db.rawQuery("select threadid,downlength from filedownlog " +
                "where downpath=?",new String[]{path});
        Map<Integer,Integer> data =new HashMap<>();
        while(cursor.moveToNext()){
            data.put(cursor.getInt(cursor.getColumnIndexOrThrow("threadid")),
                    cursor.getInt(cursor.getColumnIndexOrThrow("downlength")));
        }
        cursor.close();
        db.close();
        return data;
    }
    public void save (String path,Map<Integer,Integer> map){
        SQLiteDatabase db=openHelper.getWritableDatabase();
        db.beginTransaction();
        try{
            for(Map.Entry<Integer,Integer> entry:map.entrySet()){
                db.execSQL("insert into filedownlog(downpath,threadid,downlength) values(?,?,?)",new Object[]{path,entry.getKey(),entry.getValue()});
            }
            db.setTransactionSuccessful();

        }catch (SQLiteException e){
            e.printStackTrace();
        }finally {
            db.endTransaction();
        }
        db.close();
    }
    public void update(String path,int threadid,int pos){
        SQLiteDatabase db=openHelper.getWritableDatabase();
        db.execSQL("update filedownlog set downlength=? where downpath=? and threadid=?",new Object[]{pos,path,threadid});
        db.close();
    }
    public void delete(String path){
        SQLiteDatabase db=openHelper.getWritableDatabase();
        db.execSQL("delete from filedownlog where downpath=?",new Object[]{path});
        db.close();
    }
}


public class DBOpenHelper extends SQLiteOpenHelper {
    private static final String DBNAME="eric.db";
    private static final int VERSION=1;
    public DBOpenHelper(Context context) {
        super(context, DBNAME, null, VERSION);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL("CREATE TABLE IF NOT EXISTS filedownlog(id integer primary key " +
                "autoincrement,downpath varchar(100), threadid INTEGER,downlength INTEGER)");
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        db.execSQL("DROP TABLE IF EXISTS filedownlog");
        onCreate(db);
    }
}

下载过程监听接口

public interface DownloadProgressListener {
    void onDownloadStart(int fileSize);
    void onDownloading(int size);
    void onDownloadFinished();
    void onDownloadFailure(Exception e);
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

萌面小侠Plus

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值