Android 使用AsyncTask實(shí)現(xiàn)多線程斷點(diǎn)續(xù)傳
前面一篇博客《AsyncTask實(shí)現(xiàn)斷點(diǎn)續(xù)傳》講解了如何實(shí)現(xiàn)單線程下的斷點(diǎn)續(xù)傳,也就是一個(gè)文件只有一個(gè)線程進(jìn)行下載。
對(duì)于大文件而言,使用多線程下載就會(huì)比單線程下載要快一些。多線程下載相比單線程下載要稍微復(fù)雜一點(diǎn),本博文將詳細(xì)講解如何使用AsyncTask來(lái)實(shí)現(xiàn)多線程的斷點(diǎn)續(xù)傳下載。
一、實(shí)現(xiàn)原理
多線程下載首先要通過(guò)每個(gè)文件總的下載線程數(shù)(我這里設(shè)定5個(gè))來(lái)確定每個(gè)線程所負(fù)責(zé)下載的起止位置。
long blockLength = mFileLength / DEFAULT_POOL_SIZE;
for (int i = 0; i < DEFAULT_POOL_SIZE; i++) {
long beginPosition = i * blockLength;//每條線程下載的開(kāi)始位置
long endPosition = (i + 1) * blockLength;//每條線程下載的結(jié)束位置
if (i == (DEFAULT_POOL_SIZE - 1)) {
endPosition = mFileLength;//如果整個(gè)文件的大小不為線程個(gè)數(shù)的整數(shù)倍,則最后一個(gè)線程的結(jié)束位置即為文件的總長(zhǎng)度
}
......
}
這里需要注意的是,文件大小往往不是線程個(gè)數(shù)的整數(shù)倍,所以最后一個(gè)線程的結(jié)束位置需要設(shè)置為文件長(zhǎng)度。
確定好每個(gè)線程的下載起止位置之后,需要設(shè)置http請(qǐng)求頭來(lái)下載文件的指定位置:
//設(shè)置下載的數(shù)據(jù)位置beginPosition字節(jié)到endPosition字節(jié)
Header header_size = new BasicHeader("Range", "bytes=" + beginPosition + "-" + endPosition);
request.addHeader(header_size);
以上是多線程下載的原理,但是還要實(shí)現(xiàn)斷點(diǎn)續(xù)傳需要在每次暫停之后記錄每個(gè)線程已下載的大小,下次繼續(xù)下載時(shí)從上次下載后的位置開(kāi)始下載。一般項(xiàng)目中都會(huì)存數(shù)據(jù)庫(kù)中,我這里為了簡(jiǎn)單起見(jiàn)直接存在了SharedPreferences中,已下載url和線程編號(hào)作為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();
}
下載的時(shí)候,首先獲取已下載位置,如果已經(jīng)下載過(guò),就從上次下載后的位置開(kāi)始下載:
//獲取之前下載保存的信息,從之前結(jié)束的位置繼續(xù)下載
//這里加了判斷file.exists(),判斷是否被用戶(hù)刪除了,如果文件沒(méi)有下載完,但是已經(jīng)被用戶(hù)刪除了,則重新下載
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);
//開(kāi)始下載
findViewById(R.id.begin).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
new Thread() {
@Override
public void run() {
//創(chuàng)建存儲(chǔ)文件夾
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();
}
});
}
/**
* 開(kāi)始下載
* 根據(jù)待下載文件大小計(jì)算每個(gè)線程下載位置,并創(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;//每條線程下載的開(kāi)始位置
long endPosition = (i + 1) * blockLength;//每條線程下載的結(jié)束位置
if (i == (DEFAULT_POOL_SIZE - 1)) {
endPosition = mFileLength;//如果整個(gè)文件的大小不為線程個(gè)數(shù)的整數(shù)倍,則最后一個(gè)線程的結(jié)束位置即為文件的總長(zhǎng)度
}
DownloadAsyncTask task = new DownloadAsyncTask(beginPosition, endPosition);
mTaskList.add(task);
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, mUrl, String.valueOf(i));
}
}
/**
* 更新進(jìn)度條
*/
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("下載進(jìn)度:" + percent + "%");
if (mProgressBar.getProgress() == mProgressBar.getMax()) {
Toast.makeText(MainActivity.this, "下載結(jié)束", 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));
//獲取之前下載保存的信息,從之前結(jié)束的位置繼續(xù)下載
//這里加了判斷file.exists(),判斷是否被用戶(hù)刪除了,如果文件沒(méi)有下載完,但是已經(jīng)被用戶(hù)刪除了,則重新下載
long downedPosition = mSharedPreferences.getLong(currentThreadIndex, 0);
if(file.exists() && downedPosition != 0) {
beginPosition = beginPosition + downedPosition;
current = downedPosition;
synchronized (mCurrentLength) {
mCurrentLength += downedPosition;
}
}
//設(shè)置下載的數(shù)據(jù)位置beginPosition字節(jié)到endPosition字節(jié)
Header header_size = new BasicHeader("Range", "bytes=" + beginPosition + "-" + endPosition);
request.addHeader(header_size);
//執(zhí)行請(qǐng)求獲取下載輸入流
response = client.execute(request);
is = response.getEntity().getContent();
//創(chuàng)建文件輸出流
fos = new RandomAccessFile(file, "rw");
//從文件的size以后的位置開(kāi)始寫(xiě)入,其實(shí)也不用,直接往后寫(xiě)就可以。有時(shí)候多線程下載需要用
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);
//更新界面進(jìn)度條
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實(shí)現(xiàn)斷點(diǎn)續(xù)傳》布局文件是一樣的,這里就不貼代碼了。
以上代碼親測(cè)可用,幾百M(fèi)大文件也沒(méi)問(wèn)題。
三、遇到的坑
問(wèn)題描述:在使用上面代碼下載http://ftp.neu.edu.cn/mirrors/eclipse/technology/epp/downloads/release/juno/SR2/eclipse-java-juno-SR2-linux-gtk-x86_64.tar.gz文件的時(shí)候,不知道為什么暫停時(shí)候執(zhí)行AsyncTask.cancel(true)來(lái)取消下載任務(wù),不執(zhí)行onCancel()函數(shù),也就沒(méi)有記錄該線程下載的位置。并且再次點(diǎn)擊下載的時(shí)候,5個(gè)Task都只執(zhí)行了onPreEexcute()方法,壓根就不執(zhí)行doInBackground()方法。而下載其他文件沒(méi)有這個(gè)問(wèn)題。
這個(gè)問(wèn)題折騰了我好久,它又沒(méi)有報(bào)任何異常,調(diào)試又調(diào)試不出來(lái)??碅syncTask的源碼、上stackoverflow也沒(méi)有找到原因??吹竭@個(gè)網(wǎng)站(https://groups.google.com/forum/#!topic/android-developers/B-oBiS7npfQ)時(shí),我還真以為是AsyncTask的一個(gè)bug。
百番周折,問(wèn)題居然出現(xiàn)在上面代碼239行(這里已注釋?zhuān)?。不知道為什么,?zhí)行這一句的時(shí)候,線程就阻塞在那里了,所以doInBackground()方法一直沒(méi)有結(jié)束,onCancel()方法當(dāng)然也不會(huì)執(zhí)行了。同時(shí),因?yàn)槭褂玫氖蔷€程池Executor,線程數(shù)為5個(gè),點(diǎn)擊取消之后5個(gè)線程都阻塞了,所以再次點(diǎn)擊下載的時(shí)候只執(zhí)行了onPreEexcute()方法,沒(méi)有空閑的線程去執(zhí)行doInBackground()方法。真是巨坑無(wú)比有木有。。。
雖然問(wèn)題解決了,但是為什么有的文件下載執(zhí)行到is.close()的時(shí)候線程會(huì)阻塞而有的不會(huì)?這還是個(gè)謎。如果哪位大神知道是什么原因,還望指點(diǎn)指點(diǎn)!
源碼下載:https://github.com/liuling07/MultiTaskAndThreadDownload
總結(jié)
以上所述是小編給大家介紹的Android 使用AsyncTask實(shí)現(xiàn)多線程斷點(diǎn)續(xù)傳,希望對(duì)大家有所幫助,如果大家有任何疑問(wèn)請(qǐng)給我留言,小編會(huì)及時(shí)回復(fù)大家的。在此也非常感謝大家對(duì)腳本之家網(wǎng)站的支持!
相關(guān)文章
AndriodStudio利用ListView和數(shù)據(jù)庫(kù)實(shí)現(xiàn)簡(jiǎn)單學(xué)生管理
這篇文章主要為大家詳細(xì)介紹了AndriodStudio利用ListView和數(shù)據(jù)庫(kù)實(shí)現(xiàn)簡(jiǎn)單學(xué)生管理,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-03-03
Android ScrollView實(shí)現(xiàn)下拉彈回動(dòng)畫(huà)效果
這篇文章主要為大家詳細(xì)介紹了Android ScrollView實(shí)現(xiàn)下拉彈回動(dòng)畫(huà)效果,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-08-08
Android RecyclerView滑動(dòng)刪除和拖動(dòng)排序
這篇文章主要介紹了Android RecyclerView滑動(dòng)刪除和拖動(dòng)排序的相關(guān)資料,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-07-07
Android打印機(jī)--小票打印格式及模板設(shè)置實(shí)例代碼
這篇文章主要介紹了Android打印機(jī)--小票打印格式及模板設(shè)置實(shí)例代碼,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下。2017-04-04
Android EditText每4位自動(dòng)添加空格效果
這篇文章主要給大家介紹了關(guān)于Android EditText每4位自動(dòng)添加空格效果的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用EditText具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-06-06
Android中微信搶紅包助手的實(shí)現(xiàn)詳解
本篇文章主要介紹了Android中微信搶紅包助手的實(shí)現(xiàn)詳解,通過(guò)利用AccessibilityService輔助服務(wù),監(jiān)測(cè)屏幕內(nèi)容,如監(jiān)聽(tīng)狀態(tài)欄的信息,屏幕跳轉(zhuǎn)等,以此來(lái)實(shí)現(xiàn)自動(dòng)拆紅包的功能,有興趣的可以了解一下。2017-02-02
Android序列化實(shí)現(xiàn)接口Serializable與Parcelable詳解
我們使用 Intent 傳遞數(shù)據(jù)的時(shí)候,putExtra() 所支持的數(shù)據(jù)類(lèi)型事有限的,當(dāng)需要傳遞自定義對(duì)象的時(shí)候就需要序列化。Serializable更簡(jiǎn)單但是會(huì)把整個(gè)對(duì)象進(jìn)行序列化因此效率比Parcelable低一些2022-12-12

