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

相关文章
相关标签/搜索