Android 使用AsyncTask實現(xiàn)多線程斷點續(xù)傳
前面一篇博客《AsyncTask實現(xiàn)斷點續(xù)傳》講解了如何實現(xiàn)單線程下的斷點續(xù)傳,也就是一個文件只有一個線程進行下載。
對于大文件而言,使用多線程下載就會比單線程下載要快一些。多線程下載相比單線程下載要稍微復雜一點,本博文將詳細講解如何使用AsyncTask來實現(xiàn)多線程的斷點續(xù)傳下載。
一、實現(xiàn)原理
多線程下載首先要通過每個文件總的下載線程數(shù)(我這里設定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;//如果整個文件的大小不為線程個數(shù)的整數(shù)倍,則最后一個線程的結束位置即為文件的總長度
}
......
}
這里需要注意的是,文件大小往往不是線程個數(shù)的整數(shù)倍,所以最后一個線程的結束位置需要設置為文件長度。
確定好每個線程的下載起止位置之后,需要設置http請求頭來下載文件的指定位置:
//設置下載的數(shù)據(jù)位置beginPosition字節(jié)到endPosition字節(jié)
Header header_size = new BasicHeader("Range", "bytes=" + beginPosition + "-" + endPosition);
request.addHeader(header_size);
以上是多線程下載的原理,但是還要實現(xiàn)斷點續(xù)傳需要在每次暫停之后記錄每個線程已下載的大小,下次繼續(xù)下載時從上次下載后的位置開始下載。一般項目中都會存數(shù)據(jù)庫中,我這里為了簡單起見直接存在了SharedPreferences中,已下載url和線程編號作為key值。
@Override
protected void onPostExecute(Long aLong) {
Log.i(TAG, "download success ");
//下載完成移除記錄
mSharedPreferences.edit().remove(currentThreadIndex).commit();
}
@Override
protected void onCancelled() {
Log.i(TAG, "download cancelled ");
//記錄已下載大小current
mSharedPreferences.edit().putLong(currentThreadIndex, current).commit();
}
下載的時候,首先獲取已下載位置,如果已經(jīng)下載過,就從上次下載后的位置開始下載:
//獲取之前下載保存的信息,從之前結束的位置繼續(xù)下載
//這里加了判斷file.exists(),判斷是否被用戶刪除了,如果文件沒有下載完,但是已經(jīng)被用戶刪除了,則重新下載
long downedPosition = mSharedPreferences.getLong(currentThreadIndex, 0);
if(file.exists() && downedPosition != 0) {
beginPosition = beginPosition + downedPosition;
current = downedPosition;
synchronized (mCurrentLength) {
mCurrentLength += downedPosition;
}
}
二、完整代碼
package com.bbk.lling.multithreaddownload;
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.view.View;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import org.apache.http.Header;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicHeader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
public class MainActivity extends Activity {
private static final String TAG = "MainActivity";
private static final int DEFAULT_POOL_SIZE = 5;
private static final int GET_LENGTH_SUCCESS = 1;
//下載路徑
private String downloadPath = Environment.getExternalStorageDirectory() +
File.separator + "download";
// 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";
private String mUrl = "http://p.gdown.baidu.com/c4cb746699b92c9b6565cc65aa2e086552651f73c5d0e634a51f028e32af6abf3d68079eeb75401c76c9bb301e5fb71c144a704cb1a2f527a2e8ca3d6fe561dc5eaf6538e5b3ab0699308d13fe0b711a817c88b0f85a01a248df82824ace3cd7f2832c7c19173236";
private ProgressBar mProgressBar;
private TextView mPercentTV;
SharedPreferences mSharedPreferences = null;
long mFileLength = 0;
Long mCurrentLength = 0L;
private InnerHandler mHandler = new InnerHandler();
//創(chuàng)建線程池
private Executor mExecutor = Executors.newCachedThreadPool();
private List<DownloadAsyncTask> mTaskList = new ArrayList<DownloadAsyncTask>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mProgressBar = (ProgressBar) findViewById(R.id.progressbar);
mPercentTV = (TextView) findViewById(R.id.percent_tv);
mSharedPreferences = getSharedPreferences("download", Context.MODE_PRIVATE);
//開始下載
findViewById(R.id.begin).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
new Thread() {
@Override
public void run() {
//創(chuàng)建存儲文件夾
File dir = new File(downloadPath);
if (!dir.exists()) {
dir.mkdir();
}
//獲取文件大小
HttpClient client = new DefaultHttpClient();
HttpGet request = new HttpGet(mUrl);
HttpResponse response = null;
try {
response = client.execute(request);
mFileLength = response.getEntity().getContentLength();
} catch (Exception e) {
Log.e(TAG, e.getMessage());
} finally {
if (request != null) {
request.abort();
}
}
Message.obtain(mHandler, GET_LENGTH_SUCCESS).sendToTarget();
}
}.start();
}
});
//暫停下載
findViewById(R.id.end).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
for (DownloadAsyncTask task : mTaskList) {
if (task != null && (task.getStatus() == AsyncTask.Status.RUNNING || !task.isCancelled())) {
task.cancel(true);
}
}
mTaskList.clear();
}
});
}
/**
* 開始下載
* 根據(jù)待下載文件大小計算每個線程下載位置,并創(chuàng)建AsyncTask
*/
private void beginDownload() {
mCurrentLength = 0L;
mPercentTV.setVisibility(View.VISIBLE);
mProgressBar.setProgress(0);
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;//如果整個文件的大小不為線程個數(shù)的整數(shù)倍,則最后一個線程的結束位置即為文件的總長度
}
DownloadAsyncTask task = new DownloadAsyncTask(beginPosition, endPosition);
mTaskList.add(task);
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, mUrl, String.valueOf(i));
}
}
/**
* 更新進度條
*/
synchronized public void updateProgress() {
int percent = (int) Math.ceil((float)mCurrentLength / (float)mFileLength * 100);
// Log.i(TAG, "downloading " + mCurrentLength + "," + mFileLength + "," + percent);
if(percent > mProgressBar.getProgress()) {
mProgressBar.setProgress(percent);
mPercentTV.setText("下載進度:" + percent + "%");
if (mProgressBar.getProgress() == mProgressBar.getMax()) {
Toast.makeText(MainActivity.this, "下載結束", Toast.LENGTH_SHORT).show();
}
}
}
@Override
protected void onDestroy() {
for(DownloadAsyncTask task: mTaskList) {
if(task != null && task.getStatus() == AsyncTask.Status.RUNNING) {
task.cancel(true);
}
mTaskList.clear();
}
super.onDestroy();
}
/**
* 下載的AsyncTask
*/
private class DownloadAsyncTask extends AsyncTask<String, Integer , Long> {
private static final String TAG = "DownloadAsyncTask";
private long beginPosition = 0;
private long endPosition = 0;
private long current = 0;
private String currentThreadIndex;
public DownloadAsyncTask(long beginPosition, long endPosition) {
this.beginPosition = beginPosition;
this.endPosition = endPosition;
}
@Override
protected Long doInBackground(String... params) {
Log.i(TAG, "downloading");
String url = params[0];
currentThreadIndex = url + params[1];
if(url == null) {
return null;
}
HttpClient client = new DefaultHttpClient();
HttpGet request = new HttpGet(url);
HttpResponse response = null;
InputStream is = null;
RandomAccessFile fos = null;
OutputStream output = null;
try {
//本地文件
File file = new File(downloadPath + File.separator + url.substring(url.lastIndexOf("/") + 1));
//獲取之前下載保存的信息,從之前結束的位置繼續(xù)下載
//這里加了判斷file.exists(),判斷是否被用戶刪除了,如果文件沒有下載完,但是已經(jīng)被用戶刪除了,則重新下載
long downedPosition = mSharedPreferences.getLong(currentThreadIndex, 0);
if(file.exists() && downedPosition != 0) {
beginPosition = beginPosition + downedPosition;
current = downedPosition;
synchronized (mCurrentLength) {
mCurrentLength += downedPosition;
}
}
//設置下載的數(shù)據(jù)位置beginPosition字節(jié)到endPosition字節(jié)
Header header_size = new BasicHeader("Range", "bytes=" + beginPosition + "-" + endPosition);
request.addHeader(header_size);
//執(zhí)行請求獲取下載輸入流
response = client.execute(request);
is = response.getEntity().getContent();
//創(chuàng)建文件輸出流
fos = new RandomAccessFile(file, "rw");
//從文件的size以后的位置開始寫入,其實也不用,直接往后寫就可以。有時候多線程下載需要用
fos.seek(beginPosition);
byte buffer [] = new byte[1024];
int inputSize = -1;
while((inputSize = is.read(buffer)) != -1) {
fos.write(buffer, 0, inputSize);
current += inputSize;
synchronized (mCurrentLength) {
mCurrentLength += inputSize;
}
this.publishProgress();
if (isCancelled()) {
return null;
}
}
} catch (MalformedURLException e) {
Log.e(TAG, e.getMessage());
} catch (IOException e) {
Log.e(TAG, e.getMessage());
} finally{
try{
/*if(is != null) {
is.close();
}*/
if (request != null) {
request.abort();
}
if(output != null) {
output.close();
}
if(fos != null) {
fos.close();
}
} catch(Exception e) {
e.printStackTrace();
}
}
return null;
}
@Override
protected void onPreExecute() {
Log.i(TAG, "download begin ");
super.onPreExecute();
}
@Override
protected void onProgressUpdate(Integer... values) {
super.onProgressUpdate(values);
//更新界面進度條
updateProgress();
}
@Override
protected void onPostExecute(Long aLong) {
Log.i(TAG, "download success ");
//下載完成移除記錄
mSharedPreferences.edit().remove(currentThreadIndex).commit();
}
@Override
protected void onCancelled() {
Log.i(TAG, "download cancelled ");
//記錄已下載大小current
mSharedPreferences.edit().putLong(currentThreadIndex, current).commit();
}
@Override
protected void onCancelled(Long aLong) {
Log.i(TAG, "download cancelled(Long aLong)");
super.onCancelled(aLong);
mSharedPreferences.edit().putLong(currentThreadIndex, current).commit();
}
}
private class InnerHandler extends Handler {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case GET_LENGTH_SUCCESS :
beginDownload();
break;
}
super.handleMessage(msg);
}
}
}
布局文件和前面一篇博客《AsyncTask實現(xiàn)斷點續(xù)傳》布局文件是一樣的,這里就不貼代碼了。
以上代碼親測可用,幾百M大文件也沒問題。
三、遇到的坑
問題描述:在使用上面代碼下載http://ftp.neu.edu.cn/mirrors/eclipse/technology/epp/downloads/release/juno/SR2/eclipse-java-juno-SR2-linux-gtk-x86_64.tar.gz文件的時候,不知道為什么暫停時候執(zhí)行AsyncTask.cancel(true)來取消下載任務,不執(zhí)行onCancel()函數(shù),也就沒有記錄該線程下載的位置。并且再次點擊下載的時候,5個Task都只執(zhí)行了onPreEexcute()方法,壓根就不執(zhí)行doInBackground()方法。而下載其他文件沒有這個問題。
這個問題折騰了我好久,它又沒有報任何異常,調(diào)試又調(diào)試不出來??碅syncTask的源碼、上stackoverflow也沒有找到原因。看到這個網(wǎng)站(https://groups.google.com/forum/#!topic/android-developers/B-oBiS7npfQ)時,我還真以為是AsyncTask的一個bug。
百番周折,問題居然出現(xiàn)在上面代碼239行(這里已注釋)。不知道為什么,執(zhí)行這一句的時候,線程就阻塞在那里了,所以doInBackground()方法一直沒有結束,onCancel()方法當然也不會執(zhí)行了。同時,因為使用的是線程池Executor,線程數(shù)為5個,點擊取消之后5個線程都阻塞了,所以再次點擊下載的時候只執(zhí)行了onPreEexcute()方法,沒有空閑的線程去執(zhí)行doInBackground()方法。真是巨坑無比有木有。。。
雖然問題解決了,但是為什么有的文件下載執(zhí)行到is.close()的時候線程會阻塞而有的不會?這還是個謎。如果哪位大神知道是什么原因,還望指點指點!
源碼下載:https://github.com/liuling07/MultiTaskAndThreadDownload
總結
以上所述是小編給大家介紹的Android 使用AsyncTask實現(xiàn)多線程斷點續(xù)傳,希望對大家有所幫助,如果大家有任何疑問請給我留言,小編會及時回復大家的。在此也非常感謝大家對腳本之家網(wǎng)站的支持!
相關文章
AndriodStudio利用ListView和數(shù)據(jù)庫實現(xiàn)簡單學生管理
這篇文章主要為大家詳細介紹了AndriodStudio利用ListView和數(shù)據(jù)庫實現(xiàn)簡單學生管理,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-03-03
Android ScrollView實現(xiàn)下拉彈回動畫效果
這篇文章主要為大家詳細介紹了Android ScrollView實現(xiàn)下拉彈回動畫效果,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-08-08
Android序列化實現(xiàn)接口Serializable與Parcelable詳解
我們使用 Intent 傳遞數(shù)據(jù)的時候,putExtra() 所支持的數(shù)據(jù)類型事有限的,當需要傳遞自定義對象的時候就需要序列化。Serializable更簡單但是會把整個對象進行序列化因此效率比Parcelable低一些2022-12-12

