OkHttp 的 IO 操作和进度监听,四年Android面试遇到的问题整理

val sink = OutputStreamSink(getOutputStream(), timeout)
return timeout.sink(sink)
}

fun Socket.source(): Source {
val timeout = SocketAsyncTimeout(this)
val source = InputStreamSource(getInputStream(), timeout)
return timeout.source(source)
}

okio 将 socket 的 OutputStreamInputStream 转换成 SinkSource 的操作用到了适配器模式,或者说 okio 就是在用适配器将整个 Java 的 IO 世界适配到 ok 的 IO 世界,提供更简洁高效的 IO 操作。

sink = rawSocket.sink().buffer() 后面的 buffer() 这里也简单说一下,用 RealBufferedSink 封装了一下,类似装饰器模式(不是很严谨),增加了缓存操作,使得写入的数据会先写到缓存中,在合适时机在真正写入原来的 sink 中。buffer() 还有另一个很实用功能我们后面再说。

/**

  • Returns a new sink that buffers writes to sink. The returned sink will batch writes to sink.
  • Use this wherever you write to a sink to get an ergonomic and efficient access to data.
    */
    fun Sink.buffer(): BufferedSink = RealBufferedSink(this)

1.3、请求体的写入

至此,我们知道了请求行、请求头的写入以及 socket 从哪里来的,接下来就是请求体的写入了,让我们再回到 CallServerInterceptor 继续看。

class CallServerInterceptor {
// …
override fun intercept(chain: Interceptor.Chain): Response {
// …
if (HttpMethod.permitsRequestBody(request.method) && requestBody != null) {
// 有请求体,创建用来写入的 Sink,将请求体内容写入
val bufferedRequestBody = exchange.createRequestBody(request, false).buffer()
// 将请求体写入创建的 Sink 中,这里是阻塞的
requestBody.writeTo(bufferedRequestBody)
// 写完了关闭
bufferedRequestBody.close()
// …
} else {
// 没有请求体
exchange.noRequestBody()
}
// …
}
// …
}

class Exchange {
fun createRequestBody(request: Request, duplex: Boolean): Sink {
this.isDuplex = duplex
val contentLength = request.body!!.contentLength()
eventListener.requestBodyStart(call)
// 拿到 codec 的用于写入请求体的 sink
val rawRequestBody = codec.createRequestBody(request, contentLength)
// 包装一下,增加了长度相关的检查和出错的事件处理,不深入理解
return RequestBodySink(rawRequestBody, contentLength)
}
}

class Http1ExchangeCodec {
override fun createRequestBody(request: Request, contentLength: Long): Sink {
return when {
// HTTP/1 不支持
request.body != null && request.body.isDuplex() -> throw ProtocolException(
“Duplex connections are not supported for HTTP/1”)
// chunked 类型的请求体,长度未知
request.isChunked -> newChunkedSink() // Stream a request body of unknown length.
// 长度固定的请求体
contentLength != -1L -> newKnownLengthSink() // Stream a request body of a known length.
// 非法情况
else -> // Stream a request body of a known length.
throw IllegalStateException(
“Cannot stream a request body without chunked encoding or a known content length!”)
}
}
}

简单来说就是 Http1ExchangeCodec 提供一个写入请求体 RequestBodySink,封装后通过 RequestBody.writeTo 写入请求体。如果是上传文件之类的需求,这里的请求体会很大,有可能需要监听上传进度,就需要在这里做文章了。

RequestBody.writeTo 到底干了啥呢,来看下一个 File 的实现

abstract class RequestBody {
/** Returns a new request body that transmits the content of this. */
@JvmStatic
@JvmName(“create”)
fun File.asRequestBody(contentType: MediaType? = null): RequestBody {
return object : RequestBody() {
override fun contentType() = contentType

override fun contentLength() = length()

override fun writeTo(sink: BufferedSink) {
source().use { source -> sink.writeAll(source) }
}
}
}
}

这里将 File 转成 Source,然后 writeAll 全部写入?全部写入?那文件很大的话,内存不就爆掉了吗?说实话,第一次看到这里的时候我是有点懵的,仔细研究下才明白过来。

