前言概要:

1,进大公司还是小公司,真的有的选?
2,校招的潜规则
3,校招想进大厂怎么办
4,社招的潜规则
5,社招想进大厂怎么办?

前言

现在Android界的网络请求已经是OkHttp和Retrofit的天下了,Retrofit本质上也是将请求委托给了OkHttp,所以我们如果想要能够更加全面的使用和掌握OkHttp,了解其源码是必不可少的。 如果对Http还有不了解的小伙伴,可以一下这篇文章,带你全面掌握Http协议 面试官的这份HTTP灵魂追问你Hold住吗?

作者:Mlx
链接:https://juejin.im/post/6881436122950402056

OK,出发~

简单使用

同步方式和异步方式殊途同归,而且异步方式更多了异步和线程的概念,所以本文将直接分析异步,同步的话流程也是一样的。

val client=OkHttpClient().newBuilder().build()
val request=Request.Builder().url("https://www.baidu.com").build()
val call = client.newCall(request)
call.enqueue(object :Callback{override fun onFailure(call: Call, e: IOException) {println("onFailure:$e")}override fun onResponse(call: Call, response: Response) {println("onResponse:${response.body?.string()}")}})

这可以说是最简单的使用方式了。

使用 OkHttp 基本是以下四步:

  1. 创建 OkHttpClient 对象
  2. 创建Request请求对象
  3. 创建Call对象
  4. 同步请求调用call.execute();异步请求调用call.enqueue(callback)

这都是啥?一个简单的请求为啥需要这么多对象呢?

简单架构

那好,我们来分析一下他们的职责,其实啊一次网络请求和寄快递没啥区别。

首先,你在淘宝买了一个东西,发现不满意,要换货,你得去快递点(OkHttpClient ),然后填写快递单(Request)表明要寄给谁,寄的啥东西,填完以后你交个快递小哥,小哥把你这个快递给你寄出去(Call)。如果是同步请求(call.execute)的话,你就在这等着快递寄出去,然后卖家再发给你换好的货(Response)。显然,这么等的话怕是等个三四天,所以我们把快递给了快递小哥以后,告诉小哥我的电话(CallBack回调),说货到了通知我(触发回调 onResponse/onFailure)。

这样一来是不是很简单了

什么?还不够清晰明了?靠,只能把大宝贝儿掏出来了,看图

好,到此结束,OKHTTP分析完毕!

什么?这就完事了?我不是听说还有啥拦截器,什么责任链,什么缓存balabla的,你这叫我和面试官如何对线?

好吧,那我们接着下面看OkHttp做了什么。

构建OkHttpClient对象

我们一步一步来,先分析我们如何鬼使神差的走进快递点的,啊不,构建OkHttpClient对象的

val client=OkHttpClient().newBuilder().build()

这行代码是构建OkHttpClient对象的,很明显有个Builder对象,这明眼人一看就知道是构建者模式去构建OkHTTPClient对象。

什么?构建者模式是什么

构建者模式一般适用于创建比较复杂的对象使用的,可以屏蔽用户对细节的感知balabala

我最讨厌说定义了,我这么说吧,你如果创建一个对象构造函数中参数特别多的时候,大部分情况下你用构建者就行了。

为什么?就比如说你想整个车,你可以直接说,来奔驰,给我整俩车。你对如何造车完全不知道,但这并不影响你提车泡妹。

可是你不是说参数比较多的时候用构建者么?

没错,参数比较多的时候你是不是得一个个传参,还得小心参数的位置啥的?当然了,在Kotlin中可以指定哪个参数,它本质上和构建者也差不多。你也不想提车的时候还得小心翼翼的和厂家一一说明要求吧,我得有个四个轮的车,白色的,能坐四个人的等等要求。

所以构建者模式就是为了解决此问题,你可以没有要求,我就使用默认值,你可以有很多要求,甚至可以重复,反正我只在你提车的时候最终确认即可给你符合你要求的车。

所以如果使用构建者模式的话,我们不必为参数的位置和忘了传什么参数而烦忧,我们可以写出如下代码:

val client=OkHttpClient().newBuilder().dispatcher().callTimeout().authenticator().addInterceptor().cache().build()

你看你可以使用构建者模式去创建出符合你所有要求的对象,不用担心参数位置,或者忘记传哪个参数。

当然,本篇不是讲设计模式的,随后我将会开辟一个新的系列,专门讲设计模式。这里点到为止。

我们继续回到我们的OkHttp来,使用Builder对象创建了OkHttpclient对象过程中,其实给OkHttpclient创建了一系列的配置,我们这里简要的看一下,在newBuilder方法中创建了一个Builder对象:

class Builder constructor() {internal var dispatcher: Dispatcher = Dispatcher()//调度器,一会讲internal val interceptors: MutableList<Interceptor> = mutableListOf()//拦截器,一会也会讲internal val networkInterceptors: MutableList<Interceptor> = mutableListOf()//网络拦截器,也会讲...//定义很多其他对象
}

在这里省略了很多对象,只列出了三个比较重要的对象,一会都会讲的。

build方法真正构建了OkHttpclient对象:

fun build(): OkHttpClient = OkHttpClient(this)

这个this是什么?

这个build方法是Builder对象的build方法,this当然是Builder对象啦。这里真正创建了一个OkHTTPClient对象,并把Builder对象自身传了过去。在OkHttpclient对象的构造方法中有如下代码:

open class OkHttpClient internal constructor(builder: Builder
) : Cloneable, Call.Factory, WebSocket.Factory {@get:JvmName("dispatcher") val dispatcher: Dispatcher = builder.dispatcher //调度器@get:JvmName("interceptors") val interceptors: List<Interceptor> =builder.interceptors.toImmutableList()//拦截器集合@get:JvmName("networkInterceptors") val networkInterceptors: List<Interceptor> =builder.networkInterceptors.toImmutableList()//网络拦截器集合
}

可以看到,最终builder对象中的拦截器全部给了OkHttpClient对象,这也正是构建者模式的体现之处。

简而言之,就是OkHttpClient里面定义了很多我们不知道的对象。正如你寄快递的时候,你并不知道快递是如何寄出去的,OkHttp帮你完成了很多的细节工作。

接下来就是构建Request对象了

构建Request对象

你想寄快递,必须填写快递单,写你要寄给谁,寄到哪里,怎么寄平邮还是快件,这一切就是Request对象的职责。它负责记录你的请求信息。

val request=Request.Builder().url("https://www.baidu.com").build()

经过上面的分析,我们已经知道,构建者Builder中所有的参数最终全部会给到Request对象中。所以我们直接看看Request对象的构造方法中有什么东西~

class Request internal constructor(@get:JvmName("url") val url: HttpUrl,//ur,也就是你要发送的地方@get:JvmName("method") val method: String, //请求方法,GET/POST@get:JvmName("headers") val headers: Headers,//请求头,一些备注信息或者请求信息@get:JvmName("body") val body: RequestBody?,//请求的内容,就是你具体要发送的快递不能internal val tags: Map<Class<*>, Any>
) {....
}

注释已经讲的很清楚了,关于HTTP网络这块,大家不太懂的可以看我之前的文章,从HTTP表层到原理全部都有讲解。

接下来也没啥内容了,该轮到Call对象了

创建Call对象

val call = client.newCall(request)

正如你想寄快递,你必须填写好快递单交给快递站的小哥才行。

这里你已经填好了快递单,你需要交给快递站,然后快递站派出快递小哥帮你发快递。

快递小哥就是Call,我们看看是Call是什么东东:

interface Call : Cloneable {fun request(): Request//返回请求信息,就是返回你填写的快递单@Throws(IOException::class)fun execute(): Response//同步请求,就是你在快递站傻等~fun enqueue(responseCallback: Callback)//异步请求,接受回调参数。就是你告诉快递小哥你电话,让小哥寄快递fun cancel()//取消请求,就是你取消寄快递fun isExecuted(): Boolean//是否正在请求,就是你的快递寄出去了没,寄到哪了fun isCanceled(): Boolean//是否是取消状态fun timeout(): Timeout //超时时间,就是你这个快递寄出去半年了都没动静,你就认为中途丢了public override fun clone(): Callfun interface Factory {fun newCall(request: Request): Call}
}

Call本质上是个接口,也符合我们的面向接口编程。它里面定义了很多方法,在上面注释中我也都详细阐述了。可是接口并不能具体帮我们干活啊,就像你知道你要找一个快递小哥,快递小哥是一个抽象的概念并不是一个具体存在的人,你只知道快递小哥能帮你寄快递,这个寄快递就是一个抽象的方法。所以我们需要知道到底是哪个快递小哥帮我们寄了快递。

我们看一下这个newCall方法:

//OkHttpClient.kt
override fun newCall(request: Request): Call = RealCall(this, request, forWebSocket = false)

啊知道了,RealCall才是真正为我们服务的那个快递小哥。创建RealCall的过程中,我们传进去一共三个参数,第一个this自然就是OkHttpclient对象本身了,第二个参数是请求信息也就是快递单,第三个参数是用于指明不是webSocket请求。暂时不用管第三个参数

好,我们现在万事俱备只欠东风,所有的对象都已经创建完成了,可以寄快递了吧?

发送请求

发送请求就是将你的电话号告诉给快递小哥,快递小哥寄出去以后,收到快递再给你打电话告诉你快递到了或者丢了。对应的就是以下代码。

call.enqueue(object :Callback{override fun onFailure(call: Call, e: IOException) {println("onFailure:$e")}override fun onResponse(call: Call, response: Response) {println("onResponse:${response.body?.string()}")}
})

前面也说了call.enqueue是一个异步方法,我们既然是从源码开始学习的,自然不能只停留在表面,当然要深入源码去探索,看看这个发送的过程到底是怎么样的过程。在上面我们知道,这个快递小哥是一个宽泛的概念(接口),真正执行任务的时候要具体到某个人的(实现类),这个具体的人就是RealCall对象,所以我们看看RealCall.enqueue方法。

RealCall.enqueue

//RealCall.kt
override fun enqueue(responseCallback: Callback) {//检查是否执行了两遍,也就是说你是不是同一个快递想要发两次,这当然是不行的。check(executed.compareAndSet(false, true)) { "Already Executed" }//请求后会立即调用,相当于监听请求的开始事件callStart()//将请求交给调度器来决定什么时候请求client.dispatcher.enqueue(AsyncCall(responseCallback))
}

这个方法很有意思,前面两行注释已经有了,就不讲了,最后一行首先创建了一个AsyncCall对象,并且把回调给了AsyncCall。这里先不研究AsyncCall,一会再说。接下来调用了dispatcher.enqueue方法,什么时候发送和发送的过程交给了dispatcher(调度器)。由它来决定何时,如何发送。

不太明白?

dispatcher是调度员的意思,在你寄快递的例子中就是本地物流中心,它负责决定整个收发快递的流程,比如发送的策略,是否发送等等。

我们看一下定义

Dispatcher

class Dispatcher constructor() {//最大请求数,也就是最多只有64辆发送快递的车,用完了就没了@get:Synchronized var maxRequests = 64  //同一主机最大请求数量,加入你退货的目的地是北京,发往北京的快递车最多只有5辆@get:Synchronized var maxRequestsPerHost = 5//线程安全的单例模式,线程池的获取用于线程调度。@get:Synchronized@get:JvmName("executorService") val executorService: ExecutorServiceget() {if (executorServiceOrNull == null) {executorServiceOrNull = ThreadPoolExecutor(0, Int.MAX_VALUE, 60, TimeUnit.SECONDS,SynchronousQueue(), threadFactory("$okHttpName Dispatcher", false))}return executorServiceOrNull!!
}//准备的队列,也就是进入了打包的车private val readyAsyncCalls = ArrayDeque<AsyncCall>()//异步发送队列,也就是进入了准备出发的车private val runningAsyncCalls = ArrayDeque<AsyncCall>()//同步发送队列,同样是进入了准备出发的车private val runningSyncCalls = ArrayDeque<RealCall>()...
}

这个Dispatcher有上面这么几个比较重要的字段,通过上面的注释可以得到,Dispatcher对象定义了最大请求数和同一主机的最大请求数。。然后三个队列,我们只研究准备队列和异步发送队列,因为同步发送队列是一样的道理。

我们知道了这个Dispatcher对象的几个字段之后,我们接着看它的enqueue方法

Dispatcher.enqueue

//Dispatcher.kt
internal fun enqueue(call: AsyncCall) {//确保线程安全synchronized(this) {//把快递放到打包车中readyAsyncCalls.add(call)//如果不是websocket,前面可知forWebSocket为falseif (!call.call.forWebSocket) {//查询有没有退货到北京的车val existingCall = findExistingCallWithHost(call.host)//复用退货到北京的计数器callsPerHost,用于统计发往北京快递车数量if (existingCall != null) call.reuseCallsPerHostFrom(existingCall)}}//准备发送快递promoteAndExecute()
}

首先是放进准备队列中,也就是把快递放进了打包的车中。然后先看看有没有到北京的快递车,有的话直接用它的计数器,用来统计现在有几辆发往北京的快递车了,也就是检查同一个主机已经有几个请求了。然后准备发送

