Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android 實現簡易下載管理器 (暫停、斷點續傳、多線程下載)

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

編輯:關於Android編程

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

這裡寫圖片描述

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

其實下載管理器的實現是挺簡單的,我們需要弄清楚幾點就行了<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxibG9ja3F1b3RlPg0KCTxwPjEuy/nT0MjOzvG1xEJlYW7TprjDtObU2sTEwO8s08PKssO0tOY/PGJyIC8+DQoJMi7I57rOxdC2z8jOzvHKx7fx0tG05tTaPzxiciAvPg0KCTMuyOe6zsXQts/Izs7xysfQwrXEyM7O8bvyyse007XItP3W0LvWuLS1xMjOzvE/PGJyIC8+DQoJNC7TprjDyOe6zrDRz8LU2MHQse20q7XduPhBZGFwdGVyPzxiciAvPg0KCTUuyOe6zr2rz8LU2LXEvfi2yLSrtd2z9silPzxiciAvPg0KCTYuyOe6ztPQ0KfCyrXYy6LQws/Uyr61xMHQse0/IChMaXN0VmlldyC78iBSZWN5Y2xlVmlldyk8L3A+DQo8L2Jsb2NrcXVvdGU+DQo8aDIgaWQ9"服務基礎">服務基礎

首先我們需要明確一點,下載我們應該使用服務來進行,這樣我們才能進行後台下載。
所以我們就開始創建我們的Service:

public class OCDownloadService extends Service{

    ... ...

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

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

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

    }
    ... ...
}

然後我們在AndroidManifest.xml裡面注冊一下:

下載請求的檢查與處理

然後我們就開始進入正題 !
首先第一點,我們使用HashMap來當作儲存下載任務信息的總表,這樣的好處是我們可以在查找任務的時候通過 Key 來查詢,而不需要通過遍歷 List 的方法來獲取任務信息。而且我們傳遞的時候可以直接使用它的一份Copy就行了,不需要把自己傳出去。

下面我們來看代碼:

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

public class OCDownloadService extends Service{

    static final int MAX_DOWNLOADING_TASK = 2; //最大同時下載數
    private LocalBroadcastManager broadcastManager;
    private HashMap 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 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{
        //我們等下再說這裡
        ... ...
    }

}

在大家看了一遍之後我再解釋一遍流程:

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

下載線程的實現

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

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

        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.下載失敗、下載完成,都會自動尋找仍在隊列中的等待任務進行下載

廣播更新消息

在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);
    }

下載管理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);
    }    

}

廣播更新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. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved