安卓下載任務(wù)管理

下載頁面UI設(shè)計參照 網(wǎng)易云音樂

下載功能

  • 多任務(wù)并行下載
  • 斷點續(xù)傳(需服務(wù)器支持)

項目地址:https://github.com/4ndroidev/DownloadManager.git

效果圖

image
image

實現(xiàn)原理

下載任務(wù)流程圖

image
image

由上圖可知,任務(wù)執(zhí)行流程大致如下

  1. 創(chuàng)建任務(wù),并做準(zhǔn)備,設(shè)置監(jiān)聽器等操作
  2. 根據(jù)任務(wù)創(chuàng)建實際下載工作,添加到任務(wù)隊列,等待或直接執(zhí)行
  3. 用戶操作,進行暫停,恢復(fù),或刪除

核心類分析

功能
DownloadTask 下載任務(wù),保存部分關(guān)鍵信息,非實際下載工作
DownloadInfo 下載信息,保存所有信息
DownloadJob 實現(xiàn)Runnable接口,實際下載工作,負責(zé)網(wǎng)絡(luò)請求,數(shù)據(jù)庫信息更新
DownloadManager 單例,創(chuàng)建下載任務(wù),提供獲取正在下載任務(wù),所有下載信息,設(shè)置監(jiān)聽器等接口
DownloadEngine 負責(zé)創(chuàng)建線程池,根據(jù)任務(wù)創(chuàng)建下載工作,調(diào)度工作及通知
DownloadProvider 負責(zé)下載信息數(shù)據(jù)庫增刪查改

類關(guān)聯(lián)關(guān)系

關(guān)聯(lián) 關(guān)系
DownloadTask - DownloadInfo n - 1
DownloadTask - DownloadJob n - 0...1
DownloadJob - DownloadInfo 1 - 1

下載工作

斷點續(xù)傳的關(guān)鍵點:

  • 使用Range這個Header來指定開始下載位置
  • 文件讀寫則使用RandomAccessFile,可在指定偏移量讀寫文件
  • 注意RandomAccessFile打開模式不要加入s,同步模式會拖慢下載速度
package com.grocery.download.library;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;

import static com.grocery.download.library.DownloadState.STATE_FAILED;
import static com.grocery.download.library.DownloadState.STATE_FINISHED;
import static com.grocery.download.library.DownloadState.STATE_PAUSED;
import static com.grocery.download.library.DownloadState.STATE_RUNNING;
import static com.grocery.download.library.DownloadState.STATE_WAITING;

/**
 * Created by 4ndroidev on 16/10/6.
 */

// one-to-one association with DownloadInfo
public class DownloadJob implements Runnable {

  private boolean isPaused;
  private DownloadInfo info;
  private DownloadEngine engine;
  private List<DownloadListener> listeners;

  private Runnable changeState = new Runnable() {
    @Override
    public void run() {
      synchronized (DownloadJob.class) {
        for (DownloadListener listener : listeners) {
          listener.onStateChanged(info.key, DownloadJob.this.info.state);
        }
        switch (info.state) {
          case STATE_RUNNING:
            engine.onJobStarted(info);
            break;
          case STATE_FINISHED:
            engine.onJobCompleted(true, info);
            clear();
            break;
          case STATE_FAILED:
          case STATE_PAUSED:
            engine.onJobCompleted(false, info);
            break;
        }
      }
    }
  };

  private Runnable changeProgress = new Runnable() {
    @Override
    public void run() {
      synchronized (DownloadJob.class) {
        for (DownloadListener listener : listeners) {
          listener.onProgressChanged(info.key, DownloadJob.this.info.finishedLength, DownloadJob.this.info.contentLength);
        }
      }
    }
  };

  public DownloadJob(DownloadEngine engine, DownloadInfo info) {
    this.engine = engine;
    this.info = info;
    this.listeners = new ArrayList<>();
  }

  DownloadInfo getInfo() {
    return info;
  }

  void addListener(DownloadListener listener) {
    synchronized (DownloadJob.class) {
      if (listener == null || listeners.contains(listener)) return;
      listener.onStateChanged(info.key, info.state);
      listeners.add(listener);
    }
  }

  void removeListener(DownloadListener listener) {
    synchronized (DownloadJob.class) {
        if (listener == null || !listeners.contains(listener)) return;
        listeners.remove(listener);
    }
  }

  boolean isRunning() {
    return STATE_RUNNING == info.state;
  }

  void enqueue() {
    resume();
  }

  void pause() {
    isPaused = true;
    if (info.state != STATE_WAITING) return;
    onStateChanged(STATE_PAUSED, false);
  }

  void resume() {
    if (isRunning()) return;
    onStateChanged(STATE_WAITING, false);
    isPaused = false;
    engine.executor.submit(this);
  }

  private void clear() {
    listeners.clear();
    engine = null;
    info = null;
  }

