【Android开发经验】关于“多线程断点续传下载”功能的一个简单实现和讲解

转载请注明出处:http://blog.csdn.net/zhaokaiqiang1992

    上班第一天,在技术群里面和大家闲扯,无意中谈到了关于框架的使用,一个同学说为了用xUtils的断线续传下载功能,把整个库引入到了项目中,在google的官方建议中,是非常不建议这种做法的,集合框架虽然把很多功能集成起来,但是代码越多,出现问题的可能越大,而且无形之中增加了APK的大小,因此,得不偿失。所以,这篇文章主要就“断线续传”下载功能,简单的说下思路和代码实现,因为这类代码比较多,所以找了一个写的不错的demo,简单优化了一下。

    在贴代码之前,我们先分析一下需求和解决思路。首先是下载功能,我们简单的使用HttpURLConnection就可以了,没有引入框架的必要,然后就是断点续传了,其实断点续传指的就是我们可以随时停止我们的下载任务,当下次再次开始的时候,可以从上次下载到的位置继续下载,节省下载时间,很方便也很实用,做法无非就是在下载的过程中,纪录下下载到的位置,当再次开始下载的时候,我们从上一次的位置继续请求服务器即可。说到这里,有个类不得不提,那就是RandomAccessFile,这个类是实现断点续传功能的核心类,RandomAccessFile允许我们从我们想要的位置进行读写操作,因此,我们可以把我们要下载的文件切分成几部分,然后开启多个线程,分别从文件不同的位置进行下载,这样等所有的部分都下载完成之后,我们就能够得到一个完整的文件了,这就是多线程下载的原理,完成上面几个步骤,我们的多线程断线续传下载功能就基本完成了,下面是在网上找的一个Demo,我对代码进行了部分修改,从代码里面,我们看一下如何进行代码的实现。

    首先,如果要实现断点续传,我们就要纪录每个线程下载的文件的位置,可以使用文件,也可以使用sp,也可以使用DB,这个Demo里面使用的DB,我们首先看一下数据库的Helper实现类,里面存储主键、线程号、开始位置、结束位置、完成位置和下载地址即可。

    DownLoadHelper.java

[java]  view plain copy
  1. public class DownLoadHelper extends SQLiteOpenHelper {  
  2.   
  3.     private static final String SQL_NAME = "download.db";  
  4.     private static final int DOWNLOAD_VERSION = 1;  
  5.   
  6.     public DownLoadHelper(Context context) {  
  7.         super(context, SQL_NAME, null, DOWNLOAD_VERSION);  
  8.     }  
  9.   
  10.     /** 
  11.      * 在download.db数据库下创建一个download_info表存储下载信息 
  12.      */  
  13.     @Override  
  14.     public void onCreate(SQLiteDatabase db) {  
  15.         db.execSQL("create table download_info(_id integer PRIMARY KEY AUTOINCREMENT, thread_id integer, "  
  16.                 + "start_pos integer, end_pos integer, compelete_size integer,url char)");  
  17.     }  
  18.   
  19.     @Override  
  20.     public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {  
  21.   
  22.     }  
  23.   
  24. }  

     有了Helper之后,我们在创建一个Sql工具类,完成对表的数据操作

    DownlaodSqlTool.java