我们来看下写入用的 sink 创建的地方 val bufferedRequestBody = exchange.createRequestBody(request, false).buffer(),最后是个 buffer(),上面说了这个玩意是个 RealBufferedSink,我们看下它的 writeAll 方法。

internal actual class RealBufferedSink actual constructor(
@JvmField actual val sink: Sink
) : BufferedSink {
override fun writeAll(source: Source) = commonWriteAll(source)
override fun emitCompleteSegments() = commonEmitCompleteSegments()
}

internal inline fun RealBufferedSink.commonWriteAll(source: Source): Long {
var totalBytesRead = 0L
while (true) {
// 每次从 source 中读取 Segment.SIZE(8192)字节到缓存 buffer 中
val readCount: Long = source.read(buffer, Segment.SIZE.toLong())
// 读完了就返回
if (readCount == -1L) break
// 更新读到的字节数
totalBytesRead += readCount
// 这里才写入 sink 中
emitCompleteSegments()
}
return totalBytesRead
}

internal inline fun RealBufferedSink.commonEmitCompleteSegments(): BufferedSink {
check(!closed) { “closed” }
// 获取写入的字节数(其实里面的逻辑我也没深入研究)
val byteCount = buffer.completeSegmentByteCount()
// 写入原始的 sink
if (byteCount > 0L) sink.write(buffer, byteCount)
return this
}

核心原理就在 RealBufferedSink.commonWriteAll 里面,可以看到每次只往 buffer 里写 8192 的字节数,这个字节数刚好是一个 Segment(你可以认为是个数组,用来做缓存管理,不了解也没关系)大小,然后写入到 sink 中,如此往复直到 source 里的内容被读完。这个过程是不是像极了用 Java IO 来读写文件或网络的过程,而 okio 都帮我们做好了。

回到 FileRequestBody,这样每次只从文件读 8KB,然后写入 socket,循环往复,并不是一次性读到内存里,再写入网路,所以内存也就不会爆掉。至此整个请求写入的 IO 操作就讲完了。

1.4、请求的响应处理

class CallServerInterceptor(private val forWebSocket: Boolean) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
// 读取响应行和响应头
responseBuilder = exchange.readResponseHeaders(expectContinue = false)!!
// 构建 Response
var response = responseBuilder
.request(request)
.handshake(exchange.connection.handshake())
.sentRequestAtMillis(sentRequestMillis)
.receivedResponseAtMillis(System.currentTimeMillis())
.build()
// 有响应体时提供 ResponseBody
response.newBuilder()
.body(exchange.openResponseBody(response))
.build()
// …
return response
}
}

// 这里不在贴 Exchange 里的代码了

class Http1ExchangeCodec {
override fun readResponseHeaders(expectContinue: Boolean): Response.Builder? {
// 用 headersReader 读取响应行
val statusLine = StatusLine.parse(headersReader.readLine())
val responseBuilder = Response.Builder()
.protocol(statusLine.protocol)
.code(statusLine.code)
.message(statusLine.message)
// 读取响应头
.headers(headersReader.readHeaders())
// …
}

override fun openResponseBodySource(response: Response): Source {
return when {
// 没有 body
!response.promisesBody() -> newFixedLengthSource(0)
// chunked 类型响应体,长度未知
response.isChunked -> newChunkedSource(response.request.url)
else -> {
val contentLength = response.headersContentLength()
if (contentLength != -1L) {
// 响应体长度固定
newFixedLengthSource(contentLength)
} else {
// 响应体长度未知
newUnknownLengthSource()
}
}
}
}
}

和写入请求的过程差不多,先读取响应行和响应头,最后提供响应体 ResponseBody 供读取,我们在 Callback.onResponse 回调中拿到的 Response 中的 ResponseBody 还只有一个 BufferedSource,响应体并没有完全返回,想想如果响应体很大,全部放到内存里返回也会爆内存的,所以这里只给了个 BufferedSource 供我们自己处理。

至此,OkHttp 请求和响应的 IO 操作就讲完了,接下来我们就来搞一个拦截器来提供通用的上传和下载的进度监听。

2、OkHttp 上传下载进度监听

2.1、如何实现

通过上面的 IO 操作介绍,我们已经知道了上传请求体最终会到 RequestBody.writeTo,我们可以在上传东西的时候在这里监听,比如上传文件,计算总体长度,然后每次上传一段(比如 8192 字节),回调进度,直到上传完成;同样的,下载的时候,我们需要在 Callback.onResponse 里监听 Responsebody.source 的读取进度。

