【面试 反思】Retrofit源码与设计 7 连问

前言

在实际项目中往往是使用Retrofit来做网络请求工作。Retrofit采用RESTful风格,本质上只是对OkHttp进行封装,今天我们根据几个问题来进一步学习一下Retrofit的源码与设计思想。

1. 使用方法

直接看一下官方介绍的使用方法。

public final class SimpleService {public static final String API_URL = "https://api.github.com";public static class Contributor {public final String login;public final int contributions;public Contributor(String login, int contributions) {this.login = login;this.contributions = contributions;}}public interface GitHub {@GET("/repos/{owner}/{repo}/contributors")Call<List<Contributor>> contributors(@Path("owner") String owner, @Path("repo") String repo);}public static void main(String... args) throws IOException {// Create a very simple REST adapter which points the GitHub API.Retrofit retrofit =new Retrofit.Builder().baseUrl(API_URL).client(new OkHttpClient().newBuilder().connectTimeout(10, TimeUnit.SECONDS).build()).addConverterFactory(GsonConverterFactory.create()).build();// Create an instance of our GitHub API interface.GitHub github = retrofit.create(GitHub.class);// Create a call instance for looking up Retrofit contributors.Call<List<Contributor>> call = github.contributors("square", "retrofit");// Fetch and print a list of the contributors to the library.List<Contributor> contributors = call.execute().body();for (Contributor contributor : contributors) {System.out.println(contributor.login + " (" + contributor.contributions + ")");}}
}

可以简单的概括成三步:

  1. 构建 retrofit 实例。
  2. 构建 API 接口实例。
  3. 执行请求,解析响应。

2. 流程解析

我们按照它的使用方法来分析一下它的流程。

2.1 构建 Retrofit 实例

从使用方法可以看出是使用建造者模式来构建实例。

Retrofit retrofit =new Retrofit.Builder().baseUrl(API_URL).client(new OkHttpClient().newBuilder().connectTimeout(10, TimeUnit.SECONDS).build()).addConverterFactory(GsonConverterFactory.create()).addCallAdapterFactory(RxJavaCallAdapterFactory.create()).build();

这一步就不具体展开了,看几个参数。

public static final class Builder {//实际的请求调用,如 okhttp3.OkHttpClientprivate @Nullable okhttp3.Call.Factory callFactory;//基础URL,如:域名private @Nullable HttpUrl baseUrl;//数据转换器列表private final List<Converter.Factory> converterFactories = new ArrayList<>();//请求适配器列表private final List<CallAdapter.Factory> callAdapterFactories = new ArrayList<>();

2.2 构建 API 接口实例

按照官方的使用方法介绍,我们会将我们的API方法放在一个接口中,然后通过注解来设置请求参数。在使用时,通过retrofit.create(Class<T>)方法将这个接口实例化,然后调用其方法。 如:

public interface GitHub {@GET("/repos/{owner}/{repo}/contributors")Call<List<Contributor>> contributors(@Path("owner") String owner, @Path("repo") String repo);
}//实例化API接口
GitHub github = retrofit.create(GitHub.class);
//调用接口中某条API
Call<List<Contributor>> call = github.contributors("square", "retrofit");

看一下源码

public <T> T create(final Class<T> service) {//验证 api servicevalidateServiceInterface(service);return (T)//这里采用了动态代理模式, service 就是被代理类//todo 为什么要采用动态代理,有什么好处吗?用别的行不行?Proxy.newProxyInstance(service.getClassLoader(),new Class<?>[] {service},new InvocationHandler() {private final Object[] emptyArgs = new Object[0];@Overridepublic @Nullable Object invoke(Object proxy, Method method, @Nullable 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);}args = args != null ? args : emptyArgs;Platform platform = Platform.get();//如果不是系统默认方法,通过loadServiceMethod()方法返回一个ServiceMethod,并调用invoke方法return platform.isDefaultMethod(method)? platform.invokeDefaultMethod(method, service, proxy, args): loadServiceMethod(method).invoke(args);}});}

做了两件事:

  1. 验证我们的API接口类。
  2. 利用动态代理在运行期间实例化API接口。
  private void validateServiceInterface(Class<?> service) {//service 必须是 interface,否则抛出异常if (!service.isInterface()) {throw new IllegalArgumentException("API declarations must be interfaces.");}...省略代码...//是否立即验证API接口中的所有方法,由用户设置,默认为falseif (validateEagerly) {Platform platform = Platform.get();//遍历 service 中定义的所有方法for (Method method : service.getDeclaredMethods()) {//如果该方法不是系统默认方法且方法修饰符不是静态方法就执行loadServiceMethod方法if (!platform.isDefaultMethod(method) && !Modifier.isStatic(method.getModifiers())) {//加载请求方法。loadServiceMethod(method);}}}}

从这我们也可以看出,我们的API方法必须方法接口中。如果开始验证接口,会遍历其声明的所有方法,过滤掉系统默认方法与静态方法,然后执行loadServiceMethod(method)

扩充一下:

getMethods(): 返回由类或接口声明的以及从超类和超接口继承的所有公共方法。 getDeclaredMethods(): 返回类声明的方法,包括 public, protected, default (package),但不包括继承的方法。所以,相对比于 getMethods 方法,getDeclaredMethods速度更快,尤其是在复杂的类中,如在Activity类中。

最终都是通过loadServiceMethod(method) 方法来加载一个ServiceMethod

看一下HttpServiceMethod.parseAnnotations()方法,我将其简化了一下,如下:

HttpServiceMethod.javastatic <ResponseT, ReturnT> HttpServiceMethod<ResponseT, ReturnT> parseAnnotations(Retrofit retrofit, Method method, RequestFactory requestFactory) {//获取方法的注解信息Annotation[] annotations = method.getAnnotations();//适配器类型,就是Retrofit.addCallAdapterFactory()添加的类型。Type adapterType;//方法的返回类型adapterType = method.getGenericReturnType();//实例化一个 CallAdapter 对象CallAdapter<ResponseT, ReturnT> callAdapter =createCallAdapter(retrofit, method, adapterType, annotations);//检查 responseType,如果不合格则抛出异常Type responseType = callAdapter.responseType();//实例化一个Converter对象,将 okhttp3.ResponseBody 转换成 ResponseT 类型Converter<ResponseBody, ResponseT> responseConverter =createResponseConverter(retrofit, method, responseType);okhttp3.Call.Factory callFactory = retrofit.callFactory;//不是kotlin挂起方法,返回 CallAdapted,其实也就是调用 callAdapter.adapter 方法return new CallAdapted<>(requestFactory, callFactory, responseConverter, callAdapter);}

实例化了 ServiceMethod 后,调用invoke方法。

HttpServiceMethod.java@Overridefinal @Nullable ReturnT invoke(Object[] args) {//新建一个 OkHttpCall 请求Call<ResponseT> call = new OkHttpCall<>(requestFactory, args, callFactory, responseConverter);//然后调用 adapt 方法,CallAdapted 有重写 adapt 方法,然后调用 callAdapter.adapt(call) 方法return adapt(call, args);}protected abstract @Nullable ReturnT adapt(Call<ResponseT> call, Object[] args);

从上述代码中可以看出,invoke方法就是实例化一个Call请求,然后调用adapter方法,在这里adapter是一个抽象方法,所以具体实现方法就需要看它的具体实现类CallAdapter。 这里的 CallAdapter 就是通过.addCallAdapterFactory()方法所添加的CallAdapter,以及根据平台默认提供的DefaultCallAdapterFactory中的CallAdapter,执行其adapter方法,最终返回Call<Object>

2.3 执行请求,解析响应

在上一步中,我们对API接口进行了实例化,通过CallAdapter对请求进行适配,最终得到一个Call<Object>对象。

接着下一步,就是执行这个Call<Object>请求,最终得到我们想要的Object对象。

例如一开始使用方法中所介绍的:

//已经得到了Call<List<Contributor>>对象,执行call,得到List<Contributor>
List<Contributor> contributors = call.execute().body();

调用 execute 执行同步请求获取到Response,然后获取其请求体。

OkHttpCall.java@Overridepublic Response<T> execute() throws IOException {okhttp3.Call call;synchronized (this) {//判断请求是否已经被执行,如果已被执行则抛出异常if (executed) throw new IllegalStateException("Already executed.");executed = true;//获取最原始的请求,通过createRawCall()创建okhttp3.Callcall = getRawCall();}if (canceled) {call.cancel();}//执行请求,并且解析响应,将okhttp3.response 转换成 retrofit2.responsereturn parseResponse(call.execute());}private okhttp3.Call createRawCall() throws IOException {//构造原始请求okhttp3.Call call = callFactory.newCall(requestFactory.create(args));if (call == null) {throw new NullPointerException("Call.Factory returned null.");}return call;}/*** 解析响应,就是就okhttp3.response 转换成 retrofit2.response*/Response<T> parseResponse(okhttp3.Response rawResponse) throws IOException {...省略代码...try {//利用converter转换成我们期望的类型T body = responseConverter.convert(catchingBody);return Response.success(body, rawResponse);} catch (RuntimeException e) {...省略代码...}

从源码中也可以看出,请求的实际工作还是通过okhttp来完成的,这边Retrofit就是负责请求与响应转换工作,将retrofit2.Call转换成okhttp3.Call,将okhttp3.response转换成retrofit2.response

3. 为什么要引入CallAdapter与Converter?

如果你熟悉okHttp的话,你应该知道,当我们请求的时候,要先通过OkHttpClient.newCall(request)方法将request转换成Call对象,然后再执行这个Call对象拿到response

但是Retrofit不光光只支持Call,他还可以将请求适配成Observable类型,方便与RxJava2结合起来一起使用。这就是通过CallAdapter来进行适配工作的,例如通过默认的DefaultCallAdapterFactory将请求转换成Call<Object>,通过RxJava2CallAdapter将请求转换成Observable<Object>

回到okHttp,大部分业务情况下,我们在拿到响应体后都会将其进行反序列化成对象,方便调用。显然,Retrofit就考虑到了这一点,所以他默认提供了GsonConverterFactory,来帮助我们做这一步反序列化工作。这就是通过Converter来完成的,同时它也支持用户进行自定义。

4. CallAdapter 是如何工作的?

作为请求适配器,我们将CallAdapter工作流程分为三步:添加、匹配、工作。

添加

可以通过addCallAdapterFactory(CallAdapter.Factory)方法来添加请求适配器工厂类,添加成功后会被保存在callAdapterFactories列表中。另外,Retrofit会根据Platform来添加默认的请求适配器,例如:DefaultCallAdapterFactory等等,同样也加入到callAdapterFactories列表中。

匹配

思考一下:所有添加的请求适配器都会被保存在callAdapterFactories列表中,那在实际请求中是如何匹配出相对应的适配器的呢?

HttpServiceMethod.parseAnnotations()方法中,我们有实例化一个CallAdapter对象。(具体流程就不再次展开了,请回头看 2.2 构建 API 接口实例 中所介绍内容。)

HttpServiceMethod.javastatic <ResponseT, ReturnT> HttpServiceMethod<ResponseT, ReturnT> parseAnnotations(Retrofit retrofit, Method method, RequestFactory requestFactory) {//实例化一个 CallAdapter 对象CallAdapter<ResponseT, ReturnT> callAdapter =createCallAdapter(retrofit, method, adapterType, annotations);···省略代码···//不是kotlin挂起方法,返回 CallAdapted,其实也就是调用 callAdapter.adapter 方法return new CallAdapted<>(requestFactory, callFactory, responseConverter, callAdapter);}

匹配工作其实就在createCallAdapter()方法中,一步步走下来,最终到Retrofit.nextCallAdapter()方法中:

Retrofit.javapublic CallAdapter<?, ?> nextCallAdapter(@Nullable CallAdapter.Factory skipPast, Type returnType, Annotation[] annotations) {int start = callAdapterFactories.indexOf(skipPast) + 1;for (int i = start, count = callAdapterFactories.size(); i < count; i++) {//通过方法的返回值类型与注解信息来找到匹配的CallAdapterCallAdapter<?, ?> adapter = callAdapterFactories.get(i).get(returnType, annotations, this);if (adapter != null) {return adapter;}}···省略代码···//如果找不到匹配的CallAdapter,则抛出异常throw new IllegalArgumentException(builder.toString());}

简单概括一下,就是通过方法的返回值类型与注解信息,遍历callAdapterFactories列表,找到匹配的CallAdapter,如果找不到则抛出IllegalArgumentException异常。

工作

找到匹配的CallAdapter后,剩下就是看看他是如何工作的。

如上一步匹配过程所介绍,在找到匹配的callAdapter后,会通过它来实例化一个CallAdapted对象。

  static final class CallAdapted<ResponseT, ReturnT> extends HttpServiceMethod<ResponseT, ReturnT> {private final CallAdapter<ResponseT, ReturnT> callAdapter;CallAdapted(RequestFactory requestFactory,okhttp3.Call.Factory callFactory,Converter<ResponseBody, ResponseT> responseConverter,CallAdapter<ResponseT, ReturnT> callAdapter) {//将responseConverter传给父类。super(requestFactory, callFactory, responseConverter);this.callAdapter = callAdapter;}@Overrideprotected ReturnT adapt(Call<ResponseT> call, Object[] args) {return callAdapter.adapt(call);}}

CallAdapted很简单,就是继承了HttpServiceMethod,然后复写了adapt方法。也就是说,最终执行的,其实就是我们上一步匹配到的CallAdapter对象的adapt方法。

比如匹配到的是DefaultCallAdapterFactory中的CallAdapter,最终执行的就是其adapt方法,具体代码细节这边就不展示了,有兴趣同学请自行查阅。

另外,我这边展示的是不支持kotlin挂起函数的情况,当然即使是kotlin挂起函数,过程也是一样的,也是执行其子类的adapt方法。

5. Converter 是如何工作的?

作为数据转换器,我们同样将Converter工作流程分为三步:添加、匹配、工作。

添加

可以通过addConverterFactory(Converter.Factory)方法来添加数据装换器工厂类,添加成功后会被保存在converterFactories列表中。另外,Retrofit会根据Platform来添加默认的数据转换器,例如OptionalConverterFactory,同样也加入到converterFactories列表中。

匹配

跟上述所介绍的 4. CallAdapter 是如何工作的 一样,同样在HttpServiceMethod.parseAnnotations()方法中,会实例化一个Converter对象。

匹配工作其实就在createResponseConverter()方法中,一步步走下来,最终到Retrofit.nextResponseBodyConverter()方法中:

  public <T> Converter<ResponseBody, T> nextResponseBodyConverter(@Nullable Converter.Factory skipPast, Type type, Annotation[] annotations) {int start = converterFactories.indexOf(skipPast) + 1;for (int i = start, count = converterFactories.size(); i < count; i++) {//通过转换类型与注解信息来找到匹配的ConverterConverter<ResponseBody, ?> converter =converterFactories.get(i).responseBodyConverter(type, annotations, this);if (converter != null) {//noinspection uncheckedreturn (Converter<ResponseBody, T>) converter;}}···省略代码···//如果找不到匹配的Converter,则抛出异常throw new IllegalArgumentException(builder.toString());}

简单概括一下,就是通过转换类型与注解信息,遍历converterFactories列表,找到匹配的Converter,如果找不到则抛出IllegalArgumentException异常。

工作

跟上述所介绍的 4. CallAdapter 是如何工作的 一样,我们找到匹配的Converter后,通过它来实例化一个CallAdapted。但不同的是,我们会将responseConverter传给父类,也就是HttpServiceMethod,然后当其调用invoke方法时,我们通过responseConverter来实例化一个OkHttpCall对象,最终将这个OkHttpCall对象传给adapter方法执行。

最终当执行请求时,OkHttpCall执行parseResponse来解析响应,调用responseConverter.convert()方法,将ResponseBody数据转换我们想要的类型。

6. 说说使用到了哪些设计模式

动态代理模式

Retrofit内部通过动态代理+反射来拿到用户定义在接口中的请求参数,从而来构建实际请求。具体细节这边就不再次展开了,可以回头查看 2.2 构建 API 接口实例 这一部分内容。

为什么要使用动态代理来获取API方法?

不知道你们有没有这个疑问,为什么我们的API方法需要定义在interface中呢?又为什么要通过动态代理+反射的形式来拿到请求参数呢?

Retrofit按照RESTful风格设计并通过注解来定义API方法的请求参数,并将这些API方法放到interface中。因为interface是不能被实例化的,所以这里采用动态代理在运行期间实例化API接口,获取到方法的请求参数。

再进一步:

解耦,将实际业务与Retrofit隔离开来。用户只需通过注解方法来定义请求参数,而实际请求的构建则通过Retrofit内部来实现。

此时再反过来看为何放在interface中?相信你心中已有答案了吧。

策略模式

当针对同一类型问题有多种处理方式,仅仅是具体行为有差别时,就可以使用策略模式。

例如:Retrofit中的请求适配器

  • 抽象策略:CallAdapter
  • 策略具体实现:DefaultCallAdapterFactory.get()RxJava2CallAdapter

即提供默认的请求适配器,也支持用户自定义,符合开闭原则,达到很好的可扩展性。

适配器模式

Retrofit会帮我们构建实际请求,内部通过默认的DefaultCallAdapterFactory来将请求转换成Call<Object>,同时Retrofit也支持其它平台,比如为了适配RxJava特性,将请求转换成Observable<Object>

  • Target(目标角色): Call<Object>, Observable<Object>
  • adaptee(需要适配的对象): OkHttpCall
  • adapter(适配器):DefaultCallAdapterFactory.get()RxJava2CallAdapter

工厂方法模式

我们以Converter来举例。

  • 抽象工厂:Converter.Factory
  • 具体工厂:GsonConverterFactoryBuiltInConverters等等。
  • 抽象产品:Converter
  • 具体产品:GsonResponseBodyConverterGsonRequestBodyConverterToStringConverter等等。

这边就不具体展开分析各个类了,有兴趣的同学可自行查阅。

建造者模式

在构建Retrofit实例的时候,就用到了建造者模式。建造者模式在开源库中的出现的次数真的很频繁,为了适配不同的用户的各种需求,需提供各种各样的参数与方法来供用户自行选择,所以使用建造者模式,之所以很常见,是因为这样很合理。

7. 使用过程中踩过什么坑?

关于BaseUrl的使用曾经踩过坑,某天我将baseUrl改了一下,然后发现请求接口时服务器一直返回404,但是当我尝试用Postman去调试接口的时候,发现接口是好的,也就推测出来是我的代码出问题了。

最终发现,问题出在:拼接成完整的URLapi被删除了。

Base URL: http://example.com/api/
Endpoint: /foo/bar/ 
Result: http://example.com/foo/bar/

正确的使用方式为:Endpoint不以斜杠开头。

Base URL: http://example.com/api/ 
Endpoint: foo/bar/ 
Result: http://example.com/api/foo/bar/

总结

本文,我们以几个问题的形式展开来对Retrofit源码及设计思想进行解析,相信你对源码有了进一步的了解。Retrofit本质只是对okHttp进行封装,出发点肯定是让网络请求变得更加容易,考虑适配各种用户需求,Jake Wharton大神用了很多设计模式,真的太让人膜拜了。

到此,关于Retrofit的源码解析就结束啦。

推荐更多Android学习笔记参考

Android 性能优化篇:https://qr18.cn/FVlo89
Android 车载篇:https://qr18.cn/F05ZCM
Android 逆向安全学习笔记:https://qr18.cn/CQ5TcL
Android Framework底层原理篇:https://qr18.cn/AQpN4J
Android 音视频篇:https://qr18.cn/Ei3VPD
Jetpack全家桶篇(内含Compose):https://qr18.cn/A0gajp
Kotlin 篇:https://qr18.cn/CdjtAF
Gradle 篇:https://qr18.cn/DzrmMB
OkHttp 源码解析笔记:https://qr18.cn/Cw0pBD
Flutter 篇:https://qr18.cn/DIvKma
Android 八大知识体:https://qr18.cn/CyxarU
Android 核心笔记:https://qr21.cn/CaZQLo
Android 往年面试题锦:https://qr18.cn/CKV8OZ
2023年最新Android 面试题集:https://qr18.cn/CgxrRy
Android 车载开发岗位面试习题:https://qr18.cn/FTlyCJ
音视频面试题锦:https://qr18.cn/AcV6Ap

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/104163.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

[MyBatis系列③]动态SQL

目录 1、简介 2、if标签 3、foreach标签 4、SQL抽取 ⭐MyBatis系列①&#xff1a;增删改查 ⭐MyBatis系列②&#xff1a;两种Dao开发方式 1、简介 开发中在MyBatis映射文件配置SQL语句&#xff0c;但是前面配置的都是比较简单的&#xff0c;不涉及稍复杂的业务场景。想要应…

SpringBootWeb案例 Part 5

4. 配置文件 员工管理的增删改查功能我们已开发完成&#xff0c;但在我们所开发的程序中还一些小问题&#xff0c;下面我们就来分析一下当前案例中存在的问题以及如何优化解决。 4.1 参数配置化 在我们之前编写的程序中进行文件上传时&#xff0c;需要调用AliOSSUtils工具类&…

STM32使用PID调速

STM32使用PID调速 PID原理 PID算法是一种闭环控制系统中常用的算法&#xff0c;它结合了比例&#xff08;P&#xff09;、积分&#xff08;I&#xff09;和微分&#xff08;D&#xff09;三个环节&#xff0c;以实现对系统的控制。它的目的是使 控制系统的输出值尽可能接近预…

Facebook HiPlot “让理解高维数据变得容易”

在这个全球信息化的时代&#xff0c;数据量呈爆炸式增长&#xff0c;数据的复杂性也是如此。如何有效地处理高维数据并找到隐藏在其中的相关性和模式是一个严峻的挑战。近年来&#xff0c;可视化和可视化分析已被应用于该任务&#xff0c;并取得了一些积极成果。Facebook的新Hi…

『C语言入门』初识C语言

文章目录 前言C语言简介一、Hello World&#xff01;1.1 编写代码1.2 代码解释1.3 编译和运行1.4 结果 二、数据类型2.1 基本数据类型2.2 复合数据类型2.3 指针类型2.4 枚举类型 三、C语言基础3.1 变量和常量3.2 运算符3.3 控制流语句3.4 注释单行注释多行注释注释的作用 四、 …

element表格多选实现

表格实现多选 实现表格多选很简单&#xff0c;只需要在表格里加上一列即可&#xff0c;加完之后就会在表格里出现一列白色的四方块按钮&#xff0c;可以多选&#xff0c;也可以单选 <el-table-columntype"selection"width"55"align"center"&…

iOS App逆向之:iOS应用砸壳技术

在iOS逆向&#xff0c;有一项关键的技术叫做“iOS砸壳”&#xff08;iOS App Decryption&#xff09;。自iOS 5版本以来&#xff0c;苹果引入了应用程序加密机制&#xff0c;使得大部分应用都需要进行砸壳操作才能进行逆向分析。因此作为开发者、逆向工程师和安全研究人员都需要…

Python Opencv实践 - 图像直方图自适应均衡化

import cv2 as cv import numpy as np import matplotlib.pyplot as pltimg cv.imread("../SampleImages/cat.jpg", cv.IMREAD_GRAYSCALE) print(img.shape)#整幅图像做普通的直方图均衡化 img_hist_equalized cv.equalizeHist(img)#图像直方图自适应均衡化 #1. 创…

【动手学深度学习】--21.锚框

锚框 学习视频&#xff1a;锚框【动手学深度学习v2】 官方笔记&#xff1a;锚框 1.锚框 目标检测算法通常会在输入图像中采样大量的区域&#xff0c;然后判断这些区域中是否包含我们感兴趣的目标&#xff0c;并调整区域边界从而更准确地预测目标的真实边界框&#xff08;gro…

hive问题总结

往往用了很久的函数却只知道其单一的应用场景&#xff0c;本文将不断完善所遇到的好用的hive内置函数。 1.聚合函数或者求最大最小值函数搭配开窗函数使用可以实现滑动窗口 例&#xff1a; SELECT event,time,session_id,COLLECT_LIST(event) OVER (PARTITION BY session_id …

ChatGPT + Flutter快速开发多端聊天机器人App

下载地址&#xff1a;ChatGPT Flutter快速开发多端聊天机器人App 下载地址&#xff1a;ChatGPT Flutter快速开发多端聊天机器人App

PL 侧驱动和fpga 重加载的方法

可以解决很多的问题 时钟稳定后加载特定fpga ip &#xff08;要不内核崩的一塌糊涂&#xff09;fpga 稳定复位软件决定fpga ip 加载的时序 dluash load /usr/local/scripts/si5512_setup.lua usleep 30 mkdir -p /lib/firmware cp -rf /usr/local/firmare/{*.bit.bin,*.dtbo} …

Flutter实现动画列表AnimateListView

由于业务需要&#xff0c;在打开列表时&#xff0c;列表项需要一个从右边飞入的动画效果&#xff0c;故封装一个专门可以执行动画的列表组件&#xff0c;可以自定义自己的动画&#xff0c;内置有水平滑动&#xff0c;缩放等简单动画。花里胡哨的动画效果由你自己来定制吧。 功…

206.Flink(一):flink概述,flink集群搭建,flink中执行任务,单节点、yarn运行模式,三种部署模式的具体实现

一、Flink概述 1.基本描述 Flink官网地址:Apache Flink — Stateful Computations over Data Streams | Apache Flink Flink是一个框架和分布式处理引擎,用于对无界和有界数据流进行有状态计算。 2.有界流和无界流 无界流(流): 有定义流的开始,没有定义结束。会无休止…

【视频】Python用LSTM长短期记忆神经网络对不稳定降雨量时间序列进行预测分析|数据分享...

全文下载链接&#xff1a;http://tecdat.cn/?p23544 在本文中&#xff0c;长短期记忆网络——通常称为“LSTM”——是一种特殊的RNN递归神经网络&#xff0c;能够学习长期依赖关系&#xff08;点击文末“阅读原文”获取完整代码数据&#xff09;。 本文使用降雨量数据&#xf…

Docker file解析

文章目录 简介构建的三步骤Docker执行Dockerfile的大致流程DockerFile常用保留字指令创建第一个Dockerfile镜像的缓存特性 Docker file 解析 简介 Dockerfile是用来构建Docker镜像的文本文件&#xff0c;是由一条条构建镜像所需的指令和参数构成的脚本&#xff0c;记录了镜像构…

ffmpeg,nginx,vlc把rtsp流转hls

ffmpeg:rtsp>hls流; nginx 托管hls流服务; vlc测试hls流服务; 参考了很多相关文档和资料,由于比较乱就不在一一引用介绍了&#xff0c;下面的是实操OK的例子&#xff1b; 1&#xff09;ffmpeg (ffmpeg-4.4.1-full_build)&#xff0c;要用full版本&#xff0c;否则会缺某些…

华为数通方向HCIP-DataCom H12-821题库(单选题:61-80)

第61题 关于 BGP 的Keepalive报文消息的描述,错误的是 A、Keepalive周期性的在两个BGP邻居之间发送 B、Keepalive报文主要用于对等路由器间的运行状态和链路的可用性确认 C、Keepalive 报文只包含一个BGP数据报头 D、缺省情况下,Keepalive 的时间间隔是180s 答案&#xff…

videojs 实现自定义组件(视频画质/清晰度切换) React

前言 最近使用videojs作为视频处理第三方库&#xff0c;用来对接m3u8视频类型。这里总结一下自定义组件遇到的问题及实现&#xff0c;目前看了许多文章也不全&#xff0c;官方文档写的也不是很详细&#xff0c;自己摸索了一段时间陆陆续续完成了&#xff0c;这是实现后的效果.…

MyBatis分页与特殊字符处理

文章目录 一、分页1.1 分页插件PageHelper1.2 使用1.2.1 导入pom依赖1.2.2 Mybatis.cfg.xml配置拦截器1.2.3. 配置 Mapper.xml1.2.4 测试 二、特殊字符处理2.1 使用CDATA区段2.2 使用实体引用 一、分页 1.1 分页插件PageHelper PageHelper 是 Mybatis 的一个插件。官网 Page…