Dispatcher.promoteAndExecute

private fun promoteAndExecute(): Boolean {this.assertThreadDoesntHoldLock()//收集所有要发出去的快递,也就是要执行的请求val executableCalls = mutableListOf<AsyncCall>()val isRunning: Booleansynchronized(this) {val i = readyAsyncCalls.iterator()//得到迭代器对象用于遍历while (i.hasNext()) {//遍历val asyncCall = i.next()//得到下一个快递//很重要的判断,如果64个快递车都出发了,也就没快递车了,那么将无法请求if (runningAsyncCalls.size >= this.maxRequests) break //发往北京的5辆快递车也都走了,那么继续下一个快递if (asyncCall.callsPerHost.get() >= this.maxRequestsPerHost) continue // Host max capacity.//上面都通过了,说明有车,并且发往北京的快递车也还有,那么就离开打包车i.remove()//把去往北京的快递车数量+1asyncCall.callsPerHost.incrementAndGet()//把快递保存起来executableCalls.add(asyncCall)//将快递放到即将出发的车中runningAsyncCalls.add(asyncCall)}isRunning = runningCallsCount() > 0}//循环遍历,将所有的快递寄出去for (i in 0 until executableCalls.size) {val asyncCall = executableCalls[i]asyncCall.executeOn(executorService)}return isRunning
}

上面的代码很长,但其实只做了这么三件事

  • 先判断还有没有快递车了,没有车的话什么快递都寄不出去了,跳出循环
  • 在判断同一目的地的车辆还有没有了,目的地如果是北京,那就是判断去北京的车还有没有了,没有的话就看下一个快递。如果还有车的话,就把它装进即将出发的车里,然后计数+1,表示又占用了一辆去北京的车
  • 再次循环,将所有要寄出去的快递寄出去

最终啊快递出发还是回到了AsyncCall的executeOn方法,我们先看看AsyncCall对象是什么

internal inner class AsyncCall(private val responseCallback: Callback
) : Runnable {
}

其它的我们都不需要关注,我们只需要关注两点,第一它接受了回调,也就是你的电话号。第二,它继承了Runnable,这是什么?这不就是线程么,创建Thread,实现Runnable接口。没错就是这样喵

上面说到,最终发送是调用了asyncCall.executeOn(executorService)方法,那我们具体看一下这个方法

asyncCall.executeOn

fun executeOn(executorService: ExecutorService) {client.dispatcher.assertThreadDoesntHoldLock()//暂时定义发送没成功var success = falsetry {//使用线程池来执行自身executorService.execute(this)//发送成功了success = true} catch (e: RejectedExecutionException) {val ioException = InterruptedIOException("executor rejected")ioException.initCause(e)noMoreExchanges(ioException)//失败回调responseCallback.onFailure(this@RealCall, ioException)} finally {if (!success) {//结束client.dispatcher.finished(this) }}
}

别看代码很长,其实就是做了一件事,这个AsyncCall把自己加入到了线程池,然后由线程池来开启线程执行自己的run方法。

小伙伴会问了,什么是线程池?

线程池可有讲究了,咱们本篇研究的是OkHttp,这里就不讨论线程池了。以后的文章中会专门研究线程的种种,包括线程池。这里咱们只研究一点,就是这是一个什么样的线程池

还记得刚才Dispatcher类中定义了单例并且线程安全的线程池么

