1.今天帶來的是Android原生下載的上篇,主要核心是斷點續傳,多線程下載將會在下篇介紹
2.本例使用了Activity
,Service
,BroadcastReceiver
三個組件
3.本例使用了兩個線程:LinkURLThread
作一些初始工做,DownLoadThread
進行核心下載工做
4.本例使用SQLite進行暫停時的進度保存,使用Handler進行消息的傳遞,使用Intent進行數據傳遞
5.對着代碼,整理了一下思路,畫了一幅下面的流程圖,感受思路清晰多了
6.本例比較基礎,但串聯了Android的不少知識點,做爲總結仍是很不錯的。java
改善了一下界面UI,整個畫風都不一樣了,我的感受還不錯,用了之前的自定義進度條:詳見android
先實現上面一半的代碼:git
既然是下載,固然要有連接了,就那掘金的apk來測試吧!查看方式:github
public class FileBean implements Serializable {
private int id;//文件id
private String url;//文件下載地址
private String fileName;//文件名
private long length;//文件長度
private long loadedLen;//文件已下載長度
//構造函數、get、set、toString省略...
}
複製代碼
Cons.java
不管是Intent添加的Action,仍是Intent傳遞數據的標示,或Handler發送消息的標示
一個項目中確定會有不少這樣的常量,若是散落各處感受會很亂,我習慣使用一個Cons類統一處理數據庫
//intent傳遞數據----開始下載時,傳遞FileBean到Service 標示
public static final String SEND_FILE_BEAN = "send_file_bean";
//廣播更新進度
public static final String SEND_LOADED_PROGRESS = "send_loaded_length";
//下載地址
public static final String URL = "https://imtt.dd.qq.com/16891/4611E43165D203CB6A52E65759FE7641.apk?fsname=com.daimajia.gold_5.6.2_196.apk&csr=1bbd";
//文件下載路徑
public static final String DOWNLOAD_DIR =
Environment.getExternalStorageDirectory().getAbsolutePath() + "/b_download/";
//Handler的Message處理的常量
public static final int MSG_CREATE_FILE_OK = 0x00;
複製代碼
界面比較簡單,就不貼了編程
/**
* 點擊下載時邏輯
*/
private void start() {
//建立FileBean對象
FileBean fileBean = new FileBean(0, Cons.URL, "掘金.apk", 0, 0);
Intent intent = new Intent(MainActivity.this, DownLoadService.class);
intent.setAction(Cons.ACTION_START);
intent.putExtra(Cons.SEND_FILE_BEAN, fileBean);//使用intent攜帶對象
startService(intent);//開啓服務--下載標示
mIdTvFileName.setText(fileBean.getFileName());
}
複製代碼
/**
* 點擊中止下載邏輯
*/
private void stop() {
Intent intent = new Intent(MainActivity.this, DownLoadService.class);
intent.setAction(Cons.ACTION_STOP);
startService(intent);//啓動服務---中止標示
}
複製代碼
public class DownLoadService extends Service {
@Override//每次啓動服務會走此方法
public int onStartCommand(Intent intent, int flags, int startId) {
if (intent.getAction() != null) {
switch (intent.getAction()) {
case Cons.ACTION_START:
FileBean fileBean = (FileBean) intent.getSerializableExtra(Cons.SEND_FILE_BEAN);
L.d("action_start:" + fileBean + L.l());
break;
case Cons.ACTION_STOP:
L.d("action_stop:");
break;
}
}
return super.onStartCommand(intent, flags, startId);
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
}
複製代碼
不要忘記註冊Service:
<service android:name=".service.DownLoadService"/>
經過點擊兩個按鈕,測試能夠看出FileBean對象的傳遞和下載開始、中止的邏輯沒有問題bash
1).鏈接網絡文件
2).獲取文件長度
3).建立等大的本地文件:RandomAccessFile
4).從mHandler的消息池中拿個消息,附帶mFileBean和MSG_CREATE_FILE_OK標示發送給mHandler服務器
/**
* 做者:張風捷特烈<br/>
* 時間:2018/11/12 0012:13:42<br/>
* 郵箱:1981462002@qq.com<br/>
* 說明:鏈接url作一些準備工做:獲取文件大小。建立文件夾及等大的文件
*/
public class LinkURLThread extends Thread {
private FileBean mFileBean;
private Handler mHandler;
public LinkURLThread(FileBean fileBean, Handler handler) {
mFileBean = fileBean;
mHandler = handler;
}
@Override
public void run() {
HttpURLConnection conn = null;
RandomAccessFile raf = null;
try {
//1.鏈接網絡文件
URL url = new URL(mFileBean.getUrl());
conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(5000);
conn.setRequestMethod("GET");
if (conn.getResponseCode() == 200) {
//2.獲取文件長度
long len = conn.getContentLength();
if (len > 0) {
File dir = new File(Cons.DOWNLOAD_DIR);
if (!dir.exists()) {
dir.mkdir();
}
//3.建立等大的本地文件
File file = new File(dir, mFileBean.getFileName());
//建立隨機操做的文件流對象,可讀、寫、刪除
raf = new RandomAccessFile(file, "rwd");
raf.setLength(len);//設置文件大小
mFileBean.setLength(len);
//4.從mHandler的消息池中拿個消息,附帶mFileBean和MSG_CREATE_FILE_OK標示發送給mHandler
mHandler.obtainMessage(Cons.MSG_CREATE_FILE_OK, mFileBean).sendToTarget();
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (conn != null) {
conn.disconnect();
}
try {
if (raf != null) {
raf.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
複製代碼
因爲Service也是運行在主線程的,訪問網絡的耗時操做是進制的,因此須要新開線程
因爲子線程不能更新UI,這裏使用傳統的Handler進行線程間通訊微信
/**
* 處理消息使用的Handler
*/
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case Cons.MSG_CREATE_FILE_OK:
FileBean fileBean = (FileBean) msg.obj;
//已在主線程,可更新UI
ToastUtil.showAtOnce(DownLoadService.this, "文件長度:" + fileBean.getLength());
download(fileBean);
break;
}
}
};
//下載的Action時開啓線程:
new LinkURLThread(fileBean, mHandler).start();
複製代碼
可見開啓線程後,拿到文件大小,Handler發送消息到Service,再在Service(主線程)進行UI的顯示(吐司)網絡
先說一下數據庫是幹嗎用的:記錄下載線程的
信息
、信息
、信息
!
當暫停時,將當前下載的進度及線程信息保存到數據庫中,當再點擊開始是從數據庫查找線程信息,恢復下載
private int id;//線程id
private String url;//線程所下載文件的url
private long start;//線程開始的下載位置(爲多線程準備)
private long end;//線程結束的下載位置
private long loadedLen;//該線程已下載的長度
//構造函數、get、set、toString省略...
複製代碼
關於SQLite可詳見SI--安卓SQLite基礎使用指南:
/**
* 做者:張風捷特烈<br/>
* 時間:2018/11/12 0012:14:19<br/>
* 郵箱:1981462002@qq.com<br/>
* 說明:下載的數據庫幫助類
*/
public class DownLoadDBHelper extends SQLiteOpenHelper {
public DownLoadDBHelper(@Nullable Context context) {
super(context, Cons.DB_NAME, null, Cons.VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(Cons.DB_SQL_CREATE);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL(Cons.DB_SQL_DROP);
db.execSQL(Cons.DB_SQL_CREATE);
}
}
複製代碼
Cons.java
/**
* 數據庫相關常量
*/
public static final String DB_NAME = "download.db";//數據庫名
public static final int VERSION = 1;//版本
public static final String DB_TABLE_NAME = "thread_info";//數據庫名
public static final String DB_SQL_CREATE = //建立表
"CREATE TABLE " + DB_TABLE_NAME + "(\n" +
"_id INTEGER PRIMARY KEY AUTOINCREMENT,\n" +
"thread_id INTEGER,\n" +
"url TEXT,\n" +
"start INTEGER,\n" +
"end INTEGER,\n" +
"loadedLen INTEGER\n" +
")";
public static final String DB_SQL_DROP =//刪除表表
"DROP TABLE IF EXISTS " + DB_TABLE_NAME;
public static final String DB_SQL_INSERT =//插入
"INSERT INTO " + DB_TABLE_NAME + " (thread_id,url,start,end,loadedLen) values(?,?,?,?,?)";
public static final String DB_SQL_DELETE =//刪除
"DELETE FROM " + DB_TABLE_NAME + " WHERE url = ? AND thread_id = ?";
public static final String DB_SQL_UPDATE =//更新
"UPDATE " + DB_TABLE_NAME + " SET loadedLen = ? WHERE url = ? AND thread_id = ?";
public static final String DB_SQL_FIND =//查詢
"SELECT * FROM " + DB_TABLE_NAME + " WHERE url = ?";
public static final String DB_SQL_FIND_IS_EXISTS =//查詢是否存在
"SELECT * FROM " + DB_TABLE_NAME + " WHERE url = ? AND thread_id = ?";
複製代碼
提供數據庫操做的接口 ,至於爲何要一個dao的接口,直接用實現類不行嗎,這裏重點說一下
接口體現的是一種能力保證,實現類的對象是具備這種能力的對象之一。
若是你很是肯定這種實現不會改變(即這裏肯定一種用SQLite),直接使用實現類固然能夠。
不過若是你不想存入數據庫了,而是存在文件裏或SP裏,那全部與實現類相關的部分都要修改,若是散佈各個地方,還不崩潰。
使用接口的好處在於,無論你黑貓白狗(實現方案),幫我抓住耗子(解決問題)就好了。
因此你徹底能夠寫一套在文件裏儲存線程信息的方案,而後實現dao裏的方法,
再只要更換代碼中的dao實現就能夠輕鬆地將黑貓(數據庫實現)切換成白狗(文件操做實現),
固然你也能夠準備一頭貓頭鷹(SP實現),或一門滅鼠大炮(網絡流實現),這樣就讓下載邏輯和存儲邏輯解耦
你想上午讓白狗(文件操做實現)抓老鼠,下午讓白貓(數據庫實現),晚上讓貓頭鷹(SP實現),都不是問題
這就是面相接口編程的好處,若是你遇到相似的情形,不少實現都各有優劣,你徹底能夠面相接口,後期再根據不一樣的需求寫實現
複製代碼
/**
* 做者:張風捷特烈<br/>
* 時間:2018/11/12 0012:14:36<br/>
* 郵箱:1981462002@qq.com<br/>
* 說明:數據訪問接口
*/
public interface DownLoadDao {
/**
* 在數據庫插入線程信息
*
* @param threadBean 線程信息
*/
void insertThread(ThreadBean threadBean);
/**
* 在數據庫刪除線程信息
*
* @param url 下載的url
* @param threadId 線程的id
*/
void deleteThread(String url, int threadId);
/**
* 在數據庫更新線程信息---下載進度
*
* @param url 下載的url
* @param threadId 線程的id
*/
void updateThread(String url, int threadId ,long loadedLen);
/**
* 獲取一個文件下載的全部線程信息(多線程下載)
* @param url 下載的url
* @return 線程信息集合
*/
List<ThreadBean> getThreads(String url);
/**
* 判斷數據庫中該線程信息是否存在
*
* @param url 下載的url
* @param threadId 線程的id
*/
boolean isExist(String url, int threadId);
}
複製代碼
一些基礎的SQL操做,我的習慣原生的SQL,在每次操做以後不要忘記關閉db,以及遊標
/**
* 做者:張風捷特烈<br/>
* 時間:2018/11/12 0012:14:43<br/>
* 郵箱:1981462002@qq.com<br/>
* 說明:數據訪問接口實現類
*/
public class DownLoadDaoImpl implements DownLoadDao {
private DownLoadDBHelper mDBHelper;
private Context mContext;
public DownLoadDaoImpl(Context context) {
mContext = context;
mDBHelper = new DownLoadDBHelper(mContext);
}
@Override
public void insertThread(ThreadBean threadBean) {
SQLiteDatabase db = mDBHelper.getWritableDatabase();
db.execSQL(Cons.DB_SQL_INSERT,
new Object[]{threadBean.getId(), threadBean.getUrl(),
threadBean.getStart(), threadBean.getEnd(), threadBean.getLoadedLen()});
db.close();
}
@Override
public void deleteThread(String url, int threadId) {
SQLiteDatabase db = mDBHelper.getWritableDatabase();
db.execSQL(Cons.DB_SQL_DELETE,
new Object[]{url, threadId});
db.close();
}
@Override
public void updateThread(String url, int threadId, long loadedLen) {
SQLiteDatabase db = mDBHelper.getWritableDatabase();
db.execSQL(Cons.DB_SQL_UPDATE,
new Object[]{loadedLen, url, threadId});
db.close();
}
@Override
public List<ThreadBean> getThreads(String url) {
SQLiteDatabase db = mDBHelper.getWritableDatabase();
Cursor cursor = db.rawQuery(Cons.DB_SQL_FIND, new String[]{url});
List<ThreadBean> threadBeans = new ArrayList<>();
while (cursor.moveToNext()) {
ThreadBean threadBean = new ThreadBean();
threadBean.setId(cursor.getInt(cursor.getColumnIndex("thread_id")));
threadBean.setUrl(cursor.getString(cursor.getColumnIndex("url")));
threadBean.setStart(cursor.getLong(cursor.getColumnIndex("start")));
threadBean.setEnd(cursor.getLong(cursor.getColumnIndex("end")));
threadBean.setLoadedLen(cursor.getLong(cursor.getColumnIndex("loadedLen")));
threadBeans.add(threadBean);
}
cursor.close();
db.close();
return threadBeans;
}
@Override
public boolean isExist(String url, int threadId) {
SQLiteDatabase db = mDBHelper.getWritableDatabase();
Cursor cursor = db.rawQuery(Cons.DB_SQL_FIND_IS_EXISTS, new String[]{url, threadId + ""});
boolean exists = cursor.moveToNext();
cursor.close();
db.close();
return exists;
}
}
複製代碼
注意請求中使用Range後,服務器返回的成功狀態碼是206:不是200,表示:部份內容和範圍請求成功 註釋寫的很詳細了,就不贅述了
/**
* 做者:張風捷特烈<br/>
* 時間:2018/11/12 0012:15:10<br/>
* 郵箱:1981462002@qq.com<br/>
* 說明:下載線程
*/
public class DownLoadThread extends Thread {
private ThreadBean mThreadBean;//下載線程的信息
private FileBean mFileBean;//下載文件的信息
private long mLoadedLen;//已下載的長度
public boolean isDownLoading;//是否在下載
private DownLoadDao mDao;//數據訪問接口
private Context mContext;//上下文
public DownLoadThread(ThreadBean threadBean, FileBean fileBean, Context context) {
mThreadBean = threadBean;
mDao = new DownLoadDaoImpl(context);
mFileBean = fileBean;
mContext = context;
}
@Override
public void run() {
if (mThreadBean == null) {//1.下載線程的信息爲空,直接返回
return;
}
//2.若是數據庫沒有此下載線程的信息,則向數據庫插入該線程信息
if (!mDao.isExist(mThreadBean.getUrl(), mThreadBean.getId())) {
mDao.insertThread(mThreadBean);
}
HttpURLConnection conn = null;
RandomAccessFile raf = null;
InputStream is = null;
try {
//3.鏈接線程的url
URL url = new URL(mThreadBean.getUrl());
conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(5000);
conn.setRequestMethod("GET");
//4.設置下載位置
long start = mThreadBean.getStart() + mThreadBean.getLoadedLen();//開始位置
//conn設置屬性,標記資源的位置(這是給服務器看的)
conn.setRequestProperty("Range", "bytes=" + start + "-" + mThreadBean.getEnd());
//5.尋找文件的寫入位置
File file = new File(Cons.DOWNLOAD_DIR, mFileBean.getFileName());
//建立隨機操做的文件流對象,可讀、寫、刪除
raf = new RandomAccessFile(file, "rwd");
raf.seek(start);//設置文件寫入位置
//6.下載的核心邏輯
Intent intent = new Intent(Cons.ACTION_UPDATE);//更新進度的廣播intent
mLoadedLen += mThreadBean.getLoadedLen();
//206-----部份內容和範圍請求 不要200寫順手了...
if (conn.getResponseCode() == 206) {
//讀取數據
is = conn.getInputStream();
byte[] buf = new byte[1024 * 4];
int len = 0;
long time = System.currentTimeMillis();
while ((len = is.read(buf)) != -1) {
//寫入文件
raf.write(buf, 0, len);
//發送廣播給Activity,通知進度
mLoadedLen += len;
if (System.currentTimeMillis() - time > 500) {//減小UI的渲染速度
mContext.sendBroadcast(intent);
intent.putExtra(Cons.SEND_LOADED_PROGRESS,
(int) (mLoadedLen * 100 / mFileBean.getLength()));
mContext.sendBroadcast(intent);
time = System.currentTimeMillis();
}
//暫停保存進度到數據庫
if (!isDownLoading) {
mDao.updateThread(mThreadBean.getUrl(), mThreadBean.getId(), mLoadedLen);
return;
}
}
}
//下載完成,刪除線程信息
mDao.deleteThread(mThreadBean.getUrl(), mThreadBean.getId());
//下載完成後,發送完成度100%的廣播
intent.putExtra(Cons.SEND_LOADED_PROGRESS, 100);
mContext.sendBroadcast(intent);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (conn != null) {
conn.disconnect();
}
try {
if (raf != null) {
raf.close();
}
if (is != null) {
is.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
複製代碼
注意這裏並不是只能用BroadcastReceiver,任何線程間通訊均可以,只是將進度從下載線程拿過來而已
/**
* 做者:張風捷特烈<br/>
* 時間:2018/11/12 0012:16:05<br/>
* 郵箱:1981462002@qq.com<br/>
* 說明:更新ui的廣播接收者
*/
public class UpdateReceiver extends BroadcastReceiver {
private ProgressBar[] mProgressBar;
public UpdateReceiver(ProgressBar... progressBar) {
mProgressBar = progressBar;
}
@Override
public void onReceive(Context context, Intent intent) {
if (Cons.ACTION_UPDATE.equals(intent.getAction())) {
int progress = intent.getIntExtra(Cons.SEND_LOADED_PROGRESS, 0);
for (ProgressBar progressBar : mProgressBar) {
progressBar.setProgress(progress);
}
}
}
}
複製代碼
在接收到Handler的信息後調用下載函數
/**
* 下載邏輯
*
* @param fileBean 文件信息對象
*/
public void download(FileBean fileBean) {
//從數據獲取線程信息
List<ThreadBean> threads = mDao.getThreads(fileBean.getUrl());
if (threads.size() == 0) {//若是沒有線程信息,就新建線程信息
mThreadBean = new ThreadBean(
0, fileBean.getUrl(), 0, fileBean.getLength(), 0);//初始化線程信息對象
} else {
mThreadBean = threads.get(0);//不然取第一個
}
mDownLoadThread = new DownLoadThread(mThreadBean, fileBean, this);//建立下載線程
mDownLoadThread.start();//開始線程
mDownLoadThread.isDownLoading = true;
}
複製代碼
@Override//每次啓動服務會走此方法
public int onStartCommand(Intent intent, int flags, int startId) {
mDao = new DownLoadDaoImpl(this);
if (intent.getAction() != null) {
switch (intent.getAction()) {
case Cons.ACTION_START:
FileBean fileBean = (FileBean) intent.getSerializableExtra(Cons.SEND_FILE_BEAN);
if (mDownLoadThread != null) {
if (mDownLoadThread.isDownLoading) {
return super.onStartCommand(intent, flags, startId);
}
}
new LinkURLThread(fileBean, mHandler).start();
break;
case Cons.ACTION_STOP:
if (mDownLoadThread != null) {
mDownLoadThread.isDownLoading = false;
}
break;
}
}
return super.onStartCommand(intent, flags, startId);
}
複製代碼
/**
* 註冊廣播接收者
*/
private void register() {
//註冊廣播接收者
mUpdateReceiver = new UpdateReceiver(mProgressBar,mIdRoundPb);
IntentFilter filter = new IntentFilter();
filter.addAction(Cons.ACTION_UPDATE);
registerReceiver(mUpdateReceiver, filter);
}
@Override
protected void onDestroy() {
super.onDestroy();
if (mUpdateReceiver != null) {//註銷廣播
unregisterReceiver(mUpdateReceiver);
}
}
複製代碼
下載完後,安裝正常,打開正常,下載OK
項目源碼 | 日期 | 備註 |
---|---|---|
V0.1--無 | 2018-11-12 | Android原生下載(上篇)基本邏輯+斷點續傳 |
V0.1--無 | 2018-11-13 | UI界面優化 |
筆名 | 微信 | 愛好 | |
---|---|---|---|
張風捷特烈 | 1981462002 | zdl1994328 | 語言 |
個人github | 個人簡書 | 個人CSDN | 我的網站 |
1----本文由張風捷特烈原創,轉載請註明 2----歡迎廣大編程愛好者共同交流 3----我的能力有限,若有不正之處歡迎你們批評指證,一定虛心改正 4----看到這裏,我在此感謝你的喜歡與支持