Retrofit2是一个基于OkHttp进行封装的网络请求框架,Retrofit中使用了大量的@GET
、@POST
这类注解方法,刚开始学习的时候会感觉特别的不适应,但只要理解了这些注解方法和使用逻辑,会发现网络请求也可以做的很优雅
一、案例
先写一个简单的程序跑起来,再详细讲解
首先添加依赖
compile 'com.squareup.retrofit2:retrofit:2.1.0'
Retrofit最新的版本可以去主页查看:GitHub Retrofit
这里使用的是豆瓣免费API,可以去豆瓣开发者官网了解一下,返回的是一串JSON数据
自己定义一个接口类HTMLStringBiz
和请求方法getHTMLString
,通过这个方法来获取返回的JSON数据
public interface HTMLStringBiz {
@GET("book/{id}")
Call<ResponseBody> getHTMLString(@Path("id") String bookid);
}
然后在程序中触发以下方法
private void sendRequest() {
String url = "https://api.douban.com/v2/";// URL地址
String bookId = "1220562";// 书籍编号
Retrofit build = new Retrofit.Builder()
.baseUrl(url)
.build();
HTMLStringBiz biz = build.create(HTMLStringBiz.class);
Call<ResponseBody> call = biz.getHTMLString(bookId);
call.enqueue(new Callback<ResponseBody>() {
@Override
public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
try {
Log.e(TAG, "返回的结果" + new String(response.body().bytes()));
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void onFailure(Call<ResponseBody> call, Throwable t) {
}
});
}
运行一下,后台就会返回一串JSON数据了(别忘了添加访问网络的权限)
此处我们使用的是enqueue
异步请求的方式,Retrofit中Callback
里的onResponse
和onFailure
回调方法就是运行在UI线程的,取得了数据后就可以直接设置在界面中进行展示了,非常便捷
二、详细分析
1.设置URL地址
Retrofit中请求类型的指定都是通过@XXX
这种方式设置的
@GET("book/{id}")
Call<ResponseBody> getHTMLString(@Path("id") String bookid);
通过@GET
指定下面的getHTMLString
方法将是以get方式发送请求的
在@GET("book/{id}")
中的{id}
是我们自定义的一个占位符,和@Path("id")
是对应的,可以理解为,@Path("id")
后面所传入的参数,将会替换@GET("book/{id}")
中的{id}
占位符。
Call<ResponseBody>
指定请求方法返回类型,此处我们需要获取的是JSON数据,所以传入的是一个ResponseBody
类,当需要进行JSON解析时,可以把ResponseBody
替换成指定的Bean实体类,这个下面细说
通过以下方式创建一个Retrofit实例
Retrofit build = new Retrofit.Builder()
.baseUrl(url)
.build();
和Retrofit1.x的版本是不同的,之前获取Retrofit的实例用的是RestAdapter
,Retrofit2现在换成了Retrofit
baseUrl
传入的是一个URL地址,它将会和@GET("book/{id}")
里面的"book/{id}"
组合成一个完整的URL地址。在baseUrl
里传入的URL地址写法是有讲究的,baseUrl
中的url地址最后面的斜杠“/”不要放在@GET
这类注解里的前面,baseUrl
必须要以“/”结尾。当然,也不必非要把一个url地址拆解成baseUrl
和@GET
两个部分来写,也可以使用@Url
注解方式直接传入一个url地址
@GET
Call<ResponseBody> getHTMLStringUrl(@Url String url);
Retrofit build = new Retrofit.Builder()
.baseUrl("https:xxx")
.build();
HTMLStringBiz biz = build.create(HTMLStringBiz.class);
Call<ResponseBody> call = biz.getHTMLStringUrl("https://api.douban.com/v2/book/1220562");
需要注意了,哪怕指定了@Url
,也必须加上baseUrl
并传入一个头部以“https:”或“http:”为开头的字符串,不然会报错,具体的原因可以深入Retrofit源码中的HttpUrl#parse
查看到
ParseResult parse(HttpUrl base, String input) {
int pos = skipLeadingAsciiWhitespace(input, 0, input.length());
int limit = skipTrailingAsciiWhitespace(input, pos, input.length());
// Scheme.
int schemeDelimiterOffset = schemeDelimiterOffset(input, pos, limit);
if (schemeDelimiterOffset != -1) {
if (input.regionMatches(true, pos, "https:", 0, 6)) {
this.scheme = "https";
pos += "https:".length();
} else if (input.regionMatches(true, pos, "http:", 0, 5)) {
this.scheme = "http";
pos += "http:".length();
} else {
return ParseResult.UNSUPPORTED_SCHEME; // Not an HTTP scheme.
}
} else if (base != null) {
this.scheme = base.scheme;
} else {
return ParseResult.MISSING_SCHEME; // No scheme.
}
......
}
从源码可以看出,HttpUrl#parse
方法对传入的url地址进行了校验,只要开头不是以“https:”、“http:”开头的字符串,都将返回UNSUPPORTED_SCHEME
不符合规则,以至于我们就算使用了@Url
传入了个完整的地址,也需要在baseUrl
中传入一个符合规则的Url地址,哪怕这个传入的地址是无效的(这设计感觉有点不合理啊)
Url的地址还可以直接写在注解里
@GET("https://api.douban.com/v2/book/1220562")
Call<ResponseBody> getHTMLStringUrl();
更加具体的URL使用方式可以查看baseUrl
的源码注释,描述的非常详细。
2.添加解析器
如果需要进行GSON解析,我们可以不必在onResponse
返回json数据后再对数据进行GSON解析,可以在创建Retrofit对象时就添加addConverterFactory
方法,并传入GsonConverterFactory.create()
GSON解析器
Retrofit build = new Retrofit.Builder()
.baseUrl(url)
.addConverterFactory(GsonConverterFactory.create())
.build();
使用此GSON解析器需要添加依赖,注意版本号要和Retrofit依赖的版本号保持一致
compile 'com.squareup.retrofit2:converter-gson:2.1.0'
当然,Retrofit不可能只支持一种解析方式,像jackson、xml这些肯定是支持的,官网上显示他支持以下解析器,如果以下这些还不能满足需求,那么也可以自己定义一个解析器
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
Scalars: com.squareup.retrofit2:converter-scalars
使用了GSON解析器后,Call<T>
里的T
需要改成对应的实体Bean,然后就可以在onResponse
回调中通过response.body()
方法获取经过解析后的数据实体类了
3.创建接口请求方法
创建了Retrofit实例后,就可以通过Retrofit实例创建接口请求类,从而调用自定义的请求方法了
HTMLStringBiz biz = build.create(HTMLStringBiz.class);
Call<ResponseBody> call = biz.getHTMLString(bookId);
通过调用getHTMLString
方法,传入一个书本id,这个传入的值将会替换{id}
占位符,最终组合成一个完整的url地址,也就是https://api.douban.com/v2/book/1220562
这里我们给Retrofit传入了一个HTMLStringBiz
接口的Class,却生成了一个HTMLStringBiz
对象?这也是Retrofit最经典的地方了,点进去看看build.create
的源码
public <T> T create(final Class<T> service) {
Utils.validateServiceInterface(service);
if (validateEagerly) {
eagerlyValidateMethods(service);
}
return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },
new InvocationHandler() {
private final Platform platform = Platform.get();
@Override public Object invoke(Object proxy, Method method, 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);
}
if (platform.isDefaultMethod(method)) {
return platform.invokeDefaultMethod(method, service, proxy, args);
}
ServiceMethod serviceMethod = loadServiceMethod(method);
OkHttpCall okHttpCall = new OkHttpCall<>(serviceMethod, args);
return serviceMethod.callAdapter.adapt(okHttpCall);
}
});
}
首先会通过Utils.validateServiceInterface
方法检查传入的Class是否为一个接口,并且此接口不能有拓展的其它接口
static <T> void validateServiceInterface(Class<T> service) {
if (!service.isInterface()) {
throw new IllegalArgumentException("API declarations must be interfaces.");
}
// Prevent API interfaces from extending other interfaces. This not only avoids a bug in
// Android (http://b.android.com/58753) but it forces composition of API declarations which is
// the recommended pattern.
if (service.getInterfaces().length > 0) {
throw new IllegalArgumentException("API interfaces must not extend other interfaces.");
}
}
如果不符合条件,则会抛出IllegalArgumentException
异常,如果符合要求,接下来就会调用eagerlyValidateMethods
遍历接口中的所有方法加入到serviceMethodCache
缓存中
private final Map<Method, ServiceMethod> serviceMethodCache = new LinkedHashMap<>();
......
ServiceMethod loadServiceMethod(Method method) {
ServiceMethod result;
synchronized (serviceMethodCache) {
result = serviceMethodCache.get(method);
if (result == null) {
result = new ServiceMethod.Builder(this, method).build();
serviceMethodCache.put(method, result);
}
}
return result;
}
为了提高效率,防止重复解析,会先从serviceMethodCache
缓存中获取该方法,如果不存在再进行创建
通过ServiceMethod.Builder(this, method).build()
方法,会把我们定义的接口方法变成一个http请求方法,在调用build
方法时,会遍历传入的Method
,然后调用parseMethodAnnotation
方法,可以查看一下ServiceMethod#parseMethodAnnotation
源码
private void parseMethodAnnotation(Annotation annotation) {
if (annotation instanceof DELETE) {
parseHttpMethodAndPath("DELETE", ((DELETE) annotation).value(), false);
} else if (annotation instanceof GET) {
parseHttpMethodAndPath("GET", ((GET) annotation).value(), false);
} else if (annotation instanceof HEAD) {
parseHttpMethodAndPath("HEAD", ((HEAD) annotation).value(), false);
if (!Void.class.equals(responseType)) {
throw methodError("HEAD method must use Void as response type.");
}
} else if (annotation instanceof PATCH) {
parseHttpMethodAndPath("PATCH", ((PATCH) annotation).value(), true);
} else if (annotation instanceof POST) {
parseHttpMethodAndPath("POST", ((POST) annotation).value(), true);
} else if (annotation instanceof PUT) {
parseHttpMethodAndPath("PUT", ((PUT) annotation).value(), true);
} else if (annotation instanceof OPTIONS) {
parseHttpMethodAndPath("OPTIONS", ((OPTIONS) annotation).value(), false);
} else if (annotation instanceof HTTP) {
HTTP http = (HTTP) annotation;
parseHttpMethodAndPath(http.method(), http.path(), http.hasBody());
} else if (annotation instanceof retrofit2.http.Headers) {
......
}
调用parseMethodAnnotation
方法将会确定接口请求的类型和注解方式,确定完接口请求方式后,最终它会把网络请求交给OkHttp3来处理
ServiceMethod serviceMethod = loadServiceMethod(method);
OkHttpCall okHttpCall = new OkHttpCall<>(serviceMethod, args);
return serviceMethod.callAdapter.adapt(okHttpCall);
可以发现,通过serviceMethod
初始化创建了一个OkHttpCall
对象,最后通过callAdapter.adapt
方法返回一个代理的Call
实例,该实例就是对应的OkHttp的call实例
从上面的分析应该可以发现了,我们传入的HTMLStringBiz.class
,并没有生成一个继承至此接口的实现类,而是通过动态代理的方式创建了一个代理类而已
4.发送请求
通过上面的介绍,我们知道Retrofit最终会把网络请求交给OkHttp来处理,并使用一个动态代理对象来处理其返回结果,请求的方式分为enqueue
异步请求和execute
同步请求
enqueue
和execute
的具体实现在OkHttpCall
类中
@Override public void enqueue(final Callback<T> callback) {
......
okhttp3.Call call;
......
if (failure != null) {
callback.onFailure(this, failure);
return;
}
if (canceled) {
call.cancel();
}
call.enqueue(new okhttp3.Callback() {
@Override public void onResponse(okhttp3.Call call, okhttp3.Response rawResponse)
throws IOException {
Response<T> response;
try {
response = parseResponse(rawResponse);
} catch (Throwable e) {
callFailure(e);
return;
}
callSuccess(response);
}
......
}
发送请求时,调用的是okhttp3.Call的enqueue
方法,此处是对okhttp请求回调做的一层封装,在此处也可以发现,Retrofit是支持取消请求的,调用cancel
方法就好了
如果请求成功的话,就会执行callSuccess
方法
private void callSuccess(Response<T> response) {
try {
callback.onResponse(OkHttpCall.this, response);
} catch (Throwable t) {
t.printStackTrace();
}
}
最终会通过callback.onResponse
把值回调给我们
同样的,调用execute
方法进行同步请求时的过程也是如此,交给okhttp处理网络请求,通过对返回的封装,使用Callback代理call实例的返回,最终把网络请求返回的数据传递给我们使用
call.enqueue(new Callback<ResponseBody>() {
@Override
public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
try {
Log.e(TAG, "返回的结果" + new String(response.body().bytes()));
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void onFailure(Call<ResponseBody> call, Throwable t) {
}
});
请求成功则回调onResponse
,失败回调onFailure
,其回调方法都是执行在UI线程的,可直接在回调中更新UI界面
三、总结
到此为止,一次完整的网络请求就结束了,对其流程及源码进行了简单的分析,会发现Retrofit的主要源码并不多,但设计的非常精巧,对okhttp的封装非常的到位,使用起来变得简洁明了,可自定义解析器,还支持和RxJava的配合使用,可扩展性非常强
Retrofit还有很多其它的注解方法,就不全部列举了,其实只要了解了Retrofit的运行原理,其它的注解方法看看源码注释就能理解和使用了