Android网络开发(二、okhttp&retrofit)

298 阅读8分钟

一、概述

  在Android开发中,一般很少直接使用HttpURLConnection / HttpsURLConnection,比较常用的开源网络库有okhttp、retrofit等。

二、okhttp简单用法

  okhttp支持异步网络请求/同步网络请求,它默认支持:(1)HTTP/2 相同域名下的请求共用一个socket;(2)通过连接池减少请求时延(如果HTTP/2不可用);(3)支持GZIP压缩;(4)支持Response缓存。

2.1 添加依赖

  在build.gradle中添加okhttp依赖如下:

dependencies {
    implementation("com.squareup.okhttp3:okhttp:4.9.3")
}

2.2 接收数据

  OKhttp使用Call来表示一次网络请求,请求建立后通过Response读取返回的内容。okhttp支持异步请求和同步请求:
(1)异步方式不会阻塞线程,在网络结果返回后会在其他线程通过callback返回;
(2)同步方式会阻塞当前线程直到结果返回后再继续执行;不能在主线程调用。使用方式如下:

// 1.单例模式,构建的okHttpClient被所有请求复用
object HttpManager {
    val okHttpClient = OkHttpClient.Builder()
        .connectionPool(ConnectionPool(2, 35, TimeUnit.SECONDS))
        .connectTimeout(10, TimeUnit.SECONDS)
        .readTimeout(15, TimeUnit.SECONDS)
        .writeTimeout(15, TimeUnit.SECONDS)
        .build()
}

fun sendRequest() {
    // 2.构建request
    val request = Request.Builder()
        .url("https://www.baidu.com").post(requestBody).addHeader("COOKIE", "uid=bc").build()

    // 3.异步发送请求
    HttpManager.okHttpClient.newCall(request).enqueue(object : Callback {
        override fun onFailure(call: Call, e: IOException) {
            TODO("Not yet implemented")
        }

        override fun onResponse(call: Call, response: Response) {
            // logcat输出response的内容
            // ps:如果response内容不是字符串,则用response.body().inputStream()读取
            Log.d("BCHttpDemo", response.body?.string() ?: "")
        }
    })
    
    // 4.同步发送请求;不能在主线程发送,否则会抛NetworkOnMainThreadException异常
    okHttpClient.newCall(request).execute()
}

  运行后,可以看到logcat输出了百度的HTML内容如下所示:

com.bc.example D/BCHttpDemo: <!DOCTYPE html>
    <!--STATUS OK--><html> <head><meta http-equiv=content-type content=text/html;charset=utf-8><meta http-equiv=X-UA-Compatible content=IE=Edge><meta content=always name=referrer><link rel=stylesheet type=text/css href=https://ss1.bdstatic.com/5eN1bjq8AAUYm2zgoY3K/r/www/cache/bdorz/baidu.min.css><title>百度一下,你就知道</title></head> <body link=#0000cc> <div id=wrapper> <div id=head> <div class=head_wrapper> <div class=s_form> <div class=s_form_wrapper> <div id=lg> <img hidefocus=true src=//www.baidu.com/img/bd_logo1.png width=270 height=129> </div> <form id=form name=f action=//www.baidu.com/s class=fm> <input type=hidden name=bdorz_come value=1> <input type=hidden name=ie value=utf-8> <input type=hidden name=f value=8> <input type=hidden name=rsv_bp value=1> <input type=hidden name=rsv_idx value=1> <input type=hidden name=tn value=baidu><span class="bg s_ipt_wr"><input id=kw name=wd class=s_ipt value maxlength=255 autocomplete=off autofocus=autofocus></span><span class="bg s_btn_wr"><input type=submit id=su value=百度一下 class="bg s_btn" autofocus></span> </form> </div> </div> <div id=u1> <a href=http://news.baidu.com name=tj_trnews class=mnav>新闻</a> <a href=https://www.hao123.com name=tj_trhao123 class=mnav>hao123</a> <a href=http://map.baidu.com name=tj_trmap class=mnav>地图</a> <a href=http://v.baidu.com name=tj_trvideo class=mnav>视频</a> <a href=http://tieba.baidu.com name=tj_trtieba class=mnav>贴吧</a> <noscript> <a href=http://www.baidu.com/bdorz/login.gif?login&amp;tpl=mn&amp;u=http%3A%2F%2Fwww.baidu.com%2f%3fbdorz_come%3d1 name=tj_login class=lb>登录</a> </noscript> <script>document.write('<a href="http://www.baidu.com/bdorz/login.gif?login&tpl=mn&u='+ encodeURIComponent(window.location.href+ (window.location.search === "" ? "?" : "&")+ "bdorz_come=1")+ '" name="tj_login" class="lb">登录</a>');
                    </script> <a href=//www.baidu.com/more/ name=tj_briicon class=bri style="display: block;">更多产品</a> </div> </div> </div> <div id=ftCon> <div id=ftConw> <p id=lh> <a href=http://home.baidu.com>关于百度</a> <a href=http://ir.baidu.com>About Baidu</a> </p> <p id=cp>&copy;2017&nbsp;Baidu&nbsp;<a href=http://www.baidu.com/duty/>使用百度前必读</a>&nbsp; <a href=http://jianyi.baidu.com/ class=cp-feedback>意见反馈</a>&nbsp;京ICP证030173号&nbsp; <img src=//www.baidu.com/img/gs.gif> </p> </div> </div> </div> </body> </html>

