Android 實現簡易下載管理器 (暫停、斷點續傳、多線程下載)

什麼都先別說,先看預覽圖!html

預覽圖中是限制了同時最大下載數爲 2 的.android

其實下載管理器的實現是挺簡單的,咱們須要弄清楚幾點就好了數據庫

1.全部任務的Bean應該存在哪裏,用什麼存? 
2.如何判斷任務是否已存在? 
3.如何判斷任務是新的任務或是從等待中恢復的任務? 
4.應該如何把下載列表傳遞給Adapter? 
5.如何將下載的進度傳遞出去? 
6.如何有效率地刷新顯示的列表? (ListView 或 RecycleView)緩存

服務基礎
首先咱們須要明確一點,下載咱們應該使用服務來進行,這樣咱們才能進行後臺下載。 
因此咱們就開始建立咱們的Service:服務器

public class OCDownloadService extends Service{ide

    ... ...ui

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        //當服務被Bind的時候咱們就返回一個帶有服務對象的類給Bind服務的Activity
        return new GetServiceClass();
    }this

    /**
     * 傳遞服務對象的類
     */
    public class GetServiceClass extends Binder{url

        public OCDownloadService getService(){
            return OCDownloadService.this;
        }.net

    }
    ... ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
而後咱們在AndroidManifest.xml裏面註冊一下:

<service android:name=".OCDownloader.OCDownloadService"/>
1
下載請求的檢查與處理
而後咱們就開始進入正題 ! 
首先第一點,咱們使用HashMap來看成儲存下載任務信息的總表,這樣的好處是咱們能夠在查找任務的時候經過 Key 來查詢,而不須要經過遍歷 List 的方法來獲取任務信息。並且咱們傳遞的時候能夠直接使用它的一份Copy就好了,不須要把本身傳出去。

下面咱們來看代碼:

(關於Service的生命週期啥的我就再也不重複說了。我這裏使用的是本地廣播來傳輸下載信息的更新。剩下的在代碼註釋中有詳細的解釋)

public class OCDownloadService extends Service{

    static final int MAX_DOWNLOADING_TASK = 2; //最大同時下載數
    private LocalBroadcastManager broadcastManager;
    private HashMap<String,DLBean> allTaskList;
    private OCThreadExecutor threadExecutor;

    private boolean keepAlive = false;
    private int runningThread = 0;

    @Override
    public void onCreate() {
        super.onCreate();

        //建立任務線程池
        if (threadExecutor == null){
            threadExecutor = new OCThreadExecutor(MAX_DOWNLOADING_TASK,"downloading");
        }

        //建立總表對象
        if (allTaskList == null){
            allTaskList = new HashMap<>();
        }

        //建立本地廣播器
        if (broadcastManager == null){
            broadcastManager = LocalBroadcastManager.getInstance(this);
        }
    }

    /**
     * 下載的請求就是從這裏傳進來的,咱們在這裏進行下載任務的前期處理
     */
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {

        //檢測傳過來的請求是否完整。咱們只須要 下載網址、文件名、下載路徑 便可。
        if (intent != null && intent.getAction() != null && intent.getAction().equals("NewTask")){
            String url = intent.getExtras().getString("url");
            String title = intent.getExtras().getString("title");
            String path = intent.getExtras().getString("path");

            //檢測獲得的數據是否有效
            if (TextUtils.isEmpty(url) || TextUtils.isEmpty(title) || TextUtils.isEmpty(path)){
                Toast.makeText(OCDownloadService.this,"Invail data",Toast.LENGTH_SHORT).show();
                return super.onStartCommand(intent, flags, startId);
            }else {

                //若是有效則執行檢查步驟
                checkTask(new DLBean(title,url,path));
            }
        }
        return super.onStartCommand(intent, flags, startId);
    }

