Android多线程+单线程+断点续传+进度条显示下载

效果图
download.gif
白话分析:

多线程:肯定是多个线程咯
断点:线程停止下载的位置
续传:线程从停止下载的位置上继续下载,直到完成任务为止。

核心分析:
断点:

当前线程已经下载的数据长度

续传:

向服务器请求上次线程停止下载位置的数据

con.setRequestProperty("Range", "bytes=" + start + "-" + end);
分配线程:
 int currentPartSize = fileSize / mThreadNum;
定义位置

定义线程开始下载的位置和结束的位置

for (int i = 0; i < mThreadNum; i++) {
int start = i * currentPartSize;//计算每条线程下载的开始位置
 int end = start + currentPartSize-1;//线程结束的位置
  if(i==mThreadNum-1){
      end=fileSize;
   }}
创建数据库:

由于每一个文件要分成多个部分,要被不同的线程同时进行下载。当然要创建线程表,保存当前线程下载开始的位置和结束的位置,还有完成进度等。创建file表,保存当前下载的文件信息,比如:文件名,url,下载进度等信息

线程表:
public static final String CREATE_TABLE_SQL="create table "+TABLE_NAME+"(_id integer primary "
        +"key autoincrement, threadId, start , end, completed, url)";
file表:
public static final String CREATE_TABLE_SQL="create table "+TABLE_NAME+"(_id integer primary" +
        " key autoincrement ,fileName, url, length, finished)";
创建线程类

无非就2个类,一个是线程管理类DownLoadManager.java,核心方法:start(),stop(),restart(),addTask().clear()。另一个是线程任务类
DownLoadTask.java,就是一个线程类,用于下载线程分配好的任务。后面会贴出具体代码。

创建数据库方法类

无非就是单例模式,封装一些增删改查等基础数据库方法,后面会贴出具体代码。

创建实体类

也就是创建ThreadInfoFileInfo这2个实体类,把下载文件信息和线程信息暂时存储起来。

引入的第三方开源库

NumberProgressBar是一个关于进度条的开源库,挺不错的。直达链接

代码具体分析

1.首先是创建实体类,文件的实体类FileInfo,肯定有fileName,url,length,finised,isStop,isDownloading这些属性。线程的实体类ThreadInfo肯定有threadId,start,end,completed,url这些属性。这些都很简单

//ThreadInfo.java
public class ThreadInfo {
private String threadId;
private int start;
private int end;
private int compeleted;
private String url;


public ThreadInfo(){

}

public ThreadInfo(String threadId, int start, int end, int compeleted, String url) {
    this.threadId = threadId;
    this.start = start;
    this.end = end;
    this.compeleted = compeleted;
    this.url = url;
}

public String getThreadId() {
    return threadId;
}

public void setThreadId(String threadId) {
    this.threadId = threadId;
}

public int getStart() {
    return start;
}

public void setStart(int start) {
    this.start = start;
}

public int getEnd() {
    return end;
}

public void setEnd(int end) {
    this.end = end;
}

public int getCompeleted() {
    return compeleted;
}

public void setCompeleted(int compeleted) {
    this.compeleted = compeleted;
}

public String getUrl() {
    return url;
}

public void setUrl(String url) {
    this.url = url;
}

}

//FileInfo.java

public class FileInfo {
private String fileName; //文件名
private String url;  //下载地址
private int length;  //文件大小
private int finished; //下载已完成进度
private boolean isStop=false; //是否暂停下载
private boolean isDownloading=false; //是否正在下载
public FileInfo(){

}

public FileInfo(String fileName,String url){
    this.fileName=fileName;
    this.url=url;
}

public String getFileName() {
    return fileName;
}

public void setFileName(String fileName) {
    this.fileName = fileName;
}

public String getUrl() {
    return url;
}

public void setUrl(String url) {
    this.url = url;
}

public int getLength() {
    return length;
}

public void setLength(int length) {
    this.length = length;
}

public int getFinished() {
    return finished;
}

public void setFinished(int finished) {
    this.finished = finished;
}

public boolean isStop() {
    return isStop;
}

public void setStop(boolean stop) {
    isStop = stop;
}

public boolean isDownloading() {
    return isDownloading;
}

public void setDownloading(boolean downloading) {
    isDownloading = downloading;
}

@Override
public String toString() {
    return "FileInfo{" +
            "fileName='" + fileName + '\'' +
            ", url='" + url + '\'' +
            ", length=" + length +
            ", finished=" + finished +
            ", isStop=" + isStop +
            ", isDownloading=" + isDownloading +
            '}';
}}