@get:Synchronized
@get:JvmName("executorService") val executorService: ExecutorServiceget() {if (executorServiceOrNull == null) {//定义了一个缓存线程池executorServiceOrNull = ThreadPoolExecutor(0, Int.MAX_VALUE, 60, TimeUnit.SECONDS,SynchronousQueue(), threadFactory("$okHttpName Dispatcher", false))}return executorServiceOrNull!!
}

这里就是定义了一个缓存的线程池,什么是缓存线程池?就是具有缓存功能的,如果一个线程工作完了并且60s之内又有请求过来,就复用刚才那个线程,这提高了性能。

然后交由线程池之后,线程池会开启线程执行asyncCall的run方法

asyncCall.run

override fun run() {threadName("OkHttp ${redactedUrl()}") {//定义响应标志位,用于表示请求是否成功var signalledCallback = falsetimeout.enter()try {//寄快递过程中~~~得到了换回来的货物val response = getResponseWithInterceptorChain()//走到这步没出错,代表寄快递成功了  signalledCallback = true//打电话给你表示货到了  responseCallback.onResponse(this@RealCall, response)} catch (e: IOException) {//如果寄快递过程没出错,但是try/catch还是报异常了只能说 onResponse中出错了,这就是用户的锅if (signalledCallback) {Platform.get().log("Callback failure for ${toLoggableString()}", Platform.INFO, e)} else {//失败回调  responseCallback.onFailure(this@RealCall, e)}} catch (t: Throwable) {cancel()//取消请求//okhttp自己的锅,发送过程中出现了错误  if (!signalledCallback) {val canceledException = IOException("canceled due to $t")canceledException.addSuppressed(t)//失败回调  responseCallback.onFailure(this@RealCall, canceledException)}throw t} finally {//结束  client.dispatcher.finished(this)}}}
}

走到这一步不容易,核心流程算是走完了。最终是通过getResponseWithInterceptorChain方法发出去快递并且得到换回来的货。

看懵了?不要紧,我们来梳理一下流程

乌拉!我要放大招了!

流程如下:

  1. 进入快递点,对应创建okhttpClient对象

  2. 填写快递单,对应创建Request对象

  3. 将快递单交给快递小哥,对应创建RealCall对象并将Request对象交给它

  4. 让小哥寄快递并告诉自己的手机号,对应调用Call的enqueue方法,并传递回调

  5. 小哥把快递交给了调度员,并且把电话号告诉了开车的老司机,然后调度员把快递放到了打包车中,对应dispatcher的enqueue方法和

  6. 调度员询问还有没有车了,以及有没有北京的车了两个判断。对应dispatcher的promoteAndExecute方法

  7. 答案是有车的话,就把老司机叫过来,给他分配一辆车,让他开车去把快递送出去。对应于线程池分配一个线程去执行AsyncCall的run方法

  8. 老司机开车一路上会遇到安检,收费站等等地方,对应于拦截器链。

  9. 老司机不管是送到了以后拿到了要换的货还是没送到,老司机都会回来,并且照着快递小哥给的电话给你打电话告诉你结果。对应于回调方法的触发

这边是主线流程,是不是相当的简单呢?和我们寄快递没有一点点差别,所以说啊,代码来源于生活,又抽象与生活。

相信有小伙伴肯定不满足于如此简单的主线流程,那么我们就进入OkHttp的支线剧情,看看拦截器链具体做了什么

前方高能!!

前方高能!!

前方高能!!

如果你只是想大概关注一下,可以直接拉到后面看结论,前面将要关门放狗,啊不,放源码了!

拦截器链

拦截器嘛,顾名思义就是拦截你的。

比如安检就是拦截你的,老司机开车进出北京市,那肯定要过安检的,毕竟安全为重。上路以后还得过收费站,中间在经过休息区等等。所以拦截器链的作用就是对请求做各种各样的操作的。

拦截器链采用的是责任链模式。看到了吗,一个设计优秀的框架必然会使用很多的经典的设计模式来保证代码的健壮性。所以我们也应该学习这种设计模式,不过今天就不学了,可以关注我,随后我会发布设计模式专栏。

在上面的代码介绍中,我们最终是走到了getResponseWithInterceptorChain方法,通过这个方法得到了reponse响应,也就是我要换的货。

我们就从这个方法开始看 ,究竟什么是责任链,什么是拦截器

@Throws(IOException::class)
internal fun getResponseWithInterceptorChain(): Response {// 构建完整的拦截器栈。val interceptors = mutableListOf<Interceptor>()interceptors += client.interceptors //用户自定义的拦截器//添加默认拦截器interceptors += RetryAndFollowUpInterceptor(client)interceptors += BridgeInterceptor(client.cookieJar)interceptors += CacheInterceptor(client.cache)interceptors += ConnectInterceptorif (!forWebSocket) {//添加用户定义的网络拦截器interceptors += client.networkInterceptors}//默认的拦截器interceptors += CallServerInterceptor(forWebSocket)//定义真正的拦截器链   val chain = RealInterceptorChain(call = this,interceptors = interceptors,index = 0,exchange = null,request = originalRequest,connectTimeoutMillis = client.connectTimeoutMillis,readTimeoutMillis = client.readTimeoutMillis,writeTimeoutMillis = client.writeTimeoutMillis)var calledNoMoreExchanges = falsetry {//责任链的精髓所在,这一行代码就开启责任链的传递,然后责任链中的所有拦截器层层调用,然后层层返回val response = chain.proceed(originalRequest)//这一行代码是责任链全部执行完毕且不会出错才会执行到这里if (isCanceled()) {response.closeQuietly()throw IOException("Canceled")}return response} catch (e: IOException) {calledNoMoreExchanges = truethrow noMoreExchanges(e) as Throwable} finally {if (!calledNoMoreExchanges) {noMoreExchanges(null)}}
}

如果自己不定义拦截器的话,OkHttp默认的是五个拦截器。

可能有的小伙伴不太明白责任链到底是怎么运作的

很简单,当调用了chain.proceed方法以后即进入下一个拦截器的方法,下一个拦截器依然调用chain.proceed方法,重复不断的进入下一个拦截器。这就是请求层层被拦截,响应传回来的时候会被最后一个拦截器拦截,然后在层层的传回来。

如图所示

这里暂不讨论用户自定义的拦截器,我们看下默认的五个拦截器分别是做什么的

这是五个拦截器的作用。我们一个个来看

RetryAndFollowUpInterceptor

总的来说RetryAndFollowUpInterceptor就是负责重试和请求重定向的一个拦截器,它还额外做了一个工作就是创建了一个ExchangeFinder对象,这个对象就是用来管理连接池为后来的连接做准备的。

class RetryAndFollowUpInterceptor(private val client: OkHttpClient) : Interceptor {companion object {//最大重试次数或者重定向次数private const val MAX_FOLLOW_UPS = 20}@Throws(IOException::class)override fun intercept(chain: Interceptor.Chain): Response {...while (true) {//创建了ExchangeFinder对象,这个对象用于管理连接池以及call.enterNetworkInterceptorExchange(request, newExchangeFinder)var response: Responsevar closeActiveExchange = truetry {...try {//让下一个拦截器来处理response = realChain.proceed(request)newExchangeFinder = true} catch (e: RouteException) {...} catch (e: IOException) {...}...//后面的拦截器执行完了,拿到Response,解析看下是否需要重试或重定向,需要则返回新的Requestval followUp = followUpRequest(response, exchange)//新的Request为空,直接返回responseif (followUp == null) {if (exchange != null && exchange.isDuplex) {call.timeoutEarlyExit()}closeActiveExchange = falsereturn response}val followUpBody = followUp.body//如果RequestBody有值且只许被调用一次,直接返回response  if (followUpBody != null && followUpBody.isOneShot()) {closeActiveExchange = falsereturn response}response.body?.closeQuietly()//超过重试最大次数抛出异常if (++followUpCount > MAX_FOLLOW_UPS) {throw ProtocolException("Too many follow-up requests: $followUpCount")}//将新的请求赋值给request,继续循环request = followUppriorResponse = response} finally {call.exitNetworkInterceptorExchange(closeActiveExchange)}}}}

BridgeInterceptor

桥接,负责把应用请求转换成网络请求,把网络响应转换成应用响应,就是添加各种响应头信息的。负责把用户构造的请求转换为发送给服务器的请求,把服务器返回的响应转换为对用户友好的响应。 在Request阶段配置用户信息,并添加一些请求头。在Response阶段,进行gzip解压。

有以下几点需要注意:

  • 开发者没有添加Accept-Encoding时,自动添加Accept-Encoding: gzip
  • 自动添加Accept-Encoding,会对request,response进行自动解压
  • 手动添加Accept-Encoding,不负责解压缩
  • 自动解压时移除Content-Length,所以上层Java代码想要contentLength时为-1
  • 自动解压时移除 Content-Encoding
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {...val body = userRequest.bodyif (body != null) {//添加各种请求头信息requestBuilder.header("Content-Type", contentType.toString())requestBuilder.header("Content-Length", contentLength.toString())requestBuilder.header("Host", userRequest.url.toHostHeader())requestBuilder.header("Connection", "Keep-Alive")requestBuilder.header("Accept-Encoding", "gzip")requestBuilder.header("Cookie", cookieHeader(cookies))//让下一个拦截器来做操作val networkResponse = chain.proceed(requestBuilder.build())cookieJar.receiveHeaders(userRequest.url, networkResponse.headers)val responseBuilder = networkResponse.newBuilder().request(userRequest)//这里有个坑:如果你在请求的时候主动添加了"Accept-Encoding: gzip" ,transparentGzip=false,那你就要自己解压if (transparentGzip &&"gzip".equals(networkResponse.header("Content-Encoding"), ignoreCase = true) &&networkResponse.promisesBody()) {val responseBody = networkResponse.bodyif (responseBody != null) {val gzipSource = GzipSource(responseBody.source())val strippedHeaders = networkResponse.headers.newBuilder().removeAll("Content-Encoding").removeAll("Content-Length").build()responseBuilder.headers(strippedHeaders)val contentType = networkResponse.header("Content-Type")responseBuilder.body(RealResponseBody(contentType, -1L, gzipSource.buffer()))}}return responseBuilder.build()
}

CacheInterceptor

我们知道为了节省流量和提高响应速度,Okhttp是有自己的一套缓存机制的,CacheInterceptor就是用来负责读取缓存以及更新缓存的。它内部的实现是使用的OKIO是进行读取和写入的。

@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {//读取候选缓存val cacheCandidate = cache?.get(chain.request())//定义了缓存的策略val strategy = CacheStrategy.Factory(now, chain.request(), cacheCandidate).compute()//根据策略,不使用网络,又没有缓存的直接报错,并返回错误码504。if (networkRequest == null && cacheResponse == null) {return ...}// 根据策略,不使用网络,有缓存的直接返回。if (networkRequest == null) {return ...}var networkResponse: Response? = nulltry {//让下一个拦截器进行工作  networkResponse = chain.proceed(networkRequest)} finally {...   }//  接收到网络结果,如果响应code式304,则使用缓存,返回缓存结果if (cacheResponse != null) {if (networkResponse?.code == HTTP_NOT_MODIFIED) {...return response.also {listener.cacheHit(call, it)}} else {cacheResponse.body?.closeQuietly()}}//7\. 读取网络结果。val response = networkResponse!!.newBuilder().cacheResponse(stripBody(cacheResponse)).networkResponse(stripBody(networkResponse)).build()//8\. 对数据进行缓存。if (cache != null) {//省略判断,这里使用了OKio来进行缓存的写入 return cacheWritingResponse(cacheRequest, response)}//返回响应结果  return response
}

整个方法的流程如下所示:

  1. 读取候选缓存,
  2. 创建缓存策略,强制缓存、对比缓存等
  3. 根据策略,不使用网络,又没有缓存的直接报错,并返回错误码504。
  4. 根据策略,不使用网络,有缓存的直接返回。
  5. 前面两个都没有返回,继续执行下一个Interceptor,即ConnectInterceptor。
  6. 接收到网络结果,如果响应code式304,则使用缓存,返回缓存结果。
  7. 读取网络结果。
  8. 对数据进行缓存。
  9. 返回网络读取的结果。

关于缓存方面,一会放在后面专门讲缓存

ConnectInterceptor

顾名思义,这个拦截器就是用来建立连接的。

还记得在之前的RetryAndFollowUpInterceptor中定义的ExchangeFinder对象吗,它里面包含了一个连接池,用于在连接池中取得连接对象。

object ConnectInterceptor : Interceptor {@Throws(IOException::class)override fun intercept(chain: Interceptor.Chain): Response {val realChain = chain as RealInterceptorChain//开始连接工作val exchange = realChain.call.initExchange(chain)//将exchange对象传递到下一个拦截器中val connectedChain = realChain.copy(exchange = exchange)//进行下一个拦截器的工作  return connectedChain.proceed(realChain.request)}
}

这个拦截器里面的代码很少,但是工作作了不少。它真正的建立了连接。并且将exchange对象传递给下一个拦截器,这个对象的作用是什么呢?

exchange对象主要就是用来发送HTTP请求和接受响应的。

不妨我们看看连接是如何建立的?如果你并不想关注连接是如何建立的,可以跳过这一部分,直接看结果

RealCall.initExchange

internal fun initExchange(chain: RealInterceptorChain): Exchange {...val exchangeFinder = this.exchangeFinder!! //在RetryAndFollowUpInterceptor定义的对象,里面有个连接池val codec = exchangeFinder.find(client, chain)//查找连接,返回的是ExchangeCodec对象val result = Exchange(this, eventListener, exchangeFinder, codec)//创建Exchange用来发送和接受请求this.interceptorScopedExchange = resultthis.exchange = result...return result
}

ExchangeFinder.find

fun find(client: OkHttpClient,chain: RealInterceptorChain): ExchangeCodec {try {//查找可用的连接  val resultConnection = findHealthyConnection(...)//创建新的ExchangeCodec对象return resultConnection.newCodec(client, chain)} catch (e: RouteException) {...}
}

ExchangeFinder.findHealthyConnection

@Throws(IOException::class)
private fun findHealthyConnection(connectTimeout: Int,readTimeout: Int,writeTimeout: Int,pingIntervalMillis: Int,connectionRetryEnabled: Boolean,doExtensiveHealthChecks: Boolean):RealConnection {while (true) {//查找连接  val candidate = findConnection(...)//确认连接是正常的if (candidate.isHealthy(doExtensiveHealthChecks)) {return candidate}//如果不正常则从连接池取出candidate.noNewExchanges()...}
}

ExchangeFinder.findConnection

@Throws(IOException::class)
private fun findConnection(connectTimeout: Int,readTimeout: Int,writeTimeout: Int,pingIntervalMillis: Int,connectionRetryEnabled: Boolean
): RealConnection {if (call.isCanceled()) throw IOException("Canceled")// 尝试重用连接val callConnection = call.connection // This may be mutated by releaseConnectionNoEvents()!if (callConnection != null) {...}// 尝试从池中获取连接if (connectionPool.callAcquirePooledConnection(address, call, null, false)) {...}...// 创建一个新的连接val newConnection = RealConnection(connectionPool, route)call.connectionToCancel = newConnectiontry {//连接服务器  newConnection.connect(connectTimeout,readTimeout,writeTimeout,pingIntervalMillis,connectionRetryEnabled,call,eventListener)} finally {call.connectionToCancel = null}...synchronized(newConnection) {//添加到连接池中  connectionPool.put(newConnection)call.acquireConnectionNoEvents(newConnection)}return newConnection
}

工作很简单,就是先尝试重用连接,如果没法重用就从连接池中取一个连接,如果没取到就创建新的连接。

可以看到,链接对象最终是RealConnection。,并且调用了RealConnection.connect方法来进行连接。并且经过调用,最后到了RealConnection.connectSocket方法。

RealConnection.connectSocket

@Throws(IOException::class)
private fun connectSocket(connectTimeout: Int,readTimeout: Int,call: Call,eventListener: EventListener
) {...//创建Socket对象val rawSocket = when (proxy.type()) {Proxy.Type.DIRECT, Proxy.Type.HTTP -> address.socketFactory.createSocket()!!else -> Socket(proxy)}this.rawSocket = rawSockettry {//真正进行Socket连接  Platform.get().connectSocket(rawSocket, route.socketAddress, connectTimeout)} catch (e: ConnectException) {throw ConnectException("Failed to connect to ${route.socketAddress}").apply {initCause(e)}}try {//okio中的接口,用来输入,类似于  InputStreamsource = rawSocket.source().buffer()//okio中的接口 ,用来输出,类似于OutputStream sink = rawSocket.sink().buffer()} catch (npe: NullPointerException) {if (npe.message == NPE_THROW_WITH_NULL) {throw IOException(npe)}}
}

这个方法的内容也很简单,我做了部分删减。主要就是创建了Socket对象,并且使用Socket对象建立了连接,然后使用OKio中的接口获得输入/输出流。

ConnectInterceptor小结

ConnectInterceptor是一个特别重要的拦截器,在这个拦截器中真正的建立了连接,并且获得了输入输出流,为将来的输入输出进行了准备。

总的来说就是做了这么几个工作:

  1. 首先查找是否有可用的连接,没有的话就尝试是否有能重用的连接,没有的话就去连接池中找,连接池中也没有就创建新的连接对象RealConnection
  2. 然后调用连接对象RealConnection的connect方法,最终创建了Socket对象用于真正的连接,然后使用了OkIO的的输入输出流,为输入输出做准备
  3. 最终返回Exchange对象,它是负责发送请求和接受响应的,而真正具体干活的是ExchangeCodec对象。
  4. 将Exchange对象放到拦截链中,让下一个拦截器进行真正的请求和响应

CallServerInterceptor

class CallServerInterceptor(private val forWebSocket: Boolean) : Interceptor {@Throws(IOException::class)override fun intercept(chain: Interceptor.Chain): Response { val exchange = realChain.exchange!!val request = realChain.requestval requestBody = request.body  //写入请求头,如果是GET那么请求已经结束exchange.writeRequestHeaders(request)var responseBuilder: Response.Builder? = nullif (HttpMethod.permitsRequestBody(request.method) && requestBody != null) {//如果熟悉HTTP的小伙伴应该知道POST请求会发送两个包,先发送请求头,获得相应为100后再发送请求体。if ("100-continue".equals(request.header("Expect"), ignoreCase = true)) {exchange.flushRequest()responseBuilder = exchange.readResponseHeaders(expectContinue = true)exchange.responseHeadersStart()}//写入请求体requestBody.writeTo(bufferedRequestBody)//读取响应头  responseBuilder = exchange.readResponseHeaders(expectContinue = false)!!var response = responseBuilder.request(request).handshake(exchange.connection.handshake()).sentRequestAtMillis(sentRequestMillis).receivedResponseAtMillis(System.currentTimeMillis()).build()var code = response.coderesponse = if (forWebSocket && code == 101) {...} else {//读取响应体  response.newBuilder().body(exchange.openResponseBody(response)).build()}...return response}
}

为了便于观看,我删除了一些判断逻辑,但是不影响整体流程

流程大致为

  1. 先写入请求头,如果是GET请求的话就已经请求完毕,POST请求的话是先发送请求头再发送请求体,会发送两个TCP包
  2. 然后读取响应头,接着判断过后,读取响应体。
  3. 最终将响应的结果返回,这个结果会层层的向上传递,经过上面所有的拦截器。
  4. 最终走到了我们自定义的回调处。

拦截器总结

拦截器采用了责任链模式,层层向下请求,请求后的结果层层向上传递

  • 首先经过了RetryAndFollowUpInterceptor拦截器,这个拦截器负责重试和重定向,最大重试次数为20次。并且在这个对象中创建了ExchangeFinder对象,用于管理连接池等,为随后的链接做准备
  • 经过BridgeInterceptor拦截器,这个拦截器主要就是帮我们添加一些请求头的和压缩/解压缩的。在这个拦截器中表明了,如果用户自定义了gzip请求头,需要自行解压缩,OkHttp则不再负责解压缩
  • CacheInterceptor是负责缓存的,并且内部使用的是OKio进行的缓存,缓存策略下面会有讲。
  • ConnectInterceptor拦截器是负责建立连接的,最终是通过RealConnection对象建立的Socket连接,并且获得了输入输出流为下一步读写做准备。RealConnection对象的获取是优先复用的,没有复用的就从连接池里取,连接池也没的话在创建新的,并加入连接池
  • CallServerInterceptor拦截器就是最终的拦截器了,它将负责数据真正的读取和写入。

OkHttp缓存策略

首先学习一下HTTP缓存相关的理论知识,是实现Okhttp机制的基础。

HTTP的缓存机制也是依赖于请求和响应header里的参数类实现的。缓存分为两种,一种是强制缓存,一种是对比缓存

强制缓存:

客户端先看有没有缓存,有缓存直接拿缓存,如果没缓存的话就请求服务器然后将结果缓存,以备下次请求。

强制缓存使用的的两个标识:

  • Expires:Expires的值为服务端返回的到期时间,即下一次请求时,请求时间小于服务端返回的到期时间,直接使用缓存数据。到期时间是服务端生成的,客户端和服务端的时间可能有误差。
  • Cache-Control:Expires有个时间校验的问题,所有HTTP1.1采用Cache-Control替代Expires。

Cache-Control的取值有以下几种:

  • private: 客户端可以缓存。
  • public: 客户端和代理服务器都可缓存。
  • max-age=xxx: 缓存的内容将在 xxx 秒后失效
  • no-cache: 需要使用对比缓存来验证缓存数据。
  • no-store: 所有内容都不会缓存,强制缓存,对比缓存都不会触发。

对比缓存:

对比缓存需要服务端参与判断是否继续使用缓存,当客户端第一次请求数据时,服务端会将缓存标识(Last-Modified/If-Modified-Since与Etag/If-None-Match)与数据一起返回给客户端,客户端将两者都备份到缓存中 ,再次请求数据时,客户端将上次备份的缓存 标识发送给服务端,服务端根据缓存标识进行判断,如果返回304,则表示通知客户端可以继续使用缓存。

Last-Modified/If-Modified-Since

Last-Modified 表示资源上次修改的时间。

当客户端发送第一次请求时,服务端返回资源上次修改的时间:

Last-Modified: Tue, 12 Jan 2016 09:31:27 GMT

客户端再次发送,会在header里携带If-Modified-Since。将上次服务端返回的资源时间上传给服务端。

If-Modified-Since: Tue, 12 Jan 2016 09:31:27 GMT

服务端接收到客户端发来的资源修改时间,与自己当前的资源修改时间进行对比,如果自己的资源修改时间大于客户端发来的资源修改时间,则说明资源做过修改, 则返回200表示需要重新请求资源,否则返回304表示资源没有被修改,可以继续使用缓存。

上面是一种时间戳标记资源是否修改的方法,还有一种资源标识码ETag的方式来标记是否修改,如果标识码发生改变,则说明资源已经被修改,ETag优先级高于Last-Modified。

Etag/If-None-Match

ETag是资源文件的一种标识码,当客户端发送第一次请求时,服务端会返回当前资源的标识码:

ETag: "5694c7ef-24dc"

客户端再次发送,会在header里携带上次服务端返回的资源标识码:

If-None-Match:"5694c7ef-24dc"

服务端接收到客户端发来的资源标识码,则会与自己当前的资源吗进行比较,如果不同,则说明资源已经被修改,则返回200,如果相同则说明资源没有被修改,返回 304,客户端可以继续使用缓存。

这是流程图:

Okhttp的缓存策略就是根据上述流程图实现的,具体的实现类是CacheStrategy,CacheStrategy的构造函数里有两个参数

class CacheStrategy internal constructor(val networkRequest: Request?,val cacheResponse: Response?
)

这两个参数参数的含义如下:

  • networkRequest:网络请求。
  • cacheResponse:缓存响应,基于DiskLruCache实现的文件缓存,key是请求中url的md5,value是文件中查询到的缓存

CacheStrategy 根据之前缓存结果与当前将要发生的 request 的Header 计算缓存策略。规则如下

networkRequest cacheResponse CacheStrategy
null null only-if-cached(表明不进行网络请求,且缓存不存在或者过期,一定会返回 503 错误)
null non-null 不进行网络请求,而且缓存可以使用,直接返回缓存,不用请求网络
non-null null 需要进行网络请求,而且缓存不存在或者过期,直接访问网络。
non-null not-null Header 中含有 ETag/Last-Modified 标识,需要在条件请求下使用,还是需要访问网络。

具体流程如下所示:

  • 读取候选缓存。
  • 根据候选缓存创建缓存策略。
  • 根据缓存策略,如果不进行网络请求,而且没有缓存数据时,报错返回错误码 504。
  • 根据缓存策略,如果不进行网络请求,缓存数据可用,则直接返回缓存数据。
  • 缓存无效,则继续执行网络请求。
  • 通过服务端校验后,缓存数据可以使用(返回 304),则直接返回缓存数据,并且更新缓存。
  • 读取网络结果,构造 response,对数据进行缓存。

整个流程就是这样,另外说一点,Okhttp的缓存是根据服务器header自动的完成的,整个流程也是根据RFC文档写死的,客户端不必要进行手动控制。

Okhttp的磁盘缓存机制是基于DiskLruCache做的,即最近最少使用算法来进行缓存的,使用okio作为输入输出。

OkHttp连接池

因为HTTP是基于TCP,TCP连接时需要经过三次握手,为了加快网络访问速度,我们可以Reuqst的header中将Connection设置为keepalive来复用连接。

Okhttp支持5个并发KeepAlive,默认链路生命为5分钟(链路空闲后,保持存活的时间),连接池有ConectionPool实现,对连接进行回收和管理。

连接池真正的实现类是RealConnectionPool。其中用来保存连接的是这样的对象:

private val connections = ConcurrentLinkedQueue<RealConnection>()

ConcurrentLinkedQueue是一个基于链接节点的无界线程安全队列。该队列的元素遵循先进先出的原则。头是最先加入的,尾是最近加入的。和其他并发集合一样。把一个元素放入到队列的线程的优先级高与对元素的访问和移除的线程。

添加连接到线程池:

fun put(connection: RealConnection) {connection.assertThreadHoldsLock()//添加链接到线程池connections.add(connection)//清除闲置的连接cleanupQueue.schedule(cleanupTask)
}

清除闲置连接最主要 就是当闲置连接大于最大保持存活时间,清除出连接池。所以每当有新连接添加到线程池的时候,都会清除闲置的连接。并且用ConcurrentLinkedQueue线程安全的数据队列来保存连接。

Application Interceptors与Network Interceptors

很多小伙伴搞不清这两者的区别,从上面的图中可以看到Application Interceptors处于第一位,而Network Interceptors处于倒数第二位。这就是它们的本质区别,调用的时机不同,也就会出现以下现象。

Application Interceptors是用于在请求发送前网络响应后的拦截器

Network Interceptors在这一层拦截器中可以获取到最终发送请求的 request ,其中也包括重定向的数据,也可以获取到真正发生网络请求回来的 response 响应,从而修改对应的请求或者响应数据。

Application Interceptors只关心发出去的请求和最终响应的结果,其他的一概不关心。

这里放一个okhttp官方的区别最为明了

Application interceptors

  • Don’t need to worry about intermediate responses like redirects and retries. 不需要去关心中发生的重定向和重试操作。因为它处于第一个拦截器,会获取到最终的响应 response 。
  • Are always invoked once, even if the HTTP response is served from the cache. 只会被调用一次,即使这个响应是从缓存中获取的。
  • Observe the application’s original intent. Unconcerned with -OkHttp-injected headers like If-None-Match. 只关注最原始的请求,不去关系请求的资源是否发生了改变,我只关注最后的 response 结果而已。
  • Permitted to short-circuit and not call Chain.proceed(). 因为是第一个被执行的拦截器,因此它有权决定了是否要调用其他拦截,也就是 Chain.proceed() 方法是否要被执行。
  • Permitted to retry and make multiple calls to Chain.proceed() 因为是第一个被执行的拦截器,因此它有可以多次调用 Chain.proceed() 方法,其实也就是相当与重新请求的作用了。

Network Interceptors

  • Able to operate on intermediate responses like redirects and retries. 因为 NetworkInterceptor 是排在第 6 个拦截器中,因此可以操作经过 RetryAndFollowup 进行失败重试或者重定向之后得到的resposne。
  • Not invoked for cached responses that short-circuit the network. 对于从缓存获取的 response 则不会去触发 NetworkInterceptor 。因为响应直接从 CacheInterceptor 返回了。
  • Observe the data just as it will be transmitted over the network. 观察将在网络上传输的数据
  • Access to the Connection that carries the request. 访问携带请求的连接

文末

对文章有何见解,或者有何技术问题,都可以在评论区一起留言讨论,一定会回复的。
也欢迎大家来我的B站找我玩,各类Android架构师进阶技术难点的视频讲解,任君白嫖。
B站直通车:https://space.bilibili.com/544650554

喜欢文章的话,留个赞再走呗~

资源分享

  • 最新大厂面试专题

这个题库内容是比较多的,除了一些流行的热门技术面试题,如Kotlin,数据库,Java虚拟机面试题,数组,Framework ,混合跨平台开发,等

  • 对应导图的Android高级工程师进阶系统学习视频
    最近热门的,NDK,热修复,MVVM,源码等一系列系统学习视频都有!

hort-circuit and not call Chain.proceed(). 因为是第一个被执行的拦截器,因此它有权决定了是否要调用其他拦截,也就是 Chain.proceed() 方法是否要被执行。

  • Permitted to retry and make multiple calls to Chain.proceed() 因为是第一个被执行的拦截器,因此它有可以多次调用 Chain.proceed() 方法,其实也就是相当与重新请求的作用了。

Network Interceptors

  • Able to operate on intermediate responses like redirects and retries. 因为 NetworkInterceptor 是排在第 6 个拦截器中,因此可以操作经过 RetryAndFollowup 进行失败重试或者重定向之后得到的resposne。
  • Not invoked for cached responses that short-circuit the network. 对于从缓存获取的 response 则不会去触发 NetworkInterceptor 。因为响应直接从 CacheInterceptor 返回了。
  • Observe the data just as it will be transmitted over the network. 观察将在网络上传输的数据
  • Access to the Connection that carries the request. 访问携带请求的连接

文末

对文章有何见解,或者有何技术问题,都可以在评论区一起留言讨论,一定会回复的。
也欢迎大家来我的B站找我玩,各类Android架构师进阶技术难点的视频讲解,任君白嫖。
B站直通车:https://space.bilibili.com/544650554

喜欢文章的话,留个赞再走呗~

资源分享

  • 最新大厂面试专题

这个题库内容是比较多的,除了一些流行的热门技术面试题,如Kotlin,数据库,Java虚拟机面试题,数组,Framework ,混合跨平台开发,等

[外链图片转存中…(img-lnKPM2Bz-1623419179693)]

  • 对应导图的Android高级工程师进阶系统学习视频
    最近热门的,NDK,热修复,MVVM,源码等一系列系统学习视频都有!

[外链图片转存中…(img-blroYOAd-1623419179694)]

下载方法:点赞+关注后 点击【Android高级工程师进阶学习】即可领取!

Android开发之路:搞懂这几个问题,就掌握了屏幕适配原理相关推荐

  1. android开发之路05

    一.获取res资源文件下values中内容的方式: 1.string.xml文件-------------------------------> 取值方式:getResource().getSt ...

  2. android开发之Intent.setFlags()_让Android点击通知栏信息后返回正在运行的程序

    android开发之Intent.setFlags()_让Android点击通知栏信息后返回正在运行的程序   在应用里使用了后台服务,并且在通知栏推送了消息,希望点击这个消息回到activity, ...

  3. android之json解析优化,Android开发之json解析

    目前正在尝试着写app,发现看懂代码和能写出来差距很大,最关键的是java基础比较的差,因为只会python,java基础只学习了一个礼拜就过了.感觉java写出来的代码不如python简单明了. 上 ...

  4. Android开发之旅:android架构

    引言 通过前面两篇: Android 开发之旅:环境搭建及HelloWorld Android 开发之旅:HelloWorld项目的目录结构 我 们对android有了个大致的了解,知道如何搭建and ...

  5. 04_HUD_Qt_for_Python开发之路2

    欢迎来到我的博客,希望这篇文章对你有所帮助,如果觉得不错,请点赞搜藏哈. 文章目录 HUD_Qt_for_Python开发之路2 1 设置程序名称 2 隐藏窗口标题栏 3 设置窗口透明裁剪 4 修改下 ...

  6. 我的App开发之路:从零开始到上线赚钱

    我的App开发之路:从零开始到上线赚钱 背景介绍 本人从第一次写代码赚钱开始,到现在已经12年了,使用过多种操作系统.编程语言. 现在作为一名个人开发者,开发一些小应用. 本文就应用软件开发做个简单总 ...

  7. 05_HUD_Qt_for_Python开发之路3

    欢迎来到我的博客,希望这篇文章对你有所帮助,如果觉得不错,请点赞搜藏哈. 文章目录 HUD_Qt_for_Python开发之路3 1 UI文件与Python结合 1.1 将UI文件编译为一个Pytho ...

  8. Android开发之TextView高级应用

    Android开发之TextView高级应用 我们平时使用TextView往往让它作为一个显示文字的容器,但TextView的功能并不局限于此.以下就和大家分享一下TextView的一些使用技巧. A ...

  9. android注册弹窗,Android开发之PopupWindow创建弹窗、对话框的方法详解

    本文实例讲述了Android开发之PopupWindow创建弹窗.对话框的方法.分享给大家供大家参考,具体如下: 简介: PopupWindow 可创建类似对话框风格的窗口 效果: 使用方法: 使用P ...

最新文章

  1. (0042) iOS 开发之iOS10.3新特性:如何动态更换APP图标
  2. SQLServer存储引擎——05.索引的结构和分类
  3. linux手机远程桌面连接软件下载,HomeCenter手机远程桌面下载_HomeCenter手机远程桌面官方下载-太平洋下载中心...
  4. php png 输出,PHP直接输出一张图片代码写法
  5. 前端架构设计1:代码核心
  6. 设计灵感|海报设计常见中看起来很高级的文字排版
  7. ubuntu下字体美化设置
  8. 百度开源深度学习框架【飞桨——PaddlePaddle】学习实践一
  9. jsp 弹出窗口设置大全
  10. 五颜六色 CSS颜色代码表
  11. 调用讯飞开放平台的语音转写api
  12. MySQL 第六章过程式数据库对象.1存储对象
  13. 量化交易让股市成为你的印钞机
  14. Linux 小程序开发日记(终章)(pyqt+yolov5)
  15. 如何测量两组汇编指令的执行效率
  16. FairyGUI按钮动效的混用
  17. UML ~ Unified Modeling Language ~ 统一建模语言。+ 软件设计原则。
  18. 最长回文子串(C++)
  19. 8位寄存器置位与清零操作
  20. 项目经理如何做好任务分解,制定项目计划

热门文章

  1. 制作Mac版的星际争霸II(StarCraft II)
  2. HTTP 503 Service Temporarily Unavailable
  3. android 2.1你好八月,八月你好优美句子
  4. 解决阿里巴巴JSONObject工具 com.alibaba.fastjson.JSONObject cannot be cast to 的问题
  5. 【大爱】C++STL 常用 函数 用法(转载自duoduo3_69)
  6. 面经 - 阿里巴巴 - HR面试
  7. RocketMq部署与使用
  8. 在谷歌chrome中打开、编辑、保存金山 WPS Office完美解决方案!
  9. 获取微信用户在微信小店的订单
  10. F - Ilya Muromets Gym - 100513F