    /**
     * 檢查新的下載任務
     * @param requestBean   下載對象的信息Bean
     */
    private synchronized void checkTask(@Nullable DLBean requestBean){
        if (requestBean != null){

            //先檢查是否存在同名的文件
            if (new File(requestBean.getPath()+"/"+requestBean.getTitle()).exists()){
                Toast.makeText(OCDownloadService.this,"File is already downloaded",Toast.LENGTH_SHORT).show();
            }else {

                //再檢查是否在總表中
                if (allTaskList.containsKey(requestBean.getUrl())){
                    DLBean bean = allTaskList.get(requestBean.getUrl());
                    //檢測當前的狀態
                    //若是是 暫停 或 失敗 狀態的則看成新任務開始下載
                    switch (bean.getStatus()){
                        case DOWNLOADING:
                            Toast.makeText(OCDownloadService.this,"Task is downloading",Toast.LENGTH_SHORT).show();
                            return;
                        case WAITTING:
                            Toast.makeText(OCDownloadService.this,"Task is in the queue",Toast.LENGTH_SHORT).show();
                            return;
                        case PAUSED:
                        case FAILED:
                            requestBean.setStatus(OCDownloadStatus.WAITTING);
                            startTask(requestBean);
                            break;
                    }
                }else {
                    //若是不存在,則添加到總表
                    requestBean.setStatus(OCDownloadStatus.WAITTING);
                    allTaskList.put(requestBean.getUrl(),requestBean);
                    startTask(requestBean);
                }

            }

        }

    }

    /**
     * 將任務添加到下載隊列中
     * @param requestBean   下載對象的信息Bean
     */
    private void startTask(DLBean requestBean){
        if (runningThread < MAX_DOWNLOADING_TASK){
            //若是當前還有空閒的位置則直接下載 , 不然就是在等待中
            requestBean.setStatus(OCDownloadStatus.DOWNLOADING);
            runningThread += 1;
            threadExecutor.submit(new FutureTask<>(new DownloadThread(requestBean)),requestBean.getUrl());
        }
        updateList();
    }

    /**
     * 獲得一份總表的 ArrayList 的拷貝
     * @return  總表的拷貝
     */
    public ArrayList<DLBean> getTaskList(){
        return new ArrayList<>(allTaskList.values());
    }

    /**
     * 更新整個下載列表
     */
    private void updateList(){
        //咱們等下再說這裏
        ... ...
    }

    /**
     * 更新當前項目的進度
     * @param totalSize 下載文件的總大小
     * @param downloadedSize    當前下載的進度
     */
    private void updateItem(DLBean bean , long totalSize, long downloadedSize){
        //咱們等下再說這裏
        ... ...
    }

    /**
     * 執行的下載任務的Task
     */
    private class DownloadThread implements Callable<String>{
        //咱們等下再說這裏
        ... ...
    }

}
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
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
在你們看了一遍以後我再解釋一遍流程:

1.收到新的任務請求 
2.判斷任務的信息是否完整 
3.檢查任務是否存在於總表,並檢查狀態 
4.若是任務不存在總表中 或 任務以前是暫停、失敗狀態則看成新任務,不然提示任務已存在 
5.若是當前已是最大下載數,則任務標記爲等待,不執行;不然開始下載