2.实体类写完了,那么接下来写创建一个类,继承SQLiteOpenHelper类,来管理数据库连接,主要作用:管理数据库的初始化,并允许应用程序通过该类获取SQLiteDatabase对象。

public class ThreadHelper extends SQLiteOpenHelper{
public static final String TABLE_NAME="downthread";
public static final String CREATE_TABLE_SQL="create table "+TABLE_NAME+"(_id integer primary "
        +"key autoincrement, threadId, start , end, completed, url)";


public ThreadHelper(Context context, String name, int version) {
    super(context, name, null, version);
}

@Override
public void onCreate(SQLiteDatabase db) {
    db.execSQL(CREATE_TABLE_SQL);

}

@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {

}}

3.接下来封装一些数据库的增删改查操作,用的单例模式,用双重检验锁实现单例。好处:既能很大程度上确保线程安全,又能实现延迟加载。 缺点:使用volatile关键字会使JVM对该代码的优化丧失,影响性能。并且在一些高并发的情况,仍然可能会创建多个实例,这称为双重检验锁定失效。单例模式

public class Thread {
private SQLiteDatabase db;
public static final String DB_NAME="downthread.db3";
public static final int VERSION=1;
private  Context mContext;
private volatile static Thread t=null;

private Thread(){
    mContext= BaseApplication.getContext();
    db=new ThreadHelper(mContext,DB_NAME,VERSION).getReadableDatabase();
}

public static Thread getInstance(){
    if(t==null){
        synchronized (Thread.class){
            if(t==null){
                t=new Thread();
            }
        }
    }
    return t;
}

public SQLiteDatabase getDb(){
    return db;
}


//保存当前线程下载进度
public synchronized void insert(ThreadInfo threadInfo){
    ContentValues values=new ContentValues();
    values.put("threadId",threadInfo.getThreadId());
    values.put("start",threadInfo.getStart());
    values.put("end",threadInfo.getEnd());
    values.put("completed",threadInfo.getCompeleted());
    values.put("url",threadInfo.getUrl());
    long rowId=db.insert(ThreadHelper.TABLE_NAME,null,values);
    if(rowId!=-1){
        UtilsLog.i("插入线程记录成功");
    }else{
        UtilsLog.i("插入线程记录失败");
    }
}


//查询当前线程 下载的进度
public synchronized ThreadInfo query(String threadId,String queryUrl){
    Cursor cursor=db.query(ThreadHelper.TABLE_NAME,null,"threadId= ? and url= ?",new String[]{threadId,queryUrl},null,null,null);
    ThreadInfo info=new ThreadInfo();
    if(cursor!=null){
        while (cursor.moveToNext()){
            int start=cursor.getInt(2);
            int end=cursor.getInt(3);
            int completed=cursor.getInt(4);
            String url=cursor.getString(5);
            info.setThreadId(threadId);
            info.setStart(start);
            info.setEnd(end);
            info.setCompeleted(completed);
            info.setUrl(url);
        }
        cursor.close();
    }
    return  info;
}


//更新当前线程下载进度
public synchronized void update(ThreadInfo info){
    ContentValues values=new ContentValues();
    values.put("start",info.getStart());
    values.put("completed",info.getCompeleted());
    db.update(ThreadHelper.TABLE_NAME,values,"threadId= ? and url= ?",new String[]{info.getThreadId(),info.getUrl()});
}

//关闭db
public void close(){
    db.close();
}

//判断多线程任务下载 是否第一次创建线程
public boolean isExist(String url){
    Cursor cursor=db.query(ThreadHelper.TABLE_NAME,null,"url= ?",new String[]{url},null,null,null);
    boolean isExist=cursor.moveToNext();
    cursor.close();
    return isExist;
}

public synchronized void delete(ThreadInfo info){
    long rowId=db.delete(ThreadHelper.TABLE_NAME,"url =? and threadId= ?",new String[]{info.getUrl(),info.getThreadId()});
    if(rowId!=-1){
        UtilsLog.i("删除下载线程记录成功");
    }else{
        UtilsLog.i("删除下载线程记录失败");
    }
}


public synchronized void delete(String url){
    long rowId=db.delete(ThreadHelper.TABLE_NAME,"url =? ",new String[]{url});
    if(rowId!=-1){
        UtilsLog.i("删除下载线程记录成功");
    }else{
        UtilsLog.i("删除下载线程记录失败");
    }
}}

