多線程下載是加快下載速度的一種方式,通過開啟多個線程去執行一個任務,可以使任務的執行速度變快。多線程的任務下載時常都會使用得到斷點續傳下載,就是我們在一次下載未結束時退出下載,第二次下載時會接着第一次下載的進度繼續下載。對於android中的下載,我想分多個部分去講解分析。今天,我們就首先開始android中下載斷點續傳代碼的實現。源碼下載:java多線程斷點續傳(一) 。關於多線程下載單個文件的實現,請參見博客:android程序---->android多線程下載(二)
目錄導航
android中斷點續傳的思路
一、 斷點續傳的實現步驟:
第一步: 我們要獲得下載資源的的長度,用http請求中HttpURLConnection的getContentLength()方法
第二步:在本地創建一個文件,設計其長度。File file = new File()
第三步:從數據庫中獲得上次下載的進度,當暫停下載時,存儲下載的狀態,用到數據庫的知識
第四步:從上次下載的位置下載數據,同時保存進度到數據庫:RandomAccessFile的seek方法與HttpURLConnection的setRequestProperty方法
第五步:將下載進度回傳到Activity,可以通過Intent將數據廣播到Activity中
第六步:下載完成后刪除下載信息,在數據庫中刪除相應的信息
二、 斷點續傳實現的流程圖:
android斷點續傳基本的UI編寫
明白了上述的實現流程,現在我們開始一個android項目,開始斷點續傳代碼的編寫,項目結構如下:
運行的截圖如下:
一、 編寫基本的UI,三個TextView,分別顯示文件名、下載進度和下載速度,一個ProgressBar。二個Button,分別用於開始下載、暫停下載和取消下載。
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" tools:context="com.example.linux.continuedownload.MainActivity"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content"> <TextView android:id="@+id/textView" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <TextView android:layout_marginLeft="80dp" android:id="@+id/progress" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <TextView android:layout_marginLeft="80dp" android:id="@+id/speed" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </LinearLayout> <ProgressBar android:visibility="invisible" android:id="@+id/progressBar" style="?android:attr/progressBarStyleHorizontal" android:layout_width="match_parent" android:layout_height="wrap_content" /> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content"> <Button android:id="@+id/start" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="開始下載" /> <Button android:layout_marginLeft="20dp" android:id="@+id/stop" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="暫停下載" /> <Button android:layout_marginLeft="20dp" android:id="@+id/cancel" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="取消下載" /> </LinearLayout> </LinearLayout>
二、 在MainActivity中初始化一些組件,綁定按鈕的事件:
在onCreate方法中初始化一些組件:
// 初始化組件 textView = (TextView) findViewById(R.id.textView); progressView = (TextView) findViewById(R.id.progress); speedView = (TextView) findViewById(R.id.speed); progressBar = (ProgressBar) findViewById(R.id.progressBar); progressBar.setMax(100); startButton = (Button) findViewById(R.id.start); stopButton = (Button) findViewById(R.id.stop); cancelButton = (Button) findViewById(R.id.cancel); // 創建一個文件信息對象 final FileInfo fileInfo = new FileInfo(0, fileUrl, "huhx.apk", 0, 0);
在onCreate方法中綁定開始下載按鈕事件:點擊start按鈕,設置進度條可見,並且設置start的Action,啟動服務。
startButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { textView.setText(fileInfo.getFileName()); progressBar.setVisibility(View.VISIBLE); // 通過Intent傳遞參數給service Intent intent = new Intent(MainActivity.this, DownloadService.class); intent.setAction(DownloadService.ACTION_START); intent.putExtra("fileInfo", fileInfo); startService(intent); } });
在onCreate方法中綁定暫停下載按鈕事件:點擊stop按鈕,設置stop的Action,啟動服務。
stopButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // 通過Intent傳遞參數給service Intent intent = new Intent(MainActivity.this, DownloadService.class); intent.setAction(DownloadService.ACTION_STOP); intent.putExtra("fileInfo", fileInfo); startService(intent); } });
在onCreate方法中綁定取消下載按鈕事件:點擊cancel按鈕,設置cancel的Action,啟動服務,之后更新UI。
cancelButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // 通過Intent傳遞參數給service Intent intent = new Intent(MainActivity.this, DownloadService.class); intent.setAction(DownloadService.ACTION_CANCEL); intent.putExtra("fileInfo", fileInfo); startService(intent); // 更新textView和progressBar的顯示UI textView.setText(""); progressBar.setVisibility(View.INVISIBLE); progressView.setText(""); speedView.setText(""); } });
注冊廣播,用於Service向Activity傳遞一些下載進度信息:
// 靜態注冊廣播 IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(DownloadService.ACTION_UPDATE); registerReceiver(broadcastReceiver, intentFilter); /** * 更新UI */ BroadcastReceiver broadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (DownloadService.ACTION_UPDATE.equals(intent.getAction())) { int finished = intent.getIntExtra("finished", 0); int speed = intent.getIntExtra("speed", 0); Log.i("Main", finished + ""); progressBar.setProgress(finished); progressView.setText(finished + "%"); speedView.setText(speed + "KB/s"); } } };
三、 在AndroidManifest.xm文件中聲明權限,定義服務
<service android:name="com.huhx.services.DownloadService" android:exported="true" /> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
android斷點續傳的工具類
二、 我們定義一些實體類,用於斷點續傳過程的信息的良好封裝:
下載文件信息: 省略了get和set方法,以及toString和構造方法
public class FileInfo implements Serializable{ // 文件Id,用於標識文件 private int fileId; // 文件的下載地址 private String url; // 文件的名稱 private String fileName; // 文件的長度,也就是大小 private int length; // 文件已經的下載量 private int finished; }
下載資源的線程信息:省略同上
public class ThreadInfo { // 線程ID private int threadId; // 下載資源的地址 private String url; //下載資源的開始處 private int start; //下載資源的結束處 private int end; //資源已經的下載量 private int finished; }
三、 我們開始數據庫方面的編寫,它用於存儲更新線程的下載的進度信息
首先我們要創建一個數據庫的工具類:
package com.huhx.util; import android.content.Context; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; /** * Created by huhx on 2016/4/9. */ public class SqliteDBHelper extends SQLiteOpenHelper { private static final String DB_NAME = "download.db"; private static final int version = 1; private static final String CREATE_THREADINFO = "create table thread_info(_id integer primary key autoincrement, " + "thread_id integer, url text, start integer, end integer, finished integer)"; private static final String DROP_THREADINFO = "drop table if exists thread_info"; public SqliteDBHelper(Context context) { super(context, DB_NAME, null, version); } @Override public void onCreate(SQLiteDatabase db) { db.execSQL(CREATE_THREADINFO); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { db.execSQL(DROP_THREADINFO); db.execSQL(CREATE_THREADINFO); } }
定義一個Dao接口,用於數據庫對線程信息的CRUD操作:
/** * Created by Linux on 2016/4/9. */ public interface ThreadDao { // 插入線程信息 public void insertThread(ThreadInfo threadInfo); // 刪除線程信息 public void deleteThread(String url, int threadId); // 刪除所有關於這個url的線程 public void deleteThread(String url); // 更新線程信息 public void updateThread(String url, int threadId, int finished); // 查詢線程信息 public List<ThreadInfo> queryThread(String url); // 線程信息是否存在 public boolean isThreadInfoExist(String url, int threadId); }
具體實現上述Dao的Impl類:
package com.huhx.util; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import com.huhx.model.ThreadInfo; import java.util.ArrayList; import java.util.List; /** * Created by huhx on 2016/4/9. */ public class ThreadDaoImpl implements ThreadDao { private SqliteDBHelper sqliteDBHelper; public ThreadDaoImpl(Context context) { sqliteDBHelper = new SqliteDBHelper(context); } @Override public void insertThread(ThreadInfo threadInfo) { SQLiteDatabase database = sqliteDBHelper.getWritableDatabase(); Object[] objects = new Object[]{ threadInfo.getThreadId(), threadInfo.getUrl(), threadInfo.getStart(), threadInfo.getEnd(), threadInfo.getFinished() }; database.execSQL("insert into thread_info(thread_id, url, start, end, finished) values(?,?,?,?,?)", objects); database.close(); } @Override public void deleteThread(String url, int threadId) { SQLiteDatabase database = sqliteDBHelper.getWritableDatabase(); Object[] objects = new Object[]{ url, threadId }; database.execSQL("delete from thread_info where url = ? and thread_id = ?", objects); database.close(); } @Override public void deleteThread(String url) { SQLiteDatabase database = sqliteDBHelper.getWritableDatabase(); Object[] objects = new Object[]{ url }; database.execSQL("delete from thread_info where url = ?", objects); database.close(); } @Override public void updateThread(String url, int threadId, int finished) { SQLiteDatabase database = sqliteDBHelper.getWritableDatabase(); Object[] objects = new Object[]{ finished, url, threadId }; database.execSQL("update thread_info set finished = ? where url = ? and thread_id = ?", objects); database.close(); } @Override public List<ThreadInfo> queryThread(String url) { SQLiteDatabase database = sqliteDBHelper.getWritableDatabase(); List<ThreadInfo> threadInfos = new ArrayList<>(); Cursor cursor = database.rawQuery("select * from thread_info where url = ?", new String[]{url}); while (cursor.moveToNext()) { ThreadInfo threadInfo = new ThreadInfo(); threadInfo.setThreadId(cursor.getInt(cursor.getColumnIndex("thread_id"))); threadInfo.setUrl(cursor.getString(cursor.getColumnIndex("url"))); threadInfo.setStart(cursor.getInt(cursor.getColumnIndex("start"))); threadInfo.setEnd(cursor.getInt(cursor.getColumnIndex("end"))); threadInfo.setFinished(cursor.getInt(cursor.getColumnIndex("finished"))); threadInfos.add(threadInfo); } cursor.close(); database.close(); return threadInfos; } @Override public boolean isThreadInfoExist(String url, int threadId) { SQLiteDatabase database = sqliteDBHelper.getWritableDatabase(); Cursor cursor = database.rawQuery("select * from thread_info where url = ? and thread_id = ?", new String[]{url, threadId+""}); boolean isExist = cursor.moveToNext(); cursor.close(); database.close(); return isExist; } }
下載暫停取消的具體流程
四、 最后我們開始最重要的Service以及核心的下載代碼的編寫,我們按照上述的開始、暫停、取消的順序,來講解斷點續傳的實現過程。
我們在DownloadService中onStartCommand方法中接收的Intent,關於Service的使用請參見:android基礎---->service的生命周期
@Override public int onStartCommand(Intent intent, int flags, int startId) { // 獲得Activity傳過來的參數 if (ACTION_START.equals(intent.getAction())) { FileInfo fileInfo = (FileInfo) intent.getSerializableExtra("fileInfo"); // 啟動初始化線程 new InitThread(fileInfo).start(); } else if (ACTION_STOP.equals(intent.getAction())) { FileInfo fileInfo = (FileInfo) intent.getSerializableExtra("fileInfo"); if (downloadTask != null) { downloadTask.isPause = true; } } else if (ACTION_CANCEL.equals(intent.getAction())) { FileInfo fileInfo = (FileInfo) intent.getSerializableExtra("fileInfo"); if (downloadTask != null) { downloadTask.isPause = true; } // 刪除本地文件 File file = new File(DOWNLOAD_PATH, fileInfo.getFileName()); if (file.exists()) { file.delete(); } handler.obtainMessage(DOWNLOAD_CANCEL, fileInfo).sendToTarget(); } return super.onStartCommand(intent, flags, startId); }
五、 文件的開始下載流程:
開始下載時,啟動一個初始化線程,並把文件信息傳遞給線程,該線程通過Http請求得到文件的長度,在本地創建下載文件的載體,設置大小並發送下載的消息給Handler:
/** * 初始化子線程 */ class InitThread extends Thread { private FileInfo fileInfo = null; public InitThread(FileInfo fileInfo) { this.fileInfo = fileInfo; } @Override public void run() { // 連接網絡文件 HttpURLConnection connection = null; RandomAccessFile randomAccessFile = null; try { URL url = new URL(fileInfo.getUrl()); connection = (HttpURLConnection) url.openConnection(); connection.setConnectTimeout(3000); connection.setRequestMethod("GET"); connection.connect(); int length = -1; if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) { // 獲取文件的長度 length = connection.getContentLength(); } if (length <= 0) { return; } // 在本地創建文件 File dir = new File(DOWNLOAD_PATH); if (dir.exists()) { dir.mkdir(); } File file = new File(dir, fileInfo.getFileName()); // 設置文件長度 randomAccessFile = new RandomAccessFile(file, "rwd"); randomAccessFile.setLength(length); fileInfo.setLength(length); handler.obtainMessage(DOWNLOAD_MESSAGE, fileInfo).sendToTarget(); } catch (Exception e) { e.printStackTrace(); } finally { try { randomAccessFile.close(); connection.disconnect(); } catch (IOException e) { e.printStackTrace(); } } } }
handler接收消息,並加以處理:注意這里有兩種消息,我們暫時只考慮DOWNLOAD_MESSAGE消息,它啟動下載任務
private Handler handler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { case DOWNLOAD_MESSAGE: FileInfo fileInfo = (FileInfo) msg.obj; // 啟動下載任務 downloadTask = new DownloadTask(DownloadService.this, fileInfo); downloadTask.download(); break; case DOWNLOAD_CANCEL: FileInfo fileCancelInfo = (FileInfo) msg.obj; downloadTask = new DownloadTask(DownloadService.this); downloadTask.cancelDownload(fileCancelInfo); break; } } };
在download方法中,首先判斷是否有線程下載過文件,如果沒有就創建一個。有的話,從數據庫直接得到。而且開啟了下載的任務線程
public void download() { // 讀取數據庫的線程信息 List<ThreadInfo> threadInfos = threadDao.queryThread(fileInfo.getUrl()); ThreadInfo threadInfo = null; if (threadInfos.size() == 0) { threadInfo = new ThreadInfo(0, fileInfo.getUrl(), 0, fileInfo.getLength(), 0); } else { threadInfo = threadInfos.get(0); } new DownloadThread(threadInfo).start(); }
在下載的線程中,通過Http請求數據並通過字節流的方式存儲在本地的文件中。間隔500毫秒,就發送一次更新UI的廣播。如果收到了暫停的信號,就暫停下載。在下載完成之后,刪除數據庫中的線程信息
class DownloadThread extends Thread { private ThreadInfo threadInfo = null; public DownloadThread(ThreadInfo threadInfo) { this.threadInfo = threadInfo; } @Override public void run() { // 向數據庫插入線程信息 if (!threadDao.isThreadInfoExist(threadInfo.getUrl(), threadInfo.getThreadId())) { threadDao.insertThread(threadInfo); } HttpURLConnection connection = null; RandomAccessFile randomAccessFile = null; InputStream inputStream = null; try { URL url = new URL(threadInfo.getUrl()); connection = (HttpURLConnection) url.openConnection(); connection.setConnectTimeout(5000); connection.setRequestMethod("GET"); int start = threadInfo.getStart() + threadInfo.getFinished(); connection.setRequestProperty("Range", "bytes=" + start + "-" + threadInfo.getEnd()); File file = new File(DownloadService.DOWNLOAD_PATH, fileInfo.getFileName()); randomAccessFile = new RandomAccessFile(file, "rwd"); randomAccessFile.seek(start); Intent intent = new Intent(DownloadService.ACTION_UPDATE); // 開始下載 finished += threadInfo.getFinished(); if (connection.getResponseCode() == HttpURLConnection.HTTP_PARTIAL) { inputStream = connection.getInputStream(); byte[] buffer = new byte[4 * 1024]; int len = -1; long time = System.currentTimeMillis(); long time1; while ((len = inputStream.read(buffer)) != -1) { randomAccessFile.write(buffer, 0, len); finished += len; if ((time1 = System.currentTimeMillis() - time) > 500) { time = System.currentTimeMillis(); intent.putExtra("finished", finished * 100 / fileInfo.getLength()); intent.putExtra("speed", (int) (len / time1)); context.sendBroadcast(intent); } if (isPause) { threadDao.updateThread(threadInfo.getUrl(), threadInfo.getThreadId(), finished); return; } } // 刪除線程信息,再次發送廣播避免上面的廣播延遲 intent.putExtra("finished", finished * 100 / fileInfo.getLength()); context.sendBroadcast(intent); threadDao.deleteThread(threadInfo.getUrl(), threadInfo.getThreadId()); Log.i("Main", "finished: " + finished + ", and file length: " + fileInfo.getLength()); } } catch (Exception e) { e.printStackTrace(); } finally { try { connection.disconnect(); randomAccessFile.close(); inputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } }
六、 文件的暫停下載流程:如果下載任務在啟動,那么設置isPause為true,在上述的講解中我們知道,此時字節流停止的傳輸。
if (downloadTask != null) { downloadTask.isPause = true; }
七、 文件的取消下載流程:
暫停下載的流程,然后刪除本地文件,最后發送取消下載的消息:
FileInfo fileInfo = (FileInfo) intent.getSerializableExtra("fileInfo"); if (downloadTask != null) { downloadTask.isPause = true; } // 刪除本地文件 File file = new File(DOWNLOAD_PATH, fileInfo.getFileName()); if (file.exists()) { file.delete(); } handler.obtainMessage(DOWNLOAD_CANCEL, fileInfo).sendToTarget();
handler處理取消下載的消息:調用DownloadTask的cancelDownload方法,並把文件信息傳入
case DOWNLOAD_CANCEL: FileInfo fileCancelInfo = (FileInfo) msg.obj; downloadTask = new DownloadTask(DownloadService.this); downloadTask.cancelDownload(fileCancelInfo); break;
在cancelDownload方法中刪除數據庫中的線程信息:
// 取消下載任務 public void cancelDownload(FileInfo fileInfo) { threadDao.deleteThread(fileInfo.getUrl()); }
最后在MainActivity中更新UI:
// 更新textView和progressBar的顯示UI textView.setText(""); progressBar.setVisibility(View.INVISIBLE); progressView.setText(""); speedView.setText("");
友情鏈接
- handler的原理 android高級---->Handler的原理
- service的生命周期 android基礎---->service的生命周期
- android中廣播的使用 android基礎---->Broadcast的使用
- android中數據庫的使用 android基礎---->SQLite數據庫的使用
- 測試的斷點續傳源碼下載
關於android中多線程的下載,請參見我的博客: android程序---->android多線程下載(二)