下載線程的實現
下面咱們來看是如何下載的,這就會講到斷點續傳的問題了,首先這個斷點續傳的功能得服務器支持才能夠。而後咱們在下載的時候生成一個臨時文件,在下載完成以前咱們將這個任務的全部數據存入這個文件中,直到下載完成,咱們纔將名字更改回正式的。網上有人將數據存入數據庫中,我以爲這種方式雖然避免了臨時文件的產生,可是這效率就…………

    /**
     * 執行的下載任務方法
     */
    private class DownloadThread implements Callable<String>{

        private DLBean bean;
        private File downloadFile;
        private String fileSize = null;

        public DownloadThread(DLBean bean) {
            this.bean = bean;
        }

        @Override
        public String call() throws Exception {

            //先檢查是否有以前的臨時文件
            downloadFile = new File(bean.getPath()+"/"+bean.getTitle()+".octmp");
            if (downloadFile.exists()){
                fileSize = "bytes=" + downloadFile.length() + "-";
            }

            //建立 OkHttp 對象相關
            OkHttpClient client = new OkHttpClient();

            //若是有臨時文件,則在下載的頭中添加下載區域
            Request request;
            if ( !TextUtils.isEmpty(fileSize) ){
                request = new Request.Builder().url(bean.getUrl()).header("Range",fileSize).build();
            }else {
                request = new Request.Builder().url(bean.getUrl()).build();
            }
            Call call = client.newCall(request);
            try {
                bytes2File(call);
            } catch (IOException e) {
                Log.e("OCException",""+e);
                if (e.getMessage().contains("interrupted")){
                    Log.e("OCException","Download task: "+bean.getUrl()+" Canceled");
                    downloadPaused();
                }else {
                    downloadFailed();
                }
                return null;
            }
            downloadCompleted();
            return null;
        }

        /**
         * 當產生下載進度時
         * @param downloadedSize    當前下載的數據大小
         */
        public void onDownload(long downloadedSize) {
            bean.setDownloadedSize(downloadedSize);
            Log.d("下載進度", "名字:"+bean.getTitle()+"  總長:"+bean.getTotalSize()+"  已下載:"+bean.getDownloadedSize() );
            updateItem(bean, bean.getTotalSize(), downloadedSize);
        }

        /**
         * 下載完成後的操做
         */
        private void downloadCompleted(){
            //當前下載數減一
            runningThread -= 1;
            //將臨時文件名更改回正式文件名
            downloadFile.renameTo(new File(bean.getPath()+"/"+bean.getTitle()));
            //從總表中移除這項下載信息
            allTaskList.remove(bean.getUrl());
            //更新列表
            updateList();
            if (allTaskList.size() > 0){
                //執行剩餘的等待任務
                checkTask(startNextTask());
            }
            threadExecutor.removeTag(bean.getUrl());
        }

        /**
         * 下載失敗後的操做
         */
        private void downloadFailed(){
            runningThread -= 1;
            bean.setStatus(OCDownloadStatus.FAILED);
            if (allTaskList.size() > 0){
                //執行剩餘的等待任務
                checkTask(startNextTask());
            }
            updateList();
            threadExecutor.removeTag(bean.getUrl());
        }

        /**
         * 下載暫停後的操做
         */
        private void downloadPaused(){
            runningThread -= 1;
            bean.setStatus(OCDownloadStatus.PAUSED);
            if (allTaskList.size() > 0){
                //執行剩餘的等待任務
                checkTask(startNextTask());
            }
            updateList();
            threadExecutor.removeTag(bean.getUrl());
        }

        /**
         * 查找一個等待中的任務
         * @return  查找到的任務信息Bean , 沒有則返回 Null
         */
        private DLBean startNextTask(){
            for (DLBean dlBean : allTaskList.values()) {
                if (dlBean.getStatus() == OCDownloadStatus.WAITTING) {
                    //在找到等待中的任務以後,咱們先把它的狀態設置成 暫停 ,再進行建立
                    dlBean.setStatus(OCDownloadStatus.PAUSED);
                    return dlBean;
                }
            }
            return null;
        }

        /**
         * 將下載的數據存到本地文件
         * @param call  OkHttp的Call對象
         * @throws IOException  下載的異常
         */
        private void bytes2File(Call call) throws IOException{

            //設置輸出流. 
            OutputStream outPutStream;

            //檢測是否支持斷點續傳
            Response response = call.execute();
            ResponseBody responseBody = response.body();
            String responeRange = response.headers().get("Content-Range");
            if (responeRange == null || !responeRange.contains(Long.toString(downloadFile.length()))){

                //最後的標記爲 true 表示下載的數據能夠從上一次的位置寫入,不然會清空文件數據.
                outPutStream = new FileOutputStream(downloadFile,false);
            }else {
                outPutStream = new FileOutputStream(downloadFile,true);
            }

            InputStream inputStream = responseBody.byteStream();

            //若是有下載過的歷史文件,則把下載總大小設爲 總數據大小+文件大小 . 不然就是總數據大小
            if ( TextUtils.isEmpty(fileSize) ){
                bean.setTotalSize(responseBody.contentLength());
            }else {
                bean.setTotalSize(responseBody.contentLength() + downloadFile.length());
            }

            int length;
            //設置緩存大小
            byte[] buffer = new byte[1024];

            //開始寫入文件
            while ((length = inputStream.read(buffer)) != -1){
                outPutStream.write(buffer,0,length);
                onDownload(downloadFile.length());
            }

            //清空緩衝區
            outPutStream.flush();
            outPutStream.close();
            inputStream.close();
        }

    }
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
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
代碼實現的步驟:

1.檢測是否存在本地文件並由此設置請求頭內的請求長度範圍 
2.訪問網址並獲取到返回的頭,檢測是否支持斷點續傳,由此設置是否從新開始寫入數據 
3.獲取輸入流,開始寫入數據 
4.若是拋出了異常,而且異常不爲中斷,則爲下載失敗,不然不做響應 
5.下載失敗、下載完成,都會自動尋找仍在隊列中的等待任務進行下載

廣播更新消息
在Service這裏面咱們什麼都不用管,就是把數據廣播出去就好了

    /**
     * 更新整個下載列表
     */
    private void updateList(){
        broadcastManager.sendBroadcast(new Intent("update_all"));
    }

    /**
     * 更新當前項目的進度
     * @param totalSize 下載文件的總大小
     * @param downloadedSize    當前下載的進度
     */
    private void updateItem(DLBean bean , long totalSize, long downloadedSize){
        int progressBarLength = (int) (((float)  downloadedSize / totalSize) * 100);
        Intent intent = new Intent("update_singel");
        intent.putExtra("progressBarLength",progressBarLength);
        intent.putExtra("downloadedSize",String.format("%.2f", downloadedSize/(1024.0*1024.0)));
        intent.putExtra("totalSize",String.format("%.2f", totalSize/(1024.0*1024.0)));
        intent.putExtra("item",bean);
        broadcastManager.sendBroadcast(intent);
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
下載管理Activity 實現
Service作好了以後,咱們接下來就是要作查看任務的Activity了! 
這個Activity用於展現下載任務、暫停繼續終止任務。

咱們先看整個Activity的基礎部分,咱們以後再說接收器部分的實現。RecyclerView的Adapter點擊事件回調 和 服務鏈接這類的我就再也不贅述了。這些都不是咱們關心的重點,須要注意的就是服務和廣播要注意解除綁定和解除註冊。

public class OCDownloadManagerActivity extends AppCompatActivity implements OCDownloadAdapter.OnRecycleViewClickCallBack{

    RecyclerView downloadList;
    OCDownloadAdapter downloadAdapter;
    OCDownloadService downloadService;
    LocalBroadcastManager broadcastManager;
    UpdateHandler updateHandler;
    ServiceConnection serviceConnection;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_download_manager);

        //RecycleView 的 Adapter 建立與點擊事件的綁定
        downloadAdapter = new OCDownloadAdapter();
        downloadAdapter.setRecycleViewClickCallBack(this);

        //RecyclerView 的建立與相關操做
        downloadList = (RecyclerView)findViewById(R.id.download_list);
        downloadList.setLayoutManager(new LinearLayoutManager(this,LinearLayoutManager.VERTICAL,false));
        downloadList.setHasFixedSize(true);
        downloadList.setAdapter(downloadAdapter);

        //廣播過濾器的建立
        IntentFilter intentFilter = new IntentFilter();
        intentFilter.addAction("update_all");       //更新整個列表的 Action
        intentFilter.addAction("update_singel");    //更新單獨條目的 Action

        //廣播接收器 與 本地廣播 的建立和註冊
        updateHandler = new UpdateHandler();
        broadcastManager = LocalBroadcastManager.getInstance(this);
        broadcastManager.registerReceiver(updateHandler,intentFilter);

        //建立服務鏈接
        serviceConnection = new ServiceConnection() {
            @Override
            public void onServiceConnected(ComponentName name, IBinder service) {
                //當服務鏈接上的時候
                downloadService = ((OCDownloadService.GetServiceClass)service).getService();
                downloadAdapter.updateAllItem(downloadService.getTaskList());
            }

            @Override
            public void onServiceDisconnected(ComponentName name) {
                //當服務斷開鏈接的時候
                if (broadcastManager != null && updateHandler != null){
                    broadcastManager.unregisterReceiver(updateHandler);
                }
            }
        };

        //鏈接服務並進行綁定
        startService(new Intent(this,OCDownloadService.class));
        bindService(new Intent(this,OCDownloadService.class),serviceConnection,BIND_AUTO_CREATE);    

    }

    /**
     * RecyclerView 的單擊事件
     * @param bean  點擊條目中的 下載信息Bean
     */
    @Override
    public void onRecycleViewClick(DLBean bean) {
        if (downloadService != null){
            downloadService.clickTask(bean.getUrl(),false);
        }
    }

    /**
     * RecyclerView 的長按事件
     * @param bean  點擊條目中的 下載信息Bean
     */
    @Override
    public void onRecycleViewLongClick(DLBean bean) {
        if (downloadService != null){
            downloadService.clickTask(bean.getUrl(),true);
        }
    }

    /**
     * 本地廣播接收器  負責更新UI
     */
    class UpdateHandler extends BroadcastReceiver{
        ... ...
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();

        //解綁接收器
        broadcastManager.unregisterReceiver(updateHandler);

        //解綁服務
        unbindService(serviceConnection);
    }    

}
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
87
88
89
90
91
92
93
94
95
96
97
98
99
廣播更新UI
接下來咱們來實現廣播接收器部分,也就是列表的刷新。