2.3 发送数据

  在构建Request时,通过Request.Builder().url().post(requestBody)设置需要用POST方式发送的数据(支持传送多种格式,例如json、form、ProtoBuf等),如下所示:

// 1.构建okHttpClient,建议单例模式
val okHttpClient = OkHttpClient.Builder()
    .connectionPool(ConnectionPool(2, 35, TimeUnit.SECONDS))
    .connectTimeout(10, TimeUnit.SECONDS)
    .readTimeout(15, TimeUnit.SECONDS)
    .writeTimeout(15, TimeUnit.SECONDS)
    .build()
// 2.构造需要发送的JSON数据
val jsonObject = JSONObject()
jsonObject.put("name", "BC")
val jsonMediaType = "application/json; charset=utf-8".toMediaTypeOrNull()
// 设置RequestBody的数据格式为json类型
val requestBody = RequestBody.create(jsonMediaType, jsonObject.toString())

// 3.构建request
val request = Request.Builder()
    .url("https://www.baidu.com").post(requestBody).addHeader("COOKIE", "cookie").build()

// 4.异步发送请求
okHttpClient.newCall(request).enqueue(object : Callback {
    override fun onFailure(call: Call, e: IOException) {
        TODO("Not yet implemented")
    }

    override fun onResponse(call: Call, response: Response) {
        Log.d("BCHttpDemo", response.body?.string() ?: "")
    }
})

三、Retrofit简单用法

  Retrofit网络库也是一个比较常用的网络库,它底层基于okhttp。Retrofit通过在接口上添加注解来动态生成网络请求,并且支持多种ConverterFactory(例如GsonConverterFactory)、CallAdapterFactory(例如RxJavaCallAdapterFactory)将输入/输出转换为指定的格式。

3.1 基础用法

3.1.1 添加依赖

  在build.gradle中添加Retrofit依赖如下:

dependencies {
    implementation("com.squareup.retrofit2:retrofit:2.9.0")
}

3.1.2 定义网络请求接口

public interface SampleRequestInterface {
    @HTTP(method = "GET", path = "/api", hasBody = true)
    Call<BaseResponse> getCall();
}

3.1.3 Retrofit发送请求

private void sendRequest() {
        try {
            Retrofit retrofit = new Retrofit.Builder()
                    .baseUrl("http://www.baidu.com")
                    .addConverterFactory(GsonConverterFactory.create())
                    .build();
            // 通过代理实现
            SampleRequestInterface sampleRequest = retrofit.create(SampleRequestInterface.class);
            Call<BaseResponse> call = sampleRequest.getCall();
            // 1.异步发送请求
            call.enqueue(new Callback<BaseResponse>() {
                @Override
                public void onResponse(Call call, Response response) {
                    response.body();
                }

                @Override
                public void onFailure(Call call, Throwable t) {

                }
            });
            // 2.同步发送请求
            Response<BaseResponse> response = call.execute();
            response.body();
        } catch (Exception exception) {
            exception.printStackTrace();
        }
    }

3.2 Retrofit动态代理

  在上述retrofit.create创建请求的源码实现中,Retrofit采用了动态代理的模式(代理模式参考:juejin.cn/post/696842… ):

