前面一篇博客《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