Android原生下載(上篇)基本邏輯+斷點續傳
零、前言
1.今天帶來的是Android原生下載的上篇,主要核心是斷點續傳,多執行緒下載將會在下篇介紹
2.本例使用了 Activity
, Service
, BroadcastReceiver
三個元件
3.本例使用了兩個執行緒: LinkURLThread
做一些初始工作, DownLoadThread
進行核心下載工作
4.本例使用SQLite進行暫停時的進度儲存,使用Handler進行訊息的傳遞,使用Intent進行資料傳遞
5.對著程式碼,整理了一下思路,畫了一幅下面的流程圖,感覺思路清晰多了
6.本例比較基礎,但串聯了Android的很多知識點,作為總結還是很不錯的。

斷點續傳邏輯總覽.png
一、前置準備工作
先實現上面一半的程式碼:

初始準備.png
1.關於下載的連結:
既然是下載,當然要有連結了,就那掘金的apk來測試吧!檢視方式:

檢視下載地址.png
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的協作
介面比較簡單,就不貼了

介面.png
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物件的傳遞和下載開始、停止的邏輯沒有問題

測試.png
二、下載的初始執行緒及使用:
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的顯示(吐司)

初始連線執行緒測試.png
三、資料庫相關操作:

資料庫相關.png
先說一下資料庫是幹嘛用的:記錄下載執行緒的 資訊
、 資訊
、 資訊
!
當暫停時,將當前下載的進度及執行緒資訊儲存到資料庫中,當再點選開始是從資料庫查詢執行緒資訊,恢復下載
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可詳見 ofollow,noindex">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
提供資料庫操作的介面
/** * 作者:張風捷特烈<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

下載核心執行緒.png
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
/** * 作者:張風捷特烈<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); mProgressBar.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); IntentFilter filter = new IntentFilter(); filter.addAction(Cons.ACTION_UPDATE); registerReceiver(mUpdateReceiver, filter); } @Override protected void onDestroy() { super.onDestroy(); if (mUpdateReceiver != null) {//登出廣播 unregisterReceiver(mUpdateReceiver); } }

資料庫.png
下載完後,安裝正常,開啟正常,下載OK

掘金.png
後記:捷文規範
1.本文成長記錄及勘誤表
專案原始碼 | 日期 | 備註 |
---|---|---|
V0.1--無 | 2018-11-12 | Android原生下載(上篇)基本邏輯+斷點續傳 |
2.更多關於我
筆名 | 微信 | 愛好 | |
---|---|---|---|
張風捷特烈 | 1981462002 | zdl1994328 | 語言 |
我的github | 我的簡書 | 我的CSDN | 個人網站 |
3.宣告
1----本文由張風捷特烈原創,轉載請註明
2----歡迎廣大程式設計愛好者共同交流
3----個人能力有限,如有不正之處歡迎大家批評指證,必定虛心改正
4----看到這裡,我在此感謝你的喜歡與支援