[java]  view plain copy
  1. public class DownlaodSqlTool {  
  2.   
  3.     private DownLoadHelper dbHelper;  
  4.   
  5.     public DownlaodSqlTool(Context context) {  
  6.         dbHelper = new DownLoadHelper(context);  
  7.     }  
  8.   
  9.     /** 
  10.      * 创建下载的具体信息 
  11.      */  
  12.     public void insertInfos(List<DownloadInfo> infos) {  
  13.         SQLiteDatabase database = dbHelper.getWritableDatabase();  
  14.         for (DownloadInfo info : infos) {  
  15.             String sql = "insert into download_info(thread_id,start_pos, end_pos,compelete_size,url) values (?,?,?,?,?)";  
  16.             Object[] bindArgs = { info.getThreadId(), info.getStartPos(),  
  17.                     info.getEndPos(), info.getCompeleteSize(), info.getUrl() };  
  18.             database.execSQL(sql, bindArgs);  
  19.         }  
  20.     }  
  21.   
  22.     /** 
  23.      * 得到下载具体信息 
  24.      */  
  25.     public List<DownloadInfo> getInfos(String urlstr) {  
  26.         List<DownloadInfo> list = new ArrayList<DownloadInfo>();  
  27.         SQLiteDatabase database = dbHelper.getWritableDatabase();  
  28.         String sql = "select thread_id, start_pos, end_pos,compelete_size,url from download_info where url=?";  
  29.         Cursor cursor = database.rawQuery(sql, new String[] { urlstr });  
  30.         while (cursor.moveToNext()) {  
  31.             DownloadInfo info = new DownloadInfo(cursor.getInt(0),  
  32.                     cursor.getInt(1), cursor.getInt(2), cursor.getInt(3),  
  33.                     cursor.getString(4));  
  34.             list.add(info);  
  35.         }  
  36.         return list;  
  37.     }  
  38.   
  39.     /** 
  40.      * 更新数据库中的下载信息 
  41.      */  
  42.     public void updataInfos(int threadId, int compeleteSize, String urlstr) {  
  43.         SQLiteDatabase database = dbHelper.getWritableDatabase();  
  44.         String sql = "update download_info set compelete_size=? where thread_id=? and url=?";  
  45.         Object[] bindArgs = { compeleteSize, threadId, urlstr };  
  46.         database.execSQL(sql, bindArgs);  
  47.     }  
  48.   
  49.     /** 
  50.      * 关闭数据库 
  51.      */  
  52.     public void closeDb() {  
  53.         dbHelper.close();  
  54.     }  
  55.   
  56.     /** 
  57.      * 下载完成后删除数据库中的数据 
  58.      */  
  59.     public void delete(String url) {  
  60.         SQLiteDatabase database = dbHelper.getWritableDatabase();  
  61.         database.delete("download_info""url=?"new String[] { url });  
  62.     }  
  63. }  

     数据库相关的类就这些,断点续传的功能已经完成,下面看下载如何实现。

    首先,为了操作方便,我们对下载的文件抽取实体类

    DownloadInfo.java

[java]  view plain copy
  1. public class DownloadInfo {  
  2.   
  3.     private int threadId;// 下载器id  
  4.     private int startPos;// 开始点  
  5.     private int endPos;// 结束点  
  6.     private int compeleteSize;// 完成度  
  7.     private String url;// 下载文件的URL地址  
  8.   
  9.     public DownloadInfo(int threadId, int startPos, int endPos,  
  10.             int compeleteSize, String url) {  
  11.         this.threadId = threadId;  
  12.         this.startPos = startPos;  
  13.         this.endPos = endPos;  
  14.         this.compeleteSize = compeleteSize;  
  15.         this.url = url;  
  16.     }  
  17.   
  18.     public DownloadInfo() {  
  19.     }  
  20.   
  21.     public String getUrl() {  
  22.         return url;  
  23.     }  
  24.   
  25.     public void setUrl(String url) {  
  26.         this.url = url;  
  27.     }  
  28.   
  29.     public int getThreadId() {  
  30.         return threadId;  
  31.     }  
  32.   
  33.     public void setThreadId(int threadId) {  
  34.         this.threadId = threadId;  
  35.     }  
  36.   
  37.     public int getStartPos() {  
  38.         return startPos;  
  39.     }  
  40.   
  41.     public void setStartPos(int startPos) {  
  42.         this.startPos = startPos;  
  43.     }  
  44.   
  45.     public int getEndPos() {  
  46.         return endPos;  
  47.     }  
  48.   
  49.     public void setEndPos(int endPos) {  
  50.         this.endPos = endPos;  
  51.     }  
  52.   
  53.     public int getCompeleteSize() {  
  54.         return compeleteSize;  
  55.     }  
  56.   
  57.     public void setCompeleteSize(int compeleteSize) {  
  58.         this.compeleteSize = compeleteSize;  
  59.     }  
  60.   
  61.     @Override  
  62.     public String toString() {  
  63.         return "DownloadInfo [threadId=" + threadId + ", startPos=" + startPos  
  64.                 + ", endPos=" + endPos + ", compeleteSize=" + compeleteSize  
  65.                 + "]";  
  66.     }  
  67. }  

     实体类抽取之后,我们就可以实现下载功能了, DownloadHttpTool是实现下载功能的主要类

    DownloadHttpTool.java