public <T> T create(final Class<T> service) {
    Utils.validateServiceInterface(service);
    if (validateEagerly) {
      eagerlyValidateMethods(service);
    }
    return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },
        new InvocationHandler() {
          private final Platform platform = Platform.get();

          @Override public Object invoke(Object proxy, Method method, Object... args)
              throws Throwable {
            // If the method is a method from Object then defer to normal invocation.
            if (method.getDeclaringClass() == Object.class) {
              return method.invoke(this, args);
            }
            if (platform.isDefaultMethod(method)) {
              return platform.invokeDefaultMethod(method, service, proxy, args);
            }
            ServiceMethod serviceMethod = loadServiceMethod(method);
            OkHttpCall okHttpCall = new OkHttpCall<>(serviceMethod, args);
            return serviceMethod.callAdapter.adapt(okHttpCall);
          }
        });
  }

  关于Retrofit的的使用只做简单介绍,后面主要结合网络理论知识分析okhttp源码。

四、OkHttp源码分析

  OkHttp内部使用了线程池、连接池、建造者模式、工厂模式、责任链模式等技术,支持连接复用、GZIP压缩、Response缓存等特性。根据OkHttp的简单使用例子:

// 1.单例模式,构建的okHttpClient被所有请求复用
object HttpManager {
    val okHttpClient = OkHttpClient.Builder()
        .connectionPool(ConnectionPool(2, 35, TimeUnit.SECONDS))
        .connectTimeout(10, TimeUnit.SECONDS)
        .readTimeout(15, TimeUnit.SECONDS)
        .writeTimeout(15, TimeUnit.SECONDS)
        .build()
}

fun sendRequest() {
    // 2.构建request
    val request = Request.Builder()
        .url("https://www.baidu.com").post(requestBody).build()

    // 3.异步发送请求
    HttpManager.okHttpClient.newCall(request).enqueue(object : Callback {
        override fun onFailure(call: Call, e: IOException) { }
        override fun onResponse(call: Call, response: Response) { }
    })
}

  其中涉及到的主要类是OkHttpClient、Request、Call、Dispatcher。OkHttpClient是创建Call的工厂;Request表示一次HTTP请求发送;Call表示一次完整的网络请求,用来发送Request并读取Response;Dispatcher内部管理着线程池,是OkHttpClient用来分发Call的分发器。

4.1 OkHttpClient

  OkHttpClient本身使用了建造者模式OkHttpClient.Builder()来构造OkHttpClient对象。在构造出OkHttpClient对象后,OkHttpClient采用工厂模式来创建Call对象。
  OkHttpClient对象持有线程池、分发器Dispatcher、拦截器list,设置了网络请求的超时时间等。官方建议创建出来的OkHttpClient对象采用单例模式,之后的网络请求Call都复用这个OkHttpClient对象,减少网络延迟和资源消耗。

/**
 * Call的工厂类
 * ## OkHttpClients Should Be Shared
 * OkHttp performs best when you create a single `OkHttpClient` instance and reuse it for all of
 * your HTTP calls. This is because each client holds its own connection pool and thread pools.
 * Reusing connections and threads reduces latency and saves memory. Conversely, creating a client
 * for each request wastes resources on idle pools.
 */
open class OkHttpClient internal constructor(builder: Builder) : Cloneable,
    Call.Factory, WebSocket.Factory {
    
    /** 异步请求分发器,内部通过线程池分发异步请求 */
    val dispatcher: Dispatcher = builder.dispatcher
    /** 连接池 */
    val connectionPool: ConnectionPool = builder.connectionPool
    /** interceptors拦截器集合 */
    val interceptors: List<Interceptor> = builder.interceptors.toImmutableList()
    /** networkInterceptors拦截器集合 */
    val networkInterceptors: List<Interceptor> = builder.networkInterceptors.toImmutableList()
    /** 事件监听器工厂;可以监听callStart、dnsStart、dnsEnd、connectStart、
     * connectEnd、requestHeadersStart、requestBodyStart、responseHeadersStart、
     * responseBodyStart、callEnd、cacheHit等事件 */
    val eventListenerFactory: EventListener.Factory = builder.eventListenerFactory
    /** 缓存 */
    val cache: Cache? = builder.cache
    /** DNS */
    val dns: Dns = builder.dns
    /** socket工厂 */
    val socketFactory: SocketFactory = builder.socketFactory
    /** ssl socket工厂 */
    val sslSocketFactory: SSLSocketFactory
    /** 协议集合 */
    val protocols: List<Protocol> = builder.protocols
    /** 连接超时时间Default(单位ms),默认10s */
    val connectTimeoutMillis: Int = builder.connectTimeout
    /** 读超时时间Default(单位ms),默认10s */
    val readTimeoutMillis: Int = builder.readTimeout
    /** 写超时时间Default(单位ms),默认10s */
    val writeTimeoutMillis: Int = builder.writeTimeout

    constructor() : this(Builder())

    /** 创建Call对象 */
    override fun newCall(request: Request): Call = RealCall(this, request, forWebSocket = false)

    open fun newBuilder(): Builder = Builder(this)

    class Builder constructor() {
        internal var dispatcher: Dispatcher = Dispatcher()
        internal var connectionPool: ConnectionPool = ConnectionPool()
        internal val interceptors: MutableList<Interceptor> = mutableListOf()
        internal val networkInterceptors: MutableList<Interceptor> = mutableListOf()
        internal var eventListenerFactory: EventListener.Factory = EventListener.NONE.asFactory()
        internal var cache: Cache? = null
        internal var dns: Dns = Dns.SYSTEM
        internal var socketFactory: SocketFactory = SocketFactory.getDefault()
        internal var sslSocketFactoryOrNull: SSLSocketFactory? = null
        internal var protocols: List<Protocol> = DEFAULT_PROTOCOLS
        internal var connectTimeout = 10_000
        internal var readTimeout = 10_000
        internal var writeTimeout = 10_000

        fun build(): OkHttpClient = OkHttpClient(this)
    }
}

4.2 Request

  Request表示一次HTTP请求发送,其内部也采用了建造者模式,默认采用GET方法,并支持HEAD、POST、DELETE、PUT、PATCH方法。Request包含了发送请求的url、method、headers、body等信息,如下所示:

class Request internal constructor(
    val url: HttpUrl, val method: String,
    val headers: Headers, val body: RequestBody?,
    internal val tags: Map<Class<*>, Any>
) {
    fun newBuilder(): Builder = Builder(this)

    open class Builder {
        internal var url: HttpUrl? = null
        internal var method: String
        internal var headers: Headers.Builder
        internal var body: RequestBody? = null

        constructor() {
            this.method = "GET" 
            this.headers = Headers.Builder()
        }

        open fun url(url: String): Builder {
            return url(url.toHttpUrl())
        }

        /**
         * 添加header
         * OkHttp会自动在header中添加`Content-Length` and `Content-Encoding`
         */
        open fun addHeader(name: String, value: String) = apply {
            headers.add(name, value)
        }

        open fun get() = method("GET", null)

        open fun head() = method("HEAD", null)

        open fun post(body: RequestBody) = method("POST", body)

        open fun delete(body: RequestBodyT) = method("DELETE", body)

        open fun put(body: RequestBody) = method("PUT", body)

        open fun patch(body: RequestBody) = method("PATCH", body)

        open fun build(): okhttp3.Request {
            return Request(url, method, headers.build(), body, tags.toImmutableMap())
        }
    }
}

4.3 Dispatcher

  Call表示一次完整的网络请求,用来发送Request并读取Response。例子中在创建完OkHttpClient、Request、Call后,realCall.enqueue()会由OkHttpClient的Dispatcher进行分发,如下所示:

class RealCall(
    val client: OkHttpClient, val originalRequest: Request, val forWebSocket: Boolean
) : Call {
    override fun enqueue(responseCallback: Callback) {
        // 由OkHttpClient的Dispatcher进行分发。
        client.dispatcher.enqueue(AsyncCall(responseCallback))
    }
}

  Dispatcher内部持有线程池,下面主要分析Dispatcher是如何分发异步请求的,可以看到在Call进入队列enqueue()后,会先查找同域名下是否有其他异步请求,然后将准备状态的请求提升为运行态,然后在线程池上运行:

/**
 * Dispatcher内部通过线程池分发网络请求Call
 */
class Dispatcher {

    /** 并发的请求数 */
    var maxRequests = 64
        set(maxRequests) {
            require(maxRequests >= 1) { "max < 1: $maxRequests" }
            synchronized(this) {
                field = maxRequests
            }
            // 在修改并发数后,会检查一下是否有可以运行的请求
            promoteAndExecute()
        }

    /**
     * 每个域名下的并发请求数
     * 注意不是对单个IP地址的并发请求数,因为一个域名只能对应一个IP地址,而一个IP却可以对应多个域名
     */
    var maxRequestsPerHost = 5
        set(maxRequestsPerHost) {
            require(maxRequestsPerHost >= 1) { "max < 1: $maxRequestsPerHost" }
            synchronized(this) {
                field = maxRequestsPerHost
            }
            // 在修改并发数后,会检查一下是否有可以运行的请求
            promoteAndExecute()
        }

    /** 线程池,用来调度异步请求 */
    val executorService = ThreadPoolExecutor(0, Int.MAX_VALUE, 60, TimeUnit.SECONDS,
        SynchronousQueue(), threadFactory("$okHttpName Dispatcher", false)
    )