  private void onStateChanged(int state, boolean updateDb) {
    info.state = state;
    if (updateDb) engine.provider.update(info);
    engine.handler.removeCallbacks(changeState);
    engine.handler.post(changeState);
  }

  private void onProgressChanged(long finishedLength, long contentLength) {
    info.finishedLength = finishedLength;
    info.contentLength = contentLength;
    engine.handler.removeCallbacks(changeProgress);
    engine.handler.post(changeProgress);
  }

  private boolean prepare() {
    if (isPaused) {
      onStateChanged(STATE_PAUSED, false);
      if (!engine.provider.exists(info)) {
        engine.provider.insert(info);
      } else {
        engine.provider.update(info);
      }
      return false;
    } else {
      onStateChanged(STATE_RUNNING, false);
      onProgressChanged(info.finishedLength, info.contentLength);
      if (engine.interceptors != null) {
        for (DownloadManager.Interceptor interceptor : engine.interceptors) {
          interceptor.updateDownloadInfo(info);
        }
      }
      if (!engine.provider.exists(info)) {
        engine.provider.insert(info);
      }
      return true;
    }
  }

  @Override
  public void run() {
    if (!prepare()) return;
    long finishedLength = info.finishedLength;
    long contentLength = info.contentLength;
    HttpURLConnection connection = null;
    InputStream inputStream = null;
    RandomAccessFile randomAccessFile = null;
    try {
      connection = (HttpURLConnection) new URL(info.url).openConnection();
      connection.setAllowUserInteraction(true);
      connection.setConnectTimeout(5000);
      connection.setReadTimeout(5000);
      connection.setRequestMethod("GET");
      if (finishedLength != 0 && contentLength > 0) {
        connection.setRequestProperty("Range", "bytes=" + finishedLength + "-" + contentLength);
      } else {
        contentLength = connection.getContentLength();
      }
      int responseCode = connection.getResponseCode();
      if (contentLength > 0 && (responseCode == HttpURLConnection.HTTP_OK || responseCode == HttpURLConnection.HTTP_PARTIAL)) {
        inputStream = connection.getInputStream();
        File file = new File(info.path);
        randomAccessFile = new RandomAccessFile(file, "rw");
        randomAccessFile.seek(finishedLength);
        byte[] buffer = new byte[20480];
        int len;
        long bytesRead = finishedLength;
        while (!this.isPaused && (len = inputStream.read(buffer)) != -1) {
          randomAccessFile.write(buffer, 0, len);
          bytesRead += len;
          finishedLength = bytesRead;
          onProgressChanged(finishedLength, contentLength);
        }
        connection.disconnect();
        if (this.isPaused) {
          onStateChanged(STATE_PAUSED, true);
        } else {
          info.finishTime = System.currentTimeMillis();
          onStateChanged(STATE_FINISHED, true);
          return;
        }
      } else {
        onStateChanged(STATE_FAILED, true);
      }
    } catch (final Exception e) {
      onStateChanged(STATE_FAILED, true);
    } finally {
      try {
        if (randomAccessFile != null)
          randomAccessFile.close();
        if (inputStream != null)
          inputStream.close();
      } catch (IOException e) {
      }
      if (connection != null)
        connection.disconnect();
    }
  }
}

任務(wù)調(diào)度

package com.grocery.download.library;

import android.content.Context;
import android.os.Environment;
import android.os.Handler;
import android.os.Looper;

import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * Created by 4ndroidev on 16/10/6.
 */
public class DownloadEngine {

    public static final String DOWNLOAD_PATH = Environment.getExternalStorageDirectory().getPath() + File.separator + "Download";
    private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
    private static final int CORE_POOL_SIZE = CPU_COUNT + 1;
    private static final int KEEP_ALIVE = 10;

    /**
     * record all jobs those are not completed
     */
    private Map<String, DownloadJob> jobs;

    /**
     * record all download info
     */
    private Map<String, DownloadInfo> infos;

    /**
     * record all active jobs in order for notification, some jobs are created, but not running
     */
    private List<DownloadJob> activeJobs;

    private ThreadPoolExecutor singleExecutor;
    /**
     * for some server, the url of resource if temporary
     * maybe need setting interceptor to update the url
     */
    List<DownloadManager.Interceptor> interceptors;

    /**
     * download ThreadPoolExecutor
     */
    ThreadPoolExecutor executor;

    /**
     * provider for inserting, deleting, querying or updating the download info with the database
     */
    DownloadProvider provider;
    Handler handler;
    Context context;

    DownloadEngine(Context context, int maxTask) {
      this.context = context.getApplicationContext();
      jobs = new HashMap<>();
      infos = new HashMap<>();
      activeJobs = new ArrayList<>();
      interceptors = new ArrayList<>();
      handler = new Handler(Looper.getMainLooper());
      if (maxTask > CORE_POOL_SIZE) maxTask = CORE_POOL_SIZE;
      executor = new ThreadPoolExecutor(maxTask, maxTask, KEEP_ALIVE, TimeUnit.SECONDS, new LinkedBlockingQueue());
      executor.allowCoreThreadTimeOut(true);
      singleExecutor = new ThreadPoolExecutor(1, 1, KEEP_ALIVE, TimeUnit.SECONDS, new LinkedBlockingQueue());
      singleExecutor.allowCoreThreadTimeOut(true);
      provider = new DownloadProvider(this.context);
    }
    
