Retrofit2使用案例及源码解析

Retrofit2是一个基于OkHttp进行封装的网络请求框架,Retrofit中使用了大量的@GET@POST这类注解方法,刚开始学习的时候会感觉特别的不适应,但只要理解了这些注解方法和使用逻辑,会发现网络请求也可以做的很优雅

一、案例

先写一个简单的程序跑起来,再详细讲解
首先添加依赖

compile 'com.squareup.retrofit2:retrofit:2.1.0'

Retrofit最新的版本可以去主页查看:GitHub Retrofit

这里使用的是豆瓣免费API,可以去豆瓣开发者官网了解一下,返回的是一串JSON数据

https://api.douban.com/v2/book/1220562

自己定义一个接口类HTMLStringBiz和请求方法getHTMLString,通过这个方法来获取返回的JSON数据

public interface HTMLStringBiz {

    @GET("book/{id}")
    Call 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 call = biz.getHTMLString(bookId);
        call.enqueue(new Callback() {
            @Override
            public void onResponse(Call call, Response response) {
                try {
                    Log.e(TAG, "返回的结果" + new String(response.body().bytes()));
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }

            @Override
            public void onFailure(Call call, Throwable t) {

            }
        });
    }

运行一下,后台就会返回一串JSON数据了(别忘了添加访问网络的权限)
此处我们使用的是enqueue异步请求的方式,Retrofit中Callback里的onResponseonFailure回调方法就是运行在UI线程的,取得了数据后就可以直接设置在界面中进行展示了,非常便捷

二、详细分析

1.设置URL地址

Retrofit中请求类型的指定都是通过@XXX这种方式设置的

    @GET("book/{id}")
    Call getHTMLString(@Path("id") String bookid);

通过@GET指定下面的getHTMLString方法将是以get方式发送请求的
@GET("book/{id}")中的{id}是我们自定义的一个占位符,和@Path("id")是对应的,可以理解为,@Path("id")后面所传入的参数,将会替换@GET("book/{id}")中的{id}占位符。

Call指定请求方法返回类型,此处我们需要获取的是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 getHTMLStringUrl(@Url String url);
        Retrofit build = new Retrofit.Builder()
                .baseUrl("https:xxx")
                .build();
        HTMLStringBiz biz = build.create(HTMLStringBiz.class);
        Call 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 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需要改成对应的实体Bean,然后就可以在onResponse回调中通过response.body()方法获取经过解析后的数据实体类了

3.创建接口请求方法

创建了Retrofit实例后,就可以通过Retrofit实例创建接口请求类,从而调用自定义的请求方法了

        HTMLStringBiz biz = build.create(HTMLStringBiz.class);
        Call call = biz.getHTMLString(bookId);

通过调用getHTMLString方法,传入一个书本id,这个传入的值将会替换{id}占位符,最终组合成一个完整的url地址,也就是https://api.douban.com/v2/book/1220562

这里我们给Retrofit传入了一个HTMLStringBiz接口的Class,却生成了一个HTMLStringBiz对象?这也是Retrofit最经典的地方了,点进去看看build.create的源码

  public  T create(final Class 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  void validateServiceInterface(Class 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 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同步请求

enqueueexecute的具体实现在OkHttpCall类中

  @Override public void enqueue(final Callback 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 response;
        try {
          response = parseResponse(rawResponse);
        } catch (Throwable e) {
          callFailure(e);
          return;
        }
        callSuccess(response);
      }
      ......
  }

发送请求时,调用的是okhttp3.Call的enqueue方法,此处是对okhttp请求回调做的一层封装,在此处也可以发现,Retrofit是支持取消请求的,调用cancel方法就好了
如果请求成功的话,就会执行callSuccess方法

      private void callSuccess(Response response) {
        try {
          callback.onResponse(OkHttpCall.this, response);
        } catch (Throwable t) {
          t.printStackTrace();
        }
      }

最终会通过callback.onResponse把值回调给我们
同样的,调用execute方法进行同步请求时的过程也是如此,交给okhttp处理网络请求,通过对返回的封装,使用Callback代理call实例的返回,最终把网络请求返回的数据传递给我们使用

        call.enqueue(new Callback() {
            @Override
            public void onResponse(Call call, Response response) {
                try {
                    Log.e(TAG, "返回的结果" + new String(response.body().bytes()));
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            
            @Override
            public void onFailure(Call call, Throwable t) {

            }
        });

请求成功则回调onResponse,失败回调onFailure,其回调方法都是执行在UI线程的,可直接在回调中更新UI界面

三、总结

到此为止,一次完整的网络请求就结束了,对其流程及源码进行了简单的分析,会发现Retrofit的主要源码并不多,但设计的非常精巧,对okhttp的封装非常的到位,使用起来变得简洁明了,可自定义解析器,还支持和RxJava的配合使用,可扩展性非常强
Retrofit还有很多其它的注解方法,就不全部列举了,其实只要了解了Retrofit的运行原理,其它的注解方法看看源码注释就能理解和使用了

你可能感兴趣的:(Retrofit2使用案例及源码解析)