Android的OkHttp使用和原理

前言

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。

参考

10-OkHttp小结_哔哩哔哩_bilibili

相关推荐
我又来搬代码了2 小时前
【Android】【bug】Json解析错误Expected BEGIN_OBJECT but was STRING...
android·json·bug
CANI_PLUS11 小时前
ESP32将DHT11温湿度传感器采集的数据上传到XAMPP的MySQL数据库
android·数据库·mysql
来来走走12 小时前
Flutter SharedPreferences存储数据基本使用
android·flutter
安卓开发者14 小时前
Android模块化架构深度解析:从设计到实践
android·架构
雨白14 小时前
HTTP协议详解(二):深入理解Header与Body
android·http
阿豪元代码14 小时前
深入理解 SurfaceFlinger —— 如何调试 SurfaceFlinger
android
阿豪元代码14 小时前
深入理解 SurfaceFlinger —— 概述
android
CV资深专家16 小时前
Launcher3启动
android
stevenzqzq16 小时前
glide缓存策略和缓存命中
android·缓存·glide
雅雅姐17 小时前
Android 16 的用户和用户组定义
android