    /**
     * prepare for the task, while creating a task, should callback the download info to the listener
     * @param task
     */
    void prepare(DownloadTask task) {
      String key = task.key;
      if (!infos.containsKey(key)) {  // do not contain this info, means that it will create a download job
        if (task.listener == null) return;
        task.listener.onStateChanged(key, DownloadState.STATE_UNKNOWN);
        return;
      }
      DownloadInfo info = infos.get(key);
      task.size = info.contentLength;
      task.createTime = info.createTime;
      if (!jobs.containsKey(key)) {  // uncompleted jobs do not contain this job, means the job had completed
        if (task.listener == null) return;
        task.listener.onStateChanged(key, info.state); // info.state == DownloadState.STATE_FINISHED
      } else {
        jobs.get(key).addListener(task.listener);
      }
    }

    /**
     * if downloadJobs contains the relative job, and the job is not running, enqueue it
     * otherwise create the job and enqueue it
     * @param task
     */
    void enqueue(DownloadTask task) {
      String key = task.key;
      if (jobs.containsKey(key)) {                   // has existed uncompleted job
        DownloadJob job = jobs.get(key);
        if (job.isRunning()) return;
        job.enqueue();
        activeJobs.add(job);
      } else {
        if (infos.containsKey(key)) return;         // means the job had completed
        DownloadInfo info = task.generateInfo();
        DownloadJob job = new DownloadJob(this, info);
        infos.put(key, info);
        jobs.put(key, job);
        job.addListener(task.listener);
        job.enqueue();
        activeJobs.add(job);
      }
    }

    /**
     * remove the downloadJob and delete the relative info
     * @param task
     */
    void remove(DownloadTask task) {
      String key = task.key;
      if (!jobs.containsKey(key)) return;
      DownloadJob job = jobs.remove(task.key);
      delete(job.getInfo());
      if (!activeJobs.contains(job)) return;
      activeJobs.remove(job);
    }

    /**
     * pause the downloadJob
     * @param task
     */
    void pause(DownloadTask task) {
      String key = task.key;
      if (!jobs.containsKey(key)) return;
      jobs.get(key).pause();
    }

    /**
     * resume the downloadJob if it has not been running
     * @param task
     */
    void resume(DownloadTask task) {
      String key = task.key;
      if (!jobs.containsKey(key)) return;
      DownloadJob job = jobs.get(key);
      if (job.isRunning()) return;
      job.resume();
      activeJobs.add(job);
    }

    /**
     * delete download info, remove file
     * @param info
     */
    void delete(final DownloadInfo info) {
      if (info == null || !infos.containsValue(info)) return;
      infos.remove(info.key);
      if (isMainThread()) {
        singleExecutor.submit(new Runnable() {
          @Override
          public void run() {
            provider.delete(info);
            File file = new File(info.path);
            if (file.exists()) file.delete();
          }
        });
      } else {
        provider.delete(info);
        File file = new File(info.path);
        if (file.exists()) file.delete();
      }
    }

    /**
     * @return whether is in main thread
     */
    private boolean isMainThread() {
      return Looper.getMainLooper() == Looper.myLooper();
    }
}

使用說明

//創(chuàng)建任務(wù)
DownloadTask task = DownloadManager.get(context)
  .download(id, url, name).listener(listener).create();

//啟動任務(wù)
task.start();

//暫停任務(wù)
task.pause();

//恢復(fù)任務(wù)
task.resume();

//刪除任務(wù)
task.delete();

//暫停監(jiān)聽, 當(dāng)activity或fragment onPause時調(diào)用
task.pauseListener();

//恢復(fù)監(jiān)聽,當(dāng)activity或fragment onResume時調(diào)用
task.resumeListener();

//清理監(jiān)聽,當(dāng)activity或fragment onDestroy時調(diào)用
task.clear();
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 一.DownloadManager的介紹 1.Android涉及到的網(wǎng)絡(luò)數(shù)據(jù)請求,如果是零星數(shù)據(jù)、且數(shù)據(jù)量較?。?..
    少年的大叔心閱讀 2,363評論 0 5
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,030評論 25 709
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,554評論 19 139
  • 從Android 2.3(API level 9)開始Android用系統(tǒng)服務(wù)(Service)的方式提供了Dow...
    柨柨閱讀 2,828評論 1 4
  • 曾記否日暮之下 詩情愜意假正經(jīng), 倚劍自憐不得心, 癡情無錯不能全, 怨念默子不得言, 莫然忘情求脫俗, 靜待花開...
    余溫好似涼白開閱讀 492評論 0 0

友情鏈接更多精彩內(nèi)容