Android中實現單線程下載文件是比較容易的,可是要使得自己的應用支持多線程斷點下載就要考慮到很多細節了,今天我們一起來探討一下多線程斷點下載時怎麼實現的。
首先先畫一張圖說明一下Android中下載文件的大致流程:
上面的圖介紹的是比較清楚的,我們要下載一個文件,首先需要在Activity中選擇需要下載的目標,然後把下載的任務交個Service中(這裏爲什麼要交給Service,相信很多人都知道,我們在Activity中執行下載也是可以的,可是Activity是很容易讓用戶銷燬的,如果我們退出了Activity,可是下載線程還在執行,那麼就會導致退出的Activity無法被回收,也就導致了內存泄露,所以我們會把這種比較耗時的請求交給Service後臺任務就執行,因爲Service不是那麼容易被銷燬的),然後在Service中創建下載線程讀取網絡文件,並把文件存放在本地文件中,並且可以在下載的過程中通過發送廣播的方式通知Activity當前下載的進度。
上面的圖介紹了單線程下載的流程,理解了上面的圖之後,多線程斷點下載也就很容易理解了。改爲多線程下載只需要我們修改讀取網絡文件那部分,將網絡文件分成好幾個段,人後創建多個線程分別讀取各段的數據,然後在本地進行組裝就實現了多線成下載。例如網絡文件有100字節,我們分爲三個線程進行下載:
我們首先計算出每個線程所需要下載的字節起始終止位置,就可以從網絡上進行讀取,從而就是實現了多線程下載。
可是我們要怎樣使得我們的下載程序支持斷點下載呢?
其實原理也很簡單,我們在第一次下載時,先創建下載線程進行下載,當用戶點擊暫停下載時,我們只需要保存下載線程的信息(如:每個線程的下載起始位置、下載的進度信息等),當用戶再次點擊繼續下載時,我們只需要讀取上次的下載信息繼續接着上次的下載位置下載就可以了。
下面結合代碼簡單瞭解一下多線程斷點下載:
首先我們看一下Activity,Activity的佈局就不說了,我們在Activity中需要註冊一個廣播監聽,用於獲取下載進度並更新界面。
更新UI的廣播接收器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
BroadcastReceiver mReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (DownloadService2.ACTION_UPDATE.equals(intent.getAction())) { long finished = intent.getLongExtra( "finished" , 0 ); int id = intent.getIntExtra( "id" , 0 ); //更新相對應的進度條 listAdapter.updateProgress(id, finished); //progressBar.setProgress(finished); } else if (DownloadService2.ACTION_FINISHED.equals(intent.getAction())) { FileInfo fileinfo = (FileInfo) intent.getSerializableExtra( "fileinfo" ); //更新進度爲100 listAdapter.updateProgress(fileinfo.getId(), 100 ); Toast.makeText( MainActivity2. this , fileinfo.getFileName() + "下載完成" , Toast.LENGTH_SHORT).show(); } } }; |
註冊廣播:
1
2
3
4
5
6
7
|
private void initRegister() { //註冊廣播接收器 IntentFilter filter = new IntentFilter(); filter.addAction(DownloadService2.ACTION_UPDATE); filter.addAction(DownloadService2.ACTION_FINISHED); registerReceiver(mReceiver, filter); } |
我們點擊開始下載按鈕,會啓動下載文件的Service,Service或調用onStartCommand()方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
@Override public int onStartCommand(Intent intent, int flags, int startId) { //獲得Activity傳來的參數 if (ACTION_START.equals(intent.getAction())) { FileInfo fileInfo = (FileInfo) intent.getSerializableExtra( "fileinfo" ); Log.e(TAG, "onStartCommand: ACTION_START-" + fileInfo.toString()); new InitThread(fileInfo).start(); } else if (ACTION_PAUSE.equals(intent.getAction())) { FileInfo fileInfo = (FileInfo) intent.getSerializableExtra( "fileinfo" ); Log.e(TAG, "onStartCommand:ACTION_PAUSE- " + fileInfo.toString()); //從集合在取出下載任務 DownloadTask2 task2 = tasks.get(fileInfo.getId()); if (task2 != null ) { //停止下載任務 task2.isPause = true ; } } return super .onStartCommand(intent, flags, startId); } |
ACTION_START //指開始下載
ACTION_PAUSE //指暫停下載
上面的FileInfo是保存下載進度的實體類,當開始下載時,先創建一個線程準備初始化的數據,比如獲取網絡文件大小、在本地創建相應保存網絡數據的文件等:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
|
/** * 初始化 子線程 */ class InitThread extends Thread { private FileInfo tFileInfo; public InitThread(FileInfo tFileInfo) { this .tFileInfo = tFileInfo; } @Override public void run() { HttpURLConnection conn ; RandomAccessFile raf ; try { //連接網絡文件 URL url = new URL(tFileInfo.getUrl()); conn = (HttpURLConnection) url.openConnection(); conn.setConnectTimeout( 3000 ); conn.setRequestMethod( "GET" ); int length = - 1 ; Log.e( "getResponseCode==" , conn.getResponseCode() + "" ); if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) { //獲取文件長度 length = conn.getContentLength(); Log.e( "length==" , length + "" ); } if (length < 0 ) { return ; } //創建下載文件的目錄 File dir = new File(DOWNLOAD_PATH); if (!dir.exists()) { if (!dir.mkdir()){ return ; } } //在本地創建文件 File file = new File(dir, tFileInfo.getFileName()); raf = new RandomAccessFile(file, "rwd" ); //設置本地文件長度 raf.setLength(length); tFileInfo.setLength(length); Log.e( "tFileInfo.getLength==" , tFileInfo.getLength() + "" ); //發送消息準備下載 mHandler.obtainMessage(MSG_INIT, tFileInfo).sendToTarget(); raf.close(); conn.disconnect(); } catch (Exception e) { e.printStackTrace(); } } } |
當初始化完成後會通過Handler向主線程發送一條消息,表示已經初始化完成,可以下載,Service中的Handler如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { case MSG_INIT: FileInfo fileinfo = (FileInfo) msg.obj; Log.e( "mHandler--fileinfo:" , fileinfo.toString()); //啓動下載任務 DownloadTask2 downloadTask2 = new DownloadTask2(DownloadService2. this , fileinfo, runThreadCount); downloadTask2.download(); //將下載任務添加到集合中 tasks.put(fileinfo.getId(), downloadTask2); break ; } } }; |
在Handler中開始啓動多線程開始下載任務,多線程下載主要封裝在DownloadTask2中,下面我們看一下這個類的實現:
DownloadTask2的下載方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
public void download() { //讀取數據庫的線程信息 List<ThreadInfo> threadInfos = mThreadDAO2.getThread(mFileInfo.getUrl()); Log.e( "threadsize==" , threadInfos.size() + "" ); //如果爲空,表示是第一次下載 if (threadInfos.size() == 0 ) { //獲得每個線程下載的長度 long length = mFileInfo.getLength() / mThreadCount; for ( int i = 0 ; i < mThreadCount; i++) { ThreadInfo threadInfo = new ThreadInfo(i, mFileInfo.getUrl(), length * i, (i + 1 ) * length - 1 , 0 ); if (i + 1 == mThreadCount) { threadInfo.setEnd(mFileInfo.getLength()); } //添加到線程信息集合中 threadInfos.add(threadInfo); //向數據庫插入線程信息 mThreadDAO2.insertThread(threadInfo); } } mThradList = new ArrayList<>(); //啓動多個線程進行下載 for (ThreadInfo thread : threadInfos) { DownloadThread2 downloadThread = new DownloadThread2(thread); // downloadThread.start(); //在線程池中執行下載線程 DownloadTask2.sExecutorService.execute(downloadThread); //添加線程到集合中 mThradList.add(downloadThread); } } |
創建多線程下載之前,首先獲取下載的URL,並從數據庫中個根據URL獲取響應下載信息,如果獲取爲空,表明是第一次下載,如果獲取的不爲空,則創建線程接着獲取的信息繼續下載。
下面是下載線程的實現:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
|
class DownloadThread2 extends Thread { private ThreadInfo threadInfo; public boolean isFinished = false ; public DownloadThread2(ThreadInfo threadInfo) { this .threadInfo = threadInfo; } @Override public void run() { //向數據庫插入線程信息 // Log.e("isExists==", mThreadDAO2.isExists(threadInfo.getUrl(), threadInfo.getId()) + ""); // if (!mThreadDAO2.isExists(threadInfo.getUrl(), threadInfo.getId())) { // mThreadDAO2.insertThread(threadInfo); // } HttpURLConnection connection; RandomAccessFile raf; InputStream is; try { URL url = new URL(threadInfo.getUrl()); connection = (HttpURLConnection) url.openConnection(); connection.setConnectTimeout( 3000 ); connection.setRequestMethod( "GET" ); //設置下載位置 long start = threadInfo.getStart() + threadInfo.getFinish(); connection.setRequestProperty( "Range" , "bytes=" + start + "-" + threadInfo.getEnd()); //設置文件寫入位置 File file = new File(DownloadService2.DOWNLOAD_PATH, mFileInfo.getFileName()); raf = new RandomAccessFile(file, "rwd" ); raf.seek(start); Intent intent = new Intent(DownloadService2.ACTION_UPDATE); mFinished += threadInfo.getFinish(); Log.e( "threadInfo.getFinish==" , threadInfo.getFinish() + "" ); // Log.e("getResponseCode ===", connection.getResponseCode() + ""); //開始下載 if (connection.getResponseCode() == HttpURLConnection.HTTP_PARTIAL) { Log.e( "getContentLength==" , connection.getContentLength() + "" ); //讀取數據 is = connection.getInputStream(); byte [] buffer = new byte [ 1024 * 4 ]; int len = - 1 ; long time = System.currentTimeMillis(); while ((len = is.read(buffer)) != - 1 ) { if (isPause) { Log.e( "mfinished==pause===" , mFinished + "" ); //下載暫停時,保存進度到數據庫 mThreadDAO2.updateThread(mFileInfo.getUrl(), mFileInfo.getId(), threadInfo.getFinish()); return ; } //寫入文件 raf.write(buffer, 0 , len); //累加整個文件下載進度 mFinished += len; //累加每個線程完成的進度 threadInfo.setFinish(threadInfo.getFinish() + len); //每隔1秒刷新UI if (System.currentTimeMillis() - time > 1000 ) { //減少UI負載 time = System.currentTimeMillis(); //把下載進度發送廣播給Activity intent.putExtra( "id" , mFileInfo.getId()); intent.putExtra( "finished" , mFinished * 100 / mFileInfo.getLength()); mContext.sendBroadcast(intent); Log.e( " mFinished==update==" , mFinished * 100 / mFileInfo.getLength() + "" ); } } //標識線程執行完畢 isFinished = true ; //檢查下載任務是否完成 checkAllThreadFinished(); // //刪除線程信息 // mThreadDAO2.deleteThread(mFileInfo.getUrl(), mFileInfo.getId()); is.close(); } raf.close(); connection.disconnect(); } catch (Exception e) { e.printStackTrace(); } } } |
在下載線程中一直循環讀取網絡數據,每次循環檢查下載是否完成,如果下載完成刪除數據庫的下載信息:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
/** * 判斷所有線程是否都執行完畢 */ private synchronized void checkAllThreadFinished() { boolean allFinished = true ; //編輯線程集合 判斷是否執行完畢 for (DownloadThread2 thread : mThradList) { if (!thread.isFinished) { allFinished = false ; break ; } } if (allFinished) { //刪除線程信息 mThreadDAO2.deleteThread(mFileInfo.getUrl()); //發送廣播給Activity下載結束 Intent intent = new Intent(DownloadService2.ACTION_FINISHED); intent.putExtra( "fileinfo" , mFileInfo); mContext.sendBroadcast(intent); } } |
在下載線程中有這樣一個判斷:
1
2
3
4
5
6
|
if (isPause) { Log.e( "mfinished==pause===" , mFinished + "" ); //下載暫停時,保存進度到數據庫 mThreadDAO2.updateThread(mFileInfo.getUrl(), mFileInfo.getId(), threadInfo.getFinish()); return ; } |
isPause是下載是否暫停的標記,當用戶點擊暫停時,會把isPause賦值爲true,下載線程在下載的過程中發現isPause被賦值爲true後,就保存當前下載的具體信息,然後結束下載線程。
上面用到了數據庫的操作,用於保存下載進度信息,這裏就不具體介紹了,下面給出源碼,大家可以看看
http://download.csdn.net/download/lxk_1993/9511182
轉載請註明:Android開發中文站 » Android_Service多線程斷點下載