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

来源:互联网 发布:python 微信接口 编辑:程序博客网 时间:2024/05/16 02:13
效果图

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这些属性。这些都很简单

    //ThredInfo.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 +                '}';    }} //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();}
结束

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

原创粉丝点击