Android原生下載(上篇)基本邏輯+斷點續傳
零、前言
1.今天帶來的是Android原生下載的上篇,主要核心是斷點續傳,多執行緒下載將會在下篇介紹
2.本例使用了Activity
,Service
,BroadcastReceiver
三個元件
3.本例使用了兩個執行緒:LinkURLThread
做一些初始工作,DownLoadThread
進行核心下載工作
4.本例使用SQLite進行暫停時的進度儲存,使用Handler進行訊息的傳遞,使用Intent進行資料傳遞
5.對著程式碼,整理了一下思路,畫了一幅下面的流程圖,感覺思路清晰多了
6.本例比較基礎,但串聯了Android的很多知識點,作為總結還是很不錯的。
2018-11-13更新:
改善了一下介面UI,整個畫風都不同了,個人感覺還不錯,用了以前的自定義進度條:詳見
斷點續傳邏輯總覽
一、前置準備工作
先實現上面一半的程式碼:
1.關於下載的連結:
既然是下載,當然要有連結了,就那掘金的apk來測試吧!檢視方式:
2.檔案資訊封裝類:FileBean
public class FileBean implements Serializable {
private int id;//檔案id
private String url;//檔案下載地址
private String fileName;//檔名
private long length;//檔案長度
private long loadedLen;//檔案已下載長度
//建構函式、get、set 、toString省略...
}
複製程式碼
2.關於常量: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;
複製程式碼
2.Activity與Service的協作
介面比較簡單,就不貼了
1).Activity中:
/**
* 點選下載時邏輯
*/
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);//啟動服務---停止標示
}
複製程式碼
2).DownLoadService:下載的服務
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物件的傳遞和下載開始、停止的邏輯沒有問題
二、下載的初始執行緒及使用:
1.LinkURLThread執行緒的實現
1).連線網路檔案
2).獲取檔案長度
3).建立等大的本地檔案:RandomAccessFile
4).從mHandler的訊息池中拿個訊息,附帶mFileBean和MSG_CREATE_FILE_OK標示傳送給mHandler
/**
* 作者:張風捷特烈<br/>
* 時間:2018/11/12 0012:13:42<br/>
* 郵箱:[email protected]<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();
}
}
}
}
複製程式碼
2.在Service中的使用:DownLoadService
由於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的顯示(吐司)
三、資料庫相關操作:
先說一下資料庫是幹嘛用的:記錄下載執行緒的
資訊
、資訊
、資訊
!
當暫停時,將當前下載的進度及執行緒資訊儲存到資料庫中,當再點選開始是從資料庫查詢執行緒資訊,恢復下載
1.執行緒資訊封裝類:ThreadBean
private int id;//執行緒id
private String url;//執行緒所下載檔案的url
private long start;//執行緒開始的下載位置(為多執行緒準備)
private long end;//執行緒結束的下載位置
private long loadedLen;//該執行緒已下載的長度
//建構函式、get、set、toString省略...
複製程式碼
2.下載的資料庫幫助類:DownLoadDBHelper
關於SQLite可詳見SI--安卓SQLite基礎使用指南:
/**
* 作者:張風捷特烈<br/>
* 時間:2018/11/12 0012:14:19<br/>
* 郵箱:[email protected]<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);
}
}
複製程式碼
3.關於資料庫的常量: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 = ?";
複製程式碼
4.資料訪問介面:DownLoadDao
提供資料庫操作的介面 ,至於為什麼要一個dao的介面,直接用實現類不行嗎,這裡重點說一下
介面體現的是一種能力保證,實現類的物件是具有這種能力的物件之一。
如果你非常確定這種實現不會改變(即這裡確定一種用SQLite),直接使用實現類當然可以。
不過如果你不想存入資料庫了,而是存在檔案裡或SP裡,那所有與實現類相關的部分都要修改,如果散佈各個地方,還不崩潰。
使用介面的好處在於,不管你黑貓白狗(實現方案),幫我抓住耗子(解決問題)就行了。
所以你完全可以寫一套在檔案裡儲存執行緒資訊的方案,然後實現dao裡的方法,
再只要更換程式碼中的dao實現就可以輕鬆地將黑貓(資料庫實現)切換成白狗(檔案操作實現),
當然你也可以準備一頭貓頭鷹(SP實現),或一門滅鼠大炮(網路流實現),這樣就讓下載邏輯和儲存邏輯解耦
你想上午讓白狗(檔案操作實現)抓老鼠,下午讓白貓(資料庫實現),晚上讓貓頭鷹(SP實現),都不是問題
這就是面相介面程式設計的好處,如果你遇到類似的情形,很多實現都各有優劣,你完全可以面相介面,後期再根據不同的需求寫實現
複製程式碼
/**
* 作者:張風捷特烈<br/>
* 時間:2018/11/12 0012:14:36<br/>
* 郵箱:[email protected]<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);
}
複製程式碼
5.資料庫介面實現類:DownLoadDaoImpl
一些基礎的SQL操作,個人習慣原生的SQL,在每次操作之後不要忘記關閉db,以及遊標
/**
* 作者:張風捷特烈<br/>
* 時間:2018/11/12 0012:14:43<br/>
* 郵箱:[email protected]<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;
}
}
複製程式碼
四、核心下載執行緒:DownLoadThread 與進度廣播:BroadcastReceiver
1.下載執行緒:
注意請求中使用Range後,伺服器返回的成功狀態碼是206:不是200,表示:部分內容和範圍請求成功 註釋寫的很詳細了,就不贅述了
/**
* 作者:張風捷特烈<br/>
* 時間:2018/11/12 0012:15:10<br/>
* 郵箱:[email protected]<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();
}
}
}
}
複製程式碼
3.進度廣播:BroadcastReceiver
注意這裡並非只能用BroadcastReceiver,任何執行緒間通訊都可以,只是將進度從下載執行緒拿過來而已
/**
* 作者:張風捷特烈<br/>
* 時間:2018/11/12 0012:16:05<br/>
* 郵箱:[email protected]<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);
}
}
}
}
複製程式碼
五、將兩大部分拼合一起
1.DownLoadService:下載服務
在接收到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;
}
複製程式碼
2.開始與停止下載的優化:
@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);
}
複製程式碼
3.Activity中註冊和登出廣播
/**
* 註冊廣播接收者
*/
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
後記:捷文規範
1.本文成長記錄及勘誤表
專案原始碼 | 日期 | 備註 |
---|---|---|
V0.1--無 | 2018-11-12 | Android原生下載(上篇)基本邏輯+斷點續傳 |
V0.1--無 | 2018-11-13 | UI介面優化 |
2.更多關於我
筆名 | 微信 | 愛好 | |
---|---|---|---|
張風捷特烈 | 1981462002 | zdl1994328 | 語言 |
我的github | 我的簡書 | 我的CSDN | 個人網站 |
3.宣告
1----本文由張風捷特烈原創,轉載請註明
2----歡迎廣大程式設計愛好者共同交流
3----個人能力有限,如有不正之處歡迎大家批評指證,必定虛心改正
4----看到這裡,我在此感謝你的喜歡與支援