爲何要分開單獨更新與總體更新呢?由於在下載的過程當中的進度更新是很是很是頻繁的,若是咱們以這麼高的頻率來刷新UI,無疑會產生很大的負擔。若是列表中只有幾項的時候也許還行,但若是有1000+條的時候就很不容樂觀了 (1年前剛開始接觸這個東西的時候,是QQ中的一個好友@eprendre 告訴了我這個思路的。 若是各位dalao還有更好的方法麻煩在評論區留下您的看法)

    /**
     * 本地廣播接收器  負責更新UI
     */
    class UpdateHandler extends BroadcastReceiver{

        @Override
        public void onReceive(Context context, Intent intent) {
            switch (intent.getAction()){
                case "update_all":
                    //更新全部項目

                    downloadAdapter.updateAllItem(downloadService.getTaskList());
                    break;
                case "update_singel":
                    //僅僅更新當前項

                    DLBean bean = intent.getExtras().getParcelable("item");
                    String downloadedSize = intent.getExtras().getString("downloadedSize");
                    String totalSize = intent.getExtras().getString("totalSize");
                    int progressLength = intent.getExtras().getInt("progressBarLength");
                    //若是獲取到的 Bean 有效
                    if (bean != null){
                        View itemView = downloadList.getChildAt(downloadAdapter.getItemPosition(bean));
                        //若是獲得的View有效
                        if (itemView != null){
                            TextView textProgress = (TextView)itemView.findViewById(R.id.textView_download_length);
                            ProgressBar progressBar = (ProgressBar)itemView.findViewById(R.id.progressBar_download);

                            //更新文字進度
                            textProgress.setText(downloadedSize+"MB / "+totalSize+"MB");

                            //更新進度條進度
                            progressBar.setProgress(progressLength);
                            TextView status = (TextView)itemView.findViewById(R.id.textView_download_status);

                            //更新任務狀態
                            switch (bean.getStatus()){
                                case DOWNLOADING:
                                    status.setText("Downloading");
                                    break;
                                case WAITTING:
                                    status.setText("Waitting");
                                    break;
                                case FAILED:
                                    status.setText("Failed");
                                    break;
                                case PAUSED:
                                    status.setText("Paused");
                                    break;
                            }
                        }
                    }
                    break;
            }
        }

    }
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
這裏說一點就是 OKHttp 的下載進度監聽,我以前曾按照

http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0904/3416.html

相關文章
相關標籤/搜索