天天看点

android 多线程断点续传下载 三

今天跟大家一起分享下android开发中比较难的一个环节,可能很多人看到这个标题就会感觉头很大,的确如果没有良好的编码能力和逻辑思维,这块是很难搞明白的,前面2次总结中已经为大家分享过有关技术的一些基本要领,我们先一起简单回顾下它的基本原理。

http://blog.csdn.net/shimiso/article/details/6763664  android 多线程断点续传下载 一

http://blog.csdn.net/shimiso/article/details/6763986  android 多线程断点续传下载 二

什么是多线程下载?

多线程下载其实就是迅雷,bt一些下载原理,通过多个线程同时和服务器连接,那么你就可以榨取到较高的带宽了,大致做法是将文件切割成n块,每块交给单独一个线程去下载,各自下载完成后将文件块组合成一个文件,程序上要完成做切割和组装的小算法

什么是断点续传?

断点续传,就是当我们下载未结束时候,退出保存下载进度,当下次打开继续下载的时接着上次的进度继续下载,不用每次下载都重新开始,那么有关断点续传的原理和实现手段,可参考我以前的一篇总结http://blog.csdn.net/shimiso/article/details/5956314 里面详细讲解http协议断点续传的原理,务必要看懂,否则你无法真正理解本节代码

怎么完成多线程断点续传?

将两者合二为一需要程序记住每个文件块的下载进度,并保存入库,当下载程序启动时候你需要判断程序是否已经下载过该文件,并取出各个文件块的保存记录,换算出下载进度继续下载,在这里你需要掌握java多线程的基本知识,handler的使用,以及集合,算法,文件操作等基本技能,同时还要解决sqlite数据库的同步问题,因为它是不太怎么支持多线程操作的,控制不好经常会出现库被锁定的异常,同时在android2.3以后就不能activity中直接操作http,否则你将收到系统送上的networkonmainthreadexception异常,在ui体验上一定记住要使用异步完成,既然大致思路已经清楚,下面我们开始分析程序:

[java] view

plaincopy

package cn.demo.dbhelper;  

 import android.content.context;  

import android.database.sqlite.sqlitedatabase;  