4.基本的准备操作我们已经完成了,那么开始写关于下载的类吧。首先写的肯定是DownLoadManager类,就是管理任务下载的类。不多说,直接看代码。

public class DownLoadManager {
private Map<String, FileInfo> map = new HashMap<>();
private static int mThreadNum;
private int fileSize;
private boolean flag = false; //true第一次下载 false不是第一次下载
private List<DownLoadTask> threads;
private static FileInfo mInfo;
private static ResultListener mlistener;
public static ExecutorService executorService = Executors.newCachedThreadPool();
public static File file;
private int totalComleted;

private DownLoadManager() {
    threads = new ArrayList<>();
}

public static DownLoadManager getInstance(FileInfo info, int threadNum,ResultListener listener) {
    mlistener = listener;
    mThreadNum = threadNum;
    mInfo = info;
    return DownLoadManagerHolder.dlm;
}

private static class DownLoadManagerHolder {
    private static final DownLoadManager dlm = new DownLoadManager();
}

public void start() {
    totalComleted=0;
    clear();
    final FileInfo newInfo = DownLoad.getInstance().queryData(mInfo.getUrl());
    newInfo.setDownloading(true);
    map.put(mInfo.getUrl(),newInfo);
    prepare(newInfo);
}

//停止下载任务
public void stop() {
    map.get(mInfo.getUrl()).setDownloading(false);
    map.get(mInfo.getUrl()).setStop(true);
}


public void clear(){
    if(threads.size()>0){
        threads.clear();
    }
}

//重新下载任务
public void restart() {
    stop();
    try {
        File file = new File(com.cmazxiaoma.downloader.download.DownLoadManager.FILE_PATH, map.get(mInfo.getUrl()).getFileName());
        if (file.exists()) {
            file.delete();
        }
        java.lang.Thread.sleep(100);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    DownLoad.getInstance().resetData(mInfo.getUrl());
    start();
}

//获取当前任务状态, 是否在下载
public boolean getCurrentState() {
    return map.get(mInfo.getUrl()).isDownloading();
}

//添加下载任务
public void addTask(FileInfo info) {
    //判断数据库是否已经存在此下载信息
    if (!DownLoad.getInstance().isExist(info)) {
        DownLoad.getInstance().insertData(info);
        map.put(info.getUrl(), info);
    } else {
        DownLoad.getInstance().delete(info);
        DownLoad.getInstance().insertData(info);
        UtilsLog.i("map已经更新");
        map.remove(info.getUrl());
        map.put(info.getUrl(), info);
    }
}

private void prepare(final FileInfo newInfo) {
    new java.lang.Thread(){
        @Override
        public void run() {
            HttpURLConnection con = null;
            RandomAccessFile raf=null;
            try {
                //连接资源
                URL url = new URL(newInfo.getUrl());
                UtilsLog.i("url=" + url);
                con = (HttpURLConnection) url.openConnection();
                con.setConnectTimeout(2 * 1000);
                con.setRequestMethod("GET");
                int length = -1;
                UtilsLog.i("responseCode=" + con.getResponseCode());
                if (con.getResponseCode() == 200) {
                    length = con.getContentLength();
                    UtilsLog.i("文件大小=" + length);
                }
                if (length <= 0) {
                    return;
                }
                //创建文件保存路径
                File dir = new File(com.cmazxiaoma.downloader.download.DownLoadManager.FILE_PATH);
                if (!dir.exists()) {
                    dir.mkdirs();//建立多级文件夹
                }
                newInfo.setLength(length);
                fileSize = length;
                UtilsLog.i("当前线程Id=" + java.lang.Thread.currentThread().getId() + ",name=" + java.lang.Thread.currentThread().getName());
                int currentPartSize = fileSize / mThreadNum;
                file = new File(com.cmazxiaoma.downloader.download.DownLoadManager.FILE_PATH, newInfo.getFileName());
                raf = new RandomAccessFile(file, "rwd");
                raf.setLength(fileSize);
                if (Thread.getInstance().isExist(newInfo.getUrl())) {
                    flag = false;
                } else {
                    flag = true;
                }
                for (int i = 0; i < mThreadNum; i++) {
                    if (flag) {
                        UtilsLog.i("第一次多线程下载");
                        int start = i * currentPartSize;//计算每条线程下载的开始位置
                        int end = start + currentPartSize-1;//线程结束的位置
                        if(i==mThreadNum-1){
                            end=fileSize;
                        }
                        String threadId = "xiaoma" + i;
                        ThreadInfo threadInfo = new ThreadInfo(threadId, start, end, 0,newInfo.getUrl());
                        Thread.getInstance().insert(threadInfo);
                        DownLoadTask thread = new DownLoadTask(threadInfo,newInfo, threadId, start, end, 0);
                        DownLoadManager.executorService.execute(thread);
                        threads.add(thread);
                    } else {
                        UtilsLog.i("不是第一次多线程下载");
                        ThreadInfo threadInfo = Thread.getInstance().query("xiaoma" + i, newInfo.getUrl());
                        DownLoadTask thread = new DownLoadTask(threadInfo,newInfo,threadInfo.getThreadId(),threadInfo.getStart(),threadInfo.getEnd(),threadInfo.getCompeleted());//这里出现过问题
                        DownLoadManager.executorService.execute(thread);
                        threads.add(thread);
                    }
                }

                boolean  isCompleted=false;
                while(!isCompleted){
                    isCompleted=true;
                    for(DownLoadTask thread:threads){
                        totalComleted+=thread.completed;
                        if(!thread.isCompleted){
                            isCompleted=false;
                        }
                    }

                    if(newInfo.isStop()){
                        totalComleted=0;
                        return;
                    }

                    Message message=new Message();
                    message.what=0x555;
                    message.arg1=fileSize;
                    message.arg2=totalComleted;
                    handler.sendMessage(message);

                    if(isCompleted){
                        totalComleted=0;
                        //任务线程全部完成,清空集合
                        clear();
                        handler.sendEmptyMessage(0x666);
                        return;
                    }

                    totalComleted=0;
                    java.lang.Thread.sleep(1000);
                }
            }catch (Exception e) {
                e.printStackTrace();
            }finally {
                try {
                    if (con != null) {
                        con.disconnect();
                    }
                    if(raf!=null){
                        raf.close();
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }.start();
}


private Handler handler=new Handler(){
    @Override
    public void handleMessage(Message msg) {
        super.handleMessage(msg);
        switch (msg.what){
            case 0x555:
                if(mlistener!=null){
                    mlistener.progress(msg.arg1,msg.arg2);
                }
                break;
            case 0x666:
                if(mlistener!=null){
                    mlistener.comleted();
                }
                break;
        }
    }
};}

5.接下来呢,就是DownLoadTask类了,就是一个线程下载类。

public class DownLoadTask extends java.lang.Thread{
private int start;//当前线程的开始下载位置
private int end;//当前线程结束下载的位置
private RandomAccessFile raf;//当前线程负责下载的文件大小
public int completed=0;//当前线程已下载的字节数
private  String threadId;//自己定义的线程Id
private FileInfo info;
private ThreadInfo threadInfo;
public  boolean isCompleted=false; //true为当前线程完成任务,false为当前线程未完成任务
//保存新的start
public int  finshed=0;
public int newStart=0;


public DownLoadTask(ThreadInfo threadInfo,FileInfo info,String threadId, int start, int end,int completed){
    this.threadInfo=threadInfo;
    this.info=info;
    this.threadId=threadId;
    this.start=start;
    this.end=end;
    this.completed=completed;
}


@Override
public void run() {
        HttpURLConnection con = null;
        try {
            UtilsLog.i("start="+start+",end="+end+",completed="+completed+",threadId="+getThreadId());
            URL url = new URL(info.getUrl());
            con = (HttpURLConnection) url.openConnection();
            con.setConnectTimeout(2 * 1000);
            con.setRequestMethod("GET");
            con.setRequestProperty("Range", "bytes=" + start + "-"+end);//重点
            raf=new RandomAccessFile(DownLoadManager.file,"rwd");
            //从文件的某一位置写入
            raf.seek(start);
            if (con.getResponseCode() == 206) { //文件部分下载 返回码是206
                InputStream is = con.getInputStream();
                byte[] buffer = new byte[4096];
                int hasRead = 0;
                while ((hasRead = is.read(buffer)) != -1) {
                    //写入文件
                    raf.write(buffer, 0, hasRead);
                    //单个文件的完成程度
                    completed += hasRead;
                    threadInfo.setCompeleted(completed);
                    //保存新的start
                    finshed=finshed+hasRead;//这里出现过问题,嘻嘻
                    newStart=start+finshed;
                    threadInfo.setStart(newStart);
                   //UtilsLog.i("Thread:"+getThreadId()+",completed="     + completed);
                    //停止下载
                    if (info.isStop()) {
                        UtilsLog.i("isStop="+info.isStop());
                        //保存下载进度
                        UtilsLog.i("现在Thread:"+getThreadId()+",completed=" + completed);
                        Thread.getInstance().update(threadInfo);
                        return;
                    }
                }
                //删除该线程下载记录
                Thread.getInstance().delete(threadInfo);
                isCompleted=true;
                Thread.getInstance().update(threadInfo);
                UtilsLog.i("thread:"+getThreadId()+"已经完成任务!--"+"completed="+completed);
            }

        } catch (Exception e) {
            if (con != null) {
                con.disconnect();
            }
            try {
                if (raf != null) {
                    raf.close();
                }

            } catch (IOException e1) {
                e1.printStackTrace();
            }

        }

    }

public String getThreadId() {
    return threadId;
}}

6.接口,就是一个监听下载进度的接口,也是很简单。

public interface ResultListener{
void progress(int max, int progress);
void comleted();}
结束

大致操作就是这样,其实多线程也挺简单的。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,372评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,368评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,415评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,157评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,171评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,125评论 1 297
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,028评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,887评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,310评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,533评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,690评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,411评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,004评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,659评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,812评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,693评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,577评论 2 353

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 172,070评论 25 707
  • 体验环境 版本:V 5.7.1机型:iPhone 4s(iOS 8.4)时间:2015.7.23 简述 来往这个应...
    Hawe阅读 561评论 0 3
  • 以前很鄙视的一句话:一群人的孤单,是一个人的狂欢;一个人的狂欢,是一群人的孤单。 之所以鄙视这样看似...
    永远的小丸子呀阅读 207评论 0 1
  • 安吉的同事推荐:大竹海,江南天池,百草原,天荒坪滑雪场,漂流,kitty主题公园。爬爬山,农家乐吃饭,每天100元。
    游心于远阅读 189评论 0 0
  • 学习张爱玲这种外貌写作风格,自己写上一段。 从外面回来,她褪下格子披肩,里面穿着黑色金边短袖旗袍。人像黑瓷瓶里的...
    廖虫虫阅读 171评论 0 0