前面一篇博客《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()的时候线程会阻塞而有的不会?这还是个谜。如果哪位大神知道是什么原因,还望指点指点!
源码下载:https://github.com/liuling07/MultiTaskAndThreadDownload