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()的時候線程會阻塞而有的不會?這還是個謎。如果哪位大神知道是什么原因,還望指點指點!

 

源碼下載:https://github.com/liuling07/MultiTaskAndThreadDownload


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM