前言
OkHttp的出现代替了HttpUrlConnection,被谷歌官方收纳为底层的网络框架。特点如下:
- 支持HTTP/2框架下的socket复用
- 通过连接池减少连接的延时
- 使用GZIP进行数据压缩
- 使用缓存技术避免重复请求
当网络出现问题时,OkHttp会静默重新恢复连接,因为是静默的,所以用户无感知。
构建
首先在模块下的build.gradle文件中导入OkHttp依赖,并开启viewBinding:
Groovy
android {
...
buildFeatures {
viewBinding = true //开启viewBinding
}
}
dependencies {
...
implementation 'com.squareup.okhttp3:okhttp:3.14.+'
}
同时在AndroidManifest.xml文件中开启网络访问权限:
XML
<uses-permission android:name="android.permission.INTERNET" />
viewBinding可以去除findViewById的操作。我们在xml中简单的使用一个button开启网络服务:
XML
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<Button
android:id="@+id/btn1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="测试请求" />
<TextView
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
使用viewBinding获取控件,并新建一个OkHttpClient实例:
java
public class MainActivity extends AppCompatActivity {
ActivityMainBinding binding;
OkHttpClient okHttpClient;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
// 也可以使用okHttpClient = new OkHttpClient()来创建
okHttpClient = new OkHttpClient.Builder().build();
binding.btn1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
testGet();
}
});
}
/**
* 获取网络数据
*/
private void testGet() {
// 创建Request实例,它描述了我们需要请求的内容
Request request = new Request.Builder()
// 此处传入一个url数据请求,需要在Manifest文件开通网络权限
.url("https://api.scryfall.com/cards/search?order=name&q=name%3Dthree+visits")
.build();
// 发起网络请求可以是同步也可以是异步的
// 同步写法,因为网络请求是耗时操作,需要开辟一个新的子线程
new Thread(new Runnable() {
@Override
public void run() {
try {
// newCall是发起网络请求的方法,需要传入Request,即我们对request的描述
// 该方法的返回值是response
Response response = okHttpClient.newCall(request).execute();
String result = response.body().string();
// 更新ui需要在主线程
runOnUiThread(new Runnable() {
@Override
public void run() {
binding.textView.setText(result);
}
});
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}).start();
}
}
在使用OkHttpClient时,尽量保持单例创建,因为其构造方法中存有很多内容。
在获取网络数据时,我们首先新建一个Request实例,它是用来描述我们需要向网络请求的内容的;其次使用OkHttpClient的newCall方法装在我们的请求,并使用execute方法执行。
执行请求后会返回服务端的结果,类型是Response。此时我们就可以使用请求到的结果了。
异步请求写法如下:
java
okHttpClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {
}
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
String result = response.body().string();
runOnUiThread(new Runnable() {
@Override
public void run() {
binding.textView.setText(result);
}
});
}
});
双任务队列机制
在使用OkHttpClient的newCall方法时,会返回一个Call类型的实例。我们使用的execute等方法都是在执行Call中的方法,因此我们可以理解Call这个类就是封装了执行业务的方法。
在异步执行的方式中,我们使用了enqueue方法。查看上述的源码,可以其中发现调用了client的dispatcher().enqueue方法,并新建了一个AsyncCall实例传入(对应机制图的第一步)。
AsyncCall继承了NamedRunnable类,而这个类继承了Runnable类,因此我们主要关注AsyncCall是如何重写execute方法的。
在AsyncCall被传入的enqueue方法中,包含了我们的双队列机制的实现,。
promoteAndExcecute方法中出现了双队列。
被判断过滤后的可执行的AsyncCall会被投入一个临时的队列中用于遍历。在遍历时会执行AsyncCalls的excuteOn方法,传入一个线程池。
在这个方法中会执行线程池的execute方法,也就是AsyncCall的run方法。实际上这里做的就是把任务交给了线程池执行。
此处的线程池可以看到没有设置核心线程(corePoolSize = 0),所有的操作都是由临时线程完成。当有新任务进来时,就会创建一个执行周期为60s的临时任务;同时因为没有核心线程,被传入线程池的任务会立即执行。
而在AsyncCall的excute方法中,最后会执行finished方法。
在执行到finished方法时,会有会开始新一轮的promoteAndExecuted方法。这样做我们就达到了从等待队列中循环获取AsyncCall。
如果我们使用的是一个while循环,那么cpu就会一直要为了这个循环释放资源,而不是等待队列中有内容才去执行循环。
责任链模式和拦截器
拦截器就好比请假审批,你的请假会被人事、主管、部门领导等层层审批,最终回到你的手里。
我们可以实现一个自己的拦截器。
java
public abstract class Handler {
// 对于每个handler,它会持有下一个handler(next变量)
protected Handler next;
public Handler getNext() {
return next;
}
public void setNext(Handler next) {
this.next = next;
}
// 处理请求的接口方法
public abstract void handleRequest(String request);
}
拦截器的具体实现方法如下。
java
public class MainHandler1 extends Handler {
@Override
public void handleRequest(String request) {
if (request.equals("one")) {
Log.i("TAG", "具体处理者1处理该请求");
} else {
if (getNext() != null) {
next.handleRequest(request);
} else {
Log.i("TAG", "没有人处理请求");
}
}
}
}
可以生成多个MainHandler模拟多个拦截器。
java
Handler handler1 = new MainHandler1();
Handler handler2 = new MainHandler2();
Handler handler3 = new MainHandler3();
handler1.setNext(handler2);
handler2.setNext(handler3);
handler1.handleRequest("one");
真实的拦截器接口实现如下。
java
public class LogIntercept implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
// 该层级下的责任链请求request
Request request = chain.request();
long curTime = System.currentTimeMillis();
Log.i("TAG", "intercept: REQUEST = " + request.toString());
// 该层级下的责任链传递response
Response response = chain.proceed(request);
Log.i("TAG", "intercept: RESPONSE = " + response.toString());
Log.i("TAG", "intercept: 耗时 = " + (System.currentTimeMillis() - curTime) + "ms");
return response;
}
}
我们自己定义的拦截器会先于系统的拦截器执行。
所以如果我们的拦截器中没有很好的做到chain.request责任链请求和chain.proceed责任链传递、而导致链路首先在我们自己的拦截器断了的话,整个责任链就会断开,系统的拦截器自然也不会执行。
连接池的复用机制
TCP的三次握手和四次挥手
在挥手(即断开连接)时,服务器在接收到来自客户端的断连申请(即第一次挥手)时,会反馈两次,一次是表明接收到断连申请,但不会马上断开,因为很有可能服务器中还有客户端的数据在处理;待数据处理完毕后,会再挥手一次,表明自身已处理完毕,可以断开。
Socket连接池复用
考虑到每次连接都要三次握手、每次断开都要四次挥手显然会造成效率低下,Http协议中有一种KeepAlive机制,它可以在数据完成传输(即原本应断连的情况)后仍然保留连接状态。
在这个机制下,会有一个链路的存活时间。当存活时间到达后,该连接才真正断开。
OkHttp默认支持5个并发KeepAlive,链路默认存活时间为5分钟。
真实的连接信息保存在RealConnection中,包含socket等内容。
RealConnectionPool用于存储RealConnection的队列,同时还有对socket的清理机制。
若判断条件成立(需要被清理),会执行cleanupRunnable。