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

相关推荐
诸神黄昏EX1 小时前
Android 分区相关介绍
android
大白要努力!2 小时前
android 使用SQLiteOpenHelper 如何优化数据库的性能
android·数据库·oracle
Estar.Lee2 小时前
时间操作[取当前北京时间]免费API接口教程
android·网络·后端·网络协议·tcp/ip
Winston Wood2 小时前
Perfetto学习大全
android·性能优化·perfetto
Dnelic-5 小时前
【单元测试】【Android】JUnit 4 和 JUnit 5 的差异记录
android·junit·单元测试·android studio·自学笔记
Eastsea.Chen7 小时前
MTK Android12 user版本MtkLogger
android·framework
长亭外的少年15 小时前
Kotlin 编译失败问题及解决方案:从守护进程到 Gradle 配置
android·开发语言·kotlin
建群新人小猿17 小时前
会员等级经验问题
android·开发语言·前端·javascript·php
1024小神18 小时前
tauri2.0版本开发苹果ios和安卓android应用,环境搭建和最后编译为apk
android·ios·tauri
兰琛18 小时前
20241121 android中树结构列表(使用recyclerView实现)
android·gitee