你的位置:首页 > 操作系统

[操作系统]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       //记录已下载大小current12       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       @Override101       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    * 根据待下载文件大小计算每个线程下载位置,并创建AsyncTask115   */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   @Override149   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    * 下载的AsyncTask161   */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     @Override178     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     @Override258     protected void onPreExecute() {259       Log.i(TAG, "download begin ");260       super.onPreExecute();261     }262 263     @Override264     protected void onProgressUpdate(Integer... values) {265       super.onProgressUpdate(values);266       //更新界面进度条267       updateProgress();268     }269 270     @Override271     protected void onPostExecute(Long aLong) {272       Log.i(TAG, "download success ");273       //下载完成移除记录274       mSharedPreferences.edit().remove(currentThreadIndex).commit();275     }276 277     @Override278     protected void onCancelled() {279       Log.i(TAG, "download cancelled ");280       //记录已下载大小current281       mSharedPreferences.edit().putLong(currentThreadIndex, current).commit();282     }283 284     @Override285     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     @Override294     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()的时候线程会阻塞而有的不会?这还是个谜。如果哪位大神知道是什么原因,还望指点指点!