[java]  view plain copy
  1. public class DownloadHttpTool {  
  2.   
  3.     private static final String TAG = DownloadHttpTool.class.getSimpleName();  
  4.     // 线程数量  
  5.     private int threadCount;  
  6.     // URL地址  
  7.     private String urlstr;  
  8.     private Context mContext;  
  9.     private Handler mHandler;  
  10.     // 保存下载信息的类  
  11.     private List<DownloadInfo> downloadInfos;  
  12.     // 目录  
  13.     private String localPath;  
  14.     // 文件名  
  15.     private String fileName;  
  16.     private int fileSize;  
  17.     // 文件信息保存的数据库操作类  
  18.     private DownlaodSqlTool sqlTool;  
  19.   
  20.     // 利用枚举表示下载的三种状态  
  21.     private enum Download_State {  
  22.         Downloading, Pause, Ready, Delete;  
  23.     }  
  24.   
  25.     // 当前下载状态  
  26.     private Download_State state = Download_State.Ready;  
  27.     // 所有线程下载的总数  
  28.     private int globalCompelete = 0;  
  29.   
  30.     public DownloadHttpTool(int threadCount, String urlString,  
  31.             String localPath, String fileName, Context context, Handler handler) {  
  32.         super();  
  33.         this.threadCount = threadCount;  
  34.         this.urlstr = urlString;  
  35.         this.localPath = localPath;  
  36.         this.mContext = context;  
  37.         this.mHandler = handler;  
  38.         this.fileName = fileName;  
  39.         sqlTool = new DownlaodSqlTool(mContext);  
  40.     }  
  41.   
  42.     // 在开始下载之前需要调用ready方法进行配置  
  43.     public void ready() {  
  44.         Log.w(TAG, "ready");  
  45.         globalCompelete = 0;  
  46.         downloadInfos = sqlTool.getInfos(urlstr);  
  47.         if (downloadInfos.size() == 0) {  
  48.             initFirst();  
  49.         } else {  
  50.             File file = new File(localPath + "/" + fileName);  
  51.             if (!file.exists()) {  
  52.                 sqlTool.delete(urlstr);  
  53.                 initFirst();  
  54.             } else {  
  55.                 fileSize = downloadInfos.get(downloadInfos.size() - 1)  
  56.                         .getEndPos();  
  57.                 for (DownloadInfo info : downloadInfos) {  
  58.                     globalCompelete += info.getCompeleteSize();  
  59.                 }  
  60.                 Log.w(TAG, "globalCompelete:::" + globalCompelete);  
  61.             }  
  62.         }  
  63.     }  
  64.   
  65.     public void start() {  
  66.         Log.w(TAG, "start");  
  67.         if (downloadInfos != null) {  
  68.             if (state == Download_State.Downloading) {  
  69.                 return;  
  70.             }  
  71.             state = Download_State.Downloading;  
  72.             for (DownloadInfo info : downloadInfos) {  
  73.                 Log.v(TAG, "startThread");  
  74.                 new DownloadThread(info.getThreadId(), info.getStartPos(),  
  75.                         info.getEndPos(), info.getCompeleteSize(),  
  76.                         info.getUrl()).start();  
  77.             }  
  78.         }  
  79.     }  
  80.   
  81.     public void pause() {  
  82.         state = Download_State.Pause;  
  83.         sqlTool.closeDb();  
  84.     }  
  85.   
  86.     public void delete() {  
  87.         state = Download_State.Delete;  
  88.         compelete();  
  89.         new File(localPath + File.separator + fileName).delete();  
  90.     }  
  91.   
  92.     public void compelete() {  
  93.         sqlTool.delete(urlstr);  
  94.         sqlTool.closeDb();  
  95.     }  
  96.   
  97.     public int getFileSize() {  
  98.         return fileSize;  
  99.     }  
  100.   
  101.     public int getCompeleteSize() {  
  102.         return globalCompelete;  
  103.     }  
  104.   
  105.     /** 
  106.      * 第一次下载初始化 
  107.      */  
  108.     private void initFirst() {  
  109.         Log.w(TAG, "initFirst");  
  110.         try {  
  111.             URL url = new URL(urlstr);  
  112.             HttpURLConnection connection = (HttpURLConnection) url  
  113.                     .openConnection();  
  114.             connection.setConnectTimeout(5000);  
  115.             connection.setRequestMethod("GET");  
  116.             fileSize = connection.getContentLength();  
  117.             Log.w(TAG, "fileSize::" + fileSize);  
  118.             File fileParent = new File(localPath);  
  119.             if (!fileParent.exists()) {  
  120.                 fileParent.mkdir();  
  121.             }  
  122.             File file = new File(fileParent, fileName);  
  123.             if (!file.exists()) {  
  124.                 file.createNewFile();  
  125.             }  
  126.             // 本地访问文件  
  127.             RandomAccessFile accessFile = new RandomAccessFile(file, "rwd");  
  128.             accessFile.setLength(fileSize);  
  129.             accessFile.close();  
  130.             connection.disconnect();  
  131.         } catch (Exception e) {  
  132.             e.printStackTrace();  
  133.         }  
  134.         int range = fileSize / threadCount;  
  135.         downloadInfos = new ArrayList<DownloadInfo>();  
  136.         for (int i = 0; i < threadCount - 1; i++) {  
  137.             DownloadInfo info = new DownloadInfo(i, i * range, (i + 1) * range  
  138.                     - 10, urlstr);  
  139.             downloadInfos.add(info);  
  140.         }  
  141.         DownloadInfo info = new DownloadInfo(threadCount - 1, (threadCount - 1)  
  142.                 * range, fileSize - 10, urlstr);  
  143.         downloadInfos.add(info);  
  144.         sqlTool.insertInfos(downloadInfos);  
  145.     }  
  146.   
  147.     /** 
  148.      * 自定义下载线程 
  149.      *  
  150.      * @author zhaokaiqiang 
  151.      * @time 2015-2-25下午5:52:28 
  152.      */  
  153.     private class DownloadThread extends Thread {  
  154.   
  155.         private int threadId;  
  156.         private int startPos;  
  157.         private int endPos;  
  158.         private int compeleteSize;  
  159.         private String urlstr;  
  160.         private int totalThreadSize;  
  161.   
  162.         public DownloadThread(int threadId, int startPos, int endPos,  
  163.                 int compeleteSize, String urlstr) {  
  164.             this.threadId = threadId;  
  165.             this.startPos = startPos;  
  166.             this.endPos = endPos;  
  167.             totalThreadSize = endPos - startPos + 1;  
  168.             this.urlstr = urlstr;  
  169.             this.compeleteSize = compeleteSize;  
  170.         }  
  171.   
  172.         @Override  
  173.         public void run() {  
  174.             HttpURLConnection connection = null;  
  175.             RandomAccessFile randomAccessFile = null;  
  176.             InputStream is = null;  
  177.             try {  
  178.                 randomAccessFile = new RandomAccessFile(localPath  
  179.                         + File.separator + fileName, "rwd");  
  180.                 randomAccessFile.seek(startPos + compeleteSize);  
  181.                 URL url = new URL(urlstr);  
  182.                 connection = (HttpURLConnection) url.openConnection();  
  183.                 connection.setConnectTimeout(5000);  
  184.                 connection.setRequestMethod("GET");  
  185.                 connection.setRequestProperty("Range""bytes="  
  186.                         + (startPos + compeleteSize) + "-" + endPos);  
  187.                 is = connection.getInputStream();  
  188.                 byte[] buffer = new byte[1024];  
  189.                 int length = -1;  
  190.                 while ((length = is.read(buffer)) != -1) {  
  191.                     randomAccessFile.write(buffer, 0, length);  
  192.                     compeleteSize += length;  
  193.                     Message message = Message.obtain();  
  194.                     message.what = threadId;  
  195.                     message.obj = urlstr;  
  196.                     message.arg1 = length;  
  197.                     mHandler.sendMessage(message);  
  198.                     Log.w(TAG, "Threadid::" + threadId + "    compelete::"  
  199.                             + compeleteSize + "    total::" + totalThreadSize);  
  200.                     // 当程序不再是下载状态的时候,纪录当前的下载进度  
  201.                     if ((state != Download_State.Downloading)  
  202.                             || (compeleteSize >= totalThreadSize)) {  
  203.                         sqlTool.updataInfos(threadId, compeleteSize, urlstr);  
  204.                         break;  
  205.                     }  
  206.                 }  
  207.   
  208.             } catch (Exception e) {  
  209.                 e.printStackTrace();  
  210.                 sqlTool.updataInfos(threadId, compeleteSize, urlstr);  
  211.             } finally {  
  212.                 try {  
  213.                     if (is != null) {  
  214.                         is.close();  
  215.                     }  
  216.                     randomAccessFile.close();  
  217.                     connection.disconnect();  
  218.                 } catch (Exception e) {  
  219.                     e.printStackTrace();  
  220.                 }  
  221.             }  
  222.         }  
  223.     }  
  224. }  

    在上面的代码中,我们定义了一个线程,进行文件的多线程下载,并且在退出下载状态和完成下载的时候,纪录下载的位置,存到数据库中。在原来的代码中,是没获取一次数据,存取一次数据库,大大的增加了数据库的操作频率,降低了效率,原先下载420k左右的文件,需要操作420次数据库,现在只要一次即可。

    在initFirst()里面,首先进行了初始化,根据下载文件的大小和开启线程的数量,对下载实体类进行了初始化和赋值。在RandomAccessFile创建完毕,DownloadInfo初始化完毕之后,就可以通过start()进行文件的下载了。

    其实到这里,基本的功能已经实现了。为了使得我们的操作更加的方便,同时可以监控到下载的进度,我们对下载类进行一次封装,代码如下:

    DownloadUtil.java