    /** 准备状态的异步请求AsyncCall list */
    private val readyAsyncCalls = ArrayDeque<AsyncCall>()
    /** 运行状态的异步请求AsyncCall list */
    private val runningAsyncCalls = ArrayDeque<AsyncCall>()
    /** 运行状态的同步请求calls list */
    private val runningSyncCalls = ArrayDeque<RealCall>()

    /**
     * 异步请求AsyncCall进入队列
     */
    internal fun enqueue(call: AsyncCall) {
        synchronized(this) {
            readyAsyncCalls.add(call)
            // 1.查找同域名下是否有其他异步请求,并修改该域名下异步请求的并发数
            if (!call.call.forWebSocket) {
                val existingCall = findExistingCallWithHost(call.host)
                if (existingCall != null) call.reuseCallsPerHostFrom(existingCall)
            }
        }
        // 2.将准备状态的异步请求中符合条件的请求calls提升为运行状态,并在线程池上运行它们
        promoteAndExecute()
    }

    /**
     * 查找是否有同域名下的异步请求,有则返回该异步请求AsyncCall
     */
    private fun findExistingCallWithHost(host: String): AsyncCall? {
        for (existingCall in runningAsyncCalls) {
            if (existingCall.host == host) return existingCall
        }
        for (existingCall in readyAsyncCalls) {
            if (existingCall.host == host) return existingCall
        }
        return null
    }

    /**
     * 将准备状态的异步请求readyAsyncCalls中符合条件的请求提升为运行状态的异步请求runningAsyncCalls中,并在线程池上运行它们
     */
    private fun promoteAndExecute(): Boolean {
        val executableCalls = mutableListOf<AsyncCall>()
        val isRunning: Boolean
        synchronized(this) {
            val i = readyAsyncCalls.iterator()
            while (i.hasNext()) {
                val asyncCall = i.next()
                if (runningAsyncCalls.size >= this.maxRequests) break // 是否超过最大请求限制
                if (asyncCall.callsPerHost.get() >= this.maxRequestsPerHost) continue // 是否超过单个域名的最大请求限制
                i.remove()
                asyncCall.callsPerHost.incrementAndGet()
                executableCalls.add(asyncCall)
                runningAsyncCalls.add(asyncCall)
            }
            isRunning = runningCallsCount() > 0
        }
        // 3.遍历所有符合条件的异步请求AsyncCall并在线程池上运行
        for (i in 0 until executableCalls.size) {
            val asyncCall = executableCalls[i]
            asyncCall.executeOn(executorService)
        }
        return isRunning
    }
}

4.4 Call

  Call表示一次完整的网络请求,用来发送Request并读取Response。RealCall是Call的实现类,AsyncCall是RealCall的内部类。在OkHttpClient的Dispatcher调度执行异步AsyncCall后,会执行AsyncCall的run()方法:

/**
 * okhttp业务层和网络层的桥接层
 * 对建立连接、发送请求、读取响应、IO流进行了封装
 */
class RealCall(val client: OkHttpClient,
    /** 原始请求 */
    val originalRequest: Request, val forWebSocket: Boolean) : Call {

    internal inner class AsyncCall(
        private val responseCallback: Callback
    ) : Runnable {
        @Volatile
        var callsPerHost = AtomicInteger(0)
        val host: String
        val request: Request = originalRequest
        val call: RealCall = this@RealCall

        override fun run() {
            // 启动计时器
            timeout.enter()
            // 关键代码,通过责任链并最终返回Response
            val response = getResponseWithInterceptorChain()
            // 返回Response
            responseCallback.onResponse(this@RealCall, response)
        }
    }
    
    /**
    * 责任链模式
    */
    internal fun getResponseWithInterceptorChain(): Response {
        // 拦截器list
        val interceptors = mutableListOf<Interceptor>()
        interceptors += client.interceptors
        interceptors += RetryAndFollowUpInterceptor(client)
        interceptors += BridgeInterceptor(client.cookieJar)
        interceptors += CacheInterceptor(client.cache)
        interceptors += ConnectInterceptor
        if (!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
        )
        val response = chain.proceed(originalRequest)
        return response
    }
}

  执行建立连接、发送Request、读取Response等工作都是在RealCall责任链模式中的各拦截器Interceptor实现的,下一章再继续分析okhttp责任链。

The End

欢迎关注我,一起解锁更多技能:BC的掘金主页~💐 BC的CSDN主页~💐💐 个人信息汇总.png

OkHttp官方文档:square.github.io/okhttp/
OkHttp github:github.com/square/okht…