Author: istyras
Date: 2024-10-11
Update: 2024-10-11
本文是对 OkDownload源码分析 的第三篇。
上一篇分析,我们知道了 DownloadCall 最后会转到 DownloadChain 调用执行。
1. 真正开始下载任务的下载执行链管理器,针对单个block的下载; 2. 这里组装执行链:连接的执行链,获取数据的执行链; 3. 然后通过run方法启动。
DownloadChain 流程分析
`DownloanChain` 是一个 `Runnable` ,所以它启动的方法为 `run` 。
/**
* 执行当前链的任务
* 如果链已经完成,则抛出异常
* 设置当前线程为正在执行的线程
* 尝试启动任务,如果发生IOException,则中断执行并忽略异常
* 无论是否发生异常,都标记链为已完成,并异步释放连接
*/
@Override
public void run() {
// 检查链是否已经完成,如果已完成,则抛出异常
if (isFinished()) {
throw new IllegalAccessError("The chain has been finished!");
}
// 设置当前执行线程
this.currentThread = Thread.currentThread();
try {
// 尝试启动任务
start();
} catch (IOException ignored) {
// 如果发生IOException,中断执行并忽略异常
} finally {
// 标记链为已完成
finished.set(true);
// 异步释放连接
releaseConnectionAsync();
}
}
run方法内调用了start方法。
/**
* 开始下载任务
* 该方法负责初始化下载过程中的各个拦截器,并处理连接和获取数据的逻辑
* 它通过CallbackDispatcher来通知下载过程中的各种事件
*
* @throws IOException 如果在下载过程中发生I/O错误
* @throws InterruptException 如果下载过程中收到中断信号
*/
void start() throws IOException, InterruptException {
// 获取回调调度器实例
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);
}
对于
start方法,我们分两步来分析:连接部分 和 获取数据部分。
1.1、start 方法的连接部分
这里的 connectInterceptorList 维护了一个 Interceptor.Connect 的列表,从 start 接口的方法代码实现中可以看到,其添加了4个连接拦截器,分别为:RetryInterceptor、BreakpointInterceptor、HeaderInterceptor、CallServerInterceptor。现在先分析一下这四个连接拦截器的工作流程。
1.1.1、首先,连接拦截器的启动是在 processConnect() 方法中开始的,代码如下:
这里我们先不考虑中断的情况,按照正常的下载流程,需要一直到完成的流程来分析。
在 start 方法中,connectIndex = 0,所以,这里从 connectInterceptorList 从第一个连接拦截器开始。
1.1.2、RetryInterceptor 分析
/**
* 拦截下载连接,处理连接过程中的重试和异常捕获
* 此方法主要用于在进行下载连接时,通过缓存机制和异常处理,确保连接的稳定性和可靠性
*
* @param chain 下载链,包含下载过程中的所有步骤和数据
* @return 返回处理连接的结果
* @throws IOException 如果连接过程中发生无法重试的IO异常,则抛出此异常
*/
@NonNull @Override
public DownloadConnection.Connected interceptConnect(DownloadChain chain) throws IOException {
// 获取下载链中的缓存对象
final DownloadCache cache = chain.getCache();
while (true) {
try {
// 检查是否中断下载操作
if (cache.isInterrupt()) {
throw InterruptException.SIGNAL;
}
// 成功处理连接并返回连接结果,这里下载链的下一个任务是 BreakpointInterceptor ,
// 在其 interceptConnect 内中处理断点续传,并返回结果,同时会抛出一个 RetryException 异常
return chain.processConnect();
} catch (IOException e) {
// 处理IO异常,对于重试异常进行特殊处理
if (e instanceof RetryException) {
// 重置连接状态,准备重新尝试连接
chain.resetConnectForRetry();
continue;
}
// 捕获其他异常,进行记录或处理
chain.getCache().catchException(e);
// 如果输出流在连接时遇到阻塞异常,进行处理
chain.getOutputStream().catchBlockConnectException(chain.getBlockIndex());
// 抛出异常,终止循环
throw e;
}
}
}
这里,不考虑异常的情况下,就直接执行下一个拦截器 return chain.processConnect(); 。
1.1.3、BreakpointInterceptor 分析
因为内部要支持断点续传功能,所以需要对请求的头部信息进行加工,加上相关的头部信息,用来诊断指定的资源能否支持断点续传以及分块下载的能力。所以
BreakpointInterceptor处理的就是这个逻辑。
/**
* 拦截连接下载过程
*
* 此方法主要用于处理连接下载过程中的拦截操作,检查是否需要从中断处继续下载,或者重试下载任务
* 它通过检查资源的块数和完整性来决定如何处理当前的下载任务
*
* @param chain 下载链表,包含下载所需的所有信息和当前状态
* @return 返回连接下载的状态信息
* @throws IOException 如果存储更新失败或出现其他I/O错误,抛出此异常
*/
@NonNull @Override
public DownloadConnection.Connected interceptConnect(DownloadChain chain) throws IOException {
// 处理连接下载,下一个拦截器是 HeaderInterceptor ,
// 而这个拦截器没有多余的处理
final DownloadConnection.Connected connected = chain.processConnect();
// 获取下载信息
final BreakpointInfo info = chain.getInfo();
// 如果缓存被中断,抛出中断异常
if (chain.getCache().isInterrupt()) {
throw InterruptException.SIGNAL;
}
// 如果资源只有一个块且不是分块下载,则进行特殊处理
if (info.getBlockCount() == 1 && !info.isChunked()) {
// 获取准确的内容长度范围,因为是只有一个下载块,所以从连接返回的响应头信息中获取的是全内容长度
final long blockInstanceLength = getExactContentLengthRangeFrom0(connected);
// 获取总的实例长度,这个长度是前面ConnectTrail中获取到的,所以是准确的
final long infoInstanceLength = info.getTotalLength();
// 如果长度不一致,则进行重置block信息,因为 ConnectTrail 的获取与真正开始请求得到的响应信息不一致
// 这里的长度不一致表示后端的资源发生了变化
if (blockInstanceLength > 0 && blockInstanceLength != infoInstanceLength) {
Util.d(TAG, "SingleBlock special check: the response instance-length["
+ blockInstanceLength + "] isn't equal to the instance length from trial-"
+ "connection[" + infoInstanceLength + "]");
// 获取唯一的块信息
final BlockInfo blockInfo = info.getBlock(0);
// 判断是否从断点开始下载
boolean isFromBreakpoint = blockInfo.getRangeLeft() != 0;
// 创建新的块信息
final BlockInfo newBlockInfo = new BlockInfo(0, blockInstanceLength);
// 重置并添加新的块信息
info.resetBlockInfos();
info.addBlock(newBlockInfo);
// 如果是断点下载,抛出重试异常,因为没有办法继续下载,只能抛出重试异常,从头开始下载
if (isFromBreakpoint) {
final String msg = "Discard breakpoint because of on this special case, we have"
+ " to download from beginning";
Util.w(TAG, msg);
throw new RetryException(msg);
}
// 通知从头开始下载
OkDownload.with().callbackDispatcher().dispatch()
.downloadFromBeginning(chain.getTask(), info, CONTENT_LENGTH_CHANGED);
}
}
// 更新存储信息
final DownloadStore store = chain.getDownloadStore();
try {
if (!store.update(info)) {
throw new IOException("Update store failed!");
}
} catch (Exception e) {
throw new IOException("Update store failed!", e);
}
// 返回连接状态
return connected;
}
这个断点续传的拦截器中,连接方法开始的地方就执行 chain.processConnect(); ,所以这里继续调用下一个拦截器了。
这里,在等到
chain.processConnect();返回结果后,会解析连接请求的响应头不信息,然后
1.1.4、HeaderInterceptor 分析
/**
* 对连接头部进行处理的拦截器,用于给下载请求连接的header信息做处理
*
* @param chain 下载链,包含下载任务的相关信息和当前的下载连接
* @return 返回一个DownloadConnection.Connected对象,表示已连接到服务器
* @throws IOException 如果在连接过程中发生错误,例如未找到区块信息
*/
@NonNull
@Override
public DownloadConnection.Connected interceptConnect(DownloadChain chain) throws IOException {
// 获取下载链中的基本信息
final BreakpointInfo info = chain.getInfo();
// 在这里开始创建一个下载连接对象,如果连接对象为空,则创建一个连接对象
final DownloadConnection connection = chain.getConnectionOrCreate();
final DownloadTask task = chain.getTask();
// 添加用户自定义的请求头
final Map<String, List<String>> userHeader = task.getHeaderMapFields();
// 添加用户自定义的请求头字段,内部会校验用户是否有添加 If-Match 和 Range 的头部信息,
// 这两个信息,只能有内部添加,外部不允许用户自定义添加
if (userHeader != null) Util.addUserRequestHeaderField(userHeader, connection);
// 如果没有自定义用户代理或用户代理不包含USER_AGENT,则添加默认的用户代理
if (userHeader == null || !userHeader.containsKey(USER_AGENT)) {
Util.addDefaultUserAgent(connection);
}
// 添加范围请求头,用于指定下载的字节范围
final int blockIndex = chain.getBlockIndex();
final BlockInfo blockInfo = info.getBlock(blockIndex);
if (blockInfo == null) {
throw new IOException("No block-info found on " + blockIndex);
}
String range = "bytes=" + blockInfo.getRangeLeft() + "-";
range += blockInfo.getRangeRight();
connection.addHeader(RANGE, range);
// 记录调试信息,关于范围请求的详细信息
Util.d(TAG, "AssembleHeaderRange (" + task.getId() + ") block(" + blockIndex + ") "
+ "downloadFrom(" + blockInfo.getRangeLeft() + ") currentOffset("
+ blockInfo.getCurrentOffset() + ")");
// 如果存在ETag,则添加到请求头中
final String etag = info.getEtag();
if (!Util.isEmpty(etag)) {
connection.addHeader(IF_MATCH, etag);
}
// 检查是否中断下载
if (chain.getCache().isInterrupt()) {
throw InterruptException.SIGNAL;
}
// 通知监听器连接开始
OkDownload.with().callbackDispatcher().dispatch()
.connectStart(task, blockIndex, connection.getRequestProperties());
// 处理连接过程,传递到下一个连接拦截器上,这里会等,等到所有的拦截器都处理完毕了才会回来
// 也就是连接完成了,下面可以拿到连接的返回
DownloadConnection.Connected connected = chain.processConnect();
// 再次检查是否中断下载
if (chain.getCache().isInterrupt()) {
throw InterruptException.SIGNAL;
}
// 获取响应头字段
Map<String, List<String>> responseHeaderFields = connected.getResponseHeaderFields();
if (responseHeaderFields == null) responseHeaderFields = new HashMap<>();
// 连接返回后,通知监听器连接结束
OkDownload.with().callbackDispatcher().dispatch().connectEnd(task, blockIndex,
connected.getResponseCode(), responseHeaderFields);
// 检查预设条件是否失败
final DownloadStrategy strategy = OkDownload.with().downloadStrategy();
final DownloadStrategy.ResumeAvailableResponseCheck responseCheck =
strategy.resumeAvailableResponseCheck(connected, blockIndex, info);
// 这里会throw ResumeFailedException 和 ServerCanceledException 异常
// 当连接请求的header信息服务端不能满足预设条件,抛出异常 ResumeFailedException
responseCheck.inspect();
// 计算内容长度
final long contentLength;
final String contentLengthField = connected.getResponseHeaderField(CONTENT_LENGTH);
if (contentLengthField == null || contentLengthField.length() == 0) {
final String contentRangeField = connected.getResponseHeaderField(CONTENT_RANGE);
contentLength = Util.parseContentLengthFromContentRange(contentRangeField);
} else {
contentLength = Util.parseContentLength(contentLengthField);
}
// 设置响应内容长度并返回连接结果
chain.setResponseContentLength(contentLength);
return connected;
}
HeaderInterceptor 处理的工作为:
- 开始创建真正的连接对象
chain.getConnectionOrCreate();; - 给连接请求的header进行外部添加额外头部信息的校验;
- 添加断点续传需要的特定 Range 信息;
- 继续传递给下一个连接拦截器
chain.processConnect();,然后等待返回; - 得到连接返回后,对连接返回的响应头信息做提取与分析判断,判断该资源能否正常访问、能否支持分开请求;
- 可以先看一下
CallServerInterceptor的处理逻辑; - 从
CallServerInterceptor的处理逻辑看,请求完成后的返回相应数据就return回来了; - 收到连接返回后,这里就继续下面的逻辑,对返回的响应信息进行相关的校验;
- 然后return后给上层。
1.1.5、CallServerInterceptor 分析
@NonNull @Override
public DownloadConnection.Connected interceptConnect(DownloadChain chain) throws IOException {
OkDownload.with().downloadStrategy().inspectNetworkOnWifi(chain.getTask());
OkDownload.with().downloadStrategy().inspectNetworkAvailable();
return chain.getConnectionOrCreate().execute();
}
CallServerInterceptor 这里处理网络可用性判断,然后就执行实际的网络请求,将请求的返回给上层。
1.1.6、DownloadChain#getConnectionOrCreate 分析
/**
* 获取下载连接,如果不存在则创建新的连接
* 此方法是线程安全的,确保在多线程环境下不会创建多个连接
*
* @return 返回现有的或新创建的下载连接
* @throws IOException 如果创建连接过程中发生I/O错误
*/
@NonNull public synchronized DownloadConnection getConnectionOrCreate() throws IOException {
// 检查是否中断,如果是,抛出中断异常
if (cache.isInterrupt()) throw InterruptException.SIGNAL;
// 如果连接为空,表示需要创建新的连接
if (connection == null) {
// 定义URL变量,用于创建连接
final String url;
// 检查是否存在重定向位置,如果有,使用重定向后的URL
final String redirectLocation = cache.getRedirectLocation();
if (redirectLocation != null) {
url = redirectLocation;
} else {
// 如果没有重定向,使用原始的URL
url = info.getUrl();
}
// 打印日志,记录创建连接的URL
Util.d(TAG, "create connection on url: " + url);
// 创建新的下载连接
connection = OkDownload.with().connectionFactory().create(url);
}
// 返回现有的或新创建的连接
return connection;
}
到此为止,
start方法的连接部分就分析完成了,下面我们分析start方法的获取数据部分。
1.2、start 方法的获取数据部分
获取数据部分中 fetchInterceptorList 维护了一个 Interceptor.Fetch 拉取数据拦截器的列表,从 start 方法看到,该列表添加了三个拦截器:RetryInterceptor、BreakpointInterceptor、FetchDataInterceptor。
现在分析一下这3个连接拦截器的工作流程。
1.2.1、首先,获取数据拦截器的启动在 processFetch(); 方法中开始的,代码如下:
这里我们先不考虑中断的情况,按照正常的下载流程,需要一直到完成的流程来分析。
在 start 方法中,fetchIndex = 0,所以,这里从 fetchInterceptorList 从第一个获取数据拦截器开始。
1.2.2、RetryInterceptor 分析
RetryInterceptor 的 interceptFech 没有对数据获取做多余的处理,只是负责catch异常,然后直接就传递到下一个拦截器上。
1.2.2、BreakpointInterceptor 分析
/**
* 重写拦截下载方法,用于处理下载过程中的数据拦截和校验
*
* @param chain 下载链式调用对象,包含下载所需的所有信息和控制方法
* @return 返回本次下载拦截过程中已获取的数据长度
* @throws IOException 当下载数据长度与响应内容长度不一致时抛出此异常
*/
@Override
public long interceptFetch(DownloadChain chain) throws IOException {
// 获取响应内容的总长度
// 在 HeaderInterceptor 的 interceptConnect 中进行设置的
// 设置响应内容长度并返回连接结果
// chain.setResponseContentLength(contentLength);
final long contentLength = chain.getResponseContentLength();
// 获取当前处理的数据块索引
final int blockIndex = chain.getBlockIndex();
// 判断内容长度是否为非分块传输编码,即内容长度是否确定
final boolean isNotChunked = contentLength != CHUNKED_CONTENT_LENGTH;
// 初始化已获取的数据长度,对下面的while中得到的processFetchLength进行累加
long fetchLength = 0;
// 初始化本次处理获取的数据长度
long processFetchLength;
// 获取多点输出流对象,用于写入下载的数据块
final MultiPointOutputStream outputStream = chain.getOutputStream();
try {
// 循环获取数据,直到没有更多数据可获取
while (true) {
// 执行一次数据获取操作,
// 这个地方需要特别注意,loopFetch方法内部的操作以及当前这个方法是怎么进来的,
// 以及进来的时候对fetchIndex做了++的操作,即已经指向了FetchDataInterceptor,
// 通过loopFetch代码的分析,可以知道,loopFetch的方法,始终触发的是第3个连接拦截器,
// 也就是 FetchDataInterceptor 的interceptFetch方法,所有循环获取数据的逻辑都在这个方法中。
processFetchLength = chain.loopFetch();
// 如果本次获取的数据长度为-1,表示没有更多数据,结束循环
if (processFetchLength == -1) {
break;
}
// 累加本次获取的数据长度到总长度中
fetchLength += processFetchLength;
}
} finally {
// 完成下载,刷新数据并增加字节计数,但不回调
chain.flushNoCallbackIncreaseBytes();
// 如果用户没有取消下载,则标记数据块写入完成
if (!chain.getCache().isUserCanceled()) outputStream.done(blockIndex);
}
// 如果内容长度是确定的(非分块传输编码)
if (isNotChunked) {
// 检查并处理本地持久化数据
outputStream.inspectComplete(blockIndex);
// 校验获取的数据长度是否与响应内容长度一致
if (fetchLength != contentLength) {
// 如果不一致,抛出异常
throw new IOException("Fetch-length isn't equal to the response content-length, "
+ fetchLength + "!= " + contentLength);
}
}
// 返回本次下载拦截过程中已获取的数据长度
return fetchLength;
}
1.2.3、FetchInterceptor 分析
这个拦截器是真正对下载数据进行获取与存储的地方,
inputStream.read(readBuffer);从connected的返回数据流中一段一段的读取内容,然后写入到对应文件的相应位置。
/**
* 拦截并处理下载过程中的数据获取
*
* @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;
}
这里每执行一次,会回到 BreakpointInterceptor.interceptFetch 方法的循环体内,所以最终的全部都下载完成后,最后回到 BreakpointInterceptor.interceptFetch 方法体中,跳出循环体,继续下方的代码执行。在这里最终对 outputStream 进行是否完成的标记处理。
到此,对 `DownloadChain` 就分析完成了。
单纯的下载流程,大部分就都分析完成了。
当然,我们在分析的过程中,有意的忽略了下载过程中持久化各种状态数据的过程,因为我打算将这部分的实现单独出来分析。
至于,对Http网络框架的使用上,应该比较简单就不进行特别的分析,这部分代码看一下就明白。
所以,下一篇,我们就开始分析下载信息存储相关的功能实现。