更好的Android多線程下載框架

/**
 * 作者:Pich
 * 原文鏈接:http://me.woblog.cn/
 * QQ群:129961195
 * Github:https://github.com/lifengsofts
 */

概述

為什么是更好的Android多線程下載框架呢,原因你懂的,廣告法嘛!

本篇我們我們就來聊聊多線程下載框架,先聊聊我們框架的特點:

  1. 多線程
  2. 多任務
  3. 斷點續傳
  4. 支持大文件
  5. 可以自定義下載數據庫
  6. 高度可配置,像超時時間這類
  7. 業務數據和下載數據分離

下面我們在說下該框架能實現那些的應用場景:

  1. 該框架可以很方便的下載單個文件,并且顯示各種狀態,包括開始下載,下載中,下載失敗,刪除等狀態。
  2. 也可以實現常見的需要下載功能應用,比如:某某手機助手,在該應用內可以說是下載是核心功能,所以對框架的穩定性,代碼可靠性,框架擴展性依賴很大,所以該框架真是從這種出發點而生的。通常這類應用的表示形式分三個頁面需要用到下載功能,一個列表用來顯示來自業務數據的列表,在該列表右邊可以點擊單個條目,或者多選實現下載,點擊每個條目進入詳情,同時還有個一個下載管理,包括大概兩個界面,正在下載,下載完成的,在這幾個界面都需要一個核心的功能就是都可以暫停,恢復,刪除并且能顯示下載進度。在列表一個最重要的問題就是界面刷新,如果每次更新都刷新整個列表,那么這將是異常災難,而我們這個框架正好解決了該問題,采用了回調單個條目并更新該條目的進度和狀態。

該項目狀態

該項目的雛形始于14年的公司項目需要用到多線程下載,但當時實現的單線程多任務斷點續傳,后面不斷完善,在這之間遇到過很多坑,也對一個下載框架有了更深的認識,所以在16年又重寫了該框架。

項目的Github地址:https://github.com/lifengsofts/AndroidDownloader

項目的官網地址:http://i.woblog.cn/AndroidDownloader

項目還處于發展狀態,但已經趨于穩定,并且有一定的編碼規范,同時采用了多個開源項目的質量控制方案以保證每次代碼提交的可靠性。

下面上幾張框架Demo的截圖,這樣用戶在心中有一個自己的概念,但是推薦各位還是講Demo下載到本地親自,運行一下。

截圖

AndroidDownloader Sample Screenshots
AndroidDownloader Sample Screenshots

AndroidDownloader Sample Screenshots
AndroidDownloader Sample Screenshots
AndroidDownloader Sample Screenshots
AndroidDownloader Sample Screenshots

AndroidDownloader Sample Screenshots
AndroidDownloader Sample Screenshots

第一個界面是單獨下載一個文件。

第二個界面是應用中最常用的一個界面,該界面來自業務數據。

第三個頁面是離線管理中的下載中的界面。

第四個頁面是離線管理中的下載完成的界面。

可以看到他們在每個界面都能暫停下載,繼續下載,以及刪除,并且都能拿到進度,狀態等信息。

下面就來看看這么強大的下載框架那該如何來使用呢?

添加權限

我相信這一步任何一個項目都已經添加了,但是還是不得不提一下。

該框架需要網絡訪問權限,如果你是講文件下載到存儲卡,那相應的需要添加存儲卡訪問權限。

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

配置Service

因為該框架采用才service中下載一個文件的。這樣做的目的是下載任務一般都需要在后頭下載,如果在Activity中來做這類任務,我想任何一個新手都知道這樣不行。

<service android:name="cn.woblog.android.downloader.DownloadService">
  <intent-filter>
    <action android:name="cn.woblog.android.downloader.DOWNLOAD_SERVICE" />
  </intent-filter>
</service>

添加依賴

我們提供了多種集成方式,比如:gradle,maven,jar。選擇適合你自己的就行了。

Gradle

在module目錄下面的build.gradle文件中添加如下內容:

compile 'cn.woblog.android:downloader:1.0.1'

Maven

或者你使用的Maven依賴管理工具。那道理其實是一樣的,在pom文件中添加:

<dependency>
  <groupId>cn.woblog.android</groupId>
  <artifactId>downloader</artifactId>
  <version>1.0.0</version>
</dependency>

或者你也可以參考該鏈接使用Snapshots版本

混淆配置

如果你的項目使用了混淆規則,那么一定要加上。

-keep public class * implements cn.woblog.android.downloader.db.DownloadDBController
-keep class cn.woblog.android.downloader.domain.** { *; }

創建下載管理器

現在萬事俱備只欠東風了,接下來只需要創建一個下載管理器,該框架所有的操作都是通過該來實現的:

downloadManager = DownloadService.getDownloadManager(context.getApplicationContext());

或者你可以使用更詳細的來配置該框架:

Config config = new Config();
//set database path.
//    config.setDatabaseName("/sdcard/a/d.db");
//      config.setDownloadDBController(dbController);

//set download quantity at the same time.
config.setDownloadThread(3);

//set each download info thread number
config.setEachDownloadThread(2);

// set connect timeout,unit millisecond
config.setConnectTimeout(10000);

// set read data timeout,unit millisecond
config.setReadTimeout(10000);
downloadManager = DownloadService.getDownloadManager(this.getApplicationContext(), config);

下載一個文件

//create download info set download uri and save path.
final DownloadInfo downloadInfo = new DownloadInfo.Builder().setUrl("http://example.com/a.apk")
    .setPath("/sdcard/a.apk")
    .build();

//set download callback.
downloadInfo.setDownloadListener(new DownloadListener() {

  @Override
  public void onStart() {
    tv_download_info.setText("Prepare downloading");
  }

  @Override
  public void onWaited() {
    tv_download_info.setText("Waiting");
    bt_download_button.setText("Pause");
  }

  @Override
  public void onPaused() {
    bt_download_button.setText("Continue");
    tv_download_info.setText("Paused");
  }

  @Override
  public void onDownloading(long progress, long size) {
    tv_download_info
        .setText(FileUtil.formatFileSize(progress) + "/" + FileUtil
            .formatFileSize(size));
    bt_download_button.setText("Pause");
  }

  @Override
  public void onRemoved() {
    bt_download_button.setText("Download");
    tv_download_info.setText("");
    downloadInfo = null;
  }

  @Override
  public void onDownloadSuccess() {
    bt_download_button.setText("Delete");
    tv_download_info.setText("Download success");
  }

  @Override
  public void onDownloadFailed(DownloadException e) {
    e.printStackTrace();
    tv_download_info.setText("Download fail:" + e.getMessage());
  }
});

//submit download info to download manager.
downloadManager.download(downloadInfo);

下載一個文件時直接創建一個DownloadInfo,然后設置下載鏈接和下載路徑。再添加一個監聽。就可以提交到下載框架了。

通過下載監聽器我們可以獲取到很多狀態。開始下載,等待中,暫停完成,下載中,刪除成功,下載成功,下載失敗等狀態。

在列表控件使用

我們這里演示如何在RecyclerView這類列表控件使用。當然如果你用的是ListView那道理是一樣的。

class ViewHolder extends RecyclerView.ViewHolder {

  private final ImageView iv_icon;
  private final TextView tv_size;
  private final TextView tv_status;
  private final ProgressBar pb;
  private final TextView tv_name;
  private final Button bt_action;
  private DownloadInfo downloadInfo;

  public ViewHolder(View view) {
    super(view);

    iv_icon = (ImageView) view.findViewById(R.id.iv_icon);
    tv_size = (TextView) view.findViewById(R.id.tv_size);
    tv_status = (TextView) view.findViewById(R.id.tv_status);
    pb = (ProgressBar) view.findViewById(R.id.pb);
    tv_name = (TextView) view.findViewById(R.id.tv_name);
    bt_action = (Button) view.findViewById(R.id.bt_action);
  }

  @SuppressWarnings("unchecked")
  public void bindData(final MyDownloadInfo data, int position, final Context context) {
    Glide.with(context).load(data.getIcon()).into(iv_icon);
    tv_name.setText(data.getName());

    // Get download task status
    downloadInfo = downloadManager.getDownloadById(data.getUrl().hashCode());

    // Set a download listener
    if (downloadInfo != null) {
      downloadInfo
          .setDownloadListener(new MyDownloadListener(new SoftReference(ViewHolder.this)) {
            //  Call interval about one second
            @Override
            public void onRefresh() {
              if (getUserTag() != null && getUserTag().get() != null) {
                ViewHolder viewHolder = (ViewHolder) getUserTag().get();
                viewHolder.refresh();
              }
            }
          });

    }

    refresh();

//      Download button
    bt_action.setOnClickListener(new OnClickListener() {
      @Override
      public void onClick(View v) {
        if (downloadInfo != null) {

          switch (downloadInfo.getStatus()) {
            case DownloadInfo.STATUS_NONE:
            case DownloadInfo.STATUS_PAUSED:
            case DownloadInfo.STATUS_ERROR:

              //resume downloadInfo
              downloadManager.resume(downloadInfo);
              break;

            case DownloadInfo.STATUS_DOWNLOADING:
            case DownloadInfo.STATUS_PREPARE_DOWNLOAD:
            case STATUS_WAIT:
              //pause downloadInfo
              downloadManager.pause(downloadInfo);
              break;
            case DownloadInfo.STATUS_COMPLETED:
              downloadManager.remove(downloadInfo);
              break;
          }
        } else {
//            Create new download task
          File d = new File(Environment.getExternalStorageDirectory().getAbsolutePath(), "d");
          if (!d.exists()) {
            d.mkdirs();
          }
          String path = d.getAbsolutePath().concat("/").concat(data.getName());
          downloadInfo = new Builder().setUrl(data.getUrl())
              .setPath(path)
              .build();
          downloadInfo
              .setDownloadListener(new MyDownloadListener(new SoftReference(ViewHolder.this)) {

                @Override
                public void onRefresh() {
                  if (getUserTag() != null && getUserTag().get() != null) {
                    ViewHolder viewHolder = (ViewHolder) getUserTag().get();
                    viewHolder.refresh();
                  }
                }
              });
          downloadManager.download(downloadInfo);
        }
      }
    });

  }

