Android 使用AsyncTask实现多任务多线程断点续传下载
作者:残剑_
这篇博客是AsyncTask下载系列的最后一篇文章,前面写了关于断点续传的和多线程下载的博客,这篇是在前两篇的基础上面实现的,有兴趣的可以去看下。
这里模拟应用市场app下载实现了一个Demo,因为只有一个界面,所以没有将下载放到Service中,而是直接在Activity中创建。在正式的项目中,下载都是放到Service中,然后通过BroadCast通知界面更新进度。
上代码之前,先看下demo的运行效果图吧。
下面我们看代码,这里每一个文件的下载定义一个Downloador来管理下载该文件的所有线程(暂停、下载等)。Downloador创建3个DownloadTask(这里定义每个文件分配3个线程下载)来下载该文件特定的起止位置。这里要通过文件的大小来计算每个线程所下载的起止位置,详细可以参考《AsyncTask实现多线程断点续传》。
1、Downloador类
package com.bbk.lling.multitaskdownload.downloador; import android.content.Context; import android.content.Intent; import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.text.TextUtils; import android.util.Log; import android.widget.Toast; import com.bbk.lling.multitaskdownload.beans.AppContent; import com.bbk.lling.multitaskdownload.beans.DownloadInfo; import com.bbk.lling.multitaskdownload.db.DownloadFileDAO; import com.bbk.lling.multitaskdownload.db.DownloadInfoDAO; import com.bbk.lling.multitaskdownload.utils.DownloadUtils; 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 java.util.ArrayList; import java.util.List; import java.util.concurrent.Executor; import java.util.concurrent.Executors; /** * @Class: Downloador * @Description: 任务下载器 * @author: lling(www.cnblogs.com/liuling) * @Date: 2015/10/13 */ public class Downloador { public static final String TAG = "Downloador"; private static final int THREAD_POOL_SIZE = 9; //线程池大小为9 private static final int THREAD_NUM = 3; //每个文件3个线程下载 private static final int GET_LENGTH_SUCCESS = 1; public static final Executor THREAD_POOL_EXECUTOR = Executors.newFixedThreadPool(THREAD_POOL_SIZE); private List<DownloadTask> tasks; private InnerHandler handler = new InnerHandler(); private AppContent appContent; //待下载的应用 private long downloadLength; //下载过程中记录已下载大小 private long fileLength; private Context context; private String downloadPath; public Downloador(Context context, AppContent appContent) { this.context = context; this.appContent = appContent; this.downloadPath = DownloadUtils.getDownloadPath(); } /** * 开始下载 */ public void download() { if(TextUtils.isEmpty(downloadPath)) { Toast.makeText(context, "未找到SD卡", Toast.LENGTH_SHORT).show(); return; } if(appContent == null) { throw new IllegalArgumentException("download content can not be null"); } new Thread() { @Override public void run() { //获取文件大小 HttpClient client = new DefaultHttpClient(); HttpGet request = new HttpGet(appContent.getUrl()); HttpResponse response = null; try { response = client.execute(request); fileLength = response.getEntity().getContentLength(); } catch (Exception e) { Log.e(TAG, e.getMessage()); } finally { if (request != null) { request.abort(); } } //计算出该文件已经下载的总长度 List<DownloadInfo> lists = DownloadInfoDAO.getInstance(context.getApplicationContext()) .getDownloadInfosByUrl(appContent.getUrl()); for (DownloadInfo info : lists) { downloadLength += info.getDownloadLength(); } //插入文件下载记录到数据库 DownloadFileDAO.getInstance(context.getApplicationContext()).insertDownloadFile(appContent); Message.obtain(handler, GET_LENGTH_SUCCESS).sendToTarget(); } }.start(); } /** * 开始创建AsyncTask下载 */ private void beginDownload() { Log.e(TAG, "beginDownload" + appContent.getUrl()); appContent.setStatus(AppContent.Status.WAITING); long blockLength = fileLength / THREAD_NUM; for (int i = 0; i < THREAD_NUM; i++) { long beginPosition = i * blockLength;//每条线程下载的开始位置 long endPosition = (i + 1) * blockLength;//每条线程下载的结束位置 if (i == (THREAD_NUM - 1)) { endPosition = fileLength;//如果整个文件的大小不为线程个数的整数倍,则最后一个线程的结束位置即为文件的总长度 } DownloadTask task = new DownloadTask(i, beginPosition, endPosition, this, context); task.executeOnExecutor(THREAD_POOL_EXECUTOR, appContent.getUrl()); if(tasks == null) { tasks = new ArrayList<DownloadTask>(); } tasks.add(task); } } /** * 暂停下载 */ public void pause() { for (DownloadTask task : tasks) { if (task != null && (task.getStatus() == AsyncTask.Status.RUNNING || !task.isCancelled())) { task.cancel(true); } } tasks.clear(); appContent.setStatus(AppContent.Status.PAUSED); DownloadFileDAO.getInstance(context.getApplicationContext()).updateDownloadFile(appContent); } /** * 将已下载大小归零 */ protected synchronized void resetDownloadLength() { this.downloadLength = 0; } /** * 添加已下载大小 * 多线程访问需加锁 * @param size */ protected synchronized void updateDownloadLength(long size){ this.downloadLength += size; //通知更新界面 int percent = (int)((float)downloadLength * 100 / (float)fileLength); appContent.setDownloadPercent(percent); if(percent == 100 || downloadLength == fileLength) { appContent.setDownloadPercent(100); //上面计算有时候会有点误差,算到percent=99 appContent.setStatus(AppContent.Status.FINISHED); DownloadFileDAO.getInstance(context.getApplicationContext()).updateDownloadFile(appContent); } Intent intent = new Intent(Constants.DOWNLOAD_MSG); if(appContent.getStatus() == AppContent.Status.WAITING) { appContent.setStatus(AppContent.Status.DOWNLOADING); } Bundle bundle = new Bundle(); bundle.putParcelable("appContent", appContent); intent.putExtras(bundle); context.sendBroadcast(intent); } protected String getDownloadPath() { return downloadPath; } private class InnerHandler extends Handler { @Override public void handleMessage(Message msg) { switch (msg.what) { case GET_LENGTH_SUCCESS : beginDownload(); break; } super.handleMessage(msg); } } }
2、DownloadTask类
package com.bbk.lling.multitaskdownload.downloador; import android.content.Context; import android.os.AsyncTask; import android.util.Log; import com.bbk.lling.multitaskdownload.beans.DownloadInfo; import com.bbk.lling.multitaskdownload.db.DownloadInfoDAO; 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; /** * @Class: DownloadTask * @Description: 文件下载AsyncTask * @author: lling(www.cnblogs.com/liuling) * @Date: 2015/10/13 */ public class DownloadTask extends AsyncTask<String, Integer , Long> { private static final String TAG = "DownloadTask"; private int taskId; private long beginPosition; private long endPosition; private long downloadLength; private String url; private Downloador downloador; private DownloadInfoDAO downloadInfoDAO; public DownloadTask(int taskId, long beginPosition, long endPosition, Downloador downloador, Context context) { this.taskId = taskId; this.beginPosition = beginPosition; this.endPosition = endPosition; this.downloador = downloador; downloadInfoDAO = DownloadInfoDAO.getInstance(context.getApplicationContext()); } @Override protected void onPreExecute() { Log.e(TAG, "onPreExecute"); } @Override protected void onPostExecute(Long aLong) { Log.e(TAG, url + "taskId:" + taskId + "executed"); // downloador.updateDownloadInfo(null); } @Override protected void onProgressUpdate(Integer... values) { //通知downloador增加已下载大小 // downloador.updateDownloadLength(values[0]); } @Override protected void onCancelled() { Log.e(TAG, "onCancelled"); // downloador.updateDownloadInfo(null); } @Override protected Long doInBackground(String... params) { //这里加判断的作用是:如果还处于等待就暂停了,运行到这里已经cancel了,就直接退出 if(isCancelled()) { return null; } url = params[0]; if(url == null) { return null; } HttpClient client = new DefaultHttpClient(); HttpGet request = new HttpGet(url); HttpResponse response; InputStream is; RandomAccessFile fos = null; OutputStream output = null; DownloadInfo downloadInfo = null; try { //本地文件 File file = new File(downloador.getDownloadPath() + File.separator + url.substring(url.lastIndexOf("/") + 1)); //获取之前下载保存的信息 downloadInfo = downloadInfoDAO.getDownloadInfoByTaskIdAndUrl(taskId, url); //从之前结束的位置继续下载 //这里加了判断file.exists(),判断是否被用户删除了,如果文件没有下载完,但是已经被用户删除了,则重新下载 if(file.exists() && downloadInfo != null) { if(downloadInfo.isDownloadSuccess() == 1) { //下载完成直接结束 return null; } beginPosition = beginPosition + downloadInfo.getDownloadLength(); downloadLength = downloadInfo.getDownloadLength(); } if(!file.exists()) { //如果此task已经下载完,但是文件被用户删除,则需要重新设置已下载长度,重新下载 downloador.resetDownloadLength(); } //设置下载的数据位置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); downloadLength += inputSize; downloador.updateDownloadLength(inputSize); //如果暂停了,需要将下载信息存入数据库 if (isCancelled()) { if(downloadInfo == null) { downloadInfo = new DownloadInfo(); } downloadInfo.setUrl(url); downloadInfo.setDownloadLength(downloadLength); downloadInfo.setTaskId(taskId); downloadInfo.setDownloadSuccess(0); //保存下载信息到数据库 downloadInfoDAO.insertDownloadInfo(downloadInfo); return null; } } } catch (MalformedURLException e) { Log.e(TAG, e.getMessage()); } catch (IOException e) { Log.e(TAG, e.getMessage()); } finally{ try{ if (request != null) { request.abort(); } if(output != null) { output.close(); } if(fos != null) { fos.close(); } } catch(Exception e) { e.printStackTrace(); } } //执行到这里,说明该task已经下载完了 if(downloadInfo == null) { downloadInfo = new DownloadInfo(); } downloadInfo.setUrl(url); downloadInfo.setDownloadLength(downloadLength); downloadInfo.setTaskId(taskId); downloadInfo.setDownloadSuccess(1); //保存下载信息到数据库 downloadInfoDAO.insertDownloadInfo(downloadInfo); return null; } }
Downloador和DownloadTask只这个例子的核心代码,下面是关于数据库的,因为要实现断点续传必须要在暂停的时候将每个线程下载的位置记录下来,方便下次继续下载时读取。这里有两个表,一个是存放每个文件的下载状态的,一个是存放每个文件对应的每个线程的下载状态的。
3、DBHelper
package com.bbk.lling.multitaskdownload.db; import android.content.Context; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; /** * @Class: DBHelper * @Description: 数据库帮助类 * @author: lling(www.cnblogs.com/liuling) * @Date: 2015/10/14 */ public class DBHelper extends SQLiteOpenHelper { public DBHelper(Context context) { super(context, "download.db", null, 1); } @Override public void onCreate(SQLiteDatabase db) { db.execSQL("create table download_info(_id INTEGER PRIMARY KEY AUTOINCREMENT, task_id INTEGER, " + "download_length INTEGER, url VARCHAR(255), is_success INTEGER)"); db.execSQL("create table download_file(_id INTEGER PRIMARY KEY AUTOINCREMENT, app_name VARCHAR(255), " + "url VARCHAR(255), download_percent INTEGER, status INTEGER)"); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { } }
4、DownloadFileDAO,文件下载状态的数据库操作类
package com.bbk.lling.multitaskdownload.db; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.text.TextUtils; import android.util.Log; import com.bbk.lling.multitaskdownload.beans.AppContent; import java.util.ArrayList; import java.util.List; /** * @Class: DownloadFileDAO * @Description: 每个文件下载状态记录的数据库操作类 * @author: lling(www.cnblogs.com/liuling) * @Date: 2015/10/13 */ public class DownloadFileDAO { private static final String TAG = "DownloadFileDAO"; private static DownloadFileDAO dao=null; private Context context; private DownloadFileDAO(Context context) { this.context=context; } synchronized public static DownloadFileDAO getInstance(Context context){ if(dao==null){ dao=new DownloadFileDAO(context); } return dao; } /** * 获取数据库连接 * @return */ public SQLiteDatabase getConnection() { SQLiteDatabase sqliteDatabase = null; try { sqliteDatabase= new DBHelper(context).getReadableDatabase(); } catch (Exception e) { Log.e(TAG, e.getMessage()); } return sqliteDatabase; } /** * 插入数据 * @param appContent */ public void insertDownloadFile(AppContent appContent) { if(appContent == null) { return; } //如果本地已经存在,直接修改 if(getAppContentByUrl(appContent.getUrl()) != null) { updateDownloadFile(appContent); return; } SQLiteDatabase database = getConnection(); try { String sql = "insert into download_file(app_name, url, download_percent, status) values (?,?,?,?)"; Object[] bindArgs = { appContent.getName(), appContent.getUrl(), appContent.getDownloadPercent() , appContent.getStatus().getValue()}; database.execSQL(sql, bindArgs); } catch (Exception e) { Log.e(TAG, e.getMessage()); } finally { if (null != database) { database.close(); } } } /** * 根据url获取下载文件信息 * @param url * @return */ public AppContent getAppContentByUrl(String url) { if(TextUtils.isEmpty(url)) { return null; } SQLiteDatabase database = getConnection(); AppContent appContent = null; Cursor cursor = null; try { String sql = "select * from download_file where url=?"; cursor = database.rawQuery(sql, new String[] { url }); if (cursor.moveToNext()) { appContent = new AppContent(cursor.getString(1), cursor.getString(2)); appContent.setDownloadPercent(cursor.getInt(3)); appContent.setStatus(AppContent.Status.getByValue(cursor.getInt(4))); } } catch (Exception e) { Log.e(TAG, e.getMessage()); } finally { if (null != database) { database.close(); } if (null != cursor) { cursor.close(); } } return appContent; } /** * 更新下载信息 * @param appContent */ public void updateDownloadFile(AppContent appContent) { if(appContent == null) { return; } SQLiteDatabase database = getConnection(); try { Log.e(TAG, "update download_file,app name:" + appContent.getName() + ",url:" + appContent.getUrl() + ",percent" + appContent.getDownloadPercent() + ",status:" + appContent.getStatus().getValue()); String sql = "update download_file set app_name=?, url=?, download_percent=?, status=? where url=?"; Object[] bindArgs = {appContent.getName(), appContent.getUrl(), appContent.getDownloadPercent() , appContent.getStatus().getValue(), appContent.getUrl()}; database.execSQL(sql, bindArgs); } catch (Exception e) { Log.e(TAG, e.getMessage()); } finally { if (null != database) { database.close(); } } } /** * 获取所有下载文件记录 * @return */ public List<AppContent> getAll() { SQLiteDatabase database = getConnection(); List<AppContent> list = new ArrayList<AppContent>(); Cursor cursor = null; try { String sql = "select * from download_file"; cursor = database.rawQuery(sql, null); while (cursor.moveToNext()) { AppContent appContent = new AppContent(cursor.getString(1), cursor.getString(2)); appContent.setDownloadPercent(cursor.getInt(3)); appContent.setStatus(AppContent.Status.getByValue(cursor.getInt(4))); list.add(appContent); } } catch (Exception e) { Log.e(TAG, e.getMessage()); } finally { if (null != database) { database.close(); } if (null != cursor) { cursor.close(); } } return list; } /** * 根据url删除记录 * @param url */ public void delByUrl(String url) { if(TextUtils.isEmpty(url)) { return; } SQLiteDatabase database = getConnection(); try { String sql = "delete from download_file where url=?"; Object[] bindArgs = { url }; database.execSQL(sql, bindArgs); } catch (Exception e) { Log.e(TAG, e.getMessage()); } finally { if (null != database) { database.close(); } } } }
5、DownloadInfoDAO,每个线程对应下载状态的数据库操作类
package com.bbk.lling.multitaskdownload.db; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.text.TextUtils; import android.util.Log; import com.bbk.lling.multitaskdownload.beans.DownloadInfo; import java.util.ArrayList; import java.util.List; /** * @Class: DownloadInfoDAO * @Description: 每个单独线程下载信息记录的数据库操作类 * @author: lling(www.cnblogs.com/liuling) * @Date: 2015/10/13 */ public class DownloadInfoDAO { private static final String TAG = "DownloadInfoDAO"; private static DownloadInfoDAO dao=null; private Context context; private DownloadInfoDAO(Context context) { this.context=context; } synchronized public static DownloadInfoDAO getInstance(Context context){ if(dao==null){ dao=new DownloadInfoDAO(context); } return dao; } /** * 获取数据库连接 * @return */ public SQLiteDatabase getConnection() { SQLiteDatabase sqliteDatabase = null; try { sqliteDatabase= new DBHelper(context).getReadableDatabase(); } catch (Exception e) { Log.e(TAG, e.getMessage()); } return sqliteDatabase; } /** * 插入数据 * @param downloadInfo */ public void insertDownloadInfo(DownloadInfo downloadInfo) { if(downloadInfo == null) { return; } //如果本地已经存在,直接修改 if(getDownloadInfoByTaskIdAndUrl(downloadInfo.getTaskId(), downloadInfo.getUrl()) != null) { updateDownloadInfo(downloadInfo); return; } SQLiteDatabase database = getConnection(); try { String sql = "insert into download_info(task_id, download_length, url, is_success) values (?,?,?,?)"; Object[] bindArgs = { downloadInfo.getTaskId(), downloadInfo.getDownloadLength(), downloadInfo.getUrl(), downloadInfo.isDownloadSuccess()}; database.execSQL(sql, bindArgs); } catch (Exception e) { Log.e(TAG, e.getMessage()); } finally { if (null != database) { database.close(); } } } public List<DownloadInfo> getDownloadInfosByUrl(String url) { if(TextUtils.isEmpty(url)) { return null; } SQLiteDatabase database = getConnection(); List<DownloadInfo> list = new ArrayList<DownloadInfo>(); Cursor cursor = null; try { String sql = "select * from download_info where url=?"; cursor = database.rawQuery(sql, new String[] { url }); while (cursor.moveToNext()) { DownloadInfo info = new DownloadInfo(); info.setTaskId(cursor.getInt(1)); info.setDownloadLength(cursor.getLong(2)); info.setDownloadSuccess(cursor.getInt(4)); info.setUrl(cursor.getString(3)); list.add(info); } } catch (Exception e) { e.printStackTrace(); } finally { if (null != database) { database.close(); } if (null != cursor) { cursor.close(); } } return list; } /** * 根据taskid和url获取下载信息 * @param taskId * @param url * @return */ public DownloadInfo getDownloadInfoByTaskIdAndUrl(int taskId, String url) { if(TextUtils.isEmpty(url)) { return null; } SQLiteDatabase database = getConnection(); DownloadInfo info = null; Cursor cursor = null; try { String sql = "select * from download_info where url=? and task_id=?"; cursor = database.rawQuery(sql, new String[] { url, String.valueOf(taskId) }); if (cursor.moveToNext()) { info = new DownloadInfo(); info.setTaskId(cursor.getInt(1)); info.setDownloadLength(cursor.getLong(2)); info.setDownloadSuccess(cursor.getInt(4)); info.setUrl(cursor.getString(3)); } } catch (Exception e) { Log.e(TAG, e.getMessage()); } finally { if (null != database) { database.close(); } if (null != cursor) { cursor.close(); } } return info; } /** * 更新下载信息 * @param downloadInfo */ public void updateDownloadInfo(DownloadInfo downloadInfo) { if(downloadInfo == null) { return; } SQLiteDatabase database = getConnection(); try { String sql = "update download_info set download_length=?, is_success=? where task_id=? and url=?"; Object[] bindArgs = { downloadInfo.getDownloadLength(), downloadInfo.isDownloadSuccess(), downloadInfo.getTaskId(), downloadInfo.getUrl() }; database.execSQL(sql, bindArgs); } catch (Exception e) { Log.e(TAG, e.getMessage()); } finally { if (null != database) { database.close(); } } } }
具体的界面和使用代码我就不贴代码了,代码有点多。需要的可以下载Demo的源码看看。
因为还没有花太多时间去测,里面难免会有些bug,如果大家发现什么问题,欢迎留言探讨,谢谢!
源码下载:https://github.com/liuling07/MultiTaskAndThreadDownload