OkDownload是一个android下载框架,是FileDownloader的升级版本,也称FileDownloader2;
是一个支持多线程,多任务,断点续传,可靠,灵活,高性能以及强大的下载引擎。
对比FileDownloader的优势:
- 单元测试覆盖率很高,从而保证框架的可靠性。
- 简单的接口设计。
- 支持任务优先级。
- Uri文件转存储输出流。
- 核心类库更加单一和轻量级。
- 更灵活的回调机制和侦听器。
- 更灵活地扩展OkDownload的每个部分。
- 在不降低性能的情况下,更少的线程可以执行相同的操作。
- 文件IO线程池和网络IO线程池分开。
- 如果无法从响应头中找到,从URL中获取自动文件名。
- 取消和开始是非常有效的,特别是对于大量的任务,有大量的优化。
基本使用
引入
implementation 'com.liulishuo.okdownload:okdownload:1.0.5'
implementation 'com.liulishuo.okdownload:sqlite:1.0.5'
implementation 'com.liulishuo.okdownload:okhttp:1.0.5'
implementation "com.squareup.okhttp3:okhttp:3.10.0"
具体详见官方文档 Simple-Use-Guideline 和 Advanced-Use-Guideline
请通过Util.enableConsoleLog()
在控制台打印上启用日志,也可以通过Util.setLogger(Logger)
设置自己的日志记录器
开始一个任务
DownloadTask task = new DownloadTask.Builder(url, parentFile)
.setFilename(filename)
.setMinIntervalMillisCallbackProcess(30)
.setPassIfAlreadyCompleted(false)
.setPriority(10)
.build();
task.enqueue(listener);
task.cancel();
task.execute(listener);
DownloadTask.enqueue(tasks, listener);
配置下载任务
- setPreAllocateLength(boolean preAllocateLength) //在获取资源长度后,设置是否需要为文件预分配长度
- setConnectionCount(@IntRange(from = 1) int connectionCount) //需要用几个线程来下载文件
- setFilenameFromResponse(@Nullable Boolean filenameFromResponse)//如果没有提供文件名,是否使用服务器地址作为的文件名
- setAutoCallbackToUIThread(boolean autoCallbackToUIThread) //是否在主线程通知调用者
- setMinIntervalMillisCallbackProcess(int minIntervalMillisCallbackProcess) //通知调用者的频率,避免anr
- setHeaderMapFields(Map<String, List> headerMapFields)//设置请求头
- addHeader(String key, String value)//追加请求头
- setPriority(int priority)//设置优先级,默认值是0,值越大下载优先级越高
- setReadBufferSize(int readBufferSize)//设置读取缓存区大小,默认4096
- setFlushBufferSize(int flushBufferSize)//设置写入缓存区大小,默认16384
- setSyncBufferSize(int syncBufferSize)//写入到文件的缓冲区大小,默认65536
- setSyncBufferIntervalMillis(int syncBufferIntervalMillis)//写入文件的最小时间间隔
- setFilename(String filename)//设置下载文件名
- setPassIfAlreadyCompleted(boolean passIfAlreadyCompleted)//如果文件已经下载完成,再次发起下载请求时,是否忽略下载,还是从头开始下载
- setWifiRequired(boolean wifiRequired)//只允许wifi下载
案例
private DownloadTask createDownloadTask(ItemInfo itemInfo) {
return new DownloadTask.Builder(itemInfo.url, new File(Utils.PARENT_PATH))
.setFilename(itemInfo.pkgName)
.setFilenameFromResponse(false)
.setPassIfAlreadyCompleted(true)
.setConnectionCount(1)
.setPreAllocateLength(false)
.setMinIntervalMillisCallbackProcess(100)
.setWifiRequired(false)
.setAutoCallbackToUIThread(true)
.setPriority(0)
.setReadBufferSize(4096)
.setFlushBufferSize(16384)
.setSyncBufferSize(65536)
.setSyncBufferIntervalMillis(2000)
.build();
}
任务队列的构建、开始和停止
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).build();
builder.bindSetTask(task);
DownloadContext context = builder.build();
context.startOnParallel(listener);
context.stop();
获取任务状态
Status status = StatusUtil.getStatus(task);
Status status = StatusUtil.getStatus(url, parentPath, null);
Status status = StatusUtil.getStatus(url, parentPath, filename);
boolean isCompleted = StatusUtil.isCompleted(task);
boolean isCompleted = StatusUtil.isCompleted(url, parentPath, null);
boolean isCompleted = StatusUtil.isCompleted(url, parentPath, filename);
Status completedOrUnknown = StatusUtil.isCompletedOrUnknown(task);
获取断点信息
BreakpointInfo info = OkDownload.with().breakpointStore().get(id);
BreakpointInfo info = StatusUtil.getCurrentInfo(url, parentPath, null);
BreakpointInfo info = StatusUtil.getCurrentInfo(url, parentPath, filename);
BreakpointInfo info = task.getInfo();
设置任务监听
可以为任务设置五种不同类型的监听器,同时,也可以给任务和监听器建立1对1、1对多、多对1、多对多的关联。
项目提供了六种监听供选择:DownloadListener、DownloadListener1、DownloadListener3、DownloadListener3、DownloadListener4、DownloadListener4WithSpeed
具体流程详见 官方文档
设置多个监听
Combine Several DownloadListeners
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);
动态更改任务的监听
Dynamic Change Listener For tasks
UnifiedListenerManager manager = new UnifiedListenerManager();
DownloadTask task = new DownloadTask.build(url, file).build();
DownloadListener listener1 = new DownloadListener1();
DownloadListener listener2 = new DownloadListener2();
DownloadListener listener3 = new DownloadListener3();
DownloadListener listener4 = new DownloadListener4();
manager.attachListener(task, listener1);
manager.attachListener(task, listener2);
manager.detachListener(task, listener2);
manager.addAutoRemoveListenersWhenTaskEnd(task.getId());
manager.enqueueTaskWithUnifiedListener(task, listener3);
manager.attachListener(task, listener4);
全局控制
Global Control
OkDownload.with().setMonitor(monitor);
DownloadDispatcher.setMaxParallelRunningCount(3);
RemitStoreOnSQLite.setRemitToDBDelayMillis(3000);
OkDownload.with().downloadDispatcher().cancelAll();
OkDownload.with().breakpointStore().remove(taskId);
组件注入
Injection Component
If you want to inject your components, please invoke following method before you using 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());
动态串行队列
Dynamic Serial Queue
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();
源码结构
├── DownloadContext
├── DownloadContextListener
├── DownloadListener
├── DownloadMonitor
├── DownloadSerialQueue
├── DownloadTask
├── IRedirectHandler
├── OkDownload
├── OkDownloadProvider
├── RedirectUtil
├── SpeedCalculator
├── StatusUtil
├── UnifiedListenerManager
├── core
│ ├── IdentifiedTask
│ ├── NamedRunnable
│ └── Util
├── breakpoint
│ ├── BlockInfo
│ ├── BreakpointInfo
│ ├── BreakpointStore
│ └── BreakpointStoreOnCache
│ ├── DownloadStore
│ └── KeyToIdMap
├── cause
│ ├── EndCause
│ └── ResumeFailedCause
├── connection
│ ├── DownloadConnection
│ └── DownloadUrlConnection
├── dispatcher
│ ├── CallbackDispatcher
│ └── DownloadDispatcher
├── download
│ ├── BreakpointLocalCheck
│ ├── BreakpointRemoteCheck
│ ├── ConnectTrial
│ ├── DownloadCache
│ ├── DownloadCall
│ ├── DownloadChain
│ └── DownloadStrategy
├── exception
│ ├── DownloadSecurityException
│ ├── FileBusyAfterRunException
│ ├── InterruptException
│ ├── NetworkPolicyException
│ ├── PreAllocateException
│ ├── ResumeFailedException
│ ├── RetryException
│ └── ServerCancelledException
├── file
│ ├── DownloadOutputStream
│ ├── DownloadUriOutputStream
│ ├── FileLock
│ ├── MultiPointOutputStream
│ └── ProcessFileStrategy
├── interceptor
│ ├── BreakpointInterceptor
│ ├── FetchDataInterceptor
│ ├── Interceptor
│ ├── RetryInterceptor
│ └── connect
│ ├── CallServerInterceptor
│ └── HeaderInterceptor
└── listener
│ ├── DownloadListener1
│ ├── DownloadListener2
│ ├── DownloadListener3
│ ├── DownloadListener4
│ ├── DownloadListener4WithSpeed
│ ├── DownloadListenerBunch
│ └── assist
│ ├── Listener1Assist
│ ├── Listener4Assist
│ ├── Listener4SpeedAssistExtend
│ ├── ListenerAssist
│ └── ListenerModelHandler
使用案例
必要的配置
- 1、申请两个权限:WRITE_EXTERNAL_STORAGE 和 REQUEST_INSTALL_PACKAGES
- 2、配置 FileProvider
- 3、添加依赖
Activity
public class DownloadActivity extends ListActivity {
static final String URL1 = "https://oatest.dgcb.com.cn:62443/mstep/installpkg/yidongyingxiao/90.0/DGMmarket_rtx.apk";
static final String URL2 = "https://cdn.llscdn.com/yy/files/xs8qmxn8-lls-LLS-5.8-800-20171207-111607.apk";
static final String URL3 = "https://downapp.baidu.com/appsearch/AndroidPhone/1.0.78.155/1/1012271b/20190404124002/appsearch_AndroidPhone_1-0-78-155_1012271b.apk";
ProgressBar progressBar;
List<ItemInfo> list;
HashMap<String, DownloadTask> map = new HashMap<>();
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
String[] array = {"使用DownloadListener4WithSpeed",
"使用DownloadListener3",
"使用DownloadListener2",
"使用DownloadListener3",
"使用DownloadListener",
"=====删除下载的文件,并重新启动Activity=====",
"查看任务1的状态",
"查看任务2的状态",
"查看任务3的状态",
"查看任务4的状态",
"查看任务5的状态",};
setListAdapter(new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, array));
list = Arrays.asList(new ItemInfo(URL1, "com.yitong.mmarket.dg"),
new ItemInfo(URL1, "哎"),
new ItemInfo(URL2, "英语流利说"),
new ItemInfo(URL2, "百度手机助手"),
new ItemInfo(URL3, "哎哎哎"));
progressBar = new ProgressBar(this, null, android.R.attr.progressBarStyleHorizontal);
progressBar.setIndeterminate(false);
getListView().addFooterView(progressBar);
new File(Utils.PARENT_PATH).mkdirs();
}
@Override
public void onDestroy() {
super.onDestroy();
for (String key : map.keySet()) {
DownloadTask task = map.get(key);
if (task != null) {
task.cancel();
}
}
}
@Override
protected void onListItemClick(ListView l, View v, int position, long id) {
switch (position) {
case 0:
case 1:
case 2:
case 3:
case 4:
download(position);
break;
case 5:
Utils.deleateFiles(new File(Utils.PARENT_PATH), null, false);
recreate();
break;
default:
ItemInfo itemInfo = list.get(position - 6);
DownloadTask task = map.get(itemInfo.pkgName);
if (task != null) {
Toast.makeText(this, "状态为:" + StatusUtil.getStatus(task).name(), Toast.LENGTH_SHORT).show();
}
BreakpointInfo info = StatusUtil.getCurrentInfo(itemInfo.url, Utils.PARENT_PATH, itemInfo.pkgName);
if (info != null) {
float percent = (float) info.getTotalOffset() / info.getTotalLength() * 100;
Log.i("bqt", "【当前进度】" + percent + "%");
progressBar.setMax((int) info.getTotalLength());
progressBar.setProgress((int) info.getTotalOffset());
} else {
Log.i("bqt", "【任务不存在】");
}
break;
}
}
private void download(int position) {
ItemInfo itemInfo = list.get(position);
DownloadTask task = map.get(itemInfo.pkgName);
if (itemInfo.status == 0) {
if (task == null) {
task = createDownloadTask(itemInfo);
map.put(itemInfo.pkgName, task);
}
task.enqueue(createDownloadListener(position));
itemInfo.status = 1;
Toast.makeText(this, "开始下载", Toast.LENGTH_SHORT).show();
} else if (itemInfo.status == 1) {
if (task != null) {
task.cancel();
}
itemInfo.status = 2;
Toast.makeText(this, "暂停下载", Toast.LENGTH_SHORT).show();
} else if (itemInfo.status == 2) {
if (task != null) {
task.enqueue(createDownloadListener(position));
}
itemInfo.status = 1;
Toast.makeText(this, "继续下载", Toast.LENGTH_SHORT).show();
} else if (itemInfo.status == 3) {
Utils.launchOrInstallApp(this, itemInfo.pkgName);
Toast.makeText(this, "下载完成", Toast.LENGTH_SHORT).show();
}
}
private DownloadTask createDownloadTask(ItemInfo itemInfo) {
return new DownloadTask.Builder(itemInfo.url, new File(Utils.PARENT_PATH))
.setFilename(itemInfo.pkgName)
.setFilenameFromResponse(false)
.setPassIfAlreadyCompleted(true)
.setConnectionCount(1)
.setPreAllocateLength(false)
.setMinIntervalMillisCallbackProcess(100)
.setWifiRequired(false)
.setAutoCallbackToUIThread(true)
.setPriority(0)
.setReadBufferSize(4096)
.setFlushBufferSize(16384)
.setSyncBufferSize(65536)
.setSyncBufferIntervalMillis(2000)
.build();
}
private DownloadListener createDownloadListener(int position) {
switch (position) {
case 0:
return new MyDownloadListener4WithSpeed(list.get(position), progressBar);
case 1:
return new MyDownloadListener3(list.get(position), progressBar);
case 2:
return new MyDownloadListener2(list.get(position), progressBar);
case 3:
return new MyDownloadListener1(list.get(position), progressBar);
default:
return new MyDownloadListener(list.get(position), progressBar);
}
}
}
辅助工具类
public class Utils {
public static final String PARENT_PATH = Environment.getExternalStorageDirectory().getAbsolutePath() + "/aatest";
public static void launchOrInstallApp(Context context, String pkgName) {
if (!TextUtils.isEmpty(pkgName)) {
Intent intent = context.getPackageManager().getLaunchIntentForPackage(pkgName);
if (intent == null) {
installApk(context, new File(PARENT_PATH, pkgName));
} else {
context.startActivity(intent);
}
} else {
Toast.makeText(context, "包名为空!", Toast.LENGTH_SHORT).show();
installApk(context, new File(PARENT_PATH, pkgName));
}
}
public static void installApk(Context context, File file) {
Intent intent = new Intent(Intent.ACTION_VIEW);
Uri uri;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
uri = FileProvider.getUriForFile(context, context.getPackageName() + ".fileprovider", file);
} else {
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
uri = Uri.fromFile(file);
}
Log.i("bqt", "【Uri】" + uri);
intent.setDataAndType(uri, "application/vnd.android.package-archive");
context.startActivity(intent);
}
public static OkDownload buildOkDownload(Context context) {
return new OkDownload.Builder(context.getApplicationContext())
.downloadStore(Util.createDefaultDatabase(context))
.callbackDispatcher(new CallbackDispatcher())
.downloadDispatcher(new DownloadDispatcher())
.connectionFactory(Util.createDefaultConnectionFactory())
.outputStreamFactory(new DownloadUriOutputStream.Factory())
.processFileStrategy(new ProcessFileStrategy())
.downloadStrategy(new DownloadStrategy())
.build();
}
public static void deleateFiles(File dirFile, FilenameFilter filter, boolean isDeleateDir) {
if (dirFile.isDirectory()) {
for (File file : dirFile.listFiles()) {
deleateFiles(file, filter, isDeleateDir);
}
if (isDeleateDir) {
System.out.println("目录【" + dirFile.getAbsolutePath() + "】删除" + (dirFile.delete() ? "成功" : "失败"));
}
} else if (dirFile.isFile()) {
String symbol = isDeleateDir ? "\t" : "";
if (filter == null || filter.accept(dirFile.getParentFile(), dirFile.getName())) {
System.out.println(symbol + "- 文件【" + dirFile.getAbsolutePath() + "】删除" + (dirFile.delete() ? "成功" : "失败"));
} else {
System.out.println(symbol + "+ 文件【" + dirFile.getAbsolutePath() + "】不满足匹配规则,不删除");
}
} else {
System.out.println("文件不存在");
}
}
public static void dealEnd(Context context, ItemInfo itemInfo, @NonNull EndCause cause) {
if (cause == EndCause.COMPLETED) {
Toast.makeText(context, "任务完成", Toast.LENGTH_SHORT).show();
itemInfo.status = 3;
Utils.launchOrInstallApp(context, itemInfo.pkgName);
} else {
itemInfo.status = 2;
if (cause == EndCause.CANCELED) {
Toast.makeText(context, "任务取消", Toast.LENGTH_SHORT).show();
} else if (cause == EndCause.ERROR) {
Log.i("bqt", "【任务出错】");
} else if (cause == EndCause.FILE_BUSY || cause == EndCause.SAME_TASK_BUSY || cause == EndCause.PRE_ALLOCATE_FAILED) {
Log.i("bqt", "【taskEnd】" + cause.name());
}
}
}
}
辅助Bean
public class ItemInfo {
String url;
String pkgName;
int status;
public ItemInfo(String url, String pkgName) {
this.url = url;
this.pkgName = pkgName;
}
}
DownloadListener4WithSpeed
public class MyDownloadListener4WithSpeed extends DownloadListener4WithSpeed {
private ItemInfo itemInfo;
private long totalLength;
private String readableTotalLength;
private ProgressBar progressBar;
private Context context;
public MyDownloadListener4WithSpeed(ItemInfo itemInfo, ProgressBar progressBar) {
this.itemInfo = itemInfo;
this.progressBar = progressBar;
context = progressBar.getContext();
}
@Override
public void taskStart(@NonNull DownloadTask task) {
Log.i("bqt", "【1、taskStart】");
}
@Override
public void infoReady(@NonNull DownloadTask task, @NonNull BreakpointInfo info, boolean fromBreakpoint, @NonNull Listener4SpeedAssistExtend.Listener4SpeedModel model) {
totalLength = info.getTotalLength();
readableTotalLength = Util.humanReadableBytes(totalLength, true);
Log.i("bqt", "【2、infoReady】当前进度" + (float) info.getTotalOffset() / totalLength * 100 + "%" + "," + info.toString());
progressBar.setMax((int) totalLength);
}
@Override
public void connectStart(@NonNull DownloadTask task, int blockIndex, @NonNull Map<String, List<String>> requestHeaders) {
Log.i("bqt", "【3、connectStart】" + blockIndex);
}
@Override
public void connectEnd(@NonNull DownloadTask task, int blockIndex, int responseCode, @NonNull Map<String, List<String>> responseHeaders) {
Log.i("bqt", "【4、connectEnd】" + blockIndex + "," + responseCode);
}
@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) {
String readableOffset = Util.humanReadableBytes(currentOffset, true);
String progressStatus = readableOffset + "/" + readableTotalLength;
String speed = taskSpeed.speed();
float percent = (float) currentOffset / totalLength * 100;
Log.i("bqt", "【6、progress】" + currentOffset + "[" + progressStatus + "],速度:" + speed + ",进度:" + percent + "%");
progressBar.setProgress((int) currentOffset);
}
@Override
public void blockEnd(@NonNull DownloadTask task, int blockIndex, BlockInfo info, @NonNull SpeedCalculator blockSpeed) {
Log.i("bqt", "【7、blockEnd】" + blockIndex);
}
@Override
public void taskEnd(@NonNull DownloadTask task, @NonNull EndCause cause, @Nullable Exception realCause, @NonNull SpeedCalculator taskSpeed) {
Log.i("bqt", "【8、taskEnd】" + cause.name() + ":" + (realCause != null ? realCause.getMessage() : "无异常"));
Utils.dealEnd(context, itemInfo, cause);
}
}
源码分析
详见这篇文章
OkDownload
首先看一下OkDownload这个类,这个类定义了所有的下载策略,我们可以自定义一些下载策略,可以通过OkDownload的Builder构造自定义的一个OkDownload实例,再通过OkDownload.setSingletonInstance
进行设置:
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());
DownloadTask
DownloadTask下载任务类,可通过它的Builder来构造一个下载任务,我们看它是如何执行的:
public void execute(DownloadListener listener) {
this.listener = listener;
OkDownload.with().downloadDispatcher().execute(this);
}
public void enqueue(DownloadListener listener) {
this.listener = listener;
OkDownload.with().downloadDispatcher().enqueue(this);
}
可以看到都是通过downloadDispatcher来执行下载任务的,默认的downloadDispatcher是一个DownloadDispatcher
实例,我们以同步执行一个下载任务为例,看它是如何下载的:
public void execute(DownloadTask task) {
Util.d(TAG, "execute: " + task);
final DownloadCall call;
synchronized (this) {
if (inspectCompleted(task)) return;
if (inspectForConflict(task)) return;
call = DownloadCall.create(task, false, store);
runningSyncCalls.add(call);
}
syncRunCall(call);
}
void syncRunCall(DownloadCall call) {
call.run();
}
public abstract class NamedRunnable implements Runnable {
@Override
public final void run() {
String oldName = Thread.currentThread().getName();
Thread.currentThread().setName(name);
try {
execute();
} catch (InterruptedException e) {
interrupted(e);
} finally {
Thread.currentThread().setName(oldName);
finished();
}
}
protected abstract void execute() throws InterruptedException;
}
大致流程:
在execute方法里将一个DownloadTask实例又封装为了一个DownloadCall
对象,然后在syncRunCall方法里执行了DownloadCall
对象的run方法。通过看DownloadCall源码可以知道该类继承自NamedRunnable
,而NamedRunnable实现了Runnable,在run方法里调用了execute()
方法。
调用enqueue执行任务最终则是调用 getExecutorService().execute(call)
来异步执行的:
private synchronized void enqueueIgnorePriority(DownloadTask task) {
final DownloadCall call = DownloadCall.create(task, true, store);
if (runningAsyncSize() < maxParallelRunningCount) {
runningAsyncCalls.add(call);
getExecutorService().execute(call);
} else {
readyAsyncCalls.add(call);
}
}
DownloadCall
先看一下DownloadCall是如何实现execute
方法的,该方法比较长,首先执行的是inspectTaskStart()
方法:
private void inspectTaskStart() {
store.onTaskStart(task.getId());
OkDownload.with().callbackDispatcher().dispatch().taskStart(task);
}
这里的store是调用BreakpointStoreOnSQLite
的createRemitSelf
方法生成的一个实例:
public DownloadStore createRemitSelf() {
return new RemitStoreOnSQLite(this);
}
可以看到是RemitStoreOnSQLite
的一个实例,其主要用来保存任务及断点信息至本地数据库。RemitStoreOnSQLite里持有BreakpointStoreOnSQLite对象,BreakpointStoreOnSQLite里面包含了BreakpointSQLiteHelper(用于操作数据)和BreakpointStoreOnCache(用于做数据操作之前的数据缓存)。
最终会调用上述store的syncCacheToDB
方法,先删除数据库中的任务信息,若缓存(创建BreakpointStoreOnCache对象时,会调用loadToCache方法将数据库中所有任务信息进行缓存)中有该任务,则检查任务信息是否合法,若合法则再次将该任务及断点信息保存在本地数据库中。
@Override
public void syncCacheToDB(int id) throws IOException {
sqLiteHelper.removeInfo(id);
final BreakpointInfo info = sqliteCache.get(id);
if (info == null || info.getFilename() == null || info.getTotalOffset() <= 0) return;
sqLiteHelper.insert(info);
}
inspectTaskStart方法结束后,会进入一个do-while循环,首先做一些下载前的准备工作:
- 1.判断当前任务的下载链接长度是否大于0,否则就抛出异常;
- 2.从缓存中获取任务的断点信息,若没有断点信息,则创建断点信息并保存至数据库;
- 3.创建带缓存的下载输出流;
- 4.访问下载链接判断断点信息是否合理;
- 5.确定文件路径后等待文件锁释放;
- 6.判断缓存中是否有相同的任务,若有则复用缓存中的任务的分块信息;
- 7.检查断点信息是否是可恢复的,若不可恢复,则根据文件大小进行分块,重新下载,否则继续进行下一步;
- 8.判断断点信息是否是脏数据(文件存在且断点信息正确且下载链接支持断点续传);
- 9.若是脏数据则根据文件大小进行分块,重新开始下载,否则从断点位置开始下载;
- 10.开始下载。
文件分成多少块进行下载由DownloadStrategy
决定的:文件大小在0-1MB、1-5MB、5-50MB、50-100MB、100MB以上时分别开启1、2、3、4、5个线程进行下载。
我们重点看一下下载部分的源码,也就是start(cache,info)
方法:
void start(final DownloadCache cache, BreakpointInfo info) throws InterruptedException {
final int blockCount = info.getBlockCount();
final List<DownloadChain> blockChainList = new ArrayList<>(info.getBlockCount());
final List<Integer> blockIndexList = new ArrayList<>();
for (int i = 0; i < blockCount; i++) {
final BlockInfo blockInfo = info.getBlock(i);
if (Util.isCorrectFull(blockInfo.getCurrentOffset(), blockInfo.getContentLength())) {
continue;
}
Util.resetBlockIfDirty(blockInfo);
final DownloadChain chain = DownloadChain.createChain(i, task, info, cache, store);
blockChainList.add(chain);
blockIndexList.add(chain.getBlockIndex());
}
if (canceled) {
return;
}
cache.getOutputStream().setRequireStreamBlocks(blockIndexList);
startBlocks(blockChainList);
}
可以看到它是分块下载的,每一个分块都是一个DownloadChain
实例,DownloadChain实现了Runnable接口。
继续看DownloadCall的startBlocks
方法:
void startBlocks(List<DownloadChain> tasks) throws InterruptedException {
ArrayList<Future> futures = new ArrayList<>(tasks.size());
try {
for (DownloadChain chain : tasks) {
futures.add(submitChain(chain));
}
}
Future<?> submitChain(DownloadChain chain) {
return EXECUTOR.submit(chain);
}
对于每一个分块任务,都调用了submitChain
方法,即由一个线程池去处理每一个DownloadChain
分块。
DownloadChain
我们看一下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 HeaderInterceptor());
connectInterceptorList.add(new CallServerInterceptor());
connectIndex = 0;
final DownloadConnection.Connected connected = processConnect();
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);
}
可以看到它主要使用责任链模式
进行了两个链式调用:处理请求拦截链
和获取数据拦截链
。
- 处理请求拦截链包含了
RetryInterceptor
重试拦截器、BreakpointInterceptor
断点拦截器、RedirectInterceptor
重定向拦截器、HeaderInterceptor
头部信息处理拦截器、CallServerInterceptor
请求拦截器,该链式调用过程会逐个调用拦截器的interceptConnect
方法。
- 获取数据拦截链包含了
RetryInterceptor
重试拦截器、BreakpointInterceptor
断点拦截器、RedirectInterceptor
重定向拦截器、HeaderInterceptor
头部信息处理拦截器、FetchDataInterceptor
获取数据拦截器,该链式调用过程会逐个调用拦截器的interceptFetch
方法。
public class RetryInterceptor implements Interceptor.Connect, Interceptor.Fetch {
@NonNull @Override
public DownloadConnection.Connected interceptConnect(DownloadChain chain) throws IOException {
return chain.processConnect();
}
@Override
public long interceptFetch(DownloadChain chain) throws IOException {
return chain.processFetch();
}
}
每一个DownloadChain都完成后,最终会调用inspectTaskEnd
方法,从数据库中删除该任务,并回调通知任务完成。这样,一个完整的下载任务就完成了。
总体流程
总体流程如下: