先来一张Jake Wharton 关于 A Few “OK” Libraries 的简介,几个框架的关系
前面先分析了RxJava的实现,第一感觉就是和Java 8里面的stream很像,以函数式编程的形式来处理数据,个人理解的核心思想有2个:
- 函数接口 在函数的参数里面,定义了各种函数接口(functional interface),当需要这些参数的时候,有3种方式可以实现
- 通过一个匿名内部类
- 传递lambda表达式
- 在某些情况下,传递方法引用(如System.out::println)而不是lambda表达式。
- Builder模式 每次对数据的处理,都是生成的一个新的对象
RaJava更多的是可能是配合其它组件一起使用,如
Retrofit-Okhttp-Okio-RxJava 4个组件组合,在Android开发中基本上已经标配。在一般网络编程中,主要是处理http连接以及request(需要什么数据)、reponse(返回结果)。
HTTP 简介
HTTP请求报文 一个HTTP请求报文由请求行(request line)、请求头部(header)、空行和请求数据4个部分组成
请求行分为三个部分:请求方法、请求地址和协议版本
- 请求方法 HTTP/1.1 定义的请求方法有8种:GET、POST、PUT、DELETE、PATCH、HEAD、OPTIONS、TRACE。最常的两种GET和POST,如果是RESTful接口的话一般会用到GET、POST、DELETE、PUT。
- 请求地址 URL:统一资源定位符。 组成:<协议>://<主机>:<端口>/<路径>
- 协议版本:如HTTP/1.1,HTTP/2.0
请求头 为请求报文添加了一些附加信息,由“名/值”键值组成,每行一对,名和值之间使用冒号分
HTTP响应报文 主要由状态行、响应头部、空行以及响应数据组成。
状态行
由3部分组成,分别为:协议版本,状态码,状态码描述。其中协议版本与请求报文一致,状态码描述是对状态码的简单描述。
一些常用的状态码
- 100~199:指示信息,表示请求已接收,继续处理
- 200~299:请求成功,表示请求已被成功接收、理解
- 300~399:重定向,要完成请求必须进行更进一步的操作
- 400~499:客户端错误,请求有语法错误或请求无法实现
- 500~599:服务器端错误,服务器未能实现合法的请求
响应头部 与请求头部类似,为响应报文添加了一些附加信息
几个框架的分工大概如下类图
详细的流程图:
其中Retrofit的实现依赖于Okhttp,Okhttp的实现依赖于Okio
另外在Retrofit里面,可以使用将返回结果转换成RxJava里面的基础类,如Flowable/Observable,充分利用RxJava响应式编程的优势,提升数据处理效率
下面简单介绍一下4个框架的主要功能
1.Retrofit
Retrofit 是一个 RESTful 的 HTTP 网络请求框架的封装。因为和网络连接的处理是由Okhttp处理的,与服务器的Socket读写是通过Okio来实现的,是在这2个框架上的进一步封装
核心功能有2个:对request的处理以及对response结果的转换处理
1.1 使用annotation来描述request
Retrofit里面定义了很多注解,使用annotation来描述一个request应该怎么被处理
- request 方法 可以使用的注解有HTTP, GET, POST, PUT, PATCH, DELETE, OPTIONS and HEAD
- url操作 一个请求url可以通过替换方法里面的代码块和参数来动态更新,一个替换块必须用{}包起来,且在参数里面使用同样的名字用@path注解
@GET("group/{id}/users")
Call<List<User>> groupList(@Path("id") int groupId);
// query查询也可以添加
@GET("group/{id}/users")
Call<List<User>> groupList(@Path("id") int groupId, @Query("sort") String sort);
// 对于更复杂的请求参数可以使用@QueryMap
- 通过@Body注解,可以将一个对象作为request的body来使用
- 通过Headers注解可以设置请求头
@Headers({
"Accept: application/vnd.github.v3.full+json",
"User-Agent: Retrofit-Sample-App"
})
@GET("users/{username}")
Call<User> getUser(@Path("username") String username);
1.2 数据类型转换器converters
默认情况下,Retrofit只支持把HTTP body作为Okhttp的ResponseBody类型,通过@body注解只能接收RequestBody 类型
可以使用转换器来支持其它 类型:
- Gson: com.squareup.retrofit2:converter-gson
- Jackson: com.squareup.retrofit2:converter-jackson
- Moshi: com.squareup.retrofit2:converter-moshi
- Protobuf: com.squareup.retrofit2:converter-protobuf
- Wire: com.squareup.retrofit2:converter-wire
- Simple XML: com.squareup.retrofit2:converter-simplexml
- JAXB: com.squareup.retrofit2:converter-jaxb
- Scalars (primitives, boxed, and String): com.squareup.retrofit2:converter-scalars
可以自定义converters,来处理非以上几种类型的
1.3 Call Adapter Call适配器
在retrofit里面,一般会定义一个接口,如下
interface Service {
@GET("/user")
Call<User> user();
}
对象的转换过程:
Call <-- CallAdapter <-- Call <-- Call <-- Call.Factory (如 OkHttpClient)
通过Call.Factory的抽象,来兼容Okhtttp/RxJava等
如果想把获取的结果转换成RxJava里面可以处理的Observable,可以将接口的定义修改如下:
interface Service {
@GET("/user")
Observable<User> user();
}
然后在里面Retrofit里面配置CallAdapter为Rxjava
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("http://example.com")
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.build();
默认情况下,create方法会创建一个响应式类型,在后台线程里面异步执行一个http请求,有2种方式可以指定运行的线程
- 使用createSynchronous(),然后调用subscribeOn在想要运行的Scheduler上
- 使用createWithScheduler来提供一个默认的订阅Scheduler
2.Okhttp
HTTP client主要工作是接收request,然后生成一个response
Requests
每个http请求包含一个URL,一个方法(像GET/POST),和一系列headers,可能包含有一个body:一种特定类型的数据流
Responses
Response响应request,返回一个code(像200/404),一系列headers和一个可选的header
当把一个request发送到OkHttp时, OkHttp会重写request然后再发送,OkHttp可能会增加一些headers,在原始的请求里面没有的,包括Content-Length, Transfer-Encoding, User-Agent, Host, Connection, and Content-Type
也在增加一个Accept-Encoding,除非原始request里面已经有了,当需要cookies时,OkHttp会增加一个Cookie header
有些request可能有缓存的response,当缓存不是最新的时候,OkHttp会做一个有条件的GET来下载更新,这种场景下,要求headers里面加上
If-Modified-Since
If-None-Match
如果传输使用了压缩,OkHttp会丢弃响应头Content-Encoding and Content-Length,因为适用于解压后的response body
rediect
当你请求的URL被移动,服务器可能会返回一个类似302的响应,来表明文档的新URL,OkHttp会rediect来获取一个最终的response
Authenticator
如果需要鉴权,OkHttp会调用Authenticator 来处理,如果Authenticator 提供了证书,请求的时候会包含此证书
重试
有时连接会失败,OkHttp会使用一个不同的路由来重试
Call
OkHttp使用Call来建模,不管需要多少中间请求和响应,来满足request
Call执行有2种方式:
- 同步:当前线程会阻塞,直到response可读
- 异步:把请求放在另外的线程,当response可读时使用callback来回调
Calls可以在任意线程里面取消.Call的管理
- 对于同步调用,在单独的线程里面管理同时发出的请求数量
- 对于异步Call,Dispatch实现了请求数量管理策略
response里面有一个string()方法,对于小文件,可以简单高效的将response body转换成string。如果response body比较大,超过1M,则避免使用string(),因为此方法会将内容全部加载到memory里面,在这种场景下,应该把respnse当作stream来处理
同步调用,直接等待response然后再处理client.newCall(request).execute().use
异步调用,client.newCall(request).enqueue(object : Callback
则传入一个callback,当reponse可用时,回调此callbase,okhttp不支持异步API把response分块处理
访问请求头
使用addHeader来增加新的header,如果存在,不需要删除,addHeader在处理的时候,如果value存在,则先remove掉,然后再添加新的value
使用header(name)来获取一个head,使用headers(name),获取所有值,作为一个List返回
EventListener
可以用来获取http call的调用数量,使用EventListener可以监听
应用内部调用Http call的大小和频率,当调用Call太多次或Call太大,应该了解这些情况
在当前网络情况下的性能,如果网络性能不够好,应该增强网络或减少调用
Interceptor
Interceptor 是一个功能强大的机制,可以监听、重写、重试Call
OkHttp 中的拦截器分为 Application Interceptor(应用拦截器) 和 NetWork Interceptor(网络拦截器)两种,支持自定义,注册方式的区别:
- 通过调用 OkHttpClient.Builder 的 addInterceptor() 方法来注册应用拦截器
- 通过调用OkHttpClient.Builder 的 addNetworkInterceptor() 方法来注册网络拦截器
2个可以自定义的Interceptor,5个内置的Interceptor.执行顺序和功能简介
- Application Interceptor:每次网络请求它只会执行一次拦截,而且它是第一个触发拦截的,这里拦截到的url请求的信息都是最原始的信息
- RetryAndFollowUpInterceptor 的作用,一个负责失败重连的拦截器。它是 Okhttp 内置的第一个拦截器,通过 while (true) 的死循环来进行对异常结果或者响应结果判断是否要进行重新请求。
- BridgeInterceptor 为用户构建的一个 Request 请求转化为能够进行网络访问的请求,同时将网络请求回来的响应 Response 转化为用户可用的 Response
- CacheInterceptor 根据 OkHttpClient 对象的配置以及缓存策略对请求值进行缓存
- ConnectInterceptor 在 OKHTTP 底层是通过 SOCKET 的方式于服务端进行连接的,并且在连接建立之后会通过 OKIO 获取通向 server 端的输入流 Source 和输出流 Sink
- NetWork Interceptor:它位于倒数第二层,会经过 RetryAndFollowIntercptor 进行重定向并且也会通过 BridgeInterceptor 进行 request请求头和 响应 resposne 的处理
- CallServerInterceptor 在 ConnectInterceptor 拦截器的功能就是负责与服务器建立 Socket 连接,并且创建了一个 HttpStream 它包括通向服务器的输入流和输出流
3.Okio
Okio是一个实现java.io和java.nio的库,更方便访问、存储和处理数据。作为OkHttp组件的一部分,在Android中引入支持HTTP的客户端
Buffer and ByteString 持有数据
- ByteString 代表一个不可变的字节序列。对于char数据,String是基础类型。
- Buffer 可变的字节序列,像ArrayList,读写Buffer的操作与queue类似,从尾部写,从头部读,不需要管理position/limit/capacities
在内部实现,ByteString和Buffer做了一些优化来节约CPU和内存,如果把一个UTF-8字符串编码为ByteString,保存了一个引用,后面如果需要解码的时候可以直接使用,在encodeUtf8方法里面
Buffer被实现为一个segment链表,当你从一个Buffer移动数据到另外一个Buffer的时候,重新设置了segment的所属队列,没有直接copy一份数据,在多线程场景下很有帮助,在buffer.write方法里面
class Buffer implements BufferedSink,BufferedSource
Source and Sink 移动数据
在okio里面,有自己的stream类型Source和Sink,使用方式类似于InputStream和OutputStream
Source对应为输入,在BufferedSource里面封装了一个InputStream
interface Source {
long read(Buffer sink, long byteCount);
Timeout timeout();
void close();
}
interface BufferedSource extends Source {
byte readByte();
short readShort();
short readShortLe();
int readInt();
int readIntLe();
long readLong();
long readLongLe();
long readDecimalLong();
long readHexadecimalUnsignedLong();
ByteString readByteString();
ByteString readByteString(long byteCount);
byte[] readByteArray();
byte[] readByteArray(long byteCount);
int read(byte[] sink);
void readFully(byte[] sink);
int read(byte[] sink, int offset, int byteCount);
void readFully(Buffer sink, long byteCount);
long readAll(Sink sink);
String readUtf8();
String readUtf8(long byteCount);
String readUtf8Line();
String readUtf8LineStrict();
String readString(Charset charset);
String readString(long byteCount, Charset charset);
long indexOf(byte b);
long indexOf(byte b, long fromIndex);
long indexOfElement(ByteString targetBytes);
long indexOfElement(ByteString targetBytes, long fromIndex);
boolean exhausted();
void require(long byteCount);
boolean request(long byteCount);
void skip(long byteCount);
Buffer buffer();
InputStream inputStream();
}
Sink对应为输出,在BufferedSink里面封装了一个outputStream
interface Sink {
void write(Buffer sink, long byteCount);
Timeout timeout();
void close();
void flush();
}
interface BufferedSink extends Sink {
BufferedSink write(ByteString byteString);
BufferedSink write(byte[] source);
BufferedSink write(byte[] source, int offset, int byteCount);
long writeAll(Source source);
BufferedSink write(Source source, long byteCount);
BufferedSink writeUtf8(String string);
BufferedSink writeString(String string, Charset charset);
BufferedSink writeByte(int b);
BufferedSink writeShort(int s);
BufferedSink writeShortLe(int s);
BufferedSink writeInt(int i);
BufferedSink writeIntLe(int i);
BufferedSink writeLong(long v);
BufferedSink writeLongLe(long v);
BufferedSink writeDecimalLong(long v);
BufferedSink writeHexadecimalUnsignedLong(long v);
BufferedSink emitCompleteSegments();
BufferedSink emit();
Buffer buffer();
OutputStream outputStream();
}
主要差异:
- 提供了IO语义层面的超时
- 容易实现 ,容易使用
- 没有人为区分字符流和字节流,都作为数据处理,读写都按字节来操作
- 容易测试
Buffer 内部保存的数据在Segment Pool里面
BufferedSource and BufferedSink 更高效的移动数据,具体的细节,可以参考前面pdf里面的文档,后面会专门针对几个框架来分析实现