网络请求在 Android 开发中非常常见,为了降低开发周期和难度,我们经常会选用网络请求的开源库。
网络请求库的本质是 封装了网络请求
+ 异步
+ 数据处理
功能的库。
其中网络请求功能则是基于Android网络请求原生的方法(HttpClient
或者HttpURLConnection
)
Android原生网络请求方法:
HttpURLConnection是目前安卓原生仍然支持的网络请求方法。
使用网络请求,注意在manifest.xml中申请网络权限。
<uses-permission android:name="android.permission.INTERNET" />
目标网址是 https://www.baidu.com/s?wd=abc ,这是百度搜索abc。
步骤:
try {
URL url = new URL("https://www.baidu.com/s?wd=abc");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(10 * 1000);
conn.setRequestProperty("Cache-Control", "max-age=0");
conn.setDoOutput(true);
int code = conn.getResponseCode();
if (code == 200) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
InputStream inputStream = conn.getInputStream();
byte[] in = new byte[1024];
int len;
while ((len = inputStream.read(in)) > -1) {
baos.write(in, 0, len);
}
final String content = new String(baos.toByteArray());
baos.close();
inputStream.close();
conn.disconnect();
}
} catch (Exception e) {
e.printStackTrace();
}
和上文的GET类似,都是url开启连接拿到conn,然后设置参数。 这里我们用POST方法,并且带有body。服务器能收到我们传上去的参数。 假设服务器接受的是json格式。
try {
URL url = new URL("http://sample.com/sample");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST");
conn.setConnectTimeout(10 * 1000);
// 这里是示例
JSONObject bodyJson = new JSONObject();
bodyJson.put("imei", "获取imei");
bodyJson.put("deviceSn", "获取sn");
bodyJson.put("deviceBrand", Build.BRAND);
String body = bodyJson.toString();
conn.setRequestProperty("Content-Type", "application/json"); // 类型设置
conn.setRequestProperty("Cache-Control", "max-age=0");
conn.setDoOutput(true);
conn.getOutputStream().write(body.getBytes());
int code = conn.getResponseCode();
if (code == 200) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
InputStream inputStream = conn.getInputStream();
byte[] in = new byte[1024];
int len;
while ((len = inputStream.read(in)) > -1) {
baos.write(in, 0, len);
}
String content = new String(baos.toByteArray());
baos.close();
inputStream.close();
conn.disconnect();
JSONObject jsonObject = new JSONObject(content);
// 根据定义好的数据结构解析出想要的东西
}
} catch (Exception e) {
e.printStackTrace();
}
OkHttp 是一个基于Android原生网络请求的高效的 Http请求框架 ,旨在简化客户端网络请求,提高网络请求开发效率。
OKHttp具体的设计思想与特性如下:
总的来说,其设计思想是通过 简化请求过程 、提高请求效率、提高请求可靠性,从而提供 更快的网络请求响应速度 。
// build.gradle
implementation "com.squareup.okhttp3:okhttp:4.10.0"
// Android Manifest.xml
<uses-permission android:name="android.permission.INTERNET" />
public void request() {
String url = "http://www.baidu.com";
//'1. 生成OkHttpClient实例对象'
OkHttpClient okHttpClient = new OkHttpClient();
//'2. 生成Request对象'
Request request = new Request.Builder().url(url).build();
//'3. 生成Call对象'
Call call = okHttpClient.newCall(request);
//'4. 如果要执行同步请求:'
try {
call.execute();
} catch (IOException e) {
e.printStackTrace();
}
//'5. 如果要执行异步请求:'
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
}
@Override
public void onResponse(Call call, Response response) throws IOException {
}
});
}
总结下来就是下边几步:
okHttpClient
对象Request
Call
OKHttp的基本执行流程如下:
在执行同步或者异步前,我们需要先准备好okHttpClient
、Request
、Call
对象。
OkHttpClient:okHttp连接的客户端对象;
我们在代码中new一个客户端对象:
OkHttpClient okHttpClient = new OkHttpClient();
发生了什么?
源码如下:
OkHttpClient除了空参数的构造函数,还有一个传入Builder
的构造函数,而我们的new OkHttpClient()
最终也是调用了传入Builder
的构造函数,只不过传入默认的Builder对象值:
可以看到默认的连接超时,读取超时,写入超时,都为10秒。
如果不想使用默认的这些参数设置,可以构建我们自己想要的Builder
:
注意构建好builder后,不能直接使用那个传入Builder对象的OkHttpClient的构造函数,因为该构造函数的方法不是public的,应该使用builder的build()方法
OkHttpClient.Builder builder = new OkHttpClient.Builder();
builder.connectTimeout(20,TimeUnit.SECONDS);
builder.readTimeout(20,TimeUnit.SECONDS);
builder.writeTimeout(20,TimeUnit.SECONDS);
OkHttpClient okHttpClient = builder.build();
//这里不能直接使用那个传入Builder对象的OkHttpClient的构造函数,因为该构造函数的方法不是public的
OkHttpClient okHttpClient = new OkHttpClient(builder);//这样是错误的
builder.build();的源码是:
public OkHttpClient build() {
return new OkHttpClient(this);
}
除了时延参数,Builder的属性还包括:
Dispatch:调度器,用于连接请求分发调度。
Protocal:网络协议类(比如http1.1、http2.0、http3.0)
Interceptor:拦截器。
Cache:网络缓存,okhttp默认只能设置缓存GET请求,不缓存POST请求,毕竟POST请求很多都是交互的,缓存下来也没有什么意义。
Request:请求,这里实际上是http协议的请求头、请求体的实例对象。
查看Request代码:
可以看到Request具体包含了
public final class Request {
final HttpUrl url; //网络请求路径
final String method; //get、post.....
final Headers headers;//请求头
final @Nullable RequestBody body;//请求体
/**
你可以通过tags来同时取消多个请求。
当你构建一请求时,使用RequestBuilder.tag(tag)来分配一个标签。
之后你就可以用OkHttpClient.cancel(tag)来取消所有带有这个tag的call。.
*/
final Map<Class<?>, Object> tags;
.......
.......
.......
}
Call:请求调用接口,表示这个请求已经准备好可以执行。
我们生成的Request
实例,会传给OkHttpClient实例的newÇall
方法,每一个Request
对应用一个Call
,实际上Call是一个接口,newCall()
方法真正返回的对象是Call
的具体实现类RealCall
。
创建RealCall
对象后,就要调用同步或异步请求方法,所以它里面还包含同步请求 execute()
与异步请求 enqueue()
方法。
Request request = new Request.Builder().url(url).build();
Call call = okHttpClient.newCall(request);
call.execute();或者 call.enqueue(....);
Call类代码:
@Override public Call newCall(Request request) {
return RealCall.newRealCall(this, request, false /* for web socket */);
}
RealCall类代码:
static RealCall newRealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {
// Safely publish the Call instance to the EventListener.
RealCall call = new RealCall(client, originalRequest, forWebSocket);
call.eventListener = client.eventListenerFactory().create(call);
return call;
}
异步请求调用,是RealCall
的一个内部类,就是一个Runnable
,被调度器中的线程池所执行,在执行异步请求时,会将请求转为AsyncCall
,它最终被加入到线程池中执行。
不管是同步执行的call.execute()
,还是异步执行的call.enqueue();
,请求的执行都是交由调度器Dispatcher
统一管理。
enqueue()
方法将请求添加到 Call 对象时,它会首先进入异步请求就绪队列。调度器会根据并发限制和请求的优先级从这个队列中选取请求进行执行。readyAsyncCalls
队列中选取请求,并将其移动到这个队列中,然后执行请求。一旦请求执行完成,它会从这个队列中移除。execute()
)。同步请求会阻塞当前线程,直到请求完成,从队列移除。同步请求是在调用者所在线程执行,并会堵塞所在线程。而所有的异步请求,调度器dispatcher
最终交由自己的线程池executorService
实际执行的。dispatcher的线程池的创建:
public synchronized ExecutorService executorService() {
if (executorService == null) {
executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
new SynchronousQueue<>(), Util.threadFactory("OkHttp Dispatcher", false));
}
return executorService;
}
核心线程为0,每隔60秒会清空空闲的线程,而最大线程无限制,但是已经通过成员变量来进行控制了,没啥影响;这个线程池类似于cacheThreadPool,没有核心线程,最大线程无限制。
@Override
public Response execute() throws IOException {
synchronized (this) {
if (executed) throw new IllegalStateException("Already Executed");
executed = true;
}
captureCallStackTrace();
eventListener.callStart(this);
try {
//'1. 执行了dispatcher的executed方法'
client.dispatcher().executed(this);
//'2. 调用了getResponseWithInterceptorChain方法'
Response result = getResponseWithInterceptorChain();
if (result == null) throw new IOException("Canceled");
return result;
} catch (IOException e) {
eventListener.callFailed(this, e);
throw e;
} finally {
//'3. 最后一定会执行dispatcher的finished方法'
client.dispatcher().finished(this);
}
}
核心步骤就是3个:
executed()
方法,将此同步请求Call加入到正在运行的同步请求队列。finished(this)
,将执行完毕的请求从队列移出。private <T> void finished(Deque<T> calls, T call) {
Runnable idleCallback;
synchronized (this) {
// 移除队列的请求
if (!calls.remove(call)) throw new AssertionError("Call wasn't in-flight!");
idleCallback = this.idleCallback;
}
// 执行请求
boolean isRunning = promoteAndExecute();
if (!isRunning && idleCallback != null) {
// 触发空闲线程执行
idleCallback.run();
}
}
call.enqueue()
@Override public void enqueue(Callback responseCallback) {
//'1. 这里有个同步锁的抛异常操作'
synchronized (this) {
if (executed) throw new IllegalStateException("Already Executed");
executed = true;
}
captureCallStackTrace();
eventListener.callStart(this);
//'2. 调用Dispatcher里面的enqueue方法'
client.dispatcher().enqueue(new AsyncCall(responseCallback));
}
client.dispatcher().enqueue(new AsyncCall(responseCallback))
runnable
接口,最终在promoteAndExecute()
是在线程池中执行的异步请求。 void enqueue(AsyncCall call) {
synchronized (this) {
readyAsyncCalls.add(call);
// Mutate the AsyncCall so that it shares the AtomicInteger of an existing running call to
// the same host.
if (!call.get().forWebSocket) {
AsyncCall existingCall = findExistingCallWithHost(call.host());
if (existingCall != null) call.reuseCallsPerHostFrom(existingCall);
}
}
promoteAndExecute();
}
promoteAndExecute()
函数,这个函数的意思是从异步请求就绪队列中弹出请求,并尝试把它加入到正在执行的异步请求队列中。注意:asyncCall.executeOn(executorService());
private boolean promoteAndExecute() {
assert (!Thread.holdsLock(this));
List<AsyncCall> executableCalls = new ArrayList<>();
boolean isRunning;
synchronized (this) {
// 1、遍历准备要执行的请求队列
for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
AsyncCall asyncCall = i.next();
// 2、判断当前正在执行的请求个数大于最大请求个数时,则取消请求
if (runningAsyncCalls.size() >= maxRequests) break; // Max capacity.
// 3、判断当前主机的连接数超过5个时,则跳过当前请求;
if (asyncCall.callsPerHost().get() >= maxRequestsPerHost) continue; // Host max capacity.
i.remove();
asyncCall.callsPerHost().incrementAndGet();
executableCalls.add(asyncCall);
// 添加请求到正在执行的队列中
runningAsyncCalls.add(asyncCall);
}
isRunning = runningCallsCount() > 0;
}
for (int i = 0, size = executableCalls.size(); i < size; i++) {
AsyncCall asyncCall = executableCalls.get(i);
// 执行请求;
asyncCall.executeOn(executorService());
}
return isRunning;
}
getResponseWithInterceptorChain()
void executeOn(ExecutorService executorService) {
assert (!Thread.holdsLock(client.dispatcher()));
boolean success = false;
try {
// 线程池实际执行的函数
executorService.execute(this);
success = true;
} catch (RejectedExecutionException e) {
InterruptedIOException ioException = new InterruptedIOException("executor rejected");
ioException.initCause(e);
transmitter.noMoreExchanges(ioException);
responseCallback.onFailure(RealCall.this, ioException);
} finally {
if (!success) {
client.dispatcher().finished(this); // This call is no longer running!
}
}
}
@Override protected void execute() {
boolean signalledCallback = false;
transmitter.timeoutEnter();
try {
// 可以看到,异步请求最终还是走到getResponseWithInterceptorChain()这个函数。
Response response = getResponseWithInterceptorChain();
signalledCallback = true;
responseCallback.onResponse(RealCall.this, response);
} catch (IOException e) {
if (signalledCallback) {
// Do not signal the callback twice!
Platform.get().log(INFO, "Callback failure for " + toLoggableString(), e);
} else {
responseCallback.onFailure(RealCall.this, e);
}
} catch (Throwable t) {
cancel();
if (!signalledCallback) {
IOException canceledException = new IOException("canceled due to " + t);
canceledException.addSuppressed(t);
responseCallback.onFailure(RealCall.this, canceledException);
}
throw t;
} finally {
client.dispatcher().finished(this);
}
}
}
同步请求execute()核心步骤是三步:
call
请求任务加入到正在运行的同步队列runningSyncCalls
中getResponseWithInterceptorChain()
runningSyncCalls
移除。同步请求是阻塞式的。
异步请求enqueue()的核心步骤是五步。
call
对象重复添加,采用同步锁机制,第一次添加时,将标志executed
赋值为true,后续根据executed
的值判断是否重复添加。Call
转为AsynCall
,交由调度器把异步请求加入到异步请求就绪队列readyAsyncCalls
。promoteAndExecute
,尝试把异步请求从就绪队列readyAsyncCalls
加入到正在运行的异步请求队列runningAsyncCalls
。readyAsyncCalls
取出的异步请求,在线程池中执行getResponseWithInterceptorChain()
runningAsyncCalls
移除。在上面我们知道,异步请求和同步请求最终都走到了RealCall.getResponseWithInterceptorChain()
方法,即使用 拦截器链 获取本次请求的响应内容。
具体源码如下:
Response getResponseWithInterceptorChain() throws IOException {
// Build a full stack of interceptors.
List<Interceptor> interceptors = new ArrayList<>();
// 1、添加用户自定义的拦截器
interceptors.addAll(client.interceptors());
// 2、添加OKhttp自有的5大拦截器
interceptors.add(new RetryAndFollowUpInterceptor(client));
interceptors.add(new BridgeInterceptor(client.cookieJar()));
interceptors.add(new CacheInterceptor(client.internalCache()));
interceptors.add(new ConnectInterceptor(client));
if (!forWebSocket) {
interceptors.addAll(client.networkInterceptors());
}
interceptors.add(new CallServerInterceptor(forWebSocket));
// 3、将所有拦截器合并为拦截器链
Interceptor.Chain chain = new RealInterceptorChain(interceptors, transmitter, null, 0,
originalRequest, this, client.connectTimeoutMillis(),
client.readTimeoutMillis(), client.writeTimeoutMillis());
boolean calledNoMoreExchanges = false;
try {
// 4、执行责任链的proceed()
Response response = chain.proceed(originalRequest);
if (transmitter.isCanceled()) {
closeQuietly(response);
throw new IOException("Canceled");
}
return response;
} catch (IOException e) {
calledNoMoreExchanges = true;
throw transmitter.noMoreExchanges(e);
} finally {
if (!calledNoMoreExchanges) {
transmitter.noMoreExchanges(null);
}
}
}
上述的逻辑非常简单,内部会先创建一个局部拦截器集合interceptors :
proceed
方法OkHttp 内置了 5 个核心的拦截器用来完成请求生命周期中的关键处理,同时它也支持在连接开始时(应用拦截器)和响应前(网络拦截器)这两个地方通过自定义的拦截器来增强和扩展 Http 客户端。
这些拦截器通过责任链模式串联起来,允许将请求沿着处理者链发送。 收到请求后, 每个处理者均可对请求进行处理, 或将其传递给链上的下一个处理者。
RetryAndFollowUpInterceptor
:这里会对连接做一些初始化工作,处理错误重试和重定向。BridgeInterceptor
:是客户端与服务器之间的沟通桥梁,负责将用户构建的请求转换为服务器需要的请求,以及将网络请求返回回来的响应转换为用户可用的响应。主要工作是为请求添加cookie、添加固定的header,比如Host、Content-Length、Content-Type、User-Agent等等,然后保存响应结果的cookie,如果响应使用gzip压缩过,则还需要进行解压。CacheInterceptor
:缓存拦截器,如果命中缓存则不会发起网络请求。ConnectInterceptor
:连接拦截器,内部会维护一个连接池,负责连接复用、创建连接(TCP三次握手、TSL)、释放连接以及创建连接上的socket流。CallServerInterceptor
:这里就是进行网络数据的请求和响应了,也就是实际的网络I/O操作,将请求头与请求体发送给服务器,以及解析服务器返回的response。二者通常的叫法为应用拦截器和网络拦截器。
从整个责任链路来看,应用拦截器是最先执行的拦截器
,也就是用户自己设置request属性后的原始请求,而网络拦截器
位于ConnectInterceptor
和CallServerInterceptor
之间,此时网络链路已经准备好,只等待发送请求数据。
上述的各种拦截器在连接成链后,构成了责任链模式
,每个拦截器在处理请求时调用了chain.proceed(request)
方法,它会将请求传递给下一个拦截器继续处理
。类似地,如果一个拦截器在处理响应时调用了 chain.proceed(response)
方法,它会将响应传递给下一个拦截器
继续处理。
如下图,request和response是两个相反的责任链处理的流程方向。
为保证责任链能依次进行下去,必须保证除最后一个拦截器(CallServerInterceptor)外,其他所有拦截器intercept方法内部必须调用一次chain.proceed()方法,如此一来整个责任链就运行起来了。
经过责任链一级一级的递推下去,最终会执行到CallServerInterceptor的intercept方法,此方法会将网络响应的结果封装成一个Response对象并return。之后沿着责任链一级一级的回溯,最终就回到getResponseWithInterceptorChain方法的返回。
在我们的项目中,有一类请求需要在请求头 Header 中添加认证信息,使用拦截器来实现可以极大地简化代码,提高代码可读性和可维护性。核心代码只需要实现符合业务需要的拦截器如下:
第一步:
构建我们自己的拦截器类,实现拦截器接口:
public class EncryptInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request originRequest = chain.request();
// 计算认证信息
String authorization = this.encrypt(originRequest);
// 添加请求头
Request request = originRequest.newBuilder()
.addHeader("Authorization", authorization)
.build();
// 向责任链后面传递
return chain.proceed(request);
}
}
第二步
在创建OkHttpClient 客户端的时候,使用 addInterceptor() 方法将我们的拦截器注册成应用程序拦截器,即可实现自动地、无感地向请求头中添加实时的认证信息的功能。
OkHttpClient client = new OkHttpClient.Builder().addInterceptor(new EncryptInterceptor()).build();
在HTTP 1.0时代,响应使用Expires头标识缓存的有效期,其值是一个绝对时间,当客户端再次发出网络请求时可比较当前时间 和上次响应的expires时间进行比较,来决定是使用缓存还是发起新的请求。
使用Expires头最大的问题是它依赖客户端的本地时间,如果用户自己修改了本地时间,就会导致无法准确的判断缓存是否过期。
因此,从HTTP 1.1 开始使用Cache-Control头表示缓存状态,它的优先级高于Expires,常见的取值为下面的一个或多个。
强制缓存:在上述缓存头规约基础之上,强制缓存是指网络请求响应header标识了Expires或Cache-Control带了max-age信息,而此时客户端计算缓存并未过期
,则可以直接使用本地缓存内容,而不用真正的发起一次网络请求。
协商缓存:强制缓存最大的问题是,一旦服务端资源有更新,直到缓存时间截止前,客户端无法获取到最新的资源(除非请求时手动添加no-store头),另外大部分情况下服务器的资源无法直接确定缓存失效时间,所以使用协商缓存更灵活一些。
使用Last-Modify / If-Modify-Since头实现协商缓存,具体方法是服务端响应头添加Last-Modify头标识资源的最后修改时间,单位为秒,当客户端再次发起请求时添加If-Modify-Since头并赋值为上次请求拿到的Last-Modify头的值。
服务端收到请求后自行判断缓存资源是否仍然有效,如果有效则返回状态码304同时body体为空,否则下发最新的资源数据。客户端如果发现状态码是304,则取出本地的缓存数据作为响。
使用这套方案有一个问题,那就是资源文件使用最后修改时间有一定的局限性:
因此,HTTP 还提供了另外一组头信息来处理缓存,ETag/If-None-Match。流程与Last-Modify一样,只是把服务端响应的头变成Last-Modify,客户端发出的头变成If-None-Match。ETag是资源的唯一标识符
,服务端资源变化一定会导致ETag变化。具体的生成方式有服务端控制,场景的影响因素包括,文件最终修改时间、文件大小、文件编号等等。
上面讲了这么多,实际上OKHttp就是将上述流程用代码实现了一下,即:
要开启使用Okhttp的缓存其实很简单,只需要给OkHttpClient对象设置一个Cache对象即可,创建一个Cache时指定缓存保存的目录和缓存最大的大小即可,OKhttp默认只支持get请求的缓存,缓存系统内部使用LRU作为缓存的淘汰算法。
//新建一个cache,指定目录为外部目录下的okhttp_cache目录,大小为100M
Cache cache = new Cache(new File(Environment.getExternalStorageDirectory() + "/okhttp_cache/"), 100 * 1024 * 1024);
//将cache设置到OkHttpClient中,这样缓存就开始生效了。
OkHttpClient client = new OkHttpClient.Builder().cache(cache).build();
Okhttp内部维护了网络连接池,查找当前请求是否有对应可用的连接,避免每次请求重新建立和断开TCP连接。
OkHttp 实现连接复用的基本流程:
通过连接池的机制,OkHttp 能够有效地复用网络连接,避免了频繁创建和关闭连接的开销,提高了网络请求的性能和效率。连接池中的连接会在空闲一段时间后被自动关闭,以确保连接池中的连接不会无限增长。