Author: istyras
Date: 2024-10-15
Update: 2024-10-16
本文是对 OkDownload源码分析
的第四篇。
上一篇我们说过,关于 OkDownload
下载过程中的下载信息存储这部分内容会单独分析,这篇内容,我们就重点分析一下这部分内容。
1. DownloadStore
入口分析
还记得我们在第二篇《2. OkDownload源码分析——DownloadTask类分析》中,提过如何自定义构造一个 OkDownload
的实例吗?
OkDownload.Builder
中有个配置 DownloadStore
的接口,在其 build
方法中会自动配置默认的实现。
public static class Builder {
// 省略其他代码...
public OkDownload build() {
if (downloadDispatcher == null) {
downloadDispatcher = new DownloadDispatcher();
}
if (callbackDispatcher == null) {
callbackDispatcher = new CallbackDispatcher();
}
// 如果没有配置自定义的 DownloadStore 实现的话,就内部使用默认方案
if (downloadStore == null) {
downloadStore = Util.createDefaultDatabase(context);
}
if (connectionFactory == null) {
connectionFactory = Util.createDefaultConnectionFactory();
}
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;
}
}
从 build
方法源码中可以看到,默认情况下,会调用 Util.createDefaultDatabase(context);
进行创建。
public class Util {
public static @NonNull DownloadStore createDefaultDatabase(Context context) {
// You can import through com.liulishuo.okdownload:sqlite:{version}
final String storeOnSqliteClassName
= "com.liulishuo.okdownload.core.breakpoint.BreakpointStoreOnSQLite";
try {
final Constructor constructor = Class.forName(storeOnSqliteClassName)
.getDeclaredConstructor(Context.class);
return (DownloadStore) constructor.newInstance(context);
} catch (ClassNotFoundException ignored) {
} catch (InstantiationException ignored) {
} catch (IllegalAccessException ignored) {
} catch (NoSuchMethodException ignored) {
} catch (InvocationTargetException ignored) {
}
return new BreakpointStoreOnCache();
}
}
从 createDefaultDatabase
中看到,内部使用了反射的方式先查询一下是否有 BreakpointStoreOnSQLite
类,如果有怎使用 BreakpointStoreOnSQLite
实现方案,如果没有,则使用 BreakpointStoreOnCache
方案。
那么,有阅读过 OkDownload
使用文档的同学都应该会记得,其依赖配置上,单独提供了一个基于SQLite的断点信息存储方案模块给使用者进行依赖。
com.liulishuo.okdownload:sqlite:{latest_version}
上面反射创建的 BreakpointStoreOnSQLite
这个类,就是在这个模块中提供的。
特别注意:在 OkDownload 的构造函数中还有一个地方也进行 DownloadStore 的设置。
OkDownload(Context context, DownloadDispatcher downloadDispatcher,
CallbackDispatcher callbackDispatcher, DownloadStore store,
DownloadConnection.Factory connectionFactory,
DownloadOutputStream.Factory outputStreamFactory,
ProcessFileStrategy processFileStrategy, DownloadStrategy downloadStrategy) {
this.context = context;
this.downloadDispatcher = downloadDispatcher;
this.callbackDispatcher = callbackDispatcher;
this.breakpointStore = store;
this.connectionFactory = connectionFactory;
this.outputStreamFactory = outputStreamFactory;
this.processFileStrategy = processFileStrategy;
this.downloadStrategy = downloadStrategy;
// 这里 使用上面的 DownloadStore 另外创建了一个叫 延迟同步的数据存储方案,
// 将这个延迟同步数据的持久化存储方案设置给了 DownloadDispatcher .
this.downloadDispatcher.setDownloadStore(Util.createRemitDatabase(store));
}
在 OkDownload
的构造函数的最后一行,使用前面的 DownloadStore
另外创建了一个 延迟同步持久化存储
的 DownloadStore
方案。
public class Util {
public static @NonNull DownloadStore createRemitDatabase(@NonNull DownloadStore originStore) {
DownloadStore finalStore = originStore;
try {
final Method createRemitSelf = originStore.getClass()
.getMethod("createRemitSelf");
finalStore = (DownloadStore) createRemitSelf.invoke(originStore);
} catch (IllegalAccessException ignored) {
} catch (NoSuchMethodException ignored) {
} catch (InvocationTargetException ignored) {
}
Util.d("Util", "Get final download store is " + finalStore);
return finalStore;
}
}
这里可以看到,还是使用反射方式,反射查找 createRemitSelf
方法,由于 originStore
我们知道是 BreakpointStoreOnSQLite
,所以这个方法在该类里面。
public class BreakpointStoreOnSQLite implements DownloadStore {
@NonNull public DownloadStore createRemitSelf() {
return new RemitStoreOnSQLite(this);
}
}
关于 RemitStoreOnSQLite
类,稍后再分析。下面我们先分析 BreakpointStoreOnSQLite
类。
2. BreakpointStoreOnSQLite
分析
BreakpointStoreOnSQLite
-- DownloadStore
-- BreakpointStore
这个类的代码不多,我们先来看看整个类的代码。
public class BreakpointStoreOnSQLite implements DownloadStore {
private static final String TAG = "BreakpointStoreOnSQLite";
protected final BreakpointSQLiteHelper helper;
protected final BreakpointStoreOnCache onCache;
BreakpointStoreOnSQLite(BreakpointSQLiteHelper helper, BreakpointStoreOnCache onCache) {
this.helper = helper;
this.onCache = onCache;
}
public BreakpointStoreOnSQLite(Context context) {
this.helper = new BreakpointSQLiteHelper(context.getApplicationContext());
this.onCache = new BreakpointStoreOnCache(helper.loadToCache(),
helper.loadDirtyFileList(),
helper.loadResponseFilenameToMap());
}
@Nullable @Override public BreakpointInfo get(int id) {
return onCache.get(id);
}
@NonNull @Override public BreakpointInfo createAndInsert(@NonNull DownloadTask task)
throws IOException {
// 内存缓存
final BreakpointInfo info = onCache.createAndInsert(task);
// SQLite缓存
helper.insert(info);
return info;
}
@Override public void onTaskStart(int id) {
onCache.onTaskStart(id);
}
@Override public void onSyncToFilesystemSuccess(@NonNull BreakpointInfo info, int blockIndex,
long increaseLength) throws IOException {
onCache.onSyncToFilesystemSuccess(info, blockIndex, increaseLength);
final long newCurrentOffset = info.getBlock(blockIndex).getCurrentOffset();
helper.updateBlockIncrease(info, blockIndex, newCurrentOffset);
}
@Override public boolean update(@NonNull BreakpointInfo breakpointInfo) throws IOException {
final boolean result = onCache.update(breakpointInfo);
helper.updateInfo(breakpointInfo);
final String filename = breakpointInfo.getFilename();
Util.d(TAG, "update " + breakpointInfo);
if (breakpointInfo.isTaskOnlyProvidedParentPath() && filename != null) {
helper.updateFilename(breakpointInfo.getUrl(), filename);
}
return result;
}
@Override
public void onTaskEnd(int id, @NonNull EndCause cause, @Nullable Exception exception) {
onCache.onTaskEnd(id, cause, exception);
if (cause == EndCause.COMPLETED) {
helper.removeInfo(id);
}
}
@Nullable @Override public BreakpointInfo getAfterCompleted(int id) {
return null;
}
@Override public boolean markFileDirty(int id) {
if (onCache.markFileDirty(id)) {
helper.markFileDirty(id);
return true;
}
return false;
}
@Override public boolean markFileClear(int id) {
if (onCache.markFileClear(id)) {
helper.markFileClear(id);
return true;
}
return false;
}
@Override public void remove(int id) {
onCache.remove(id);
helper.removeInfo(id);
}
@Override public int findOrCreateId(@NonNull DownloadTask task) {
return onCache.findOrCreateId(task);
}
@Nullable @Override
public BreakpointInfo findAnotherInfoFromCompare(@NonNull DownloadTask task,
@NonNull BreakpointInfo ignored) {
return onCache.findAnotherInfoFromCompare(task, ignored);
}
@Override public boolean isOnlyMemoryCache() {
return false;
}
@Override public boolean isFileDirty(int id) {
return onCache.isFileDirty(id);
}
@Nullable @Override public String getResponseFilename(String url) {
return onCache.getResponseFilename(url);
}
void close() {
helper.close();
}
@NonNull public DownloadStore createRemitSelf() {
return new RemitStoreOnSQLite(this);
}
}
结合上面反射创建的代码,我们可以知道,创建这个类的构造函数为
public BreakpointStoreOnSQLite(Context context) {
this.helper = new BreakpointSQLiteHelper(context.getApplicationContext());
this.onCache = new BreakpointStoreOnCache(helper.loadToCache(),
helper.loadDirtyFileList(),
helper.loadResponseFilenameToMap());
}
BreakpointSQLiteHelper
这个类就是 BreakpointInfo
数据库的操作类了。
但是,这里还有一个 BreakpointStoreOnCache
,按照这个结构来看,数据库持久化方案中,还与一个Cache方案结合起来使用。
不难分析出 OkDownload
的数据存储默认实现的 BreakpointStoreOnSQLite
方案中,其实是提供了双缓存结构:内存缓存(BreakpointStoreOnCache
) + SQLite持久化缓存(BreakpointStoreOnSQLite
)。
这里需要和 DownloadCache
这个类区分开来,这个类我们当初分析到的时候说过,该类仅仅是一个任务下载进行时的相关状态信息的包装类而已。
3. BreakpointSQLiteHelper
分析
从 BreakpointStoreOnSQLite
的构造函数中,看到 BreakpointStoreOnCache
的初始化,调用了 BreakpointSQLiteHelper
的3个方法得到的数据作为参数进行初始化,那么,我们来看看这三个方法的具体代码实现:
3.1. loadToCache
方法分析
/**
* 从数据库加载断点信息到内存缓存
*
* 此方法涉及从两个表中加载数据:断点表和块表它首先查询断点表中的所有记录,
* 并将这些记录添加到一个列表中然后查询块表中的所有记录,并将这些记录添加到另一个列表中
* 之后,它遍历断点信息列表,为每个断点查找对应的块信息,并将其添加到断点信息对象中,
* 最后将断点信息对象放入一个稀疏数组中并返回这个稀疏数组
*
* @return SparseArray<BreakpointInfo> 包含所有断点信息的稀疏数组
*/
public SparseArray<BreakpointInfo> loadToCache() {
// 查询断点和块的游标
Cursor breakpointCursor = null;
Cursor blockCursor = null;
// 获取可写数据库实例
final SQLiteDatabase db = getWritableDatabase();
// 用于存储断点和块信息的列表
final List<BreakpointInfoRow> breakpointInfoRows = new ArrayList<>();
final List<BlockInfoRow> blockInfoRows = new ArrayList<>();
try {
// 查询断点表中的所有记录
// SELECT * FROM breakpoint
breakpointCursor = db.rawQuery(Select.ALL_FROM_BREAKPOINT, null);
while (breakpointCursor.moveToNext()) {
breakpointInfoRows.add(new BreakpointInfoRow(breakpointCursor));
}
// 查询块表中的所有记录
// SELECT * FROM block
blockCursor = db.rawQuery(Select.ALL_FROM_BLOCK, null);
while (blockCursor.moveToNext()) {
blockInfoRows.add(new BlockInfoRow(blockCursor));
}
} finally {
// 关闭游标
if (breakpointCursor != null) breakpointCursor.close();
if (blockCursor != null) blockCursor.close();
}
// 用于存储断点信息的稀疏数组
final SparseArray<BreakpointInfo> breakpointInfoMap = new SparseArray<>();
// 遍历断点信息列表,为每个断点查找对应的块信息
for (BreakpointInfoRow infoRow : breakpointInfoRows) {
final BreakpointInfo info = infoRow.toInfo();
final Iterator<BlockInfoRow> blockIt = blockInfoRows.iterator();
while (blockIt.hasNext()) {
final BlockInfoRow blockInfoRow = blockIt.next();
if (blockInfoRow.getBreakpointId() == info.id) {
info.addBlock(blockInfoRow.toInfo());
blockIt.remove();
}
}
// 将断点信息对象放入稀疏数组中
breakpointInfoMap.put(info.id, info);
}
// 返回包含所有断点信息的稀疏数组
return breakpointInfoMap;
}
3.2. loadDirtyFileList
方法分析
/**
* 加载脏文件列表
*
* 此方法负责从数据库中查询并返回所有标记为“脏”的文件的ID列表这些文件可能需要被重新处理或同步
* 使用rawQuery方法执行原始查询以获取所有标记为“脏”的文件信息
*
* @return 包含所有脏文件ID的列表如果列表为空,则返回一个空列表
*/
public List<Integer> loadDirtyFileList() {
// 初始化一个空的整型列表,用于存储脏文件的ID
final List<Integer> dirtyFileList = new ArrayList<>();
// Cursor用于遍历查询结果,初始化为null
Cursor cursor = null;
try {
// 执行原始SQL查询,获取所有标记为“脏”的文件,这里使用了SQLite的rawQuery方法
// SELECT * FROM taskFileDirty
cursor = getWritableDatabase().rawQuery(Select.ALL_FROM_TASK_FILE_DIRTY,
null);
// 遍历查询结果,每次迭代获取文件ID并添加到列表中
while (cursor.moveToNext()) {
dirtyFileList.add(cursor.getInt(cursor.getColumnIndex(ID)));
}
} finally {
// 确保在离开方法时关闭Cursor以释放资源
if (cursor != null) cursor.close();
}
// 返回包含所有脏文件ID的列表
return dirtyFileList;
}
3.3. loadResponseFilenameToMap
方法分析
/**
* 加载响应文件名到映射表中
* 此方法将数据库中的响应文件名数据加载到一个HashMap中,以便于快速查找
* 每个URL对应一个文件名,通过此方法可以快速获取指定URL的文件名
*
* @return 返回一个HashMap,其中包含URL和对应的文件名
*/
public HashMap<String, String> loadResponseFilenameToMap() {
// 声明一个Cursor对象,用于查询数据库
Cursor cursor = null;
// 获取可写数据库实例
final SQLiteDatabase db = getWritableDatabase();
// 创建一个HashMap,用于存储URL和文件名的映射关系
final HashMap<String, String> urlFilenameMap = new HashMap<>();
try {
// 执行SQL查询,获取所有响应文件名数据
// SELECT * FROM okdownloadResponseFilename
cursor = db.rawQuery(Select.ALL_FROM_RESPONSE_FILENAME, null);
// 遍历查询结果,并将URL和文件名添加到HashMap中
while (cursor.moveToNext()) {
final String url = cursor.getString(cursor.getColumnIndex(URL));
final String filename = cursor.getString(cursor.getColumnIndex(FILENAME));
urlFilenameMap.put(url, filename);
}
} finally {
// 确保在finally块中关闭Cursor,以释放资源
if (cursor != null) cursor.close();
}
// 返回包含URL和文件名映射的HashMap
return urlFilenameMap;
}
4. BreakpointStoreOnCache
分析
/**
* 构造函数
*/
public BreakpointStoreOnCache(SparseArray<BreakpointInfo> storedInfos,
List<Integer> fileDirtyList,
HashMap<String, String> responseFilenameMap) {
this.unStoredTasks = new SparseArray<>();
this.storedInfos = storedInfos;
this.fileDirtyList = fileDirtyList;
this.responseFilenameMap = responseFilenameMap;
this.keyToIdMap = new KeyToIdMap();
final int count = storedInfos.size();
sortedOccupiedIds = new ArrayList<>(count);
for (int i = 0; i < count; i++) {
// 记录所有已经分配过的id清单
sortedOccupiedIds.add(storedInfos.valueAt(i).id);
}
Collections.sort(sortedOccupiedIds);
}
持久化数据,有取就肯定有存,那么存的时机在哪里呢,我们回过头来分析代码。
5. DownloadStore
的调用分析
调用 DownloadStore
的地方,不多,我们可以通过 OkDownload
类中的 breakpointStore()
查找一下。
首先,我们最熟悉的位置就是 DownloadTask
类中的调用。
5.1. DownloadStore
在 DownloadTask
中的使用
public DownloadTask(String url, Uri uri, int priority, int readBufferSize, int flushBufferSize,
int syncBufferSize, int syncBufferIntervalMills,
boolean autoCallbackToUIThread, int minIntervalMillisCallbackProcess,
Map<String, List<String>> headerMapFields, @Nullable String filename,
boolean passIfAlreadyCompleted, boolean wifiRequired,
Boolean filenameFromResponse, @Nullable Integer connectionCount,
@Nullable Boolean isPreAllocateLength) {
// 这里省略一些代码......
// 这里通过 BreakpointStore.findOrCreateId 得到一个任务的唯一ID
this.id = OkDownload.with().breakpointStore().findOrCreateId(this);
}
那么,这个task id是如何创建的呢?
BreakpointStoreOnSQLite
中的 findOrCreateId
实现转交给了 BreakpointStoreOnCache
:
@Override public int findOrCreateId(@NonNull DownloadTask task) {
return onCache.findOrCreateId(task);
}
而 BreakpointStoreOnCache
中的实现:
/**
* 采用内存中进行自管理的ID分配方式,性能高,可控性强,就是控制复杂
* 在任务管理器中查找或创建下载任务的唯一ID
* 此方法首先尝试从已知任务中查找相同的任务,并返回其ID如果找不到,则为任务分配一个新的ID
*
* @param task 下载任务对象,用于查找或创建ID
* @return 任务的唯一ID,如果任务已存在,则返回已存在的ID;否则,为任务分配并返回一个新的ID
*/
@Override
public synchronized int findOrCreateId(@NonNull DownloadTask task) {
// 从keyToIdMap中根据任务获取候选ID
final Integer candidate = keyToIdMap.get(task);
if (candidate != null) return candidate;
// 遍历storedInfos中的任务,查找是否有相同的任务
final int size = storedInfos.size();
for (int i = 0; i < size; i++) {
final BreakpointInfo info = storedInfos.valueAt(i);
if (info != null && info.isSameFrom(task)) {
return info.id;
}
}
// 遍历unStoredTasks中的任务,查找是否有相同的任务
final int unStoredSize = unStoredTasks.size();
for (int i = 0; i < unStoredSize; i++) {
final IdentifiedTask another = unStoredTasks.valueAt(i);
if (another == null) continue;
if (another.compareIgnoreId(task)) return another.getId();
}
// 如果没有找到相同的任务,则为任务分配新的ID,并将其添加到unStoredTasks和keyToIdMap中
final int id = allocateId();
unStoredTasks.put(id, task.mock(id));
keyToIdMap.add(task, id);
return id;
}
/**
* 同步方法,用于分配一个新的ID
* 该方法通过查找未使用的ID或在已使用ID序列的末尾添加一个新的ID来工作
* 确保在多线程环境中安全地分配唯一的ID
*
* @return 分配的新ID
*/
synchronized int allocateId() {
// 初始化新ID为0,表示尚未分配
int newId = 0;
// 初始化索引为0,用于在已使用ID列表中定位新ID的位置
int index = 0;
// 初始化前一个ID为0,用于比较当前ID与前一个ID的关系
int preId = 0;
// 定义当前ID,用于存储遍历到的当前ID值
int curId;
// 遍历已排序的已使用ID列表,寻找未使用的ID
for (int i = 0; i < sortedOccupiedIds.size(); i++) {
// 获取当前ID对象
final Integer curIdObj = sortedOccupiedIds.get(i);
// 如果当前ID对象为null,表示找到未使用的ID位置
if (curIdObj == null) {
index = i;
newId = preId + 1;
break;
}
// 将当前ID对象的值赋给curId
curId = curIdObj;
// 如果前一个ID为0,表示还未存储任何ID,检查第一个ID是否为FIRST_ID
if (preId == 0) {
if (curId != FIRST_ID) {
// 如果第一个ID不是FIRST_ID,则将FIRST_ID分配为新ID
newId = FIRST_ID;
index = 0;
break;
}
// 更新前一个ID为当前ID,继续遍历
preId = curId;
continue;
}
// 如果当前ID不等于前一个ID加1,表示找到未使用的ID位置
if (curId != preId + 1) {
newId = preId + 1;
index = i;
break;
}
// 更新前一个ID为当前ID,继续遍历
preId = curId;
}
// 如果新ID仍然为0,表示未找到未使用的ID,需要在ID序列末尾添加新的ID
if (newId == 0) {
// 如果已使用ID列表为空,将FIRST_ID分配为新ID
if (sortedOccupiedIds.isEmpty()) {
newId = FIRST_ID;
} else {
// 否则,在已使用ID序列末尾加1,生成新的ID
newId = sortedOccupiedIds.get(sortedOccupiedIds.size() - 1) + 1;
index = sortedOccupiedIds.size();
}
}
// 将新ID添加到已使用ID列表中的指定位置
sortedOccupiedIds.add(index, newId);
// 返回新分配的ID
return newId;
}
BreakpointStoreOnCache
中的 task id 管理与维护,采用内存中进行自管理的ID分配方式,性能高,可控性强,就是控制复杂。
为什么这么实现呢?
- 因为 task id 的使用贯穿了整个下载流程,而且在真正任务存储到持久层(createAndInsert)之前,就需要这个ID;
- 因为 有可能没有持久层,只有内存缓存层,所以,也只能自计算自管理;
- 因为
OkDownload
为了避免下载任务排队队列中积累太多下载任务,其对持久化层的数量有一个上限限制; - 因为 task id 的使用只在下载过程中使用,一个 task 下载完成后,是会将相关的下载过程中需要的记录信息清理掉;
综合上述的几个原因,所以使用自管理ID生成的方式比较适合。
对于这个ID的生成方式,方法有很多,可以思考思考。
在 DownloadTask
的另一个调用的地方是:
/**
* Get the breakpoint info of this task.
*
* @return {@code null} Only if there isn't any info for this task yet, otherwise you can get
* the info for the task.
*/
@Nullable public BreakpointInfo getInfo() {
if (info == null) info = OkDownload.with().breakpointStore().get(id);
return info;
}
这个调用就是简单的查询,简单易懂,不做过多的解析。
总结一下
DownloadStore
在 DownloadTask
中的调用有两处:
DownloadTask
构造函数中,用于生成一个 task id;- 在
DownloadTask
的getInfo
方法中,就是一个简单的BreakpoingInfo
信息查询;
5.2. DownloadStore
在 StatusUtil
的调用
在 StatusUtil
有两处调用,都是一些基本的查询使用。
public class StatusUtil {
/**
* 判断给定的下载任务是否已完成
* 此方法专门用于查询任务状态,以确定其是否为完成状态
* 它调用了isCompletedOrUnknown方法,并通过比较Status来确定任务状态
* 使用此方法可以避免直接访问和操作任务对象,提供了更清晰的接口
*
* @param task 要检查的下载任务,不能为空
* @return 如果任务已完成,则返回true;否则返回false
*/
public static boolean isCompleted(@NonNull DownloadTask task) {
// 通过状态比较来判断任务是否已完成
return isCompletedOrUnknown(task) == Status.COMPLETED;
}
/**
* 判断下载任务的完成状态或未知状态
*
* @param task 下载任务对象,不能为空
* @return 返回任务的当前状态,可能是已完成、未知状态或空闲状态
*/
public static Status isCompletedOrUnknown(@NonNull DownloadTask task) {
// 获取断点存储对象
final BreakpointStore store = OkDownload.with().breakpointStore();
// 根据任务ID获取断点信息
final BreakpointInfo info = store.get(task.getId());
// 任务文件名,可能为空
@Nullable String filename = task.getFilename();
// 任务的父文件夹,不能为空
@NonNull final File parentFile = task.getParentFile();
// 任务的目标文件,可能为空
@Nullable final File targetFile = task.getFile();
// 如果任务有断点信息
if (info != null) {
// 如果不是分块下载且总长度未知,则视为未知状态
if (!info.isChunked() && info.getTotalLength() <= 0) {
return Status.UNKNOWN;
} else if ((targetFile != null && targetFile.equals(info.getFile()))
&& targetFile.exists()
&& info.getTotalOffset() == info.getTotalLength()) {
// 如果目标文件存在且大小匹配,则视为已完成状态
return Status.COMPLETED;
} else if (filename == null && info.getFile() != null
&& info.getFile().exists()) {
// 如果文件名为空,但断点文件存在,则视为空闲状态
return Status.IDLE;
} else if (targetFile != null && targetFile.equals(info.getFile())
&& targetFile.exists()) {
// 如果目标文件存在,则视为空闲状态
return Status.IDLE;
}
} else if (store.isOnlyMemoryCache() || store.isFileDirty(task.getId())) {
// 如果只有内存缓存,或文件已更改,则视为未知状态
return Status.UNKNOWN;
} else if (targetFile != null && targetFile.exists()) {
// 如果目标文件存在,但没有断点信息,则视为已完成状态
return Status.COMPLETED;
} else {
// 如果从响应中获取的文件名存在,则视为已完成状态
filename = store.getResponseFilename(task.getUrl());
if (filename != null && new File(parentFile, filename).exists()) {
return Status.COMPLETED;
}
}
// 如果以上条件都不满足,则视为未知状态
return Status.UNKNOWN;
}
/**
* 获取当前下载任务的断点信息
*
* @param task 下载任务对象,不能为空
* @return 返回断点信息的副本,如果找不到则返回null
*/
@Nullable public static BreakpointInfo getCurrentInfo(@NonNull DownloadTask task) {
// 获取断点存储对象
final BreakpointStore store = OkDownload.with().breakpointStore();
// 查找或创建下载任务的ID
final int id = store.findOrCreateId(task);
// 通过ID获取断点信息
final BreakpointInfo info = store.get(id);
// 返回断点信息的副本,如果找不到则返回null
return info == null ? null : info.copy();
}
}
查询相关的逻辑代码比较简单,代码中都有写明了注释,不再展开分析。
5.3. DownloadStore
在 DownloadStrategy
中的调用
public class DownloadStrategy {
// 存储中的信息有闲置的任务,并且没有再被唤醒:进程被杀后,不再触发,
// 取消后没有清理干净等情况下产生了闲置的任务信息,找到与新任务完全一致的信息,来进行信息复用
// this case meet only if there are another info task is idle and is the same after
// this task has filename.
// 判断是否可以重用另一个闲置的相同信息任务
// 仅当存在另一个闲置的相同信息任务,并且该任务有文件名时,才考虑重用
public boolean inspectAnotherSameInfo(@NonNull DownloadTask task, @NonNull BreakpointInfo info,
long instanceLength) {
// 如果任务的文件名不是来自响应,则不进行重用
if (!task.isFilenameFromResponse()) return false;
// 获取断点存储对象,用于管理断点信息
final BreakpointStore store = OkDownload.with().breakpointStore();
// 查找另一个与当前任务和信息相同的闲置任务信息
final BreakpointInfo anotherInfo = store.findAnotherInfoFromCompare(task, info);
// 如果没有找到,则不进行重用
if (anotherInfo == null) return false;
// 删除找到的另一个任务信息
store.remove(anotherInfo.getId());
// 如果另一个任务的总偏移量小于等于重用阈值,则不进行重用
if (anotherInfo.getTotalOffset()
<= OkDownload.with().downloadStrategy().reuseIdledSameInfoThresholdBytes()) {
return false;
}
// 如果两个任务的ETag不同,则不进行重用
if (anotherInfo.getEtag() != null && !anotherInfo.getEtag().equals(info.getEtag())) {
return false;
}
// 如果另一个任务的总长度与当前实例长度不同,则不进行重用
if (anotherInfo.getTotalLength() != instanceLength) {
return false;
}
// 如果另一个任务的文件不存在或不是文件,则不进行重用
if (anotherInfo.getFile() == null || !anotherInfo.getFile().exists()) return false;
// 重用另一个任务的块信息
info.reuseBlocks(anotherInfo);
// 输出重用结果的日志信息
Util.d(TAG, "Reuse another same info: " + info);
// 返回重用成功
return true;
}
/**
* 检查并设置来自存储的文件名是否有效
*
* 此方法尝试从断点存储中获取响应的文件名如果文件名存在,则将其设置到下载任务中;否则,返回false
* 主要用于确保下载任务的文件名是有效且可用的
*
* @param task 要验证文件名的下载任务,不能为空
* @return 返回{@code true}如果成功从存储中获取到有效的文件名并设置到任务中;否则返回{@code false}
*/
@VisibleForTesting
public boolean validFilenameFromStore(@NonNull DownloadTask task) {
// 从断点存储中获取响应的文件名
final String filename = OkDownload.with().breakpointStore()
.getResponseFilename(task.getUrl());
if (filename == null) return false;
// 将获取到的文件名设置到下载任务中
task.getFilenameHolder().set(filename);
return true;
}
/**
* 检查下载任务是否已完成,并根据情况更新完成集合
*
* @param task 当前正在检查的下载任务
* @param completedCollection 如果任务已完成且提供了此集合,则将任务添加到此集合中
* @return 如果任务已完成并成功更新完成集合或通知相关监听器,则返回true;否则返回false
*/
boolean inspectCompleted(@NonNull DownloadTask task,
@Nullable Collection<DownloadTask> completedCollection) {
// 如果任务已经完成且符合通过条件,则继续检查
if (task.isPassIfAlreadyCompleted() && StatusUtil.isCompleted(task)) {
// 如果任务的文件名为空且从存储中验证文件名无效,则任务未完成
if (task.getFilename() == null && !OkDownload.with().downloadStrategy()
.validFilenameFromStore(task)) {
return false;
}
// 在任务完成时验证并更新任务的信息
OkDownload.with().downloadStrategy().validInfoOnCompleted(task, store);
// 根据完成集合的情况,要么添加到集合中,要么通知监听器任务已完成
if (completedCollection != null) {
completedCollection.add(task);
} else {
OkDownload.with().callbackDispatcher().dispatch()
.taskEnd(task, EndCause.COMPLETED, null);
}
return true;
}
return false;
}
}
总结一下
DownloadStore
在 DownloadStrategy
中的调用有两处:
- 在
inspectAnotherSameInfo
方法中,用于查询是否有闲置的相同的任务信息可以复用; - 在
validFilenameFromStore
方法中,用于从 store 中查询任务完成时的文件名是否正常,判断任务是否能认定任务完成了;
分析了这么多个关于 DownloadStore
的调用,发现都没有看到相关的存储的逻辑,那么存储的逻辑在哪里呢?
还记得 RemitStoreOnSQLite
这个类么,大部分的操作在这个类中,而对 DownloadStore
的调用也是通过这个类进行。
6. RemitStoreOnSQLite
类分析
前面,我们简单分析过这个类的的构造时机。
OkDownload(Context context, DownloadDispatcher downloadDispatcher,
CallbackDispatcher callbackDispatcher, DownloadStore store,
DownloadConnection.Factory connectionFactory,
DownloadOutputStream.Factory outputStreamFactory,
ProcessFileStrategy processFileStrategy, DownloadStrategy downloadStrategy) {
this.context = context;
this.downloadDispatcher = downloadDispatcher;
this.callbackDispatcher = callbackDispatcher;
this.breakpointStore = store;
this.connectionFactory = connectionFactory;
this.outputStreamFactory = outputStreamFactory;
this.processFileStrategy = processFileStrategy;
this.downloadStrategy = downloadStrategy;
// 这里 使用上面的 DownloadStore 另外创建了一个叫 延迟同步的数据存储方案,
// 将这个延迟同步数据的持久化存储方案设置给了 DownloadDispatcher .
this.downloadDispatcher.setDownloadStore(Util.createRemitDatabase(store));
}
public class Util {
public static @NonNull DownloadStore createRemitDatabase(@NonNull DownloadStore originStore) {
DownloadStore finalStore = originStore;
try {
final Method createRemitSelf = originStore.getClass()
.getMethod("createRemitSelf");
finalStore = (DownloadStore) createRemitSelf.invoke(originStore);
} catch (IllegalAccessException ignored) {
} catch (NoSuchMethodException ignored) {
} catch (InvocationTargetException ignored) {
}
Util.d("Util", "Get final download store is " + finalStore);
return finalStore;
}
}
我们说这个类名的意思是:延迟同步持久化存储。
为什么叫延迟同步呢?
OkDownload
中为了高性能的持久化存储已下载的文件信息以及同步缓存下载相关的进度保存,其采用了一定频率的周期性同步保存信息的方案,以降低IO频率,提升性能。
6.1. 关联的几个类分析
RemitStoreOnSQLite
不是 SQLite 的实现类,只是一个包装类。
public class RemitStoreOnSQLite implements RemitSyncExecutor.RemitAgent, DownloadStore {
private static final String TAG = "RemitStoreOnSQLite";
@NonNull private final RemitSyncToDBHelper remitHelper;
@NonNull private final BreakpointStoreOnSQLite onSQLiteWrapper;
@NonNull private final BreakpointSQLiteHelper sqLiteHelper;
@NonNull private final DownloadStore sqliteCache;
RemitStoreOnSQLite(@NonNull BreakpointStoreOnSQLite sqlite) {
this.remitHelper = new RemitSyncToDBHelper(this);
this.onSQLiteWrapper = sqlite;
this.sqliteCache = onSQLiteWrapper.onCache;
this.sqLiteHelper = onSQLiteWrapper.helper;
}
@Nullable @Override public BreakpointInfo get(int id) {
return onSQLiteWrapper.get(id);
}
@NonNull @Override public BreakpointInfo createAndInsert(@NonNull DownloadTask task)
throws IOException {
// 如果没添加到数据库中的时候,则直接插入到缓存即可
// 这里的真实情况是:
// 1. 首先由 onTaskStart 方法中,触发 executor.postSyncInfoDelay(id, delayMillis);
// 2. 然后代码马上就到了这里
// 如果是新建任务,则这里为空的,那么就直接插入到缓存即可,后面会因为 delay 时间的到来,自动将数据同步到数据库中
// 如果被取消后又被重启,这直接就更新到数据库中
if (remitHelper.isNotInDatabase(task.getId())) {
return sqliteCache.createAndInsert(task);
}
// 如果已经添加到数据库中,则创建并插入到数据库中
return onSQLiteWrapper.createAndInsert(task);
}
@Override public void onTaskStart(int id) {
onSQLiteWrapper.onTaskStart(id);
// 任务开始通知,这里执行了
// executor.postSyncInfoDelay(id, 1500);
remitHelper.onTaskStart(id);
}
@Override public void onSyncToFilesystemSuccess(@NonNull BreakpointInfo info, int blockIndex,
long increaseLength) throws IOException {
// 如果任务没有添加到数据库中,则直接更新内存缓存即可
// 这个时候也是判断任务是否已经添加到数据库中,如果还是没有添加,则直接更新到内存缓存中
// 等待 delay 时间到来,会取到最新的内存缓存,存储到数据库中
if (remitHelper.isNotInDatabase(info.getId())) {
sqliteCache.onSyncToFilesystemSuccess(info, blockIndex, increaseLength);
return;
}
onSQLiteWrapper.onSyncToFilesystemSuccess(info, blockIndex, increaseLength);
}
@Override public boolean update(@NonNull BreakpointInfo info) throws IOException {
// 如果任务没有添加到数据库中,则直接更新内存缓存即可
// 这个时候也是判断任务是否已经添加到数据库中,如果还是没有添加,则直接更新到内存缓存中
// 等待 delay 时间到来,会取到最新的内存缓存,存储到数据库中
if (remitHelper.isNotInDatabase(info.getId())) return sqliteCache.update(info);
// 如果有添加到数据库中,则直接更新到数据库中
return onSQLiteWrapper.update(info);
}
/**
* 当任务结束时调用此方法来处理相应的清理工作
*
* @param id 任务的唯一标识符
* @param cause 任务结束的原因
* @param exception 如果任务异常结束,这是对应的异常对象;否则为null
*/
@Override
public void onTaskEnd(int id, @NonNull EndCause cause, @Nullable Exception exception) {
// 通知SQLite缓存任务已结束
sqliteCache.onTaskEnd(id, cause, exception);
// 根据任务结束的原因进行不同的操作
if (cause == EndCause.COMPLETED) {
// 如果任务正常完成,则移除任务ID,因为它不再需要处理
remitHelper.discard(id);
} else {
// 如果任务因其他原因结束,确保任务结果写入数据库
remitHelper.endAndEnsureToDB(id);
}
}
@Nullable @Override public BreakpointInfo getAfterCompleted(int id) {
return null;
}
@Override public boolean markFileDirty(int id) {
return onSQLiteWrapper.markFileDirty(id);
}
@Override public boolean markFileClear(int id) {
return onSQLiteWrapper.markFileClear(id);
}
@Override public void remove(int id) {
sqliteCache.remove(id);
remitHelper.discard(id);
}
@Override public int findOrCreateId(@NonNull DownloadTask task) {
return onSQLiteWrapper.findOrCreateId(task);
}
@Nullable @Override
public BreakpointInfo findAnotherInfoFromCompare(@NonNull DownloadTask task,
@NonNull BreakpointInfo ignored) {
return onSQLiteWrapper.findAnotherInfoFromCompare(task, ignored);
}
@Override public boolean isOnlyMemoryCache() {
return false;
}
@Override public boolean isFileDirty(int id) {
return onSQLiteWrapper.isFileDirty(id);
}
@Nullable @Override public String getResponseFilename(String url) {
return onSQLiteWrapper.getResponseFilename(url);
}
// following accept database operation what is controlled by helper.
// 实现将缓存同步到数据库的操作,该操作由helper控制
@Override
public void syncCacheToDB(List<Integer> idList) throws IOException {
// 获取可写的数据库实例
final SQLiteDatabase database = sqLiteHelper.getWritableDatabase();
// 开始一个数据库事务
database.beginTransaction();
try {
// 遍历id列表,将每个缓存数据同步到数据库
for (Integer id : idList) {
syncCacheToDB(id);
}
// 如果遍历没有异常,设置事务成功
database.setTransactionSuccessful();
} finally {
// 结束数据库事务,无论是否成功
database.endTransaction();
}
}
/**
* 同步缓存到数据库
* 此方法用于将内存缓存中的指定数据同步到SQLite数据库中首先,它会删除数据库中对应的旧数据,
* 然后从内存缓存中获取新的数据信息如果获取的数据为空或者数据中的文件名为空,或者总偏移量不大于0,
* 则直接返回,不执行同步操作否则,将会把新的数据信息插入到数据库中
*
* @param id 要同步的数据的唯一标识符
* @throws IOException 如果在同步过程中发生I/O错误
*/
@Override public void syncCacheToDB(int id) throws IOException {
// 删除数据库中指定id的旧数据信息:包括 BreakpointInfo 和 BlockInfo 信息
sqLiteHelper.removeInfo(id);
// 从内存缓存中获取指定id的数据信息
final BreakpointInfo info = sqliteCache.get(id);
// 如果获取的数据为空或者数据中的文件名为空,或者总偏移量不大于0,则直接返回
if (info == null || info.getFilename() == null || info.getTotalOffset() <= 0) return;
// 将新的数据信息插入到数据库中
sqLiteHelper.insert(info);
}
@Override public void removeInfo(int id) {
sqLiteHelper.removeInfo(id);
}
}
class RemitSyncToDBHelper {
private final RemitSyncExecutor executor;
long delayMillis;
RemitSyncToDBHelper(@NonNull final RemitSyncExecutor.RemitAgent agent) {
this(new RemitSyncExecutor(agent));
}
RemitSyncToDBHelper(@NonNull final RemitSyncExecutor executor) {
this.executor = executor;
this.delayMillis = 1500;
}
void shutdown() {
this.executor.shutdown();
}
/**
* 判断指定的任务ID是否没有添加到数据库中,如果没有则返回true
*
* @param id
* @return
*/
boolean isNotInDatabase(int id) {
return !executor.isInDatabase(id);
}
void onTaskStart(int id) {
// discard pending sync if we can
executor.removePostWithId(id);
executor.postSyncInfoDelay(id, delayMillis);
}
void endAndEnsureToDB(int id) {
executor.removePostWithId(id);
try {
// already synced
// 如果指定id已经持久化到数据库,则直接返回
if (executor.isInDatabase(id)) return;
// force sync for ids
// 如果没有则强制同步到数据库中
executor.postSync(id);
} finally {
// remove free state
// 同步完成后将该id从freeToDBList中移除,仅从freeToDBList中移除
executor.postRemoveFreeId(id);
}
}
/**
* 删除指定ID的持久化数据:BreakpointInfo, BlockInfo 信息
*
* 下载完成的时候、任务主动删除的时候 会调用该方法
*
* @param id
*/
void discard(int id) {
executor.removePostWithId(id);
executor.postRemoveInfo(id);
}
}
RemitSyncExecutor
使用 HandlerThread + Handler 的方式实现的一个异步操作的包装类。
主要是为了避免在主线程中执行耗时操作,同时保证每次的操作调度都有时序,不会出现乱序导致数据操作异常。
RemitSyncExecutor
这个类中的 freeToDBIdList
属性,我给改成了 addedToDBIdList
方便理解。
freeToDBIdList
的作用是记录任务ID相关的缓存数据是否有添加到数据库中,有添加到数据库中则记录到这个列表中。
而这个属性名称在使用的时候不好理解,所以改成 addedToDBIdList
方便理解。
/**
* 使用 HandlerThread + Handler 的方式实现的一个异步操作的包装类,避免在主线程中执行耗时操作,
* 同时保证每次的操作调度都有时序,不会出现乱序导致数据操作异常
*/
public class RemitSyncExecutor implements Handler.Callback {
private static final String TAG = "RemitSyncExecutor";
static final int WHAT_SYNC_BUNCH_ID = BreakpointStoreOnCache.FIRST_ID - 1;
static final int WHAT_REMOVE_FREE_BUNCH_ID = BreakpointStoreOnCache.FIRST_ID - 2;
static final int WHAT_REMOVE_FREE_ID = BreakpointStoreOnCache.FIRST_ID - 3;
static final int WHAT_REMOVE_INFO = BreakpointStoreOnCache.FIRST_ID - 4;
@NonNull private final Handler handler;
// 这个里面按照代码中的使用方式,意思是存储下载其正在/排队下载的任务ID
// 两种情况下会执行删除:主动调用remove的地方,以及下载完成后,其余的取消任务等不会删除
@NonNull private final Set<Integer> addedToDBIdList;
private
@NonNull final RemitAgent agent;
RemitSyncExecutor(@NonNull RemitAgent agent) {
this.agent = agent;
this.addedToDBIdList = new HashSet<>();
/**
* 这里使用 HandlerThread + Handler 的方式,避免在主线程中执行耗时操作,
* 同时保证每次的操作调度都有时序,不会出现乱序导致数据操作异常
*/
final HandlerThread thread = new HandlerThread("OkDownload RemitHandoverToDB");
thread.start();
handler = new Handler(thread.getLooper(), this);
}
RemitSyncExecutor(@NonNull RemitAgent agent, @Nullable Handler handler,
@NonNull Set<Integer> addedToDBIdList) {
this.agent = agent;
this.handler = handler;
this.addedToDBIdList = addedToDBIdList;
}
void shutdown() {
this.handler.getLooper().quit();
}
/**
* 判断指定的ID是否已经添加到数据库中,如果已经添加,则返回true
* @param id
* @return
*/
boolean isInDatabase(int id) {
return addedToDBIdList.contains(id);
}
public void postSyncInfoDelay(int id, long delayMillis) {
handler.sendEmptyMessageDelayed(id, delayMillis);
}
public void postSync(int id) {
handler.sendEmptyMessage(id);
}
public void postSync(List<Integer> idList) {
Message message = handler.obtainMessage(WHAT_SYNC_BUNCH_ID);
message.obj = idList;
handler.sendMessage(message);
}
public void postRemoveInfo(int id) {
Message message = handler.obtainMessage(WHAT_REMOVE_INFO);
message.arg1 = id;
handler.sendMessage(message);
}
public void postRemoveFreeIds(List<Integer> idList) {
final Message message = handler.obtainMessage(WHAT_REMOVE_FREE_BUNCH_ID);
message.obj = idList;
handler.sendMessage(message);
}
public void postRemoveFreeId(int id) {
final Message message = handler.obtainMessage(WHAT_REMOVE_FREE_ID);
message.arg1 = id;
handler.sendMessage(message);
}
void removePostWithId(int id) {
handler.removeMessages(id);
}
void removePostWithIds(int[] ids) {
for (int id : ids) {
handler.removeMessages(id);
}
}
public boolean handleMessage(Message msg) {
List<Integer> idList;
int id;
switch (msg.what) {
case WHAT_REMOVE_INFO:
id = msg.arg1;
addedToDBIdList.remove(id);
this.agent.removeInfo(id);
Util.d(TAG, "remove info " + id);
break;
case WHAT_REMOVE_FREE_BUNCH_ID:
// remove bunch free-ids
idList = (List<Integer>) msg.obj;
addedToDBIdList.removeAll(idList);
Util.d(TAG, "remove free bunch ids " + idList);
break;
case WHAT_REMOVE_FREE_ID:
// remove free-id
id = msg.arg1;
addedToDBIdList.remove(id);
Util.d(TAG, "remove free bunch id " + id);
break;
case WHAT_SYNC_BUNCH_ID:
// sync bunch id
idList = (List<Integer>) msg.obj;
try {
this.agent.syncCacheToDB(idList);
addedToDBIdList.addAll(idList);
Util.d(TAG, "sync bunch info with ids: " + idList);
} catch (IOException e) {
Util.w(TAG, "sync info to db failed for ids: " + idList);
}
break;
default:
// sync id
id = msg.what;
try {
this.agent.syncCacheToDB(id);
// 这个里面按照代码中的使用方式,意思是存储下载其正在/排队下载的任务ID
// 两种情况下会执行删除:主动调用remove的地方,以及下载完成后,其余的取消任务等不会删除
// 即被添加到数据库的id清单
addedToDBIdList.add(id);
Util.d(TAG, "sync info with id: " + id);
} catch (IOException e) {
Util.w(TAG, "sync cache to db failed for id: " + id);
}
break;
}
return true;
}
interface RemitAgent {
void syncCacheToDB(List<Integer> idList) throws IOException;
void syncCacheToDB(int id) throws IOException;
void removeInfo(int id);
}
}
7. RemitStoreOnSQLite
的执行时机
在 DownloadDispatcher
中设置的 DownloadStore
就是 RemitStoreOnSQLite
实例,这个在前面分析过了。
所以在 DownloadDispatcher
中使用的 DownloadStore
都是 RemitStoreOnSQLite
实例。
我们从 DownloadDispatcher
类中开始分析。
前面的分析我们知道,在 DownloadDispatcher
的 enqueue
和 execute
会执行 DownloadCall.create
进行 DownloadCall
对象的创建,它需要一个 DownloadStore
类型的参数。
public class DownloadDispatcher {
/**
* 执行下载任务
*
* 此方法首先检查任务是否已完成或是否存在冲突如果任务未完成且无冲突,则创建一个DownloadCall实例并将其添加到运行中的同步调用列表中
* 最后,调用syncRunCall方法来执行实际的下载操作
*
* @param task 要执行的下载任务
*/
public void execute(DownloadTask task) {
// 打印执行任务的日志信息
Util.d(TAG, "execute: " + task);
final DownloadCall call;
synchronized (this) {
// 检查任务是否已完成,如果已完成则直接返回
if (inspectCompleted(task)) return;
// 检查任务是否与正在运行的任务有冲突,如果有冲突则直接返回
if (inspectForConflict(task)) return;
// 创建DownloadCall实例,准备执行下载任务
call = DownloadCall.create(task, false, store);
// 将新创建的DownloadCall实例添加到运行中的同步调用列表中
runningSyncCalls.add(call);
}
// 执行DownloadCall实例,开始下载任务
syncRunCall(call);
}
}
所以,这里我们需要分析 DownloadCall
,前面我们分析 DownloadCall
知道这个类开始执行的入口是 execute
方法。
public class DownloadCall extends NamedRunnable implements Comparable<DownloadCall> {
/**
* 下载任务真正开始执行的位置
* DownloadDispatcher.enqueueIgnorePriority 调用 getExecutorService().execute(call); 触发进来
* 或者是
* execute(DownloadTask) 中的 DownloadCall.run 方法触发进来。
*/
@Override
public void execute() throws InterruptedException {
currentThread = Thread.currentThread();
boolean retry;
int retryCount = 0;
// ready param
final OkDownload okDownload = OkDownload.with();
final ProcessFileStrategy fileStrategy = okDownload.processFileStrategy();
// inspect task start
// Remit.1 分发下载任务开始的通知
// 任务开始通知,这里内部会执行 RemitSyncToDBHelper.onTaskStart 方法,内部执行
// executor.postSyncInfoDelay(id, 1500);
// 的操作,这是一个 handler delay 1500ms,所以会延迟执行
inspectTaskStart();
do {
// url 校验
// 0. check basic param before start
if (task.getUrl().length() <= 0) {
this.cache = new DownloadCache.PreError(
new IOException("unexpected url: " + task.getUrl()));
break;
}
// 判断一下任务是否已经被取消
if (canceled) break;
// 1. create basic info if not exist
@NonNull final BreakpointInfo info;
try {
// 根据 任务ID 获取缓存中的断点续传的数据信息,如果没有就新建一个
// 取:组件初始化的时候(BreakpointStoreOnSQLite),会使用 helper.loadToCache() 加载出所有的BreakpointInfo 信息
// 存:store.createAndInsert(task);
BreakpointInfo infoOnStore = store.get(task.getId());
if (infoOnStore == null) {
// 新建的 BreakpointInfo 信息,只有简单的信息,还没有分块信息
// Remit.2 这里创建了一个 BreakpointInfo 信息,
// 这个 store 也是 RemitStoreOnSQLite
info = store.createAndInsert(task);
} else {
// 如果是从 数据库中取出来的,那么有可能有分块信息
info = infoOnStore;
}
// 这里将 断点续传的信息 存到 DownloadTask 中,后续操作的时候,
// 能够从 Task 中再取出 BreakpointInfo 信息来操作
setInfoToTask(info);
} catch (IOException e) {
this.cache = new DownloadCache.PreError(e);
break;
}
if (canceled) break;
// ready cache.
// 创建DownloadCache;
// DownloadCache 是一个中间类,用于存放下载过程中产生的一些状态,
// 以及在完成下载任务过程中的一些操作对象存储,就是将这些信息包装在一起方便传递与管理
@NonNull final DownloadCache cache = createCache(info);
this.cache = cache;
// 执行预先检测操作,同时可以获取到文件大小、重定向信息、处理文件分块等,
// 同时可以判断一下的网络状态异常抛出异常就结束了
// 2. remote check.
final BreakpointRemoteCheck remoteCheck = createRemoteCheck(info);
try {
remoteCheck.check();
} catch (IOException e) {
cache.catchException(e);
break;
}
// 这里从上面的 BreakpointRemoteCheck 中,获取到重定向信息,设置到 task 中,
// 然后从 task 中取出来,设置到 cache 中。
cache.setRedirectLocation(task.getRedirectLocation());
// 确认文件路径后,等待文件锁释放。具体实现是通过 fileStrategy.getFileLock().waitForRelease
// 方法等待指定文件路径(task.getFile().getAbsolutePath())的文件锁被释放。
// 确保并发操作时的文件安全
// 3. waiting for file lock release after file path is confirmed.
fileStrategy.getFileLock().waitForRelease(task.getFile().getAbsolutePath());
// 4. reuse another info if another info is idle and available for reuse.
OkDownload.with().downloadStrategy()
.inspectAnotherSameInfo(task, info, remoteCheck.getInstanceLength());
try {
// 使用上面的 BreakpointRemoteCheck 信息,判断是否可以断点续传
if (remoteCheck.isResumable()) {
// 5. local check
// 可以断点续传,还需要对本地文件进行校验
final BreakpointLocalCheck localCheck = createLocalCheck(info,
remoteCheck.getInstanceLength());
localCheck.check();
// 如果 localCheck.check 校验后,是 dirty 的,那么就重新下载
if (localCheck.isDirty()) {
Util.d(TAG, "breakpoint invalid: download from beginning because of "
+ "local check is dirty " + task.getId() + " " + localCheck);
// 6. assemble block data
// 丢弃(删除)task对应的本地文件,准备重新下载
fileStrategy.discardProcess(task);
// 重新组装断点续传的分块数据
assembleBlockAndCallbackFromBeginning(info, remoteCheck,
localCheck.getCauseOrThrow());
} else {
// 如果 localCheck.check 校验后,不是 dirty 的,那么就直接从断点续传的位置开始下载
// 这里执行一下相关的回调分发
okDownload.callbackDispatcher().dispatch()
.downloadFromBreakpoint(task, info);
}
} else {
Util.d(TAG, "breakpoint invalid: download from beginning because of "
+ "remote check not resumable " + task.getId() + " " + remoteCheck);
// 6. assemble block data
// 不能恢复下载,有异常
// 丢弃(删除)task对应的本地文件,准备重新下载
fileStrategy.discardProcess(task);
// 重新组装断点续传的分块数据
assembleBlockAndCallbackFromBeginning(info, remoteCheck,
remoteCheck.getCauseOrThrow());
}
} catch (IOException e) {
cache.setUnknownError(e);
break;
}
// 7. start with cache and info.
// DownloadCache / BreakpointInfo
// 上面处理完成后,都是在这里统一开始任务下载
start(cache, info);
// 再判断 本 Runnable 是否被取消了,因为上面的操作很多都是耗时的任务
if (canceled) break;
// 8. retry if precondition failed.
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);
}
}
这个方法中的代码注释,我有标记了 Remit.{序号}
,标记的位置就是执行 RemitStoreOnSQLite
的时机,注意看一下代码注释。
理解代码中的注释以及的实现时,需要结合
和 `` 这两个类的能力进行理解。
这里我们来看看 Remit.2
标记的代码位置,还要结合 Remit.1
位置的注释理解一下。
// 新建的 BreakpointInfo 信息,只有简单的信息,还没有分块信息
// Remit.2 这里创建了一个 BreakpointInfo 信息,
// 这个 store 也是 RemitStoreOnSQLite
info = store.createAndInsert(task);
public class RemitStoreOnSQLite implements RemitSyncExecutor.RemitAgent, DownloadStore {
@NonNull @Override public BreakpointInfo createAndInsert(@NonNull DownloadTask task)
throws IOException {
// 如果没添加到数据库中的时候,则直接插入到缓存即可
// 这里的真实情况是:
// 1. 首先由 onTaskStart 方法中,触发 executor.postSyncInfoDelay(id, delayMillis);
// 2. 然后代码马上就到了这里
// 如果是新建任务,则这里为空的,那么就直接插入到缓存即可,后面会因为 delay 时间的到来,自动将数据同步到数据库中
// 如果被取消后又被重启,这直接就更新到数据库中
if (remitHelper.isNotInDatabase(task.getId())) {
return sqliteCache.createAndInsert(task);
}
// 如果已经添加到数据库中,则创建并插入到数据库中
return onSQLiteWrapper.createAndInsert(task);
}
}
上面的这两个位置,都是在任务创建的时候,执行的 RemitStoreOnSQLite
操作时机。
那么,在下载过程中是什么时候执行 RemitStoreOnSQLite
操作的呢?
在下载的过程中我们知道,写数据到文件是在 FetchDataInterceptor.interceptFetch
中操作的。
public class FetchDataInterceptor implements Interceptor.Fetch {
/**
* 拦截并处理下载过程中的数据获取
*
* @param chain 下载链式调用对象,包含下载任务的相关信息和方法
* @return 返回本次获取的数据长度,如果返回-1,表示输入流已结束
* @throws IOException 如果在数据获取或写入过程中发生I/O错误,或者缓存被中断,将抛出此异常
*/
@Override
public long interceptFetch(DownloadChain chain) throws IOException {
// 检查缓存是否被中断,如果被中断,则抛出中断异常
if (chain.getCache().isInterrupt()) {
throw InterruptException.SIGNAL;
}
// 根据下载策略检查网络连接(仅在Wi-Fi环境下进行下载的任务会进行此检查)
OkDownload.with().downloadStrategy().inspectNetworkOnWifi(chain.getTask());
// 开始获取数据,从 connected.inputStream 中获取内容数据到readBuffer中,并返回获取到的长度
int fetchLength = inputStream.read(readBuffer);
// 如果数据获取结束,则返回-1
if (fetchLength == -1) {
return fetchLength;
}
// 将获取的数据写入文件,内部也会执行 下载信息持久化的同步操作
outputStream.write(blockIndex, readBuffer, fetchLength);
// 增加已回调的数据量
chain.increaseCallbackBytes(fetchLength);
// 如果满足进度回调的最小时间间隔,则进行一次进度回调,将这个时间间隔内累计获取到的数据进度一起回调出去
// 这里就正好对应了OkDownload在初始化是配置的最小回调时间间隔的设置,控制下载进度回调的频率
if (this.dispatcher.isFetchProcessMoment(task)) {
chain.flushNoCallbackIncreaseBytes();
}
return fetchLength;
}
}
这个类执行 RemitStoreOnSQLite
的操作是在:
// 将获取的数据写入文件,内部也会执行 下载信息持久化的同步操作
outputStream.write(blockIndex, readBuffer, fetchLength);
public class MultiPointOutputStream {
/**
* 同步方法,用于将数据写入到指定的块索引位置
* 如果任务已被取消,则无需写入,因为输出流已经关闭,
* 并且如果这是此任务块的第一次写入,不需要创建新的输出流
*
* @param blockIndex 要写入的块索引位置
* @param bytes 要写入的字节数组
* @param length 要写入的字节数组的长度
* @throws IOException 如果在写入过程中发生I/O错误
*/
public synchronized void write(int blockIndex, byte[] bytes, int length) throws IOException {
// if this task has been canceled, there is no need to write because of the output stream
// has been closed and there is no need to create a new output stream if this is a first
// write of this task block
// 检查任务是否已被取消,如果已被取消,则直接返回,无需执行写入操作
if (canceled) return;
// 获取对应块索引的输出流,并通过该流将字节数组写入
outputStream(blockIndex).write(bytes, 0, length);
// because we add the length value after flush and sync,
// so the length only possible less than or equal to the real persist length.
// 因为我们是在刷新和同步后添加长度值的,
// 所以长度只可能小于或等于实际的持久化长度
// 更新所有未同步长度的总和,累加
allNoSyncLength.addAndGet(length);
// 更新特定块索引的未同步长度,累加
noSyncLengthMap.get(blockIndex).addAndGet(length);
// 检查并执行数据的持久化操作
inspectAndPersist();
}
void inspectAndPersist() throws IOException {
if (syncException != null) throw syncException;
if (syncFuture == null) {
synchronized (syncRunnable) {
if (syncFuture == null) {
syncFuture = executeSyncRunnableAsync();
}
}
}
}
// convenient for test
Future executeSyncRunnableAsync() {
return FILE_IO_EXECUTOR.submit(syncRunnable);
}
void runSyncDelayException() {
try {
runSync();
} catch (IOException e) {
syncException = e;
Util.w(TAG, "Sync to breakpoint-store for task[" + task.getId() + "] "
+ "failed with cause: " + e);
}
}
}
而 syncRunnable
为:
this.syncRunnable = new Runnable() {
@Override
public void run() {
runSyncDelayException();
}
};
这样我们就可以知道,最后执行到 runSync
方法中。
/**
* 同步执行输出流的刷新操作
* 该方法负责按照指定的缓冲区间或和缓冲区大小来周期性地刷新输出流
* 它通过阻塞当前线程来实现流的同步刷新,直到满足特定条件才继续执行刷新操作
*
* @throws IOException 如果刷新过程中发生I/O错误
*/
void runSync() throws IOException {
// 记录输出流开始刷新的任务信息和配置参数
Util.d(TAG, "OutputStream start flush looper task[" + task.getId() + "] with "
+ "syncBufferIntervalMills[" + syncBufferIntervalMills + "] " + "syncBufferSize["
+ syncBufferSize + "]");
runSyncThread = Thread.currentThread();
// syncBufferIntervalMills 这个值,
// 为 DownloadTask 创建时配置的syncBufferIntervalMillis,
// 默认为 2000ms
long nextParkMills = syncBufferIntervalMills;
// 现将前面没有处理的进度全量处理一次
flushProcess();
while (true) {
// 阻塞当前线程指定的时间
parkThread(nextParkMills);
// 检查输出流的状态
inspectStreamState(state);
// 如果没有更多的流数据,我们将刷新所有数据并退出循环
if (state.isStreamsEndOrChanged()) {
// 记录流结束或状态改变的信息
Util.d(TAG, "runSync state change isNoMoreStream[" + state.isNoMoreStream + "]"
+ " newNoMoreStreamBlockList[" + state.newNoMoreStreamBlockList + "]");
// 如果还有未同步的数据,进行刷新
if (allNoSyncLength.get() > 0) {
flushProcess();
}
// 处理不再需要流的块列表
for (Integer blockIndex : state.newNoMoreStreamBlockList) {
final Thread parkedThread = parkedRunBlockThreadMap.get(blockIndex);
parkedRunBlockThreadMap.remove(blockIndex);
if (parkedThread != null) unparkThread(parkedThread);
}
// 如果没有更多的流数据,确保所有等待的线程都恢复运行
if (state.isNoMoreStream) {
final int size = parkedRunBlockThreadMap.size();
for (int i = 0; i < size; i++) {
final Thread parkedThread = parkedRunBlockThreadMap.valueAt(i);
// 确保线程恢复运行
if (parkedThread != null) unparkThread(parkedThread);
}
parkedRunBlockThreadMap.clear();
break;
} else {
continue;
}
}
// 如果不需要根据长度进行刷新,重置下一次阻塞的时间,并继续循环
if (isNoNeedFlushForLength()) {
nextParkMills = syncBufferIntervalMills;
continue;
}
// 计算下一次阻塞的时间
nextParkMills = getNextParkMillisecond();
if (nextParkMills > 0) {
continue;
}
// 刷新输出流
flushProcess();
nextParkMills = syncBufferIntervalMills;
}
// 记录输出流结束刷新的任务信息
Util.d(TAG, "OutputStream stop flush looper task[" + task.getId() + "]");
}
/**
* 刷新并同步所有待写入的数据到文件系统。
* 此方法确保输出流中的所有缓冲数据都被写入存储介质,并在成功同步后更新相应的元数据。
*
* @throws IOException 如果在刷新或同步过程中发生 I/O 错误。
*/
void flushProcess() throws IOException {
boolean success;
final int size;
synchronized (noSyncLengthMap) {
// 确保 noSyncLengthMap 的长度等于 outputStreamMap,为同步操作做准备。
size = noSyncLengthMap.size();
}
final SparseArray<Long> increaseLengthMap = new SparseArray<>(size);
try {
for (int i = 0; i < size; i++) {
final int blockIndex = outputStreamMap.keyAt(i);
// 因为我们是在刷新和同步之前获取未同步的长度值,
// 所以这个长度只能小于或等于实际已持久化的长度。
final long noSyncLength = noSyncLengthMap.get(blockIndex).get();
if (noSyncLength > 0) {
increaseLengthMap.put(blockIndex, noSyncLength);
final DownloadOutputStream outputStream = outputStreamMap
.get(blockIndex);
outputStream.flushAndSync();
}
}
success = true;
} catch (IOException ex) {
Util.w(TAG, "输出流刷新和同步数据到文件系统失败: " + ex);
success = false;
}
if (success) {
final int increaseLengthSize = increaseLengthMap.size();
long allIncreaseLength = 0;
for (int i = 0; i < increaseLengthSize; i++) {
final int blockIndex = increaseLengthMap.keyAt(i);
final long noSyncLength = increaseLengthMap.valueAt(i);
store.onSyncToFilesystemSuccess(info, blockIndex, noSyncLength);
allIncreaseLength += noSyncLength;
noSyncLengthMap.get(blockIndex).addAndGet(-noSyncLength);
Util.d(TAG, "输出流同步成功 (" + task.getId() + ") "
+ "block(" + blockIndex + ") " + " syncLength(" + noSyncLength + ")"
+ " currentOffset(" + info.getBlock(blockIndex).getCurrentOffset()
+ ")");
}
allNoSyncLength.addAndGet(-allIncreaseLength);
lastSyncTimestamp.set(SystemClock.uptimeMillis());
}
}
这里我们就全部分析完成了。