转载

AsyncTask实现多线程断点续传

前面一篇博客 《AsyncTask实现断点续传》 讲解了如何实现单线程下的断点续传,也就是一个文件只有一个线程进行下载。

对于大文件而言,使用多线程下载就会比单线程下载要快一些。多线程下载相比单线程下载要稍微复杂一点,本博文将详细讲解如何使用AsyncTask来实现多线程的断点续传下载。

一、实现原理

多线程下载首先要通过每个文件总的下载线程数(我这里设定5个)来确定每个线程所负责下载的起止位置。

long blockLength = mFileLength / DEFAULT_POOL_SIZE;   for (int i = 0; i < DEFAULT_POOL_SIZE; i++) {    long beginPosition = i * blockLength;//每条线程下载的开始位置    long endPosition = (i + 1) * blockLength;//每条线程下载的结束位置    if (i == (DEFAULT_POOL_SIZE - 1)) {     endPosition = mFileLength;//如果整个文件的大小不为线程个数的整数倍,则最后一个线程的结束位置即为文件的总长度    }      ......   } 

这里需要注意的是,文件大小往往不是线程个数的整数倍,所以最后一个线程的结束位置需要设置为文件长度。

确定好每个线程的下载起止位置之后,需要设置http请求头来下载文件的指定位置:

1       //设置下载的数据位置beginPosition字节到endPosition字节 2       Header header_size = new BasicHeader("Range", "bytes=" + beginPosition + "-" + endPosition); 3       request.addHeader(header_size);

以上是多线程下载的原理,但是还要实现断点续传需要在每次暂停之后记录每个线程已下载的大小,下次继续下载时从上次下载后的位置开始下载。一般项目中都会存数据库中,我这里为了简单起见直接存在了SharedPreferences中,已下载url和线程编号作为key值。

 1        @Override  2         protected void onPostExecute(Long aLong) {  3             Log.i(TAG, "download success ");  4             //下载完成移除记录  5             mSharedPreferences.edit().remove(currentThreadIndex).commit();  6         }  7   8         @Override  9         protected void onCancelled() { 10             Log.i(TAG, "download cancelled "); 11             //记录已下载大小current 12             mSharedPreferences.edit().putLong(currentThreadIndex, current).commit(); 13         }

下载的时候,首先获取已下载位置,如果已经下载过,就从上次下载后的位置开始下载:

//获取之前下载保存的信息,从之前结束的位置继续下载       //这里加了判断file.exists(),判断是否被用户删除了,如果文件没有下载完,但是已经被用户删除了,则重新下载       long downedPosition = mSharedPreferences.getLong(currentThreadIndex, 0);       if(file.exists() && downedPosition != 0) {           beginPosition = beginPosition + downedPosition;           current = downedPosition;           synchronized (mCurrentLength) {                mCurrentLength += downedPosition;           }       }

二、完整代码

  1 package com.bbk.lling.multithreaddownload;   2    3 import android.app.Activity;   4 import android.content.Context;   5 import android.content.SharedPreferences;   6 import android.os.AsyncTask;   7 import android.os.Bundle;   8 import android.os.Environment;   9 import android.os.Handler;  10 import android.os.Message;  11 import android.util.Log;  12 import android.view.View;  13 import android.widget.ProgressBar;  14 import android.widget.TextView;  15 import android.widget.Toast;  16   17 import org.apache.http.Header;  18 import org.apache.http.HttpResponse;  19 import org.apache.http.client.HttpClient;  20 import org.apache.http.client.methods.HttpGet;  21 import org.apache.http.impl.client.DefaultHttpClient;  22 import org.apache.http.message.BasicHeader;  23   24 import java.io.File;  25 import java.io.IOException;  26 import java.io.InputStream;  27 import java.io.OutputStream;  28 import java.io.RandomAccessFile;  29 import java.net.MalformedURLException;  30 import java.util.ArrayList;  31 import java.util.List;  32 import java.util.concurrent.Executor;  33 import java.util.concurrent.Executors;  34   35   36 public class MainActivity extends Activity {  37     private static final String TAG = "MainActivity";  38     private static final int DEFAULT_POOL_SIZE = 5;  39     private static final int GET_LENGTH_SUCCESS = 1;  40     //下载路径  41     private String downloadPath = Environment.getExternalStorageDirectory() +  42             File.separator + "download";  43   44 //    private String mUrl = "http://ftp.neu.edu.cn/mirrors/eclipse/technology/epp/downloads/release/juno/SR2/eclipse-java-juno-SR2-linux-gtk-x86_64.tar.gz";  45     private String mUrl = "http://p.gdown.baidu.com/c4cb746699b92c9b6565cc65aa2e086552651f73c5d0e634a51f028e32af6abf3d68079eeb75401c76c9bb301e5fb71c144a704cb1a2f527a2e8ca3d6fe561dc5eaf6538e5b3ab0699308d13fe0b711a817c88b0f85a01a248df82824ace3cd7f2832c7c19173236";  46     private ProgressBar mProgressBar;  47     private TextView mPercentTV;  48     SharedPreferences mSharedPreferences = null;  49     long mFileLength = 0;  50     Long mCurrentLength = 0L;  51   52     private InnerHandler mHandler = new InnerHandler();  53   54     //创建线程池  55     private Executor mExecutor = Executors.newCachedThreadPool();  56   57     private List<DownloadAsyncTask> mTaskList = new ArrayList<DownloadAsyncTask>();  58     @Override  59     protected void onCreate(Bundle savedInstanceState) {  60         super.onCreate(savedInstanceState);  61         setContentView(R.layout.activity_main);  62         mProgressBar = (ProgressBar) findViewById(R.id.progressbar);  63         mPercentTV = (TextView) findViewById(R.id.percent_tv);  64         mSharedPreferences = getSharedPreferences("download", Context.MODE_PRIVATE);  65         //开始下载  66         findViewById(R.id.begin).setOnClickListener(new View.OnClickListener() {  67             @Override  68             public void onClick(View v) {  69                 new Thread() {  70                     @Override  71                     public void run() {  72                         //创建存储文件夹  73                         File dir = new File(downloadPath);  74                         if (!dir.exists()) {  75                             dir.mkdir();  76                         }  77                         //获取文件大小  78                         HttpClient client = new DefaultHttpClient();  79                         HttpGet request = new HttpGet(mUrl);  80                         HttpResponse response = null;  81   82                         try {  83                             response = client.execute(request);  84                             mFileLength = response.getEntity().getContentLength();  85                         } catch (Exception e) {  86                             Log.e(TAG, e.getMessage());  87                         } finally {  88                             if (request != null) {  89                                 request.abort();  90                             }  91                         }  92                         Message.obtain(mHandler, GET_LENGTH_SUCCESS).sendToTarget();  93                     }  94                 }.start();  95             }  96         });  97   98         //暂停下载  99         findViewById(R.id.end).setOnClickListener(new View.OnClickListener() { 100             @Override 101             public void onClick(View v) { 102                 for (DownloadAsyncTask task : mTaskList) { 103                     if (task != null && (task.getStatus() == AsyncTask.Status.RUNNING || !task.isCancelled())) { 104                         task.cancel(true); 105                     } 106                 } 107                 mTaskList.clear(); 108             } 109         }); 110     } 111  112     /** 113      * 开始下载 114      * 根据待下载文件大小计算每个线程下载位置,并创建AsyncTask 115      */ 116     private void beginDownload() { 117         mCurrentLength = 0L; 118         mPercentTV.setVisibility(View.VISIBLE); 119         mProgressBar.setProgress(0); 120         long blockLength = mFileLength / DEFAULT_POOL_SIZE; 121         for (int i = 0; i < DEFAULT_POOL_SIZE; i++) { 122             long beginPosition = i * blockLength;//每条线程下载的开始位置 123             long endPosition = (i + 1) * blockLength;//每条线程下载的结束位置 124             if (i == (DEFAULT_POOL_SIZE - 1)) { 125                 endPosition = mFileLength;//如果整个文件的大小不为线程个数的整数倍,则最后一个线程的结束位置即为文件的总长度 126             } 127             DownloadAsyncTask task = new DownloadAsyncTask(beginPosition, endPosition); 128             mTaskList.add(task); 129             task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, mUrl, String.valueOf(i)); 130         } 131     } 132  133     /** 134      * 更新进度条 135      */ 136     synchronized public void updateProgress() { 137         int percent = (int) Math.ceil((float)mCurrentLength / (float)mFileLength * 100); 138 //        Log.i(TAG, "downloading  " + mCurrentLength + "," + mFileLength + "," + percent); 139         if(percent > mProgressBar.getProgress()) { 140             mProgressBar.setProgress(percent); 141             mPercentTV.setText("下载进度:" + percent + "%"); 142             if (mProgressBar.getProgress() == mProgressBar.getMax()) { 143                 Toast.makeText(MainActivity.this, "下载结束", Toast.LENGTH_SHORT).show(); 144             } 145         } 146     } 147  148     @Override 149     protected void onDestroy() { 150         for(DownloadAsyncTask task: mTaskList) { 151             if(task != null && task.getStatus() == AsyncTask.Status.RUNNING) { 152                 task.cancel(true); 153             } 154             mTaskList.clear(); 155         } 156         super.onDestroy(); 157     } 158  159     /** 160      * 下载的AsyncTask 161      */ 162     private class DownloadAsyncTask extends AsyncTask<String, Integer , Long> { 163         private static final String TAG = "DownloadAsyncTask"; 164         private long beginPosition = 0; 165         private long endPosition = 0; 166  167         private long current = 0; 168  169         private String currentThreadIndex; 170  171  172         public DownloadAsyncTask(long beginPosition, long endPosition) { 173             this.beginPosition = beginPosition; 174             this.endPosition = endPosition; 175         } 176  177         @Override 178         protected Long doInBackground(String... params) { 179             Log.i(TAG, "downloading"); 180             String url = params[0]; 181             currentThreadIndex = url + params[1]; 182             if(url == null) { 183                 return null; 184             } 185             HttpClient client = new DefaultHttpClient(); 186             HttpGet request = new HttpGet(url); 187             HttpResponse response = null; 188             InputStream is = null; 189             RandomAccessFile fos = null; 190             OutputStream output = null; 191  192             try { 193                 //本地文件 194                 File file = new File(downloadPath + File.separator + url.substring(url.lastIndexOf("/") + 1)); 195  196                 //获取之前下载保存的信息,从之前结束的位置继续下载 197                 //这里加了判断file.exists(),判断是否被用户删除了,如果文件没有下载完,但是已经被用户删除了,则重新下载 198                 long downedPosition = mSharedPreferences.getLong(currentThreadIndex, 0); 199                 if(file.exists() && downedPosition != 0) { 200                     beginPosition = beginPosition + downedPosition; 201                     current = downedPosition; 202                     synchronized (mCurrentLength) { 203                         mCurrentLength += downedPosition; 204                     } 205                 } 206  207                 //设置下载的数据位置beginPosition字节到endPosition字节 208                 Header header_size = new BasicHeader("Range", "bytes=" + beginPosition + "-" + endPosition); 209                 request.addHeader(header_size); 210                 //执行请求获取下载输入流 211                 response = client.execute(request); 212                 is = response.getEntity().getContent(); 213  214                 //创建文件输出流 215                 fos = new RandomAccessFile(file, "rw"); 216                 //从文件的size以后的位置开始写入,其实也不用,直接往后写就可以。有时候多线程下载需要用 217                 fos.seek(beginPosition); 218  219                 byte buffer [] = new byte[1024]; 220                 int inputSize = -1; 221                 while((inputSize = is.read(buffer)) != -1) { 222                     fos.write(buffer, 0, inputSize); 223                     current += inputSize; 224                     synchronized (mCurrentLength) { 225                         mCurrentLength += inputSize; 226                     } 227                     this.publishProgress(); 228                     if (isCancelled()) { 229                         return null; 230                     } 231                 } 232             } catch (MalformedURLException e) { 233                 Log.e(TAG, e.getMessage()); 234             } catch (IOException e) { 235                 Log.e(TAG, e.getMessage()); 236             } finally{ 237                 try{ 238                     /*if(is != null) { 239                         is.close(); 240                     }*/ 241                     if (request != null) { 242                         request.abort(); 243                     } 244                     if(output != null) { 245                         output.close(); 246                     } 247                     if(fos != null) { 248                         fos.close(); 249                     } 250                 } catch(Exception e) { 251                     e.printStackTrace(); 252                 } 253             } 254             return null; 255         } 256  257         @Override 258         protected void onPreExecute() { 259             Log.i(TAG, "download begin "); 260             super.onPreExecute(); 261         } 262  263         @Override 264         protected void onProgressUpdate(Integer... values) { 265             super.onProgressUpdate(values); 266             //更新界面进度条 267             updateProgress(); 268         } 269  270         @Override 271         protected void onPostExecute(Long aLong) { 272             Log.i(TAG, "download success "); 273             //下载完成移除记录 274             mSharedPreferences.edit().remove(currentThreadIndex).commit(); 275         } 276  277         @Override 278         protected void onCancelled() { 279             Log.i(TAG, "download cancelled "); 280             //记录已下载大小current 281             mSharedPreferences.edit().putLong(currentThreadIndex, current).commit(); 282         } 283  284         @Override 285         protected void onCancelled(Long aLong) { 286             Log.i(TAG, "download cancelled(Long aLong)"); 287             super.onCancelled(aLong); 288             mSharedPreferences.edit().putLong(currentThreadIndex, current).commit(); 289         } 290     } 291  292     private class InnerHandler extends Handler { 293         @Override 294         public void handleMessage(Message msg) { 295             switch (msg.what) { 296                 case GET_LENGTH_SUCCESS : 297                     beginDownload(); 298                     break; 299             } 300             super.handleMessage(msg); 301         } 302     } 303  304 }

布局文件和前面一篇博客 《AsyncTask实现断点续传》 布局文件是一样的,这里就不贴代码了。

以上代码亲测可用,几百M大文件也没问题。

三、遇到的坑

问题描述:在使用上面代码下载 http://ftp.neu.edu.cn/mirrors/eclipse/technology/epp/downloads/release/juno/SR2/eclipse-java-juno-SR2-linux-gtk-x86_64.tar.gz 文件的时候,不知道为什么暂停时候执行AsyncTask.cancel(true)来取消下载任务,不执行onCancel()函数,也就没有记录该线程下载的位置。并且再次点击下载的时候,5个Task都只执行了onPreEexcute()方法,压根就不执行doInBackground()方法。而下载其他文件没有这个问题。

这个问题折腾了我好久,它又没有报任何异常,调试又调试不出来。看AsyncTask的源码、上stackoverflow也没有找到原因。看到这个网站(https://groups.google.com/forum/#!topic/android-developers/B-oBiS7npfQ)时,我还真以为是AsyncTask的一个bug。

百番周折,问题居然出现在上面代码239行(这里已注释)。不知道为什么,执行这一句的时候,线程就阻塞在那里了,所以doInBackground()方法一直没有结束,onCancel()方法当然也不会执行了。同时,因为使用的是线程池Executor,线程数为5个,点击取消之后5个线程都阻塞了,所以再次点击下载的时候只执行了onPreEexcute()方法,没有空闲的线程去执行doInBackground()方法。真是巨坑无比有木有。。。

虽然问题解决了,但是为什么有的文件下载执行到 is.close()的时候线程会阻塞而有的不会?这还是个谜。如果哪位大神知道是什么原因,还望指点指点!

正文到此结束
Loading...