轉載請註明出處: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);
}
});
GitHub的Demo地址:https://github.com/ZhaoKaiQiang/MultiThreadDownLoadDemo