但是这样实现一方面不统一,需要各个用到的地方自己实现按进度上传/下载的逻辑,而且不同的请求体或响应体的处理方式也不统一,业务侵入性太强。我们希望能够尽可能统一简单地提供进度监听的功能,就需要自定义拦截器,拦截 RequestBodyResponsebody,并替换掉原来的写入和读取操作,进而实现统一的进度监听。

2.2、想怎么用

我们首先要搞清楚这个功能最终对外暴露的 api,就是要让别人怎么用。
我在 github.com/funnywolfda… 里面提供了一套实现方案,欢迎大家提意见。我们先来看下怎么用。

// 初始化 client 时,添加下拦截器
OkHttpClient.Builder()
.addInterceptor(ProgressIntercept)
.build()
// 上传和下载进度监听
Request.Builder()
.uploadProgress(object: OkUploadListener {
override fun upload(curr: Long, contentLength: Long) {
// 当前上传的长度和需要上传的总长度
Log.d(tag, “Upload: c u r r / curr/ curr/contentLength”)
}
})
.downloadProgress(object: OkDownloadListener {
override fun download(curr: Long, contentLength: Long) {
// 当前下载的长度和需要下载的总长度
Log.d(tag, “Download: c u r r / curr/ curr/contentLength”)
}
})

首先我们初始化 OkHttpClient 的时候需要添加下 ProgressIntercept 拦截器,这里面有所有的逻辑处理。在构建请求的时候可以用扩展方法 uploadProgress 提供监听上传进度的回调,downloadProgress 提供下载进度的回调。最后拿 Request 去请求就行,外部的 RequestBodyResponseBody 正常使用,不需要关系内部细节。

2.3、具体实现

先来看 Request 的构建,这个比较简单,Request.Builder 提供 tag 方法方便我们在请求里放入自定义的数据,这里放了两个进度监听的 listener 用于回调。

fun Request.Builder.uploadProgress(listener: OkUploadListener?) = tag(OkUploadListener::class.java, listener)
fun Request.Builder.downloadProgress(listener: OkDownloadListener?) = tag(OkDownloadListener::class.java, listener)

拦截器的实现也不复杂,只是在请求前将原来的 RequestBody 替换成带上传进度回调的 ProgressRequestBody,响应回来后将原 ResponseBody 替换成带下载进度回调的 ProgressResponseBody。具体的进度如何回调就藏在这两个自定义类里面。

object ProgressIntercept: Interceptor {

override fun intercept(chain: Interceptor.Chain): Response {
val rawRequest = chain.request()
val uploadListener = rawRequest.tag(OkUploadListener::class.java)
val downloadListener = rawRequest.tag(OkDownloadListener::class.java)
// 替换请求 body 实现上传的进度监听
val request = replaceRequestBody(rawRequest, uploadListener)
val response = chain.proceed(request)
// 替换相应 body 实现下载的进度监听
return replaceResponseBody(response, downloadListener)
}

private fun replaceRequestBody(request: Request, listener: OkUploadListener?): Request {
val body = request.body
if (body == null || listener == null) {
return request
}
return request.newBuilder()
.method(request.method, ProgressRequestBody(body, listener))
.build()
}

private fun replaceResponseBody(response: Response, listener: OkDownloadListener?): Response {

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级安卓工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Android移动开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频
如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Android)
img

最后相关架构及资料领取方式:

点击我的GitHub免费领取获取往期Android高级架构资料、源码、笔记、视频。高级UI、性能优化、架构师课程、NDK、混合式开发(ReactNative+Weex)微信小程序、Flutter全方面的Android进阶实践技术,群内还有技术大牛一起讨论交流解决问题。

视频**
如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Android)
[外链图片转存中…(img-PNn1xTRY-1710846219968)]

最后相关架构及资料领取方式:

点击我的GitHub免费领取获取往期Android高级架构资料、源码、笔记、视频。高级UI、性能优化、架构师课程、NDK、混合式开发(ReactNative+Weex)微信小程序、Flutter全方面的Android进阶实践技术,群内还有技术大牛一起讨论交流解决问题。

  • 14
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值