  private void refresh() {
    if (downloadInfo == null) {
      tv_size.setText("");
      pb.setProgress(0);
      bt_action.setText("Download");
      tv_status.setText("not downloadInfo");
    } else {
      switch (downloadInfo.getStatus()) {
        case DownloadInfo.STATUS_NONE:
          bt_action.setText("Download");
          tv_status.setText("not downloadInfo");
          break;
        case DownloadInfo.STATUS_PAUSED:
        case DownloadInfo.STATUS_ERROR:
          bt_action.setText("Continue");
          tv_status.setText("paused");
          try {
            pb.setProgress((int) (downloadInfo.getProgress() * 100.0 / downloadInfo.getSize()));
          } catch (Exception e) {
            e.printStackTrace();
          }
          tv_size.setText(FileUtil.formatFileSize(downloadInfo.getProgress()) + "/" + FileUtil
              .formatFileSize(downloadInfo.getSize()));
          break;

        case DownloadInfo.STATUS_DOWNLOADING:
        case DownloadInfo.STATUS_PREPARE_DOWNLOAD:
          bt_action.setText("Pause");
          try {
            pb.setProgress((int) (downloadInfo.getProgress() * 100.0 / downloadInfo.getSize()));
          } catch (Exception e) {
            e.printStackTrace();
          }
          tv_size.setText(FileUtil.formatFileSize(downloadInfo.getProgress()) + "/" + FileUtil
              .formatFileSize(downloadInfo.getSize()));
          tv_status.setText("downloading");
          break;
        case STATUS_COMPLETED:
          bt_action.setText("Delete");
          try {
            pb.setProgress((int) (downloadInfo.getProgress() * 100.0 / downloadInfo.getSize()));
          } catch (Exception e) {
            e.printStackTrace();
          }
          tv_size.setText(FileUtil.formatFileSize(downloadInfo.getProgress()) + "/" + FileUtil
              .formatFileSize(downloadInfo.getSize()));
          tv_status.setText("success");
          break;
        case STATUS_REMOVED:
          tv_size.setText("");
          pb.setProgress(0);
          bt_action.setText("Download");
          tv_status.setText("not downloadInfo");
        case STATUS_WAIT:
          tv_size.setText("");
          pb.setProgress(0);
          bt_action.setText("Pause");
          tv_status.setText("Waiting");
          break;
      }

    }
  }
}

關鍵代碼就是bindData方法中先通過業務的id,我們這里使用的url來獲取該業務數據是否有對應的下載任務。如果有,則從新綁定監聽器,也就是這段代碼

downloadInfo
  .setDownloadListener(new MyDownloadListener(new SoftReference(ViewHolder.this)) {
    //  Call interval about one second
    @Override
    public void onRefresh() {
      if (getUserTag() != null && getUserTag().get() != null) {
        ViewHolder viewHolder = (ViewHolder) getUserTag().get();
        viewHolder.refresh();
      }
    }
  });

其中要注意到的是緩存每個條目我們使用了SoftReference,這樣做的目的內容在吃緊的情況下而已及時的是否這些條目。

接下來又一個重要的點是,設置按鈕的點擊事件,通常在這樣的列表中有一個或多個按鈕控制下載狀態。

