- 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
提供一个写入请求体 RequestBody
的 Sink
,封装后通过 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 都帮我们做好了。
回到 File
的 RequestBody
,这样每次只从文件读 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
的读取进度。
但是这样实现一方面不统一,需要各个用到的地方自己实现按进度上传/下载的逻辑,而且不同的请求体或响应体的处理方式也不统一,业务侵入性太强。我们希望能够尽可能统一简单地提供进度监听的功能,就需要自定义拦截器,拦截 RequestBody
和 Responsebody
,并替换掉原来的写入和读取操作,进而实现统一的进度监听。
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
去请求就行,外部的 RequestBody
和 ResponseBody
正常使用,不需要关系内部细节。
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 {
val body = response.body
if (body == null || listener == null) {
return response
}
return response.newBuilder()
.body(ProgressResponseBody(body, listener))
.build()
}
}
在介绍 ProgressRequestBody
和 ProgressResponseBody
之前,我们先来思考下 Sink
和 Source
的进度监听要怎么实现呢。我们来看下 Sink
接口,它只有一个 write
方法用来写入数据的,那其实我们把 Sink
装饰一下,在每次 write
的时候累加 byteCount
得到已经写入的长度,从而实现监听。
actual interface Sink : Closeable, Flushable {
// …
actual fun write(source: Buffer, byteCount: Long)
// …
}
上面的想法很理想,现实却是我们拿到的是 BufferedSink
,而不是 Sink
,BufferedSink
为了方便写入各种类型的数据,提供了一大堆写入方法,如果要按上面的想法,我们就需要重写这一大堆方法,并且在每次写入的时候都计算写入长度,这个工作量太大,难以实施。
「1.2」末尾的地方我提过 Sink.buffer()
返回的 RealBufferedSink
还有另外一个作用,这里就来揭晓。
internal actual class RealBufferedSink actual constructor(
@JvmField actual val sink: Sink
) : BufferedSink
我们可以看到 RealBufferedSink
的 sink
属性的类型是 Sink
,而不是 BufferedSink
,也就是说所有对 RealBufferedSink
的操作最终都会回归到 Sink.write
方法,这样就算我们只实现了对 Sink
的封装,增加了进度回调,只要我们再套一层 RealBufferedSink
就能保证所有调用都走我们封装好的 write
方法。说起来有点绕,我们直接看代码吧。
/**
- 带进度的 sink,每次读取都会回调累计写入的长度(从初始值开始)
*/
fun Sink.progress(initLength: Long = 0L, writeCallback: (Long)->Unit) = object: ForwardingSink(this) {
private var curr = initLength
init {
// 回调一次初始值
writeCallback(curr)
}
override fun write(source: Buffer, byteCount: Long) {
super.write(source, byteCount)
// 更新读取长度并回调
if (byteCount > 0) {
curr += byteCount
writeCallback(curr)
}
}
}
首先用一个扩展方法实现带进度回调的 Sink
,继承的 ForwardingSink
里面没啥逻辑,只是简单的装饰而已,我们复写 write
,每次写入操作都累加 byteCount
并回调,实现了写入进度的监听。
class ProgressRequestBody(
private val body: RequestBody,
private val listener: OkUploadListener
): RequestBody() {
// …
override fun writeTo(sink: BufferedSink) {
val contentLength = contentLength()
// 带进度的 sink,套上 buffer 保证所有的写入都经过 Sink.write 方法而不是 BufferedSink 的其他方法
val progressSink = sink.progress {
listener.upload(it, contentLength)
}.buffer()
body.writeTo(progressSink)
// *** 注意 ***
// progressSink 是个 buffer,走到这里 body 写完了,但是 buffer 里的不一定完全写入 sink
// 所以要手动 flush 一下,等待数据写入完毕
progressSink.flush()
}
// …
}
ProgressRequestBody
复写 writeTo
方法,把传入的 sink: BufferedSink
用 Sink.progress
实现了写入时进度监听,再套上一层 buffer()
保证外面的写入都只会调用到 Sink.progress
里面的 write
方法。写入操作有缓存,所以最后需要再 flush
一下。
这样我们的请求体上传的进度监听就完成了,响应体的下载进度监听逻辑类似,不过我们要实现的是 Source
和 ResponseBody
的进度封装。
class ProgressResponseBody(
private val body: ResponseBody,
private val listener: OkDownloadListener
): ResponseBody() {
// …
/**
- 带进度的 source,套上 buffer 保证所有的读取都经过 [Source.read] 方法而不是 [BufferedSource] 的其他方法
*/
private val progressSource = body.source().progress {
listener.download(it, contentLength)
}.buffer()
// …
override fun source(): BufferedSource = progressSource
// …
}
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)
最后
光有这些思路和搞懂单个知识的应用是还远远不够的,在Android开源框架设计思想中的知识点还是比较多的,想要搞懂还得学会整理和规划:我们常见的**Android热修复框架、插件化框架、组件化框架、图片加载框架、网络访问框架、RxJava响应式编程框架、IOC依赖注入框架、最近架构组件Jetpack等等Android第三方开源框架,**这些都是属于Android开源框架设计思想的。如下图所示:
这位阿里P8大佬针对以上知识点,熬夜整理出了一本长达1042页的完整版如何解读开源框架设计思想PDF文档,内容详细,把Android热修复框架、插件化框架、组件化框架、图片加载框架、网络访问框架、RxJava响应式编程框架、IOC依赖注入框架、最近架构组件Jetpack等等Android第三方开源框架这些知识点从源码分析到实战应用都讲的简单明了。
由于文档内容过多,篇幅受限,只能截图展示部分
整理不易,觉得有帮助的朋友可以帮忙点赞分享支持一下小编~
你的支持,我的动力;祝各位前程似锦,offer不断!!!
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
架、组件化框架、图片加载框架、网络访问框架、RxJava响应式编程框架、IOC依赖注入框架、最近架构组件Jetpack等等Android第三方开源框架这些知识点从源码分析到实战应用都讲的简单明了。**
由于文档内容过多,篇幅受限,只能截图展示部分
[外链图片转存中…(img-Wn6Jlang-1712788714618)]
[外链图片转存中…(img-TVGfvKyk-1712788714618)]
整理不易,觉得有帮助的朋友可以帮忙点赞分享支持一下小编~
你的支持,我的动力;祝各位前程似锦,offer不断!!!
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!