import android.database.sqlite.sqliteopenhelper;  

     /** 

      * 建立一个数据库帮助类 

      */  

 public class dbhelper extends sqliteopenhelper {  

     //download.db-->数据库名  

     public dbhelper(context context) {  

         super(context, "download.db", null, 1);  

     }  

      * 在download.db数据库下创建一个download_info表存储下载信息 

     @override  

     public void oncreate(sqlitedatabase db) {  

         db.execsql("create table download_info(_id integer primary key autoincrement, thread_id integer, "  

                 + "start_pos integer, end_pos integer, compelete_size integer,url char)");  

     public void onupgrade(sqlitedatabase db, int oldversion, int newversion) {  

 }  

 数据库操作要借助单例和同步,来保证线程的执行顺序,以免多个线程争相抢用sqlite资源导致异常出现

package cn.demo.dao;  

import java.util.arraylist;  

import java.util.list;  

import android.content.context;  

import android.database.cursor;  

import cn.demo.dbhelper.dbhelper;  

import cn.demo.entity.downloadinfo;  

/** 

 *  

 * 一个业务类 

 */  

public class dao {    

    private static dao dao=null;  

    private context context;   

    private  dao(context context) {   

        this.context=context;  

    }  

    public static  dao getinstance(context context){  

        if(dao==null){  

            dao=new dao(context);   

        }  

        return dao;  

    public  sqlitedatabase getconnection() {  

        sqlitedatabase sqlitedatabase = null;  

        try {   

            sqlitedatabase= new dbhelper(context).getreadabledatabase();  

        } catch (exception e) {    

        return sqlitedatabase;  

    /** 

     * 查看数据库中是否有数据 

     */  

    public synchronized boolean ishasinfors(string urlstr) {  

        sqlitedatabase database = getconnection();  

        int count = -1;  

        cursor cursor = null;  

        try {  

            string sql = "select count(*)  from download_info where url=?";  

            cursor = database.rawquery(sql, new string[] { urlstr });  

            if (cursor.movetofirst()) {  

                count = cursor.getint(0);  

            }   

        } catch (exception e) {  

            e.printstacktrace();  

        } finally {  

            if (null != database) {  

                database.close();  

            }  

            if (null != cursor) {  

                cursor.close();  

        return count == 0;  

     * 保存 下载的具体信息 

    public synchronized void saveinfos(list<downloadinfo> infos) {  

            for (downloadinfo info : infos) {  

                string sql = "insert into download_info(thread_id,start_pos, end_pos,compelete_size,url) values (?,?,?,?,?)";  

                object[] bindargs = { info.getthreadid(), info.getstartpos(),  

                        info.getendpos(), info.getcompeletesize(),  

                        info.geturl() };  

                database.execsql(sql, bindargs);  

     * 得到下载具体信息 

    public synchronized list<downloadinfo> getinfos(string urlstr) {  

        list<downloadinfo> list = new arraylist<downloadinfo>();  

            string sql = "select thread_id, start_pos, end_pos,compelete_size,url from download_info where url=?";  

            while (cursor.movetonext()) {  

                downloadinfo info = new downloadinfo(cursor.getint(0),  

                        cursor.getint(1), cursor.getint(2), cursor.getint(3),  

                        cursor.getstring(4));  

                list.add(info);  

        return list;  

     * 更新数据库中的下载信息 

    public synchronized void updatainfos(int threadid, int compeletesize, string urlstr) {  

            string sql = "update download_info set compelete_size=? where thread_id=? and url=?";  

            object[] bindargs = { compeletesize, threadid, urlstr };  

            database.execsql(sql, bindargs);  

     * 下载完成后删除数据库中的数据 

    public synchronized void delete(string url) {  

            database.delete("download_info", "url=?", new string[] { url });  

}  

package cn.demo.download;  

import java.util.map;  

import android.view.layoutinflater;  

import android.view.view;  

import android.view.view.onclicklistener;  

import android.view.viewgroup;  

import android.widget.baseadapter;  

import android.widget.button;  

import android.widget.textview;  

public class downloadadapter extends baseadapter{  

    private layoutinflater minflater;  

    private list<map<string, string>> data;  

    private context context;  

    private onclicklistener click;  

    public downloadadapter(context context,list<map<string, string>> data) {  

        minflater = layoutinflater.from(context);  

        this.data=data;  

    public void refresh(list<map<string, string>> data) {  

        this.notifydatasetchanged();  

    public void setonclick(onclicklistener click) {  

         this.click=click;  

    @override  

    public int getcount() {  

        return data.size();  

    public object getitem(int position) {  

        return data.get(position);  

    public long getitemid(int position) {  

        return position;  

    public view getview(final int position, view convertview, viewgroup parent) {  

        final map<string, string> bean=data.get(position);  

        viewholder holder = null;  

        if (convertview == null) {  

            convertview = minflater.inflate(r.layout.list_item, null);  

            holder = new viewholder();   

            holder.resoucename=(textview) convertview.findviewbyid(r.id.tv_resouce_name);  

            holder.startdownload=(button) convertview.findviewbyid(r.id.btn_start);  

            holder.pausedownload=(button) convertview.findviewbyid(r.id.btn_pause);  

            convertview.settag(holder);  

        } else {  

            holder = (viewholder) convertview.gettag();  

        }   

        holder.resoucename.settext(bean.get("name"));   

        return convertview;  

    public onclicklistener getclick() {  

        return click;  

    public void setclick(onclicklistener click) {  

        this.click = click;  

    private class viewholder {   

        public textview resoucename;  

        public button startdownload;  

        public button pausedownload;  

注意子线程不要影响主ui线程,灵活运用task和handler,各取所长,保证用户体验,handler通常在主线程中有利于专门负责处理ui的一些工作

 import java.util.arraylist;  

import java.util.hashmap;  

import android.app.listactivity;  

import android.os.asynctask;  

import android.os.bundle;  

import android.os.handler;  

import android.os.message;  

import android.widget.linearlayout;  

import android.widget.linearlayout.layoutparams;  

import android.widget.progressbar;   

import android.widget.toast;  

import cn.demo.entity.loadinfo;  

import cn.demo.service.downloader;  

 public class mainactivity extends listactivity {   

     // 固定下载的资源路径,这里可以设置网络上的地址  

     private static final string url = "http://download.haozip.com/";  

     // 固定存放下载的音乐的路径:sd卡目录下  

     private static final string sd_path = "/mnt/sdcard/";  

     // 存放各个下载器  

     private map<string, downloader> downloaders = new hashmap<string, downloader>();  

     // 存放与下载器对应的进度条  

     private map<string, progressbar> progressbars = new hashmap<string, progressbar>();  

      * 利用消息处理机制适时更新进度条 

     private handler mhandler = new handler() {  

         public void handlemessage(message msg) {  

             if (msg.what == 1) {  

                 string url = (string) msg.obj;  

                 int length = msg.arg1;  

                 progressbar bar = progressbars.get(url);  

                 if (bar != null) {  

                     // 设置进度条按读取的length长度更新  

                     bar.incrementprogressby(length);  

                     if (bar.getprogress() == bar.getmax()) {  

                         linearlayout layout = (linearlayout) bar.getparent();  

                         textview resoucename=(textview)layout.findviewbyid(r.id.tv_resouce_name);  

                         toast.maketext(mainactivity.this, "["+resoucename.gettext()+"]下载完成!", toast.length_short).show();  

                         // 下载完成后清除进度条并将map中的数据清空  

                         layout.removeview(bar);  

                         progressbars.remove(url);  

                         downloaders.get(url).delete(url);  

                         downloaders.get(url).reset();  

                         downloaders.remove(url);  

                         button btn_start=(button)layout.findviewbyid(r.id.btn_start);  

                         button btn_pause=(button)layout.findviewbyid(r.id.btn_pause);  

                         btn_pause.setvisibility(view.gone);  

                         btn_start.setvisibility(view.gone);  

                     }  

                 }  

             }  

         }  

     };  

     public void oncreate(bundle savedinstancestate) {  

         super.oncreate(savedinstancestate);  

         setcontentview(r.layout.main);   

         showlistview();  

     // 显示listview,这里可以随便添加  

     private void showlistview() {  

         list<map<string, string>> data = new arraylist<map<string, string>>();  

         map<string, string> map = new hashmap<string, string>();  

         map.put("name", "haozip_v3.1.exe");  

         data.add(map);  

         map = new hashmap<string, string>();  

         map.put("name", "haozip_v3.1_hj.exe");  

         map.put("name", "haozip_v2.8_x64_tiny.exe");  

         map.put("name", "haozip_v2.8_tiny.exe");  

         downloadadapter adapter=new downloadadapter(this,data);    

         setlistadapter(adapter);  

      * 响应开始下载按钮的点击事件 

     public void startdownload(view v) {  

         // 得到textview的内容   

         linearlayout layout = (linearlayout) v.getparent();  

         string resoucename = ((textview) layout.findviewbyid(r.id.tv_resouce_name)).gettext().tostring();  

         string urlstr = url + resoucename;  

         string localfile = sd_path + resoucename;  

         //设置下载线程数为4,这里是我为了方便随便固定的  

         string threadcount = "4";  

         downloadtask downloadtask=new downloadtask(v);  

         downloadtask.execute(urlstr,localfile,threadcount);  

    class downloadtask extends asynctask<string, integer, loadinfo>{  

        downloader downloader=null;   

        view v=null;  

        string urlstr=null;  

        public downloadtask(final view v){  

            this.v=v;  

        }    

        @override  

        protected void onpreexecute() {   

            button btn_start=(button)((view)v.getparent()).findviewbyid(r.id.btn_start);  

            button btn_pause=(button)((view)v.getparent()).findviewbyid(r.id.btn_pause);  

            btn_start.setvisibility(view.gone);  

            btn_pause.setvisibility(view.visible);  

        protected loadinfo doinbackground(string... params) {  

            urlstr=params[0];  

            string localfile=params[1];  

            int threadcount=integer.parseint(params[2]);  

             // 初始化一个downloader下载器  

             downloader = downloaders.get(urlstr);  

             if (downloader == null) {  

                 downloader = new downloader(urlstr, localfile, threadcount, mainactivity.this, mhandler);  

                 downloaders.put(urlstr, downloader);  

             if (downloader.isdownloading())  

                 return null;  

             // 得到下载信息类的个数组成集合  

             return downloader.getdownloaderinfors();   

        protected void onpostexecute(loadinfo loadinfo) {  

            if(loadinfo!=null){  

                 // 显示进度条  

                 showprogress(loadinfo, urlstr, v);  

                 // 调用方法开始下载  

                 downloader.download();  

      * 显示进度条 

     private void showprogress(loadinfo loadinfo, string url, view v) {  

         progressbar bar = progressbars.get(url);  

         if (bar == null) {  

             bar = new progressbar(this, null, android.r.attr.progressbarstylehorizontal);  

             bar.setmax(loadinfo.getfilesize());  

             bar.setprogress(loadinfo.getcomplete());  

             progressbars.put(url, bar);  

             linearlayout.layoutparams params = new layoutparams(layoutparams.fill_parent, 5);  

             ((linearlayout) ((linearlayout) v.getparent()).getparent()).addview(bar, params);  

      * 响应暂停下载按钮的点击事件 

     public void pausedownload(view v) {  

         downloaders.get(urlstr).pause();  

         button btn_start=(button)((view)v.getparent()).findviewbyid(r.id.btn_start);  

         button btn_pause=(button)((view)v.getparent()).findviewbyid(r.id.btn_pause);  

         btn_pause.setvisibility(view.gone);  

         btn_start.setvisibility(view.visible);  

 这是一个信息的实体,记录了一些字典信息,可以认为是一个简单bean对象

package cn.demo.entity;  

 /** 

  *创建一个下载信息的实体类 

  */  

 public class downloadinfo {  

     private int threadid;//下载器id  

     private int startpos;//开始点  

     private int endpos;//结束点  

     private int compeletesize;//完成度  

     private string url;//下载器网络标识  

     public downloadinfo(int threadid, int startpos, int endpos,  

             int compeletesize,string url) {  

         this.threadid = threadid;  

         this.startpos = startpos;  

         this.endpos = endpos;  

         this.compeletesize = compeletesize;  

         this.url=url;  

     public downloadinfo() {  

     public string geturl() {  

         return url;  

     public void seturl(string url) {  

         this.url = url;  

     public int getthreadid() {  

         return threadid;  

     public void setthreadid(int threadid) {  

     public int getstartpos() {  

         return startpos;  

     public void setstartpos(int startpos) {  

     public int getendpos() {  

         return endpos;  

     public void setendpos(int endpos) {  

     public int getcompeletesize() {  

         return compeletesize;  

     public void setcompeletesize(int compeletesize) {  

     public string tostring() {  

         return "downloadinfo [threadid=" + threadid  

                 + ", startpos=" + startpos + ", endpos=" + endpos  

                 + ", compeletesize=" + compeletesize +"]";  

  *自定义的一个记载下载器详细信息的类  

 public class loadinfo {  

     public int filesize;//文件大小  

     private int complete;//完成度  

     private string urlstring;//下载器标识  

     public loadinfo(int filesize, int complete, string urlstring) {  

         this.filesize = filesize;  

         this.complete = complete;  

         this.urlstring = urlstring;  

     public loadinfo() {  

     public int getfilesize() {  

         return filesize;  

     public void setfilesize(int filesize) {  

     public int getcomplete() {  

         return complete;  

     public void setcomplete(int complete) {  

     public string geturlstring() {  

         return urlstring;  

     public void seturlstring(string urlstring) {  

         return "loadinfo [filesize=" + filesize + ", complete=" + complete  

                 + ", urlstring=" + urlstring + "]";  

这是一个核心类,专门用来处理下载的

package cn.demo.service;  

 import java.io.file;  

import java.io.inputstream;  

import java.io.randomaccessfile;  

import java.net.httpurlconnection;  

import java.net.url;  

import android.util.log;  

import cn.demo.dao.dao;  

 public class downloader {  

     private string urlstr;// 下载的地址  

     private string localfile;// 保存路径  

     private int threadcount;// 线程数  

     private handler mhandler;// 消息处理器   

     private int filesize;// 所要下载的文件的大小  

     private context context;   

     private list<downloadinfo> infos;// 存放下载信息类的集合  

     private static final int init = 1;//定义三种下载的状态:初始化状态,正在下载状态,暂停状态  

     private static final int downloading = 2;  

     private static final int pause = 3;  

     private int state = init;  

     public downloader(string urlstr, string localfile, int threadcount,  

             context context, handler mhandler) {  

         this.urlstr = urlstr;  

         this.localfile = localfile;  

         this.threadcount = threadcount;  

         this.mhandler = mhandler;  

         this.context = context;  

      *判断是否正在下载  

     public boolean isdownloading() {  

         return state == downloading;  

      * 得到downloader里的信息 

      * 首先进行判断是否是第一次下载,如果是第一次就要进行初始化,并将下载器的信息保存到数据库中 

      * 如果不是第一次下载,那就要从数据库中读出之前下载的信息(起始位置,结束为止,文件大小等),并将下载信息返回给下载器 

     public loadinfo getdownloaderinfors() {  

         if (isfirst(urlstr)) {  

             log.v("tag", "isfirst");  

             init();  

             int range = filesize / threadcount;  

             infos = new arraylist<downloadinfo>();  

             for (int i = 0; i < threadcount - 1; i++) {  

                 downloadinfo info = new downloadinfo(i, i * range, (i + 1)* range - 1, 0, urlstr);  

                 infos.add(info);  

             downloadinfo info = new downloadinfo(threadcount - 1,(threadcount - 1) * range, filesize - 1, 0, urlstr);  

             infos.add(info);  

             //保存infos中的数据到数据库  

             dao.getinstance(context).saveinfos(infos);  

             //创建一个loadinfo对象记载下载器的具体信息  

             loadinfo loadinfo = new loadinfo(filesize, 0, urlstr);  

             return loadinfo;  

         } else {  

             //得到数据库中已有的urlstr的下载器的具体信息  

             infos = dao.getinstance(context).getinfos(urlstr);  

             log.v("tag", "not isfirst size=" + infos.size());  

             int size = 0;  

             int compeletesize = 0;  

             for (downloadinfo info : infos) {  

                 compeletesize += info.getcompeletesize();  

                 size += info.getendpos() - info.getstartpos() + 1;  

             return new loadinfo(size, compeletesize, urlstr);  

      * 初始化 

     private void init() {  

         try {  

             url url = new url(urlstr);  

             httpurlconnection connection = (httpurlconnection) url.openconnection();  

             connection.setconnecttimeout(5000);  

             connection.setrequestmethod("get");  

             filesize = connection.getcontentlength();  

             file file = new file(localfile);  

             if (!file.exists()) {  

                 file.createnewfile();  

             // 本地访问文件  

             randomaccessfile accessfile = new randomaccessfile(file, "rwd");  

             accessfile.setlength(filesize);  

             accessfile.close();  

             connection.disconnect();  

         } catch (exception e) {  

             e.printstacktrace();  

     }    

      * 判断是否是第一次 下载 

     private boolean isfirst(string urlstr) {  

         return dao.getinstance(context).ishasinfors(urlstr);  

      * 利用线程开始下载数据 

     public void download() {  

         if (infos != null) {  

             if (state == downloading)  

                 return;  

             state = downloading;  

                 new mythread(info.getthreadid(), info.getstartpos(),  

                         info.getendpos(), info.getcompeletesize(),  

                         info.geturl()).start();  

     public class mythread extends thread {  

         private int threadid;  

         private int startpos;  

         private int endpos;  

         private int compeletesize;  

         private string urlstr;  

         public mythread(int threadid, int startpos, int endpos,  

                 int compeletesize, string urlstr) {  

             this.threadid = threadid;  

             this.startpos = startpos;  

             this.endpos = endpos;  

             this.compeletesize = compeletesize;  

             this.urlstr = urlstr;  

         @override  

         public void run() {  

             httpurlconnection connection = null;  

             randomaccessfile randomaccessfile = null;  

             inputstream is = null;  

             try {  

                 url url = new url(urlstr);  

                 connection = (httpurlconnection) url.openconnection();  

                 connection.setconnecttimeout(5000);  

                 connection.setrequestmethod("get");  

                 // 设置范围,格式为range:bytes x-y;  

                 connection.setrequestproperty("range", "bytes="+(startpos + compeletesize) + "-" + endpos);  

                 randomaccessfile = new randomaccessfile(localfile, "rwd");  

                 randomaccessfile.seek(startpos + compeletesize);  

                 // 将要下载的文件写到保存在保存路径下的文件中  

                 is = connection.getinputstream();  

                 byte[] buffer = new byte[4096];  

                 int length = -1;  

                 while ((length = is.read(buffer)) != -1) {  

                     randomaccessfile.write(buffer, 0, length);  

                     compeletesize += length;  

                     // 更新数据库中的下载信息  

                     dao.getinstance(context).updatainfos(threadid, compeletesize, urlstr);  

                     // 用消息将下载信息传给进度条,对进度条进行更新  

                     message message = message.obtain();  

                     message.what = 1;  

                     message.obj = urlstr;  

                     message.arg1 = length;  

                     mhandler.sendmessage(message);  

                     if (state == pause) {  

                         return;  

             } catch (exception e) {  

                 e.printstacktrace();  

             }    

     //删除数据库中urlstr对应的下载器信息  

     public void delete(string urlstr) {  

         dao.getinstance(context).delete(urlstr);  

     //设置暂停  

     public void pause() {  

         state = pause;  

     //重置下载状态  

     public void reset() {  

         state = init;  

以下是些xml文件

[html] view

<?xml version="1.0" encoding="utf-8"?>  

 <linearlayout xmlns:android="http://schemas.android.com/apk/res/android"  

            android:orientation="vertical"  

            android:layout_width="fill_parent"  

            android:layout_height="wrap_content">  

     <linearlayout  

            android:orientation="horizontal"  

            android:layout_height="wrap_content"  

            android:layout_marginbottom="5dip">  

         <textview   

             android:layout_width="fill_parent"  

             android:layout_height="wrap_content"  

             android:layout_weight="1"  

             android:id="@+id/tv_resouce_name"/>  

         <button  

             android:text="下载"  

             android:id="@+id/btn_start"  

             android:onclick="startdownload"/>  

             android:text="暂停"  

             android:visibility="gone"  

             android:id="@+id/btn_pause"  

             android:onclick="pausedownload"/>  

       </linearlayout>  

 </linearlayout>  

     android:orientation="vertical"  

     android:layout_width="fill_parent"  

     android:layout_height="fill_parent"  

     android:id="@+id/llroot">  

     <listview android:id="@android:id/list"  

         android:layout_width="fill_parent"  

         android:layout_height="fill_parent">  

     </listview>  

<manifest xmlns:android="http://schemas.android.com/apk/res/android"  

    package="cn.demo.download"  

    android:versioncode="1"  

    android:versionname="1.0">  

    <uses-sdk android:minsdkversion="8" android:targetsdkversion="8" />  

    <uses-permission android:name="android.permission.internet"/>   

    <uses-permission android:name="android.permission.write_external_storage"/>  

    <application android:label="@string/app_name"  

        android:icon="@drawable/ic_launcher"  

        android:theme="@style/apptheme">  

        <activity  

                android:name=".mainactivity"   

                android:label="@string/app_name" >  

                <intent-filter>  

                    <action android:name="android.intent.action.main" />   

                    <category android:name="android.intent.category.launcher" />  

                </intent-filter>  

            </activity>  

    </application>  

</manifest>  

运行效果如下

android 多线程断点续传下载 三

源码下载地址