OkDownload是一个android下载框架,FileDownloader的升级版本,也称FileDownloader2;是一个支持多线程,多任务,断点续传,可靠,灵活,高性能以及强大的下载引擎,由于版本之间有代码差异,我们基于1.0.2版本做介绍。
// core
com.liulishuo.okdownload:okdownload:{latest_version}
// provide sqlite to store breakpoints
com.liulishuo.okdownload:sqlite:{latest_version}
// provide okhttp to connect to backend
// and then please import okhttp dependencies by yourself
com.liulishuo.okdownload:okhttp:{latest_version}
目前稳定版本是1.0.2,导入时候,请把{latest_version}替换为:1.0.2
单文件下载
DownloadTask task = new DownloadTask.Builder(url, parentFile)
.setFilename(filename)
// the minimal interval millisecond for callback progress
.setMinIntervalMillisCallbackProcess(30)
// do re-download even if the task has already been completed in the past.
.setPassIfAlreadyCompleted(false)
.build();
task.enqueue(listener);
// cancel
task.cancel();
// execute task synchronized
task.execute(listener);
多文件下载
final DownloadTask[] tasks = new DownloadTask[2];
tasks[0] = new DownloadTask.Builder("url1", "path", "filename1").build();
tasks[1] = new DownloadTask.Builder("url2", "path", "filename1").build();
DownloadTask.enqueue(tasks, listener);
多文件下载只能有一个开始下载,其他等待
DownloadContext.Builder builder = new DownloadContext.QueueSet()
.setParentPathFile(parentFile)
.setMinIntervalMillisCallbackProcess(150)
.commit();
builder.bind(url1);
builder.bind(url2).addTag(key, value);
builder.bind(url3).setTag(tag);
builder.setListener(contextListener);
DownloadTask task = new DownloadTask.Builder(url4, parentFile)
.setPriority(10).build();
builder.bindSetTask(task);
DownloadContext context = builder.build();
context.startOnParallel(listener);
// stop
context.stop();
通常使用DownloadListener来获取下载进度通知
在没有DownloadListener情况下,获取任务状态
Status status = StatusUtil.getStatus(task)
status = StatusUtil.getStatus(url, parentPath, null);
status = StatusUtil.getStatus(url, parentPath, filename);
boolean isCompleted = StatusUtil.isCompleted(task);
isCompleted = StatusUtil.isCompleted(url, parentPath, null);
isCompleted = StatusUtil.isCompleted(url, parentPath, filename);
Status completedOrUnknown = StatusUtil.isCompletedOrUnknown(task);
// If you set tag, so just get tag
task.getTag();
task.getTag(xxx);
在没有DownloadListener情况下,获取断点信息
// Note: the info will be deleted since task is completed download for data health lifecycle
BreakpointInfo info = OkDownload.with().breakpointStore().get(it);
info = StatusUtil.getCurrentInfo(url, parentPath, null);
info = StatusUtil.getCurrentInfo(url, parentPath, filename);
// Since okdownload v1.0.1-SNAPSHOT
// the info reference will be cached on the task refrence even if the task is completed download.
info = task.getInfo();
请通过Util.enableConsoleLog()在控制台打印上启用日志,您也可以通过Util.setLogger(Logger)设置自己的日志记录器
OkDownload.with().setMonitor(monitor);
DownloadDispatcher.setMaxParallelRunningCount(3);
RemitStoreOnSQLite.setRemitToDBDelayMillis(3000);
OkDownload.with().downloadDispatcher().cancelAll();
OkDownload.with().breakpointStore().remove(taskId);
如果您想注入您的组件,请在使用OkDownload之前调用以下方法:
OkDownload.Builder builder = new OkDownload.Builder(context)
.downloadStore(downloadStore)
.callbackDispatcher(callbackDispatcher)
.downloadDispatcher(downloadDispatcher)
.connectionFactory(connectionFactory)
.outputStreamFactory(outputStreamFactory)
.downloadStrategy(downloadStrategy)
.processFileStrategy(processFileStrategy)
.monitor(monitor);
OkDownload.setSingletonInstance(builder.build());
注意只能执行一次,否则报错,建议放到OkDownloadProvider的onCreate做全局设置:
public class OkDownloadProvider extends ContentProvider {
@SuppressLint("StaticFieldLeak") static Context context;
@Override
public boolean onCreate() {
context = getContext();
OkDownload.setSingletonInstance(new OkDownload.Builder(context).downloadStrategy(new DownloadStrategy(){
@Override
public int determineBlockCount(@NonNull DownloadTask task, long totalLength) {
return 1;
}
}).build());
return true;
}
}
DownloadListener listener1 = new DownloadListener1();
DownloadListener listener2 = new DownloadListener2();
DownloadListener combinedListener = new DownloadListenerBunch.Builder()
.append(listener1)
.append(listener2)
.build();
DownloadTask task = new DownloadTask.build(url, file).build();
task.enqueue(combinedListener);
// all attach or detach is based on the id of Task in fact.
UnifiedListenerManager manager = new UnifiedListenerManager();
DownloadListener listener1 = new DownloadListener1();
DownloadListener listener2 = new DownloadListener2();
DownloadListener listener3 = new DownloadListener3();
DownloadListener listener4 = new DownloadListener4();
DownloadTask task = new DownloadTask.build(url, file).build();
manager.attachListener(task, listener1);
manager.attachListener(task, listener2);
manager.detachListener(task, listener2);
// all listeners added for this task will be removed when task is end.
manager.addAutoRemoveListenersWhenTaskEnd(task.getId());
// enqueue task to start.
manager.enqueueTaskWithUnifiedListener(task, listener3);
manager.attachListener(task, listener4);
DownloadTask.Builder builder = new DownloadTask.Builder(url, file);
// Set the minimum internal milliseconds of progress callbacks to 100ms.(default is 3000)
builder.setMinIntervalMillisCallbackProcess(100);
// set the priority of the task to 10, higher means less time to wait to download.(default is 0)
builder.setPriority(10);
// set the read buffer to 8192 bytes for the response input-stream.(default is 4096)
builder.setReadBufferSize(8192);
// set the flush buffer to 32768 bytes for the buffered output-stream.(default is 16384)
builder.setFlushBufferSize(32768);
// set this task allow using 5 connections to download data.
builder.setConnectionCount(5);
// build the task.
DownloadTask task = builder.build();
private void initTask() {
final String filename = "single-test";
final String url = "https://cdn.llscdn.com/yy/files/xs8qmxn8-lls-LLS-5.8-800-20171207-111607.apk";
final File parentFile = DemoUtil.getParentFile(this);
DownloadTask task = new DownloadTask.Builder(url, parentFile)
.setFilename(filename)
// the minimal interval millisecond for callback progress
.setMinIntervalMillisCallbackProcess(16)
// ignore the same task has already completed in the past.
.setPassIfAlreadyCompleted(false)
.build();
}
private void initStatus(TextView statusTv, ProgressBar progressBar) {
//获取下载状态
final StatusUtil.Status status = StatusUtil.getStatus(task);
if (status == StatusUtil.Status.COMPLETED) {
progressBar.setProgress(progressBar.getMax());
}
statusTv.setText(status.toString());
//获取断点下载信息
final BreakpointInfo info = StatusUtil.getCurrentInfo(task);
if (info != null) {
Log.d(TAG, "init status with: " + info.toString());
DemoUtil.calcProgressToView(progressBar, info.getTotalOffset(), info.getTotalLength());
}
}
//在获取资源长度后,设置是否需要为文件预分配长度
setPreAllocateLength(boolean preAllocateLength)
//需要用几个线程来下载文件
setConnectionCount(@IntRange(from = 1) int connectionCount)
//当下载没有提供文件名的是,使用服务器地址的文件名;如果已经提供文件名,这个属性被忽略
setFilenameFromResponse(@Nullable Boolean filenameFromResponse)
//是否在主线程通知调用者
setAutoCallbackToUIThread(boolean autoCallbackToUIThread)
//通知调用者的频率,避免anr
setMinIntervalMillisCallbackProcess(int minIntervalMillisCallbackProcess)
//设置请求头
setHeaderMapFields(Map> headerMapFields)
//追加请求头
addHeader(String key, String value)
//设置优先级,默认值是0,值越大下载优先级越高
setPriority(int priority)
//设置读取缓存区大小,默认4096
setReadBufferSize(int readBufferSize)
//设置写入缓存区大小,默认16384
setFlushBufferSize(int flushBufferSize)
//写入到文件的缓冲区大小,默认65536
setSyncBufferSize(int syncBufferSize)
//写入文件的最小时间间隔
setSyncBufferIntervalMillis(int syncBufferIntervalMillis)
//设置下载文件名
setFilename(String filename)
//如果文件已经下载完成,再次发起下载请求时,是否忽略下载,还是从头开始下载
setPassIfAlreadyCompleted(boolean passIfAlreadyCompleted)
//只允许wifi下载
setWifiRequired(boolean wifiRequired)
通过对DownloadTask的封装,支持多个下载任务并行和串行下载。
初始化DownloadContext
public void initTasks(@NonNull Context context, @NonNull DownloadContextListener listener) {
final DownloadContext.QueueSet set = new DownloadContext.QueueSet();
final File parentFile = new File(DemoUtil.getParentFile(context), "queue");
this.queueDir = parentFile;
set.setParentPathFile(parentFile);
set.setMinIntervalMillisCallbackProcess(200);
final DownloadContext.Builder builder = set.commit();
String url = "http://dldir1.qq.com/weixin/android/weixin6516android1120.apk";
DownloadTask boundTask = builder.bind(url);
TagUtil.saveTaskName(boundTask, "1. WeChat");
url = "https://cdn.llscdn.com/yy/files/tkzpx40x-lls-LLS-5.7-785-20171108-111118.apk";
boundTask = builder.bind(url);
TagUtil.saveTaskName(boundTask, "2. LiuLiShuo");
DownloadContext context = builder.build();
}
DownloadContext的start方法开始下载,listener是监听回调,isSerial表示是否用串行来下载
public void start(@Nullable final DownloadListener listener, boolean isSerial)
适用于多个文件串行下载,动态增加下载文件,一个一个下载。
DownloadSerialQueue serialQueue = new DownloadSerialQueue(commonListener);
serialQueue.enqueue(task1);
serialQueue.enqueue(task2);
//暂停下载
serialQueue.pause();
//开始下载
serialQueue.resume();
int workingTaskId = serialQueue.getWorkingTaskId();
int waitingTaskCount = serialQueue.getWaitingTaskCount();
DownloadTask[] discardTasks = serialQueue.shutdown();
我们从单一文件下载开始分析,进入com.liulishuo.okdownload.sample.SingleActivity,请看initTask方法,是获取下载任务参数构建的简单例子,我们把.setFilename(filename)注释掉,方便分析后面如何从服务器获取文件名的流程。
构建一个下载任务的基础参数需要下载url和文件存储目录,其它参数可选。
private void initTask() {
// final String filename = "single-test";
final String url =
"https://cdn.llscdn.com/yy/files/xs8qmxn8-lls-LLS-5.8-800-20171207-111607.apk";
final File parentFile = DemoUtil.getParentFile(this);
task = new DownloadTask.Builder(url, parentFile)
// .setFilename(filename)
// the minimal interval millisecond for callback progress
.setMinIntervalMillisCallbackProcess(16)
// ignore the same task has already completed in the past.
.setPassIfAlreadyCompleted(false)
.build();
}
接着看initStatus方法,该方法用于查询下载任务的状态和断点下载信息。
private void initStatus(TextView statusTv, ProgressBar progressBar) {
final StatusUtil.Status status = StatusUtil.getStatus(task);
if (status == StatusUtil.Status.COMPLETED) {
progressBar.setProgress(progressBar.getMax());
}
statusTv.setText(status.toString());
final BreakpointInfo info = StatusUtil.getCurrentInfo(task);
if (info != null) {
Log.d(TAG, "init status with: " + info.toString());
DemoUtil.calcProgressToView(progressBar, info.getTotalOffset(), info.getTotalLength());
}
}
进入StatusUtil.getStatus(task),里面会调用isCompletedOrUnknown方法从BreakpointStoreOnSQLite获取状态,BreakpointStoreOnSQLite里面包含了BreakpointSQLiteHelper(用于操作数据)和BreakpointStoreOnCache(用于做数据操作之前的数据缓存)。
根据task.getId()来获取断点信息,如果没有取到状态信息,接着从DownloadDispatcher从的readyAsyncCalls和runningSyncCalls和runningAsyncCalls查询状态,如果都查询不到,返回UNKNOWN。
public static Status getStatus(@NonNull DownloadTask task) {
final Status status = isCompletedOrUnknown(task);
if (status == Status.COMPLETED) return Status.COMPLETED;
final DownloadDispatcher dispatcher = OkDownload.with().downloadDispatcher();
if (dispatcher.isPending(task)) return Status.PENDING;
if (dispatcher.isRunning(task)) return Status.RUNNING;
return status;
}
进入StatusUtil.getCurrentInfo(task),先根据task查找id,再根据id查找断点信息,查询不到返回null。
@Nullable public static BreakpointInfo getCurrentInfo(@NonNull DownloadTask task) {
final BreakpointStore store = OkDownload.with().breakpointStore();
final int id = store.findOrCreateId(task);
final BreakpointInfo info = store.get(id);
return info == null ? null : info.copy();
}
到此,初始化流程完毕,界面如下:
点击STAR进入开始下载流程,会进入task.enqueue方法,并会根据下载情况回调给UI,这里的下载DownloadListener4WithSpeed还有很多类型,可以根据需要来设置监听器,具体看上面3.3片段的截图介绍。
private void startTask(final TextView statusTv, final ProgressBar progressBar,
final TextView actionTv) {
task.enqueue(new DownloadListener4WithSpeed() {
private long totalLength;
private String readableTotalLength;
@Override public void taskStart(@NonNull DownloadTask task) {
statusTv.setText(R.string.task_start);
}
@Override
public void infoReady(@NonNull DownloadTask task, @NonNull BreakpointInfo info,
boolean fromBreakpoint,
@NonNull Listener4SpeedAssistExtend.Listener4SpeedModel model) {
statusTv.setText(R.string.info_ready);
totalLength = info.getTotalLength();
readableTotalLength = Util.humanReadableBytes(totalLength, true);
DemoUtil.calcProgressToView(progressBar, info.getTotalOffset(), totalLength);
}
@Override public void connectStart(@NonNull DownloadTask task, int blockIndex,
@NonNull Map> requestHeaders) {
final String status = "Connect Start " + blockIndex;
statusTv.setText(status);
}
@Override
public void connectEnd(@NonNull DownloadTask task, int blockIndex, int responseCode,
@NonNull Map> responseHeaders) {
final String status = "Connect End " + blockIndex;
statusTv.setText(status);
}
@Override
public void progressBlock(@NonNull DownloadTask task, int blockIndex,
long currentBlockOffset,
@NonNull SpeedCalculator blockSpeed) {
}
@Override public void progress(@NonNull DownloadTask task, long currentOffset,
@NonNull SpeedCalculator taskSpeed) {
final String readableOffset = Util.humanReadableBytes(currentOffset, true);
final String progressStatus = readableOffset + "/" + readableTotalLength;
final String speed = taskSpeed.speed();
final String progressStatusWithSpeed = progressStatus + "(" + speed + ")";
statusTv.setText(progressStatusWithSpeed);
DemoUtil.calcProgressToView(progressBar, currentOffset, totalLength);
}
@Override
public void blockEnd(@NonNull DownloadTask task, int blockIndex, BlockInfo info,
@NonNull SpeedCalculator blockSpeed) {
}
@Override public void taskEnd(@NonNull DownloadTask task, @NonNull EndCause cause,
@Nullable Exception realCause,
@NonNull SpeedCalculator taskSpeed) {
final String statusWithSpeed = cause.toString() + " " + taskSpeed.averageSpeed();
statusTv.setText(statusWithSpeed);
actionTv.setText(R.string.start);
// mark
task.setTag(null);
if (cause == EndCause.COMPLETED) {
final String realMd5 = fileToMD5(task.getFile().getAbsolutePath());
if (!realMd5.equalsIgnoreCase("f836a37a5eee5dec0611ce15a76e8fd5")) {
Log.e(TAG, "file is wrong because of md5 is wrong " + realMd5);
}
}
}
});
}
进入DownloadTask的enqueue
public void enqueue(DownloadListener listener) {
this.listener = listener;
OkDownload.with().downloadDispatcher().enqueue(this);
}
我们这里重点分析一下OkDownload.with(),downloadDispatcher().enqueue(this); 稍后分析。
在首次build OkDownload对象的时候,会指定如下策略,这些也可以在build之前自定义设置自己的策略。
DownloadDispatcher:管理下载的机制,比如最大允许几个任务在后台运行,同步下载任务的处理。
CallbackDispatcher:回调给调用者的分发器,可以传入自定义Handler和DownloadListener(没有特殊需求无需自定义),也可以由系统默认转发给DownloadTask的listener,默认是在UI线程回调,如果需要在子线程回调,请在DownloadTask设置setAutoCallbackToUIThread的值为false。
DownloadStore:断点信息存储的位置,默认是数据库+内存缓存。
DownloadConnection.Factory:选择哪个网络通讯连接,默认是okhttp
DownloadOutputStream.Factory:构建文件输出流DownloadOutputStream,是否支持随机位置写入
ProcessFileStrategy:多文件写文件的方式,默认是根据每个线程写文件的不同位置,支持同时写入。
DownloadStrategy:下载策略,多大文件分几个线程
DownloadMonitor:下载状态监听,监听的接口请参见DownloadMonitor
public static OkDownload with() {
if (singleton == null) {
synchronized (OkDownload.class) {
if (singleton == null) {
if (OkDownloadProvider.context == null) {
throw new IllegalStateException("context == null");
}
singleton = new Builder(OkDownloadProvider.context).build();
}
}
}
return singleton;
}
public OkDownload build() {
//处理下载和等待任务的管理
if (downloadDispatcher == null) {
downloadDispatcher = new DownloadDispatcher();
}
//回调给UI的事件通知
if (callbackDispatcher == null) {
callbackDispatcher = new CallbackDispatcher();
}
//管理下载进度数据,存储数据库
if (downloadStore == null) {
downloadStore = Util.createDefaultDatabase(context);
}
//指定网络通讯实现,默认使用okhttp3
if (connectionFactory == null) {
connectionFactory = Util.createDefaultConnectionFactory();
}
//构建文件输出流DownloadOutputStream,是否支持随机位置写入
if (outputStreamFactory == null) {
outputStreamFactory = new DownloadUriOutputStream.Factory();
}
//文件处理策略,主要计算多个任务写文件的位置和文件写入
if (processFileStrategy == null) {
processFileStrategy = new ProcessFileStrategy();
}
//下载策略,多大文件分几个线程
if (downloadStrategy == null) {
downloadStrategy = new DownloadStrategy();
}
OkDownload okDownload = new OkDownload(context, downloadDispatcher, callbackDispatcher,
downloadStore, connectionFactory, outputStreamFactory, processFileStrategy,
downloadStrategy);
okDownload.setMonitor(monitor);
Util.d("OkDownload", "downloadStore[" + downloadStore + "] connectionFactory["
+ connectionFactory);
return okDownload;
}
接着继续后面流程,进入DownloadDispatcher的enqueue,这里能看到maxParallelRunningCount默认值是5,表示最大允许有5个线程在下载,注意不是5个文件,因为一个文件可以分成几个线程去下载。
public void enqueue(DownloadTask task) {
skipProceedCallCount.incrementAndGet();
enqueueLocked(task);
skipProceedCallCount.decrementAndGet();
}
进入DownloadDispatcher的enqueueLocked
private synchronized void enqueueLocked(DownloadTask task) {
Util.d(TAG, "enqueueLocked for single task: " + task);
//检查是否完成
if (inspectCompleted(task)) return;
//检查是否冲突
if (inspectForConflict(task)) return;
//获取已经开始异步下载的任务数
final int originReadyAsyncCallSize = readyAsyncCalls.size();
enqueueIgnorePriority(task);
if (originReadyAsyncCallSize != readyAsyncCalls.size()) Collections.sort(readyAsyncCalls);
}
进入DownloadDispatcher的enqueueIgnorePriority
private synchronized void enqueueIgnorePriority(DownloadTask task) {
final DownloadCall call = DownloadCall.create(task, true, store);
//检查并行异步任务数量是否小于5
if (runningAsyncSize() < maxParallelRunningCount) {
runningAsyncCalls.add(call);
executorService().execute(call);
} else {
// priority
//添加到待执行任务集合
readyAsyncCalls.add(call);
}
}
executorService()的实现如下:
synchronized ExecutorService executorService() {
if (executorService == null) {
executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60, TimeUnit.SECONDS, new SynchronousQueue(),
Util.threadFactory("OkDownload Download", false));
}
return executorService;
}
executorService().execute(call);方法会调用DownloadCall的run方法,DownloadCall类继承NamedRunnable,NamedRunnable里面定义run方法如下:
public final void run() {
String oldName = Thread.currentThread().getName();
Thread.currentThread().setName(name);
try {
//回调DownloadCall的execute方法
execute();
} catch (InterruptedException e) {
//回调DownloadCall的canceled方法
canceled(e);
} finally {
Thread.currentThread().setName(oldName);
//回调DownloadCall的finished方法
finished();
}
}
由于回调DownloadCall的execute方法,我们接着看,这个方法比较长,尽量在代码中添加注释,个别重要的方法后面会单独讲解。
public void execute() throws InterruptedException {
currentThread = Thread.currentThread();
//是否重试
boolean retry;
//重试次数
int retryCount = 0;
// 准备参数
final OkDownload okDownload = OkDownload.with();
final ProcessFileStrategy fileStrategy = okDownload.processFileStrategy();
//开始任务,标记内存中任务状态并回调给调用者
inspectTaskStart();
do {
// 0.下载前先检查基础参数,URL合法性校验
if (task.getUrl().length() <= 0) {
this.cache = new DownloadCache.PreError(
new IOException("unexpected url: " + task.getUrl()));
break;
}
//中途取消任务
if (canceled) break;
// 1. 创建基础信息
@NonNull final BreakpointInfo info;
try {
BreakpointInfo infoOnStore = store.get(task.getId());
//如果数据库没有,则创建
if (infoOnStore == null) {
info = store.createAndInsert(task);
} else {
info = infoOnStore;
}
setInfoToTask(info);
} catch (IOException e) {
this.cache = new DownloadCache.PreError(e);
break;
}
//中途取消任务
if (canceled) break;
// 准备缓存
@NonNull final DownloadCache cache = createCache(info);
this.cache = cache;
// 2.检查服务器是否可以继续下载
final BreakpointRemoteCheck remoteCheck = createRemoteCheck(info);
try {
//如果下载任务没有设置文件名,会在这里优先使用url的文件名,如果获取失败,则使用url的md5代替。
remoteCheck.check();
} catch (IOException e) {
cache.catchException(e);
break;
}
// 3. 等待要写入文件锁释放
fileStrategy.getFileLock().waitForRelease(task.getFile().getAbsolutePath());
// 4. 复用空闲断点信息 BreakpointInfo
OkDownload.with().downloadStrategy()
.inspectAnotherSameInfo(task, info, remoteCheck.getInstanceLength());
try {
//服务器检查可以继续下载
if (remoteCheck.isResumable()) {
// 5. 本地文件和断点数据完整性校验
final BreakpointLocalCheck localCheck = createLocalCheck(info,
remoteCheck.getInstanceLength());
//文件存在可恢复
//断点信息正确
//文件是否支持指定位置操作
localCheck.check();
if (localCheck.isDirty()) {
Util.d(TAG, "breakpoint invalid: download from beginning because of "
+ "local check is dirty " + task.getId() + " " + localCheck);
//删除文件
fileStrategy.discardProcess(task);
// 6.计算任务分成几块去下载
assembleBlockAndCallbackFromBeginning(info, remoteCheck,
localCheck.getCauseOrThrow());
} else {
//回调给调用者
okDownload.callbackDispatcher().dispatch()
.downloadFromBreakpoint(task, info);
}
} else {
Util.d(TAG, "breakpoint invalid: download from beginning because of "
+ "remote check not resumable " + task.getId() + " " + remoteCheck);
//删除文件
fileStrategy.discardProcess(task);
// 6. 计算任务分成几块去下载
assembleBlockAndCallbackFromBeginning(info, remoteCheck,
remoteCheck.getCauseOrThrow());
}
} catch (IOException e) {
cache.setUnknownError(e);
break;
}
// 7. 开始下载
start(cache, info);
//中途取消任务
if (canceled) break;
// 8.下载失败重试
if (cache.isPreconditionFailed()
&& retryCount++ < MAX_COUNT_RETRY_FOR_PRECONDITION_FAILED) {
store.remove(task.getId());
retry = true;
} else {
retry = false;
}
} while (retry);
// finish
finishing = true;
blockChainList.clear();
final DownloadCache cache = this.cache;
if (canceled || cache == null) return;
final EndCause cause;
Exception realCause = null;
if (cache.isServerCanceled() || cache.isUnknownError()
|| cache.isPreconditionFailed()) {
// error
cause = EndCause.ERROR;
realCause = cache.getRealCause();
} else if (cache.isFileBusyAfterRun()) {
cause = EndCause.FILE_BUSY;
} else if (cache.isPreAllocateFailed()) {
cause = EndCause.PRE_ALLOCATE_FAILED;
realCause = cache.getRealCause();
} else {
cause = EndCause.COMPLETED;
}
//检查下载任务是否完成,删除断点信息,通知调用者
inspectTaskEnd(cache, cause, realCause);
重点讲一下start(cache, info);
void start(final DownloadCache cache, BreakpointInfo info) throws InterruptedException {
final int blockCount = info.getBlockCount();
final List blockChainList = new ArrayList<>(info.getBlockCount());
final long totalLength = info.getTotalLength();
//组装断点下载信息
for (int i = 0; i < blockCount; i++) {
final BlockInfo blockInfo = info.getBlock(i);
//已经下载完毕
if (Util.isCorrectFull(blockInfo.getCurrentOffset(), blockInfo.getContentLength())) {
continue;
}
//如果某一块的信息有误,从头开始下载
Util.resetBlockIfDirty(blockInfo);
blockChainList.add(DownloadChain.createChain(i, task, info, cache, store));
}
//中途取消任务
if (canceled) {
return;
}
//开始多个断点下载
startBlocks(blockChainList);
}
进入startBlocks,会调用DownloadChain的run方法
void startBlocks(List tasks) throws InterruptedException {
ArrayList futures = new ArrayList<>(tasks.size());
try {
for (DownloadChain chain : tasks) {
futures.add(submitChain(chain));
}
blockChainList.addAll(tasks);
for (Future future : futures) {
if (!future.isDone()) {
try {
future.get();
} catch (CancellationException | ExecutionException ignore) { }
}
}
} catch (Throwable t) {
for (Future future : futures) {
future.cancel(true);
}
throw t;
} finally {
blockChainList.removeAll(tasks);
}
}
进入DownloadChain的run方法
public void run() {
if (isFinished()) {
throw new IllegalAccessError("The chain has been finished!");
}
this.currentThread = Thread.currentThread();
try {
start();
} catch (IOException ignored) {
Util.w("--","ignored:"+ignored);
// interrupt.
} finally {
finished.set(true);
releaseConnectionAsync();
}
}
进入DownloadChain的start方法
void start() throws IOException {
final CallbackDispatcher dispatcher = OkDownload.with().callbackDispatcher();
// 发送请求拦截链
final RetryInterceptor retryInterceptor = new RetryInterceptor();
final BreakpointInterceptor breakpointInterceptor = new BreakpointInterceptor();
connectInterceptorList.add(retryInterceptor);
connectInterceptorList.add(breakpointInterceptor);
connectInterceptorList.add(new RedirectInterceptor());
connectInterceptorList.add(new HeaderInterceptor());
connectInterceptorList.add(new CallServerInterceptor());
connectIndex = 0;
//处理请求拦截链
final DownloadConnection.Connected connected = processConnect();
// isPreconditionFailed || isUserCanceled || isServerCanceled || isUnknownError || isFileBusyAfterRun || isPreAllocateFailed;
if (cache.isInterrupt()) {
throw InterruptException.SIGNAL;
}
dispatcher.dispatch().fetchStart(task, blockIndex, getResponseContentLength());
// 获取结果拦截链
//负责写文件的拦截器
final FetchDataInterceptor fetchDataInterceptor =
new FetchDataInterceptor(blockIndex, connected.getInputStream(),
getOutputStream(), task);
fetchInterceptorList.add(retryInterceptor);
fetchInterceptorList.add(breakpointInterceptor);
fetchInterceptorList.add(fetchDataInterceptor);
fetchIndex = 0;
final long totalFetchedBytes = processFetch();
dispatcher.dispatch().fetchEnd(task, blockIndex, totalFetchedBytes);
}
下次完成会回到DownloadCall的finished方法,取消回到DownloadCall的canceled方法,下载过程的状态会同步回调给调用者。