[java]  view plain copy
  1. public class DownloadUtil {  
  2.   
  3.     private DownloadHttpTool mDownloadHttpTool;  
  4.     private OnDownloadListener onDownloadListener;  
  5.   
  6.     private int fileSize;  
  7.     private int downloadedSize = 0;  
  8.   
  9.     @SuppressLint("HandlerLeak")  
  10.     private Handler mHandler = new Handler() {  
  11.   
  12.         @Override  
  13.         public void handleMessage(Message msg) {  
  14.             int length = msg.arg1;  
  15.             synchronized (this) {// 加锁保证已下载的正确性  
  16.                 downloadedSize += length;  
  17.   
  18.             }  
  19.             if (onDownloadListener != null) {  
  20.                 onDownloadListener.downloadProgress(downloadedSize);  
  21.             }  
  22.             if (downloadedSize >= fileSize) {  
  23.                 mDownloadHttpTool.compelete();  
  24.                 if (onDownloadListener != null) {  
  25.                     onDownloadListener.downloadEnd();  
  26.                 }  
  27.             }  
  28.         }  
  29.   
  30.     };  
  31.   
  32.     public DownloadUtil(int threadCount, String filePath, String filename,  
  33.             String urlString, Context context) {  
  34.   
  35.         mDownloadHttpTool = new DownloadHttpTool(threadCount, urlString,  
  36.                 filePath, filename, context, mHandler);  
  37.     }  
  38.   
  39.     // 下载之前首先异步线程调用ready方法获得文件大小信息,之后调用开始方法  
  40.     public void start() {  
  41.         new AsyncTask<Void, Void, Void>() {  
  42.   
  43.             @Override  
  44.             protected Void doInBackground(Void... arg0) {  
  45.                 mDownloadHttpTool.ready();  
  46.                 return null;  
  47.             }  
  48.       
  49.             @Override  
  50.             protected void onPostExecute(Void result) {  
  51.                 fileSize = mDownloadHttpTool.getFileSize();  
  52.                 downloadedSize = mDownloadHttpTool.getCompeleteSize();  
  53.                 Log.w("Tag""downloadedSize::" + downloadedSize);  
  54.                 if (onDownloadListener != null) {  
  55.                     onDownloadListener.downloadStart(fileSize);  
  56.                 }  
  57.                 mDownloadHttpTool.start();  
  58.             }  
  59.         }.execute();  
  60.     }  
  61.   
  62.     public void pause() {  
  63.         mDownloadHttpTool.pause();  
  64.     }  
  65.   
  66.     public void delete() {  
  67.         mDownloadHttpTool.delete();  
  68.     }  
  69.   
  70.     public void reset() {  
  71.         mDownloadHttpTool.delete();  
  72.         start();  
  73.     }  
  74.   
  75.     public void setOnDownloadListener(OnDownloadListener onDownloadListener) {  
  76.         this.onDownloadListener = onDownloadListener;  
  77.     }  
  78.   
  79.     // 下载回调接口  
  80.     public interface OnDownloadListener {  
  81.         public void downloadStart(int fileSize);  
  82.   
  83.         public void downloadProgress(int downloadedSize);  
  84.   
  85.         public void downloadEnd();  
  86.     }  
  87. }  

       通过对外暴露接口,我们可以实现下载进度的监听了!

    用的时候也很简单,像下面这样就ok了

[java]  view plain copy
  1. String urlString = "http://bbra.cn/Uploadfiles/imgs/20110303/fengjin/013.jpg";  
  2.         final String localPath = Environment.getExternalStorageDirectory()  
  3.                 .getAbsolutePath() + "/ADownLoadTest";  
  4.         mDownloadUtil = new DownloadUtil(2, localPath, "abc.jpg", urlString,  
  5.                 this);  
  6.         mDownloadUtil.setOnDownloadListener(new OnDownloadListener() {  
  7.   
  8.             @Override  
  9.             public void downloadStart(int fileSize) {  
  10.                 max = fileSize;  
  11.                 mProgressBar.setMax(fileSize);  
  12.             }  
  13.   
  14.             @Override  
  15.             public void downloadProgress(int downloadedSize) {  
  16.                 mProgressBar.setProgress(downloadedSize);  
  17.                 total.setText((int) downloadedSize * 100 / max + "%");  
  18.             }  
  19.   
  20.             @Override  
  21.             public void downloadEnd() {  
  22.                 Bitmap bitmap = decodeSampledBitmapFromResource(localPath  
  23.                         + File.separator + "abc.jpg"200200);  
  24.                 image.setImageBitmap(bitmap);  
  25.             }  
  26.         });  

    GitHub的Demo地址:https://github.com/ZhaoKaiQiang/MultiThreadDownLoadDemo


你可能感兴趣的:(【Android开发经验】关于“多线程断点续传下载”功能的一个简单实现和讲解)