if (downloadInfo != null) {

  switch (downloadInfo.getStatus()) {
    case DownloadInfo.STATUS_NONE:
    case DownloadInfo.STATUS_PAUSED:
    case DownloadInfo.STATUS_ERROR:

      //resume downloadInfo
      downloadManager.resume(downloadInfo);
      break;

    case DownloadInfo.STATUS_DOWNLOADING:
    case DownloadInfo.STATUS_PREPARE_DOWNLOAD:
    case STATUS_WAIT:
      //pause downloadInfo
      downloadManager.pause(downloadInfo);
      break;
    case DownloadInfo.STATUS_COMPLETED:
      downloadManager.remove(downloadInfo);
      break;
  }
} else {
//            Create new download task
  File d = new File(Environment.getExternalStorageDirectory().getAbsolutePath(), "d");
  if (!d.exists()) {
    d.mkdirs();
  }
  String path = d.getAbsolutePath().concat("/").concat(data.getName());
  downloadInfo = new Builder().setUrl(data.getUrl())
      .setPath(path)
      .build();
  downloadInfo
      .setDownloadListener(new MyDownloadListener(new SoftReference(ViewHolder.this)) {

        @Override
        public void onRefresh() {
          if (getUserTag() != null && getUserTag().get() != null) {
            ViewHolder viewHolder = (ViewHolder) getUserTag().get();
            viewHolder.refresh();
          }
        }
      });
  downloadManager.download(downloadInfo);
}

關鍵點就是如果沒有下載任務就創建一個下載任務,如果已有下載任務就根據任務現在的狀態執行相應的操作,比如當前是沒有下載,點擊就是創建一個下載任務。

接下還有一個重點就是,我們在回調監聽中調用了refresh方法,在該方法中根據狀態顯示進度和相應的操作按鈕。這樣做的好處上面已經提到了。

private void refresh() {
  if (downloadInfo == null) {
    tv_size.setText("");
    pb.setProgress(0);
    bt_action.setText("Download");
    tv_status.setText("not downloadInfo");
  } else {
    switch (downloadInfo.getStatus()) {
      case DownloadInfo.STATUS_NONE:
        bt_action.setText("Download");
        tv_status.setText("not downloadInfo");
        break;
      case DownloadInfo.STATUS_PAUSED:
      case DownloadInfo.STATUS_ERROR:
        bt_action.setText("Continue");
        tv_status.setText("paused");
        try {
          pb.setProgress((int) (downloadInfo.getProgress() * 100.0 / downloadInfo.getSize()));
        } catch (Exception e) {
          e.printStackTrace();
        }
        tv_size.setText(FileUtil.formatFileSize(downloadInfo.getProgress()) + "/" + FileUtil
            .formatFileSize(downloadInfo.getSize()));
        break;

      case DownloadInfo.STATUS_DOWNLOADING:
      case DownloadInfo.STATUS_PREPARE_DOWNLOAD:
        bt_action.setText("Pause");
        try {
          pb.setProgress((int) (downloadInfo.getProgress() * 100.0 / downloadInfo.getSize()));
        } catch (Exception e) {
          e.printStackTrace();
        }
        tv_size.setText(FileUtil.formatFileSize(downloadInfo.getProgress()) + "/" + FileUtil
            .formatFileSize(downloadInfo.getSize()));
        tv_status.setText("downloading");
        break;
      case STATUS_COMPLETED:
        bt_action.setText("Delete");
        try {
          pb.setProgress((int) (downloadInfo.getProgress() * 100.0 / downloadInfo.getSize()));
        } catch (Exception e) {
          e.printStackTrace();
        }
        tv_size.setText(FileUtil.formatFileSize(downloadInfo.getProgress()) + "/" + FileUtil
            .formatFileSize(downloadInfo.getSize()));
        tv_status.setText("success");
        break;
      case STATUS_REMOVED:
        tv_size.setText("");
        pb.setProgress(0);
        bt_action.setText("Download");
        tv_status.setText("not downloadInfo");
      case STATUS_WAIT:
        tv_size.setText("");
        pb.setProgress(0);
        bt_action.setText("Pause");
        tv_status.setText("Waiting");
        break;
    }

  }
}

到這里改下框架的核心使用方法就介紹完了。

支持

如有任何問題可以在加我們的QQ群或者在Github上提Issue,另外請提Issue或者的PR的一定要看下項目的貢獻代碼的方法以及一要求,因為如果要保證一個開源項目的質量就必須在各方面都規范化。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,076評論 25 708
  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,869評論 18 139
  • 發現 關注 消息 iOS 第三方庫、插件、知名博客總結 作者大灰狼的小綿羊哥哥關注 2017.06.26 09:4...
    肇東周閱讀 12,200評論 4 61
  • 猶豫了很久,還是選擇了這個題目,可能是年齡大了,近來越發的喜歡懷舊,父親去世距今已經十二年了,可每當想起往事,...
    素顏hb閱讀 230評論 0 0
  • “披星戴月地奔波,只為一扇窗,當你迷失在路上,能夠看見那燈光,不知不覺把他鄉,當做了故鄉,只是偶爾難過時,不經意遙...
    小賴E華姐姐閱讀 162評論 0 1