转载请注明出处:http://blog.csdn.net/zhaokaiqiang1992
上班第一天,在技术群里面和大家闲扯,无意中谈到了关于框架的使用,一个同学说为了用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); } });