Android 使用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请求头来下载文件的指定位置:
//设置下载的数据位置beginPosition字节到endPosition字节 Header header_size = new BasicHeader("Range", "bytes=" + beginPosition + "-" + endPosition); request.addHeader(header_size);
以上是多线程下载的原理,但是还要实现断点续传需要在每次暂停之后记录每个线程已下载的大小,下次继续下载时从上次下载后的位置开始下载。一般项目中都会存数据库中,我这里为了简单起见直接存在了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(); }
下载的时候,首先获取已下载位置,如果已经下载过,就从上次下载后的位置开始下载:
//获取之前下载保存的信息,从之前结束的位置继续下载 //这里加了判断file.exists(),判断是否被用户删除了,如果文件没有下载完,但是已经被用户删除了,则重新下载 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(); //创建线程池 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() { //创建存储文件夹 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(); } }); } /** * 开始下载 * 根据待下载文件大小计算每个线程下载位置,并创建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;//如果整个文件的大小不为线程个数的整数倍,则最后一个线程的结束位置即为文件的总长度 } 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)); //获取之前下载保存的信息,从之前结束的位置继续下载 //这里加了判断file.exists(),判断是否被用户删除了,如果文件没有下载完,但是已经被用户删除了,则重新下载 long downedPosition = mSharedPreferences.getLong(currentThreadIndex, 0); if(file.exists() && downedPosition != 0) { beginPosition = beginPosition + downedPosition; current = downedPosition; synchronized (mCurrentLength) { mCurrentLength += downedPosition; } } //设置下载的数据位置beginPosition字节到endPosition字节 Header header_size = new BasicHeader("Range", "bytes=" + beginPosition + "-" + endPosition); request.addHeader(header_size); //执行请求获取下载输入流 response = client.execute(request); is = response.getEntity().getContent(); //创建文件输出流 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实现断点续传》布局文件是一样的,这里就不贴代码了。
以上代码亲测可用,几百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
总结