上班第一天,在技术群里面和大家闲扯,无意中谈到了关于框架的使用,一个同学说为了用xUtils的断线续传下载功能,把整个库引入到了项目中,在google的官方建议中,是非常不建议这种做法的,集合框架虽然把很多功能集成起来,但是代码越多,出现问题的可能越大,而且无形之中增加了APK的大小,因此,得不偿失。所以,这篇文章主要就“断线续传”下载功能,简单的说下思路和代码实现,因为这类代码比较多,所以找了一个写的不错的demo,简单优化了一下。
在贴代码之前,我们先分析一下需求和解决思路。首先是下载功能,我们简单的使用HttpURLConnection就可以了,没有引入框架的必要,然后就是断点续传了,其实断点续传指的就是我们可以随时停止我们的下载任务,当下次再次开始的时候,可以从上次下载到的位置继续下载,节省下载时间,很方便也很实用,做法无非就是在下载的过程中,纪录下下载到的位置,当再次开始下载的时候,我们从上一次的位置继续请求服务器即可。说到这里,有个类不得不提,那就是RandomAccessFile,这个类是实现断点续传功能的核心类,RandomAccessFile允许我们从我们想要的位置进行读写操作,因此,我们可以把我们要下载的文件切分成几部分,然后开启多个线程,分别从文件不同的位置进行下载,这样等所有的部分都下载完成之后,我们就能够得到一个完整的文件了,这就是多线程下载的原理,完成上面几个步骤,我们的多线程断线续传下载功能就基本完成了,下面是在网上找的一个Demo,我对代码进行了部分修改,从代码里面,我们看一下如何进行代码的实现。
首先,如果要实现断点续传,我们就要纪录每个线程下载的文件的位置,可以使用文件,也可以使用sp,也可以使用DB,这个Demo里面使用的DB,我们首先看一下数据库的Helper实现类,里面存储主键、线程号、开始位置、结束位置、完成位置和下载地址即可。
DownLoadHelper.java
- public class DownLoadHelper extends SQLiteOpenHelper {
- private static final String SQL_NAME = "download.db";
- private static final int DOWNLOAD_VERSION = 1;
- public DownLoadHelper(Context context) {
- super(context, SQL_NAME, null, DOWNLOAD_VERSION);
- }
- /**
- * 在download.db数据库下创建一个download_info表存储下载信息
- */
- @Override
- public void onCreate(SQLiteDatabase db) {
- db.execSQL("create table download_info(_id integer PRIMARY KEY AUTOINCREMENT, thread_id integer, "
- + "start_pos integer, end_pos integer, compelete_size integer,url char)");
- }
- @Override
- public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
- }
- }
有了Helper之后,我们在创建一个Sql工具类,完成对表的数据操作
DownlaodSqlTool.java
- public class DownlaodSqlTool {
- private DownLoadHelper dbHelper;
- public DownlaodSqlTool(Context context) {
- dbHelper = new DownLoadHelper(context);
- }
- /**
- * 创建下载的具体信息
- */
- public void insertInfos(List<DownloadInfo> infos) {
- SQLiteDatabase database = dbHelper.getWritableDatabase();
- for (DownloadInfo info : infos) {
- String sql = "insert into download_info(thread_id,start_pos, end_pos,compelete_size,url) values (?,?,?,?,?)";
- Object[] bindArgs = { info.getThreadId(), info.getStartPos(),
- info.getEndPos(), info.getCompeleteSize(), info.getUrl() };
- database.execSQL(sql, bindArgs);
- }
- }
- /**
- * 得到下载具体信息
- */
- public List<DownloadInfo> getInfos(String urlstr) {
- List<DownloadInfo> list = new ArrayList<DownloadInfo>();
- SQLiteDatabase database = dbHelper.getWritableDatabase();
- String sql = "select thread_id, start_pos, end_pos,compelete_size,url from download_info where url=?";
- Cursor cursor = database.rawQuery(sql, new String[] { urlstr });
- while (cursor.moveToNext()) {
- DownloadInfo info = new DownloadInfo(cursor.getInt(0),
- cursor.getInt(1), cursor.getInt(2), cursor.getInt(3),
- cursor.getString(4));
- list.add(info);
- }
- return list;
- }
- /**
- * 更新数据库中的下载信息
- */
- public void updataInfos(int threadId, int compeleteSize, String urlstr) {
- SQLiteDatabase database = dbHelper.getWritableDatabase();
- String sql = "update download_info set compelete_size=? where thread_id=? and url=?";
- Object[] bindArgs = { compeleteSize, threadId, urlstr };
- database.execSQL(sql, bindArgs);
- }
- /**
- * 关闭数据库
- */
- public void closeDb() {
- dbHelper.close();
- }
- /**
- * 下载完成后删除数据库中的数据
- */
- public void delete(String url) {
- SQLiteDatabase database = dbHelper.getWritableDatabase();
- database.delete("download_info", "url=?", new String[] { url });
- }
- }
数据库相关的类就这些,断点续传的功能已经完成,下面看下载如何实现。
首先,为了操作方便,我们对下载的文件抽取实体类
DownloadInfo.java
- public class DownloadInfo {
- private int threadId;// 下载器id
- private int startPos;// 开始点
- private int endPos;// 结束点
- private int compeleteSize;// 完成度
- private String url;// 下载文件的URL地址
- public DownloadInfo(int threadId, int startPos, int endPos,
- int compeleteSize, String url) {
- this.threadId = threadId;
- this.startPos = startPos;
- this.endPos = endPos;
- this.compeleteSize = compeleteSize;
- this.url = url;
- }
- public DownloadInfo() {
- }
- public String getUrl() {
- return url;
- }
- public void setUrl(String url) {
- this.url = url;
- }
- public int getThreadId() {
- return threadId;
- }
- public void setThreadId(int threadId) {
- this.threadId = threadId;
- }
- public int getStartPos() {
- return startPos;
- }
- public void setStartPos(int startPos) {
- this.startPos = startPos;
- }
- public int getEndPos() {
- return endPos;
- }
- public void setEndPos(int endPos) {
- this.endPos = endPos;
- }
- public int getCompeleteSize() {
- return compeleteSize;
- }
- public void setCompeleteSize(int compeleteSize) {
- this.compeleteSize = compeleteSize;
- }
- @Override
- public String toString() {
- return "DownloadInfo [threadId=" + threadId + ", startPos=" + startPos
- + ", endPos=" + endPos + ", compeleteSize=" + compeleteSize
- + "]";
- }
- }
实体类抽取之后,我们就可以实现下载功能了,DownloadHttpTool是实现下载功能的主要类
DownloadHttpTool.java
- public class DownloadHttpTool {
- private static final String TAG = DownloadHttpTool.class.getSimpleName();
- // 线程数量
- private int threadCount;
- // URL地址
- private String urlstr;
- private Context mContext;
- private Handler mHandler;
- // 保存下载信息的类
- private List<DownloadInfo> downloadInfos;
- // 目录
- private String localPath;
- // 文件名
- private String fileName;
- private int fileSize;
- // 文件信息保存的数据库操作类
- private DownlaodSqlTool sqlTool;
- // 利用枚举表示下载的三种状态
- private enum Download_State {
- Downloading, Pause, Ready, Delete;
- }
- // 当前下载状态
- private Download_State state = Download_State.Ready;
- // 所有线程下载的总数
- private int globalCompelete = 0;
- public DownloadHttpTool(int threadCount, String urlString,
- String localPath, String fileName, Context context, Handler handler) {
- super();
- this.threadCount = threadCount;
- this.urlstr = urlString;
- this.localPath = localPath;
- this.mContext = context;
- this.mHandler = handler;
- this.fileName = fileName;
- sqlTool = new DownlaodSqlTool(mContext);
- }
- // 在开始下载之前需要调用ready方法进行配置
- public void ready() {
- Log.w(TAG, "ready");
- globalCompelete = 0;
- downloadInfos = sqlTool.getInfos(urlstr);
- if (downloadInfos.size() == 0) {
- initFirst();
- } else {
- File file = new File(localPath + "/" + fileName);
- if (!file.exists()) {
- sqlTool.delete(urlstr);
- initFirst();
- } else {
- fileSize = downloadInfos.get(downloadInfos.size() - 1)
- .getEndPos();
- for (DownloadInfo info : downloadInfos) {
- globalCompelete += info.getCompeleteSize();
- }
- Log.w(TAG, "globalCompelete:::" + globalCompelete);
- }
- }
- }
- public void start() {
- Log.w(TAG, "start");
- if (downloadInfos != null) {
- if (state == Download_State.Downloading) {
- return;
- }
- state = Download_State.Downloading;
- for (DownloadInfo info : downloadInfos) {
- Log.v(TAG, "startThread");
- new DownloadThread(info.getThreadId(), info.getStartPos(),
- info.getEndPos(), info.getCompeleteSize(),
- info.getUrl()).start();
- }
- }
- }
- public void pause() {
- state = Download_State.Pause;
- sqlTool.closeDb();
- }
- public void delete() {
- state = Download_State.Delete;
- compelete();
- new File(localPath + File.separator + fileName).delete();
- }
- public void compelete() {
- sqlTool.delete(urlstr);
- sqlTool.closeDb();
- }
- public int getFileSize() {
- return fileSize;
- }
- public int getCompeleteSize() {
- return globalCompelete;
- }
- /**
- * 第一次下载初始化
- */
- private void initFirst() {
- Log.w(TAG, "initFirst");
- try {
- URL url = new URL(urlstr);
- HttpURLConnection connection = (HttpURLConnection) url
- .openConnection();
- connection.setConnectTimeout(5000);
- connection.setRequestMethod("GET");
- fileSize = connection.getContentLength();
- Log.w(TAG, "fileSize::" + fileSize);
- File fileParent = new File(localPath);
- if (!fileParent.exists()) {
- fileParent.mkdir();
- }
- File file = new File(fileParent, fileName);
- if (!file.exists()) {
- file.createNewFile();
- }
- // 本地访问文件
- RandomAccessFile accessFile = new RandomAccessFile(file, "rwd");
- accessFile.setLength(fileSize);
- accessFile.close();
- connection.disconnect();
- } catch (Exception e) {
- e.printStackTrace();
- }
- int range = fileSize / threadCount;
- downloadInfos = new ArrayList<DownloadInfo>();
- for (int i = 0; i < threadCount - 1; i++) {
- DownloadInfo info = new DownloadInfo(i, i * range, (i + 1) * range
- - 1, 0, urlstr);
- downloadInfos.add(info);
- }
- DownloadInfo info = new DownloadInfo(threadCount - 1, (threadCount - 1)
- * range, fileSize - 1, 0, urlstr);
- downloadInfos.add(info);
- sqlTool.insertInfos(downloadInfos);
- }
- /**
- * 自定义下载线程
- *
- * @author zhaokaiqiang
- * @time 2015-2-25下午5:52:28
- */
- private class DownloadThread extends Thread {
- private int threadId;
- private int startPos;
- private int endPos;
- private int compeleteSize;
- private String urlstr;
- private int totalThreadSize;
- public DownloadThread(int threadId, int startPos, int endPos,
- int compeleteSize, String urlstr) {
- this.threadId = threadId;
- this.startPos = startPos;
- this.endPos = endPos;
- totalThreadSize = endPos - startPos + 1;
- this.urlstr = urlstr;
- this.compeleteSize = compeleteSize;
- }
- @Override
- public void run() {
- HttpURLConnection connection = null;
- RandomAccessFile randomAccessFile = null;
- InputStream is = null;
- try {
- randomAccessFile = new RandomAccessFile(localPath
- + File.separator + fileName, "rwd");
- randomAccessFile.seek(startPos + compeleteSize);
- URL url = new URL(urlstr);
- connection = (HttpURLConnection) url.openConnection();
- connection.setConnectTimeout(5000);
- connection.setRequestMethod("GET");
- connection.setRequestProperty("Range", "bytes="
- + (startPos + compeleteSize) + "-" + endPos);
- is = connection.getInputStream();
- byte[] buffer = new byte[1024];
- int length = -1;
- while ((length = is.read(buffer)) != -1) {
- randomAccessFile.write(buffer, 0, length);
- compeleteSize += length;
- Message message = Message.obtain();
- message.what = threadId;
- message.obj = urlstr;
- message.arg1 = length;
- mHandler.sendMessage(message);
- Log.w(TAG, "Threadid::" + threadId + " compelete::"
- + compeleteSize + " total::" + totalThreadSize);
- // 当程序不再是下载状态的时候,纪录当前的下载进度
- if ((state != Download_State.Downloading)
- || (compeleteSize >= totalThreadSize)) {
- sqlTool.updataInfos(threadId, compeleteSize, urlstr);
- break;
- }
- }
- } catch (Exception e) {
- e.printStackTrace();
- sqlTool.updataInfos(threadId, compeleteSize, urlstr);
- } finally {
- try {
- if (is != null) {
- is.close();
- }
- randomAccessFile.close();
- connection.disconnect();
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- }
- }
- }
在上面的代码中,我们定义了一个线程,进行文件的多线程下载,并且在退出下载状态和完成下载的时候,纪录下载的位置,存到数据库中。在原来的代码中,是没获取一次数据,存取一次数据库,大大的增加了数据库的操作频率,降低了效率,原先下载420k左右的文件,需要操作420次数据库,现在只要一次即可。
在initFirst()里面,首先进行了初始化,根据下载文件的大小和开启线程的数量,对下载实体类进行了初始化和赋值。在RandomAccessFile创建完毕,DownloadInfo初始化完毕之后,就可以通过start()进行文件的下载了。
其实到这里,基本的功能已经实现了。为了使得我们的操作更加的方便,同时可以监控到下载的进度,我们对下载类进行一次封装,代码如下:
DownloadUtil.java
- public class DownloadUtil {
- private DownloadHttpTool mDownloadHttpTool;
- private OnDownloadListener onDownloadListener;
- private int fileSize;
- private int downloadedSize = 0;
- @SuppressLint("HandlerLeak")
- private Handler mHandler = new Handler() {
- @Override
- public void handleMessage(Message msg) {
- int length = msg.arg1;
- synchronized (this) {// 加锁保证已下载的正确性
- downloadedSize += length;
- }
- if (onDownloadListener != null) {
- onDownloadListener.downloadProgress(downloadedSize);
- }
- if (downloadedSize >= fileSize) {
- mDownloadHttpTool.compelete();
- if (onDownloadListener != null) {
- onDownloadListener.downloadEnd();
- }
- }
- }
- };
- public DownloadUtil(int threadCount, String filePath, String filename,
- String urlString, Context context) {
- mDownloadHttpTool = new DownloadHttpTool(threadCount, urlString,
- filePath, filename, context, mHandler);
- }
- // 下载之前首先异步线程调用ready方法获得文件大小信息,之后调用开始方法
- public void start() {
- new AsyncTask<Void, Void, Void>() {
- @Override
- protected Void doInBackground(Void... arg0) {
- mDownloadHttpTool.ready();
- return null;
- }
- @Override
- protected void onPostExecute(Void result) {
- fileSize = mDownloadHttpTool.getFileSize();
- downloadedSize = mDownloadHttpTool.getCompeleteSize();
- Log.w("Tag", "downloadedSize::" + downloadedSize);
- if (onDownloadListener != null) {
- onDownloadListener.downloadStart(fileSize);
- }
- mDownloadHttpTool.start();
- }
- }.execute();
- }
- public void pause() {
- mDownloadHttpTool.pause();
- }
- public void delete() {
- mDownloadHttpTool.delete();
- }
- public void reset() {
- mDownloadHttpTool.delete();
- start();
- }
- public void setOnDownloadListener(OnDownloadListener onDownloadListener) {
- this.onDownloadListener = onDownloadListener;
- }
- // 下载回调接口
- public interface OnDownloadListener {
- public void downloadStart(int fileSize);
- public void downloadProgress(int downloadedSize);
- public void downloadEnd();
- }
- }
通过对外暴露接口,我们可以实现下载进度的监听了!
用的时候也很简单,像下面这样就ok了
- String urlString = "http://bbra.cn/Uploadfiles/imgs/20110303/fengjin/013.jpg";
- final String localPath = Environment.getExternalStorageDirectory()
- .getAbsolutePath() + "/ADownLoadTest";
- mDownloadUtil = new DownloadUtil(2, localPath, "abc.jpg", urlString,
- this);
- mDownloadUtil.setOnDownloadListener(new OnDownloadListener() {
- @Override
- public void downloadStart(int fileSize) {
- max = fileSize;
- mProgressBar.setMax(fileSize);
- }
- @Override
- public void downloadProgress(int downloadedSize) {
- mProgressBar.setProgress(downloadedSize);
- total.setText((int) downloadedSize * 100 / max + "%");
- }
- @Override
- public void downloadEnd() {
- Bitmap bitmap = decodeSampledBitmapFromResource(localPath
- + File.separator + "abc.jpg", 200, 200);
- image.setImageBitmap(bitmap);
- }
- });
Icon Image:
