前言
CronetDataSource
是 Media3
中用于基于 Chrome 网络库(Cronet)进行网络数据传输的数据源。Cronet 是 Chrome 浏览器网络堆栈的 Android 版本,它提供了与原生 Android 网络库相比的更多功能和更好的性能。顺手一提,ExoPlayer
是 Media3 中 Player
接口的默认实现。
CronetDataSource入口
java 代码解读复制代码public class CronetDataSource extends BaseDataSource implements HttpDataSource {
static {
MediaLibraryInfo.registerModule("media3.datasource.cronet");
}
}
进入 CronetDataSource 映入眼帘的是 这个静态模块的注册,在MediaLibraryInfo注入模块,我想应该主要是用于调试打印。
成员变量
java 代码解读复制代码public static final class Factory implements HttpDataSource.Factory {
@Nullable private final CronetEngine cronetEngine;
private final Executor executor;
private final RequestProperties defaultRequestProperties;
@Nullable private final DefaultHttpDataSource.Factory internalFallbackFactory;
@Nullable private HttpDataSource.Factory fallbackFactory;
@Nullable private Predicate contentTypePredicate;
@Nullable private TransferListener transferListener;
@Nullable private String userAgent;
private int requestPriority;
private int connectTimeoutMs;
private int readTimeoutMs;
private boolean resetTimeoutOnRedirects;
private boolean handleSetCookieRequests;
private boolean keepPostFor302Redirects;
}
-
cronetEngine:
这是一个CronetEngine
实例的引用。CronetEngine
是 Cronet 库的核心组件,负责执行网络请求。这个变量被标记为@Nullable
,意味着它可能为空,这通常是因为 CronetEngine 的初始化不是由这个工厂类直接负责的,或者在一些情况下可能不需要使用 CronetEngine。 -
executor:
这是一个Executor
实例,这里提供自定义线程池。 -
defaultRequestProperties:
这些是默认的 HTTP 请求属性,如请求头。这些属性会被应用到所有通过这个数据源发出的 HTTP 请求上。 -
internalFallbackFactory:
这是一个DefaultHttpDataSource.Factory
的实例,作为内部回退机制。当 CronetEngine 无法使用时,这个工厂会被用来创建备用的数据源实例。这个变量也被标记为@Nullable
,表明当CronetEngineWrapper
被删除时,这个回退机制可能也会被移除。 -
fallbackFactory:
这是一个更通用的HttpDataSource.Factory
实例,作为外部回退机制。当主要的数据源(即基于 Cronet 的数据源)无法使用时,这个工厂会被用来创建备用的数据源实例。同样,这个变量也被标记为@Nullable
。 -
contentTypePredicate:
这是一个Predicate
实例,用于过滤或选择特定内容类型的请求。只有满足这个条件的请求才会被这个数据源处理。 -
transferListener:
这是一个TransferListener
实例,用于监听数据传输过程中的事件,如传输开始、传输完成、传输失败等。 -
userAgent:
这是一个字符串,表示 HTTP 请求中的 User-Agent 头部。它用于标识发出请求的客户端类型和版本。 -
requestPriority:
这表示请求的优先级。在某些情况下,网络库可能会根据优先级来决定先处理哪些请求。 -
connectTimeoutMs:
这是连接超时的时间(以毫秒为单位)。如果在指定的时间内无法建立连接,请求将会失败。这里默认是8 * 1000,即八秒
-
readTimeoutMs:
这是读取超时的时间(以毫秒为单位)。如果在指定的时间内无法从连接中读取数据,请求将会失败。这里默认是8 * 1000,即八秒
-
resetTimeoutOnRedirects:
这是一个布尔值,表示是否在重定向时重置超时设置。如果为 true,每次重定向都会重新计算连接和读取超时。 -
handleSetCookieRequests:
这是一个布尔值,表示是否处理 Set-Cookie 响应头。如果为 true,数据源会处理服务器返回的 Set-Cookie 头部,并可能将其存储在本地以供后续请求使用。 -
keepPostFor302Redirects:
这是一个布尔值,表示在遇到 302 重定向时是否保持原始的 POST 请求方法。在某些情况下,服务器可能会用302 重定向来指示客户端用 GET 方法重新发送请求,但这个选项允许客户端保持原始的 POST 方法。
创建CronetDataSource
实例
java 代码解读复制代码
public Factory(CronetEngine cronetEngine, Executor executor) {
this.cronetEngine = Assertions.checkNotNull(cronetEngine);
this.executor = executor;
defaultRequestProperties = new RequestProperties();
internalFallbackFactory = null;
requestPriority = REQUEST_PRIORITY_MEDIUM;
connectTimeoutMs = DEFAULT_CONNECT_TIMEOUT_MILLIS;
readTimeoutMs = DEFAULT_READ_TIMEOUT_MILLIS;
}
这个Factory
构造器是用于创建CronetDataSource
实例。这里至少需要传进来两个参数,CronetEngine 和 线程池。
open 函数
接下来直达 public long open(DataSpec dataSpec)
这个函数有点长我们,一点点分析,头发又掉了几根了。
java 代码解读复制代码@UnstableApi
@Override
public long open(DataSpec dataSpec) throws HttpDataSourceException {
Assertions.checkNotNull(dataSpec);
Assertions.checkState(!opened);
// 重置同步条件
operation.close();
//重置连接超时
resetConnectTimeout();
//赋值当前数据源
currentDataSpec = dataSpec;
UrlRequest urlRequest;
try {
//构建一个 文件下载请求,如https://xxxx.com/1.ts ,这里面是靠CronetEngine去请求,并且添加监听请求回调
urlRequest = buildRequestBuilder(dataSpec).build();
//赋值当前网络请求
currentUrlRequest = urlRequest;
} catch (IOException e) {
if (e instanceof HttpDataSourceException) {
throw (HttpDataSourceException) e;
} else {
throw new OpenException(
e, dataSpec, PlaybackException.ERROR_CODE_IO_UNSPECIFIED, Status.IDLE);
}
}
//发起请求
urlRequest.start();
//回调 TransferListener
transferInitializing(dataSpec);
- 检查数据
- 重置同步条件
- 重置连接超时
- 赋值当前数据源
- 构建一个文件下载请求
- 发起请求
- 回调TransferListener
处理网络连接和打开网络连接时可能发生的异常。
java 代码解读复制代码try {
//这里开始阻塞,直到网络回调成功或者超时,当operation.open()后,这里会继续往下走。
boolean connectionOpened = blockUntilConnectTimeout();
@Nullable IOException connectionOpenException = exception;
if (connectionOpenException != null) {
@Nullable String message = connectionOpenException.getMessage();
if (message != null && Ascii.toLowerCase(message).contains("err_cleartext_not_permitted")) {
throw new CleartextNotPermittedException(connectionOpenException, dataSpec);
}
throw new OpenException(
connectionOpenException,
dataSpec,
PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED,
getStatus(urlRequest));
} else if (!connectionOpened) {
throw new OpenException(
new SocketTimeoutException(),
dataSpec,
PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_TIMEOUT,
getStatus(urlRequest));
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new OpenException(
new InterruptedIOException(),
dataSpec,
PlaybackException.ERROR_CODE_FAILED_RUNTIME_CHECK,
Status.INVALID);
}
这块主要用于处理网络连接和尝试打开网络连接时可能出现的异常。
特别注意一下 , boolean connectionOpened = blockUntilConnectTimeout()
这里会开始阻塞,直到网络回调成功或者超时,当在请求监听中调用 operation.open()后,这里会继续往下走。
处理异常的HTTP响应
java 代码解读复制代码// Check for a valid response code.
UrlResponseInfo responseInfo = Assertions.checkNotNull(this.responseInfo);
int responseCode = responseInfo.getHttpStatusCode();
Map> responseHeaders = responseInfo.getAllHeaders();
if (responseCode < 200 || responseCode > 299) {
if (responseCode == 416) {
long documentSize =
HttpUtil.getDocumentSize(getFirstHeader(responseHeaders, HttpHeaders.CONTENT_RANGE));
if (dataSpec.position == documentSize) {
opened = true;
transferStarted(dataSpec);
return dataSpec.length != C.LENGTH_UNSET ? dataSpec.length : 0;
}
}
byte[] responseBody;
try {
responseBody = readResponseBody();
} catch (IOException e) {
responseBody = Util.EMPTY_BYTE_ARRAY;
}
@Nullable
IOException cause =
responseCode == 416
? new DataSourceException(PlaybackException.ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE)
: null;
throw new InvalidResponseCodeException(
responseCode,
responseInfo.getHttpStatusText(),
cause,
responseHeaders,
dataSpec,
responseBody);
}
主要是处理HTTP响应的。它首先检查HTTP响应的状态码是否在200-299之间,这是HTTP协议中定义的成功的状态码范围。如果状态码不在这个范围内,它将根据状态码的不同情况进行不同的处理。
如果状态码是416(表示请求的Range不合法),它将尝试获取文档的大小,并与数据规范(dataSpec
)中的位置进行比较。如果位置与文档大小相同,那么它认为连接已经打开,并且可能已经读取了所有的数据,所以返回数据的长度。
如果状态码不是416,或者获取文档大小或比较位置时发生异常,它将尝试读取响应的主体内容。如果读取过程中发生IO异常,那么它将使用一个空的字节数组作为响应的主体。
最后,如果状态码不是416,会抛出一个InvalidResponseCodeException
,这个异常包含了响应的状态码、状态文本、可能的原因、响应头、数据规范和响应的主体内容。
检查内容类型
java 代码解读复制代码// Check for a valid content type.
Predicate contentTypePredicate = this.contentTypePredicate;
if (contentTypePredicate != null) {
@Nullable String contentType = getFirstHeader(responseHeaders, HttpHeaders.CONTENT_TYPE);
if (contentType != null && !contentTypePredicate.apply(contentType)) {
throw new InvalidContentTypeException(contentType, dataSpec);
}
}
这段代码的目的是确保HTTP响应的内容类型符合特定的要求或标准.
获取数据长度( Content Length)
经历的层层大关,终于获取到数据长度,一睹庐山真面目了。
java 代码解读复制代码
long bytesToSkip = responseCode == 200 && dataSpec.position != 0 ? dataSpec.position : 0;
if (!isCompressed(responseInfo)) {
if (dataSpec.length != C.LENGTH_UNSET) {
bytesRemaining = dataSpec.length;
} else {
long contentLength =
HttpUtil.getContentLength(
getFirstHeader(responseHeaders, HttpHeaders.CONTENT_LENGTH),
getFirstHeader(responseHeaders, HttpHeaders.CONTENT_RANGE));
bytesRemaining =
contentLength != C.LENGTH_UNSET ? (contentLength - bytesToSkip) : C.LENGTH_UNSET;
}
} else {.
bytesRemaining = dataSpec.length;
}
opened = true;
transferStarted(dataSpec);
skipFully(bytesToSkip, dataSpec);
- 计算需要跳过的字节数
bytesToSkip
如果我们请求了一个从非零位置开始的范围,并且收到了200而不是206,那么服务器不支持部分请求。我们需要手动跳到请求的位置。(这里似乎有点费解),由于服务器不支持部分请求,客户端需要手动跳过到请求的位置来开始读取数据。
注:206
是对资源某一部分的请求,该状态码表示客户端进行了范围请求,而服务器成功执行了这部分的GET请求。响应报文中包含由Content-Range指定范围的实体内容。
- 计算内容长度:
-
首先,代码检查响应是否已压缩(
isCompressed(responseInfo)
)。 -
如果响应未压缩,它将尝试获取内容长度:
-
如果
dataSpec.length
已设置(即不是C.LENGTH_UNSET
),则使用dataSpec.length
作为剩余字节数(bytesRemaining
)。 - 否则,从HTTP响应头中获取Content-Length
或Content-Range
来计算内容长度,并减去需要跳过的字节数(bytesToSkip
)。 -
如果响应被压缩,使用
dataSpec
长度。。
读数据 函数 read(byte[] buffer, int offset, int length)
这里主要调用 Android 自带的 ByteBuffer去读数据,比较简单一些,还是拆开一点点分析吧!!!
检查数据状态
java 代码解读复制代码 Assertions.checkState(opened);
if (length == 0) {
return 0;
} else if (bytesRemaining == 0) {
return C.RESULT_END_OF_INPUT;
}
1,先检查数据是否打开状态
2,检查数据长度
3,bytesRemaining == 0,即已经读取完所有数据。返回 C.RESULT_END_OF_INPUT
重置,阻塞和处理读异常情况
java 代码解读复制代码 ByteBuffer readBuffer = getOrCreateReadBuffer();
if (!readBuffer.hasRemaining()) {
operation.close();
readBuffer.clear();
readInternal(readBuffer, castNonNull(currentDataSpec));
//finished 在 UrlRequestCallback onSucceeded 设置为true ,代表 读取操作已成功完成
if (finished) {
bytesRemaining = 0;
return C.RESULT_END_OF_INPUT;
}
readBuffer.flip();
Assertions.checkState(readBuffer.hasRemaining());
}
如果读取缓冲区没有剩余的数据,同步条件重置,清空readBuffer, readBuffer.flip()
将 Buffer 从写模式切换到读模式
。readInternal 主要是阻塞和处理异常。
java 代码解读复制代码// Ensure we read up to bytesRemaining, in case this was a Range request with finite end, but
// the server does not support Range requests and transmitted the entire resource.
int bytesRead =
(int)
Longs.min(
bytesRemaining != C.LENGTH_UNSET ? bytesRemaining : Long.MAX_VALUE,
readBuffer.remaining(),
length);
readBuffer.get(buffer, offset, bytesRead);
if (bytesRemaining != C.LENGTH_UNSET) {
bytesRemaining -= bytesRead;
}
bytesTransferred(bytesRead);
return bytesRead;
-
从注释我们可以看到 支持
Range请求
,可以很大程度加快播放速度,特别是视频拖拽快进的时候。 -
bytesRead
取的bytesRemaining , Long.MAX_VALUE, readBuffer.remaining(), length
这几个参数里面最小的值。 -
使用
readBuffer.get()
方法将数据从读取缓冲区复制到提供的buffer
数组的指定偏移量处 -
调用
bytesTransferred()
方法来通知已传输的字节数。 -
最后实际读取的字节数
请求回调 UrlRequestCallback
onRedirectReceived
服务器可能会发送重定向响应,该响应会将流程转到 [onRedirectReceived()
] 方法。
onResponseStarted
java 代码解读复制代码public synchronized void onResponseStarted(UrlRequest request, UrlResponseInfo info) {
if (request != currentUrlRequest) {
return;
}
responseInfo = info;
operation.open();
}
这里的 operation.open()
,会唤醒 open
方法中 的 blockUntilConnectTimeout
。
应用跟踪所有重定向后,服务器会发送响应标头并调用 [onResponseStarted()
]方法。请求处于 Waiting for read() 状态。应用应调用 [read()
] 方法来尝试读取部分响应正文。调用 read()
后,请求处于读取状态,可能出现以下结果:
-
onReadCompleted 读取操作已成功完成,但还存在更多可用数据。系统会调用 [
onReadCompleted()
],并且请求再次处于正在等待 read() 状态。应用会再次调用 [read()
]方法,以继续读取响应正文。应用还可以使用 [cancel()
]方法停止读取请求。 -
onSucceeded 读取操作已成功,没有更多数据了。
-
onFailed 读取操作失败。
释放资源 close()
ini 代码解读复制代码public synchronized void close() {
if (currentUrlRequest != null) {
currentUrlRequest.cancel();
currentUrlRequest = null;
}
if (readBuffer != null) {
readBuffer.limit(0);
}
currentDataSpec = null;
responseInfo = null;
exception = null;
finished = false;
if (opened) {
opened = false;
transferEnded();
}
}
这里主要是释放资源,重置参数,和处理回调transferEnded()。
到此,全文结束了,谢谢大家。
参考资料
Cronet 请求生命周期 developer.android.google.cn/develop/con…
本人知识有限,如有描述错误之处,望虎正。
你的赞就像冬日暖阳,温暖心窝。
评论记录:
回复评论: