深入浅出 Retrofit,这么牛逼的框架你们还不来看看?

mspe9393 7年前
   <p>Android 开发中,从原生的 HttpUrlConnection 到经典的 Apache 的 HttpClient ,再到对前面这些网络基础框架的封装,比如 Volley 、 Async Http Client ,Http 相关开源框架的选择还是很多的,其中由著名的 Square 公司开源的 Retrofit 更是以其简易的接口配置、强大的扩展支持、优雅的代码结构受到大家的追捧。也正是由于 Square 家的框架一如既往的简洁优雅,所以我一直在想,Square 公司是不是只招 <strong>处女座</strong> 的程序员?</p>    <h2>1、初识 Retrofit</h2>    <p>单从 Retrofit 这个单词,你似乎看不出它究竟是干嘛的,当然,我也看不出来 :)逃。。</p>    <p><em>Retrofitting</em> refers to the addition of new technology or features to older systems.</p>    <p>–From Wikipedia</p>    <p>于是我们就明白了,冠以 Retrofit 这个名字的这个家伙,应该是某某某的 『Plus』 版本了。</p>    <h2>1.1 Retrofit 概览</h2>    <p><a href="/misc/goto?guid=4958837204152834453" rel="nofollow,noindex">Retrofit </a> 是一个 RESTful 的 HTTP 网络请求框架的封装。注意这里并没有说它是网络请求框架,主要原因在于网络请求的工作并不是 Retrofit 来完成的。 Retrofit 2.0 开始内置 <a href="/misc/goto?guid=4958860115824511695" rel="nofollow,noindex"> OkHttp </a> ,前者专注于接口的封装,后者专注于网络请求的高效,二者分工协作,宛如古人的『你耕地来我织布』,小日子别提多幸福了。</p>    <p><img src="https://simg.open-open.com/show/90c8866c1d2e122bb09f2aa58a16a0f8.png"></p>    <p>我们的应用程序通过 Retrofit 请求网络,实际上是使用 Retrofit 接口层封装请求参数、Header、Url 等信息,之后由 OkHttp 完成后续的请求操作,在服务端返回数据之后, OkHttp 将原始的结果交给 Retrofit ,后者根据用户的需求对结果进行解析的过程。</p>    <p>讲到这里,你就会发现所谓 Retrofit ,其实就是 <strong>Retrofitting OkHttp</strong> 了。</p>    <h2>1.2 Hello Retrofit</h2>    <p>多说无益,不要来段代码陶醉一下。使用 Retrofit 非常简单,首先你需要在你的 build.gradle 中添加依赖:</p>    <pre>  <code class="language-java">compile 'com.squareup.retrofit2:retrofit:2.0.2'</code></pre>    <p>你一定是想要访问 GitHub 的 api 对吧,那么我们就定义一个接口:</p>    <pre>  <code class="language-java">public interface GitHubService {    @GET("users/{user}/repos")    Call<List<Repo>> listRepos(@Path("user") String user);  }</code></pre>    <p>接口当中的 listRepos 方法,就是我们想要访问的 api 了:</p>    <p>https://api.github.com/users/{user}/repos</p>    <p>其中,在发起请求时, {user} 会被替换为方法的第一个参数 user 。</p>    <p>好,现在接口有了,我们要构造 Retrofit 了:</p>    <pre>  <code class="language-java">Retrofit retrofit = new Retrofit.Builder()      .baseUrl("https://api.github.com/")      .build();    GitHubService service = retrofit.create(GitHubService.class);</code></pre>    <p>这里的 service 就好比我们的快递哥,还是往返的那种哈~</p>    <pre>  <code class="language-java">Call<List<Repo>> repos = service.listRepos("octocat");</code></pre>    <p>发请求的代码就像前面这一句,返回的 repos 其实并不是真正的数据结果,它更像一条指令,你可以在合适的时机去执行它:</p>    <pre>  <code class="language-java">// 同步调用  List<Repo> data = repos.execute();     // 异步调用  repos.enqueue(new Callback<List<Repo>>() {              @Override              public void onResponse(Call<List<Repo>> call, Response<List<Repo>> response) {                  List<Repo> data = response.body();              }                @Override              public void onFailure(Call<List<Repo>> call, Throwable t) {                  t.printStackTrace();              }          });</code></pre>    <p>啥感觉?有没有突然觉得请求接口就好像访问自家的方法一样简单?呐,前面我们看到的,就是 Retrofit 官方的 demo 了。你以为这就够了?噗~怎么可能。。</p>    <h2>1.3 Url 配置</h2>    <p>Retrofit 支持的协议包括 GET / POST / PUT / DELETE / HEAD / PATCH ,当然你也可以直接用 HTTP 来自定义请求。这些协议均以注解的形式进行配置,比如我们已经见过 GET 的用法:</p>    <pre>  <code class="language-java">@GET("users/{user}/repos")    Call<List<Repo>> listRepos(@Path("user") String user);</code></pre>    <p>这些注解都有一个参数 value,用来配置其路径,比如示例中的 users/{user}/repos ,我们还注意到在构造 Retrofit 之时我们还传入了一个 baseUrl("https://api.github.com/") ,请求的完整 Url 就是通过 baseUrl 与注解的 value (下面称 “ path ” ) 整合起来的,具体整合的规则如下:</p>    <ul>     <li> <p>path 是绝对路径的形式: path = "/apath" , baseUrl = "http://host:port/a/b" Url = "http://host:port/apath"</p> </li>     <li> <p>path 是相对路径, baseUrl 是目录形式: path = "apath" , baseUrl = "http://host:port/a/b/" Url = "http://host:port/a/b/apath"</p> </li>     <li> <p>path 是相对路径, baseUrl 是文件形式: path = "apath" , baseUrl = "http://host:port/a/b" Url = "http://host:port/a/apath"</p> </li>     <li> <p>path 是完整的 Url: path = "http://host:port/aa/apath" , baseUrl = "http://host:port/a/b" Url = "http://host:port/aa/apath"</p> </li>    </ul>    <p>建议采用第二种方式来配置,并尽量使用同一种路径形式。如果你在代码里面混合采用了多种配置形式,恰好赶上你哪天头晕眼花,信不信分分钟写一堆 bug 啊哈哈。</p>    <h2>1.4 参数类型</h2>    <p>发请求时,需要传入参数, Retrofit 通过注解的形式令 Http 请求的参数变得更加直接,而且类型安全。</p>    <h3>1.4.1 Query & QueryMap</h3>    <pre>  <code class="language-java">@GET("/list")  Call<ResponseBody> list(@Query("page") int page);</code></pre>    <p>Query 其实就是 Url 中 ‘?’ 后面的 key-value,比如:</p>    <p><a href="/misc/goto?guid=4959748816327346992" rel="nofollow,noindex">http://www.println.net/?cate=android</a></p>    <p>这里的 cate=android 就是一个 Query ,而我们在配置它的时候只需要在接口方法中增加一个参数,即可:</p>    <pre>  <code class="language-java">interface PrintlnServer{      @GET("/")      Call<String> cate(@Query("cate") String cate);  }</code></pre>    <p>这时候你肯定想,如果我有很多个 Query ,这么一个个写岂不是很累?而且根据不同的情况,有些字段可能不传,这与方法的参数要求显然也不相符。于是,打群架版本的 QueryMap 横空出世了,使用方法很简单,我就不多说了。</p>    <h3>1.4.2 Field & FieldMap</h3>    <p>其实我们用 POST 的场景相对较多,绝大多数的服务端接口都需要做加密、鉴权和校验, GET 显然不能很好的满足这个需求。使用 POST 提交表单的场景就更是刚需了,怎么提呢?</p>    <pre>  <code class="language-java">@FormUrlEncoded     @POST("/")     Call<ResponseBody> example(         @Field("name") String name,         @Field("occupation") String occupation);</code></pre>    <p>其实也很简单,我们只需要定义上面的接口就可以了,我们用 Field 声明了表单的项,这样提交表单就跟普通的函数调用一样简单直接了。</p>    <p>等等,你说你的表单项不确定个数?还是说有很多项你懒得写? Field 同样有个打群架的版本—— FieldMap ,赶紧试试吧~~</p>    <h3>1.4.3 Part & PartMap</h3>    <p>这个是用来上传文件的。话说当年用 HttpClient 上传个文件老费劲了,一会儿编码不对,一会儿参数错误(也怪那时段位太低吧TT)。。。可是现在不同了,自从有了 Retrofit ,妈妈再也不用担心文件上传费劲了~~~</p>    <pre>  <code class="language-java">public interface FileUploadService {        @Multipart      @POST("upload")      Call<ResponseBody> upload(@Part("description") RequestBody description,                                @Part MultipartBody.Part file);  }</code></pre>    <p>如果你需要上传文件,和我们前面的做法类似,定义一个接口方法,需要注意的是,这个方法不再有 @FormUrlEncoded 这个注解,而换成了 @Multipart ,后面只需要在参数中增加 Part 就可以了。也许你会问,这里的 Part 和 Field 究竟有什么区别,其实从功能上讲,无非就是客户端向服务端发起请求携带参数的方式不同,并且前者可以携带的参数类型更加丰富,包括数据流。也正是因为这一点,我们可以通过这种方式来上传文件,下面我们就给出这个接口的使用方法:</p>    <pre>  <code class="language-java">//先创建 service  FileUploadService service = retrofit.create(FileUploadService.class);    //构建要上传的文件  File file = new File(filename);  RequestBody requestFile =          RequestBody.create(MediaType.parse("application/otcet-stream"), file);    MultipartBody.Part body =          MultipartBody.Part.createFormData("aFile", file.getName(), requestFile);    String descriptionString = "This is a description";  RequestBody description =          RequestBody.create(                  MediaType.parse("multipart/form-data"), descriptionString);    Call<ResponseBody> call = service.upload(description, body);  call.enqueue(new Callback<ResponseBody>() {    @Override    public void onResponse(Call<ResponseBody> call,                           Response<ResponseBody> response) {      System.out.println("success");    }      @Override    public void onFailure(Call<ResponseBody> call, Throwable t) {      t.printStackTrace();    }  });</code></pre>    <p>在实验时,我上传了一个只包含一行文字的文件:</p>    <pre>  <code class="language-java">Visit me: http://www.println.net</code></pre>    <p>那么我们去服务端看下我们的请求是什么样的:</p>    <p>HEADERS</p>    <pre>  <code class="language-java">Accept-Encoding: gzip  Content-Length: 470  Content-Type: multipart/form-data; boundary=9b670d44-63dc-4a8a-833d-66e45e0156ca  User-Agent: okhttp/3.2.0  X-Request-Id: 9d70e8cc-958b-4f42-b979-4c1fcd474352  Via: 1.1 vegur  Host: requestb.in  Total-Route-Time: 0  Connection: close  Connect-Time: 0</code></pre>    <p>FORM/POST PARAMETERS</p>    <pre>  <code class="language-java">description: This is a description</code></pre>    <p>RAW BODY</p>    <pre>  <code class="language-java">--9b670d44-63dc-4a8a-833d-66e45e0156ca  Content-Disposition: form-data; name="description"  Content-Transfer-Encoding: binary  Content-Type: multipart/form-data; charset=utf-8  Content-Length: 21    This is a description  --9b670d44-63dc-4a8a-833d-66e45e0156ca  Content-Disposition: form-data; name="aFile"; filename="uploadedfile.txt"  Content-Type: application/otcet-stream  Content-Length: 32    Visit me: http://www.println.net  --9b670d44-63dc-4a8a-833d-66e45e0156ca--</code></pre>    <p>我们看到,我们上传的文件的内容出现在请求当中了。如果你需要上传多个文件,就声明多个 Part 参数,或者试试 PartMap 。</p>    <h2>1.5 Converter,让你的入参和返回类型丰富起来</h2>    <h3>1.5.1 RequestBodyConverter</h3>    <p>1.4.3 当中,我为大家展示了如何用 Retrofit 上传文件,这个上传的过程其实。。还是有那么点儿不够简练,我们只是要提供一个文件用于上传,可我们前后构造了三个对象:</p>    <p><img src="https://simg.open-open.com/show/19a5fadd02549bb77f6498e93a66664e.png"></p>    <p>天哪,肯定是哪里出了问题。实际上, Retrofit 允许我们自己定义入参和返回的类型,不过,如果这些类型比较特别,我们还需要准备相应的 Converter,也正是因为 Converter 的存在, Retrofit 在入参和返回类型上表现得非常灵活。</p>    <p>下面我们把刚才的 Service 代码稍作修改:</p>    <pre>  <code class="language-java">public interface FileUploadService {        @Multipart      @POST("upload")      Call<ResponseBody> upload(@Part("description") RequestBody description,          //注意这里的参数 "aFile" 之前是在创建 MultipartBody.Part 的时候传入的          @Part("aFile") File file);  }</code></pre>    <p>现在我们把入参类型改成了我们熟悉的 File ,如果你就这么拿去发请求,服务端收到的结果会让你哭了的。。。</p>    <p>RAW BODY</p>    <pre>  <code class="language-java">--7d24e78e-4354-4ed4-9db4-57d799b6efb7  Content-Disposition: form-data; name="description"  Content-Transfer-Encoding: binary  Content-Type: multipart/form-data; charset=utf-8  Content-Length: 21    This is a description  --7d24e78e-4354-4ed4-9db4-57d799b6efb7  Content-Disposition: form-data; name="aFile"  Content-Transfer-Encoding: binary  Content-Type: application/json; charset=UTF-8  Content-Length: 35    // 注意这里!!之前是文件的内容,现在变成了文件的路径  {"path":"samples/uploadedfile.txt"}   --7d24e78e-4354-4ed4-9db4-57d799b6efb7--</code></pre>    <p>服务端收到了一个文件的路径,它肯定会觉得 <img src="https://simg.open-open.com/show/e29af9ada43e26ef7debc5ff1de98203.png"></p>    <p>好了,不闹了,这明显是 Retrofit 在发现自己收到的实际入参是个 File 时,不知道该怎么办,情急之下给 toString 了,而且还是个 JsonString (后来查证原来是使用了 GsonRequestBodyConverter。。)。</p>    <p>接下来我们就自己实现一个 FileRequestBodyConverter ,</p>    <pre>  <code class="language-java">static class FileRequestBodyConverterFactory extends Converter.Factory {      @Override      public Converter<File, RequestBody> requestBodyConverter(Type type, Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit) {        return new FileRequestBodyConverter();      }    }      static class FileRequestBodyConverter implements Converter<File, RequestBody> {        @Override      public RequestBody convert(File file) throws IOException {        return RequestBody.create(MediaType.parse("application/otcet-stream"), file);      }    }</code></pre>    <p>在创建 Retrofit 的时候记得配置上它:</p>    <pre>  <code class="language-java">addConverterFactory(new FileRequestBodyConverterFactory())</code></pre>    <p>这样,我们的文件内容就能上传了。来,看下结果吧:</p>    <p>RAW BODY</p>    <pre>  <code class="language-java">--25258f46-48b0-4a6b-a617-15318c168ed4  Content-Disposition: form-data; name="description"  Content-Transfer-Encoding: binary  Content-Type: multipart/form-data; charset=utf-8  Content-Length: 21    This is a description  --25258f46-48b0-4a6b-a617-15318c168ed4  //注意看这里,filename 没了  Content-Disposition: form-data; name="aFile"  //多了这一句  Content-Transfer-Encoding: binary  Content-Type: application/otcet-stream  Content-Length: 32    Visit me: http://www.println.net  --25258f46-48b0-4a6b-a617-15318c168ed4--</code></pre>    <p>文件内容成功上传了,当然其中还存在一些问题,这个目前直接使用 Retrofit 的 Converter 还做不到,原因主要在于我们没有办法通过 Converter 直接将 File 转换为 MultiPartBody.Part ,如果想要做到这一点,我们可以对 Retrofit 的源码稍作修改,这个我们后面再谈。</p>    <h3>1.5.2 ResponseBodyConverter</h3>    <p>前面我们为大家简单示例了如何自定义 RequestBodyConverter ,对应的, Retrofit 也支持自定义 ResponseBodyConverter 。</p>    <p>我们再来看下我们定义的接口:</p>    <pre>  <code class="language-java">public interface GitHubService {    @GET("users/{user}/repos")    Call<List<Repo>> listRepos(@Path("user") String user);  }</code></pre>    <p>返回值的类型为 List<Repo> ,而我们直接拿到的原始返回肯定就是字符串(或者字节流),那么这个返回值类型是怎么来的呢?首先说明的一点是,GitHub 的这个 api 返回的是 Json 字符串,也就是说,我们需要使用 Json 反序列化得到 List<Repo> ,这其中用到的其实是 GsonResponseBodyConverter 。</p>    <p>问题来了,如果请求得到的 Json 字符串与返回值类型不对应,比如:</p>    <p>接口返回的 Json 字符串:</p>    <pre>  <code class="language-java">{"err":0, "content":"This is a content.", "message":"OK"}</code></pre>    <p>返回值类型</p>    <pre>  <code class="language-java">class Result{      int code;//等价于 err      String body;//等价于 content      String msg;//等价于 message  }</code></pre>    <p>哇,这时候肯定有人想说,你是不是脑残,偏偏跟服务端对着干?哈哈,我只是示例嘛,而且在生产环境中,你敢保证这种情况不会发生??</p>    <p>这种情况下, Gson 就是再牛逼,也只能默默无语俩眼泪了,它哪儿知道字段的映射关系怎么这么任性啊。好,现在让我们自定义一个 Converter 来解决这个问题吧!</p>    <pre>  <code class="language-java">static class ArbitraryResponseBodyConverterFactory extends Converter.Factory{      @Override      public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit) {        return super.responseBodyConverter(type, annotations, retrofit);      }    }        static class ArbitraryResponseBodyConverter implements Converter<ResponseBody, Result>{        @Override      public Result convert(ResponseBody value) throws IOException {        RawResult rawResult = new Gson().fromJson(value.string(), RawResult.class);        Result result = new Result();        result.body = rawResult.content;        result.code = rawResult.err;        result.msg = rawResult.message;        return result;      }    }        static class RawResult{      int err;      String content;      String message;    }</code></pre>    <p>当然,别忘了在构造 Retrofit 的时候添加这个 Converter,这样我们就能够愉快的让接口返回 Result 对象了。</p>    <p>注意!! Retrofit 在选择合适的 Converter 时,主要依赖于需要转换的对象类型,在添加 Converter 时,注意 Converter 支持的类型的包含关系以及其顺序。</p>    <h2>2、Retrofit 原理剖析</h2>    <p>前一个小节我们把 Retrofit 的基本用法和概念介绍了一下,如果你的目标是学会如何使用它,那么下面的内容你可以不用看了。</p>    <p>不过呢,我就知道你不是那种浅尝辄止的人!这一节我们主要把注意力放在 Retrofit 背后的魔法上面~~</p>    <p><img src="https://simg.open-open.com/show/c201379d7a02dc74d3a6f74c9327eba7.png"></p>    <h2>2.1 是谁实际上完成了接口请求的处理?</h2>    <p>前面讲了这么久,我们始终只看到了我们自己定义的接口,比如:</p>    <pre>  <code class="language-java">public interface GitHubService {    @GET("users/{user}/repos")    Call<List<Repo>> listRepos(@Path("user") String user);  }</code></pre>    <p>而真正我使用的时候肯定不能是接口啊,这个神秘的家伙究竟是谁?其实它是 Retrofit 创建的一个代理对象了,这里涉及点儿 Java 的动态代理的知识,直接来看代码:</p>    <pre>  <code class="language-java">public <T> T create(final Class<T> service) {      Utils.validateServiceInterface(service);      if (validateEagerly) {        eagerlyValidateMethods(service);      }      //这里返回一个 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);              }              //DefaultMethod 是 Java 8 的概念,是定义在 interface 当中的有实现的方法              if (platform.isDefaultMethod(method)) {                return platform.invokeDefaultMethod(method, service, proxy, args);              }              //每一个接口最终实例化成一个 ServiceMethod,并且会缓存              ServiceMethod serviceMethod = loadServiceMethod(method);                            //由此可见 Retrofit 与 OkHttp 完全耦合,不可分割              OkHttpCall okHttpCall = new OkHttpCall<>(serviceMethod, args);              //下面这一句当中会发起请求,并解析服务端返回的结果              return serviceMethod.callAdapter.adapt(okHttpCall);            }          });    }</code></pre>    <p>简单的说,在我们调用 GitHubService.listRepos 时,实际上调用的是这里的 InvocationHandler.invoke 方法~~</p>    <h2>2.2 来一发完整的请求处理流程</h2>    <p>前面我们已经看到 Retrofit 为我们构造了一个 OkHttpCall ,实际上每一个 OkHttpCall 都对应于一个请求,它主要完成最基础的网络请求,而我们在接口的返回中看到的 Call 默认情况下就是 OkHttpCall 了,如果我们添加了自定义的 callAdapter ,那么它就会将 OkHttp 适配成我们需要的返回值,并返回给我们。</p>    <p>先来看下 Call 的接口:</p>    <p><img src="https://simg.open-open.com/show/993c3c9dfb64b6a8b12df3791e8257a0.png"></p>    <p>我们在使用接口时,大家肯定还记得这一句:</p>    <p><img src="https://simg.open-open.com/show/eeec3afe094e685c29bdca3075446f24.png"></p>    <p>这个 repos 其实就是一个 OkHttpCall 实例, execute 就是要发起网络请求。</p>    <p>OkHttpCall.execute</p>    <p><img src="https://simg.open-open.com/show/f04a04ce68bd3215a95d224f7c251baa.png"></p>    <p>我们看到 OkHttpCall 其实也是封装了 okhttp3.Call ,在这个方法中,我们通过 okhttp3.Call 发起了进攻,额,发起了请求。有关 OkHttp 的内容,我在这里就不再展开了。</p>    <p>parseResponse 主要完成了由 okhttp3.Response 向 retrofit.Response 的转换,同时也处理了对原始返回的解析:</p>    <pre>  <code class="language-java">Response<T> parseResponse(okhttp3.Response rawResponse) throws IOException {      ResponseBody rawBody = rawResponse.body();        //略掉一些代码      try {        //在这里完成了原始 Response 的解析,T 就是我们想要的结果,比如 GitHubService.listRepos 的 List<Repo>        T body = serviceMethod.toResponse(catchingBody);        return Response.success(body, rawResponse);      } catch (RuntimeException e) {        // If the underlying source threw an exception, propagate that rather than indicating it was        // a runtime exception.        catchingBody.throwIfCaught();        throw e;      }    }</code></pre>    <p>至此,我们就拿到了我们想要的数据~~</p>    <p> </p>    <p>来自:http://dev.qq.com/topic/591aa71ae315487c53deeca9</p>    <p> </p>