Retrofit源码学习

本文针对的是Retrofit 2.1.0

一、Retrofit简介

如今开发一个Android应用,几乎都需要和网络打交道,最常见的就是通过http协议来请求服务器数据或者是给服务器发送数据,虽然Android本身也提供了HttpUrlConnection和Volley来发起http请求,功能上其实也能满足,但鉴于Retrofit+OkHttp的组合如此流行,所以几个项目下来,都是用的这一套。
  Retrofit是一个针对Android和java的类型安全的、开源的http客户端,由大名鼎鼎的Square公司出品(另外还有:picasso、dagger等),通过注解的方式,让开发者定义一个接口方法来完成http请求(真正的请求其实是由OkHttp来完成的),至于这个过程是怎么实现的,后面会详细说。
相关链接:

二、使用

简单介绍下Retrofit的使用

  1. 定义一个接口:ApiService
public interface ApiService {

   /**
     * 获取协议内容
     *
     * @return 此时返回的JsonResponse里面的body字段的类型为 AgreementResponseBody
     */
    @GET("system/getAgreement")
    Call<JsonResponse> getAgreement();

    /**
     * 获取点赞信息
     * @param getApprovalInfoBean
     * @return
     */
    @POST("approval/getApproval")
    Call<JsonResponse> getApprovalInfo(@Body GetApprovalInfoBean getApprovalInfoBean);
}
   

上面就定义了两个最常用的http的方法,GET请求和POST请求,可以看到,需要使用GET或者是POST只需要在方法上加上注解就可以(当然还有其它方法,像:DELETE等)。POST方法的话因为有请求体,所以会用@Body来注解,这里放的是我自定义的一个类(因为结合了fastjson使用)。两个方法的返回都是Call<T>的形式。

  1. Retrofit的相关配置及初始化
      Retrofit使用了Builder模式,主要是做了一些配置,可以看到,实例化了一个OkHttpClient,因为要用它来进行实际的http请求。
        OkHttpClient.Builder builder = new OkHttpClient().newBuilder();
        builder.readTimeout(10, TimeUnit.SECONDS);
        builder.connectTimeout(9, TimeUnit.SECONDS);


        //添加拦截器,保留一些调试时的日志
        if (BuildConfig.DEBUG) {
            HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
            interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
            builder.addInterceptor(interceptor);
        }

        OkHttpClient okHttpClient = builder.build();

        /**
         * addConverterFactory,是为了对象的序列化和反序列化,一般就是使用json相关的工具。
         * addCallAdapterFactory,是为了返回Call类型之外的其它类型
         */
        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl(BASE_URL)
                .client(okHttpClient)
                .addConverterFactory(FastJsonConverterFactory.create())
                .build();
        api = retrofit.create(ApiService.class);
  1. 发起请求
      一般用的比较多的是异步请求,当然也可以使用同步请求。在Callback回调里面可以进行成功及失败,回调都是在主线程里面(在Android上使用,底层是通过Handler post到主线程)。

        Call<JsonResponse> call = apiService.getApprovalInfo(getApprovalInfoBean);
        requestList.add(call);

        call.enqueue(new Callback<JsonResponse>() {
            @Override
            public void onResponse(Call<JsonResponse> call, Response<JsonResponse> response) {
              
            }

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

            }
        });

至此基本的用法就大概说完了,当然还有很多其它的用法,具体可以参考下官网。基本上Http的方法都支持。

三、源码分析

  1. 源码的包结构


    image.png

    http包:


    image.png

http包里面放的都是一些自定义的注解类,平常开发者会用到的是:Call、CallAdapter、Callback、Converter、Retrofit


  1. 主要的几个类之间的关系
类图.jpg

  1. Retrofit是如何把一个接口方法转变为http请求的吗

Retrofit里面用到了大量的反射、泛型、注解,还用到了动态代理(这是将接口方法转换为http请求的关键,毕竟我们没有看到 Call<JsonResponse> getAgreement();方法的实现)。

可能自己之前写的代码比较简单,考虑的方面也比较单一,反射、注解之类的东西用的也比较少。抛开性能不说(反射会降低性能),这些功能(或者说是语法吧)能够实现一些“比较特殊”的功能,还有注解、泛型这些可以给让代码变得更加优雅,增加代码的可扩展性和动态性。感谢开源,让我们能够读到大神的源码。
  关键点是这里,使用了java的动态代理,其实上面我们定义的接口方法getAgreement()真正的执行地方是下面invoke()方法里面,通过反射调用了。

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);
          }
        });

真正的方法调用:

return method.invoke(this, args);
return platform.invokeDefaultMethod(method, service, proxy, args);
return serviceMethod.callAdapter.adapt(okHttpCall);

这里可以看到有三个return的地方,第一种情况是:
这里又有一个关键的类:ServiceMethod,关于ServiceMethod,请看下文的“4”。


  1. ServiceMethod分析(关键衔接点)

ServiceMethod就是具体来做相关的方法参数的解析(解析方法上面的注解)及解析方法的返回值。解析注解里面的值(http的参数)其实是通过正则表达式匹配来完成的。

  static final String PARAM = "[a-zA-Z][a-zA-Z0-9_-]*";
  static final Pattern PARAM_URL_REGEX = Pattern.compile("\\{(" + PARAM + ")\\}");
  static final Pattern PARAM_NAME_REGEX = Pattern.compile(PARAM);

ServiceMethod仍然采用了Builder模式(Retrofit源码里面可以看到大量使用Builder,对于有许多需要配置的参数的情况,这样写起来更优雅一点)。
最终的请求是由toRequest方法来完成:

/** Builds an HTTP request from method arguments. */
  Request toRequest(Object... args) throws IOException {
    RequestBuilder requestBuilder = new RequestBuilder(httpMethod, baseUrl, relativeUrl, headers,
        contentType, hasBody, isFormEncoded, isMultipart);

    @SuppressWarnings("unchecked") // It is an error to invoke a method with the wrong arg types.
    ParameterHandler<Object>[] handlers = (ParameterHandler<Object>[]) parameterHandlers;

    int argumentCount = args != null ? args.length : 0;
    if (argumentCount != handlers.length) {
      throw new IllegalArgumentException("Argument count (" + argumentCount
          + ") doesn't match expected count (" + handlers.length + ")");
    }

    for (int p = 0; p < argumentCount; p++) {
      handlers[p].apply(requestBuilder, args[p]);
    }

    return requestBuilder.build();
  }

返回的requestBuilder.build()产生了Request的实例,而Request是 OkHttp里面的类。由此可见,Retrofit只是封装了http的请求,最终的请求还是通过OkHttp来完成的。


  1. Call接口

可用于异步也可用于同步请求,异步的话就放到队列里,调用enqueue()方法;同步的话调用execute()方法,直接返回一个Response<T>对象。


  1. Callback接口

Call的实例调用enqueue()方法的参数,用于接收回调,包含两个回调方法:

  • http层面上的正常响应,即网络正常
/**
   * Invoked for a received HTTP response.
   * <p>
   * Note: An HTTP response may still indicate an application-level failure such as a 404 or 500.
   * Call {@link Response#isSuccessful()} to determine if the response indicates success.
   */
  void onResponse(Call<T> call, Response<T> response);
  • http层面上的异常响应
/**
   * Invoked when a network exception occurred talking to the server or when an unexpected
   * exception occurred creating the request or processing the response.
   */
  void onFailure(Call<T> call, Throwable t);

一般在正常响应里面需要判断下应用层面的情况,如:200、404等;判断完应用层面后需要再判断下自定义的情况,一般:刷新成功、刷新失败等。所以可以写一个抽象类实现Callback接口,在里面做一些自已的处理,避免写太多重复代码。


  1. Converter<F, T>接口

将F类型转换为T类型,主要用在转成json对象,如:addConverterFactory(FastJsonConverterFactory.create())。
包含了一个方法和一个抽象工厂类:

T convert(F value) throws IOException;

abstract class Factory

抽象工厂类主要有三种转换器方法:

public @Nullable Converter<ResponseBody, ?> responseBodyConverter(Type type,
        Annotation[] annotations, Retrofit retrofit) {
      return null;
    }

   
    public @Nullable Converter<?, RequestBody> requestBodyConverter(Type type,
        Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit) {
      return null;
    }

   
    public @Nullable Converter<?, String> stringConverter(Type type, Annotation[] annotations,
        Retrofit retrofit) {
      return null;
    }

分别是将ResponseBody转成自己想要的类型,将自定义类型转为RequestBody,将http的参数转为字符串类型

跟这个相关的有一个自带的类:
final class BuiltInConverters extends Converter.Factory

  • 重写了responseBodyConverter和requestBodyConverter这两个方法

  1. CallAdapter<R, T> 接口

这个接口常见的好像主要是为了配合Rxjava使用,因为还没有用过Rxjava,所以等后面用了再作更深入的分析。
  主要用于将响应的类型R转换为自己想要的类型,用的比较多的是将响应转换为rxjava支持,会用到RxJava2CallAdapterFactory.create()。

在Retrofit.Builder#addCallAdapterFactory(Factory)里调用 ,一般在初始化里面。

包含了两个方法和一个内部的抽象类

Type responseType();

T adapt(Call<R> call);

abstract class Factory

抽象工厂方法里面包含了一个get()方法,返回一个CallAdapter类型;包含两个返回类型的方法:

/**
     * Extract the upper bound of the generic parameter at {@code index} from {@code type}. For
     * example, index 1 of {@code Map<String, ? extends Runnable>} returns {@code Runnable}.
     */
    protected static Type getParameterUpperBound(int index, ParameterizedType type) {
      return Utils.getParameterUpperBound(index, type);
    }

    /**
     * Extract the raw class type from {@code type}. For example, the type representing
     * {@code List<? extends Runnable>} returns {@code List.class}.
     */
    protected static Class<?> getRawType(Type type) {
      return Utils.getRawType(type);
    }
  1. 关于http包里的自定义注解类

这里定义注解是为了后面可以解析注解得到http请求对应的方法及各种参数

  • 方法注解:GET、POST、PUT、DELETE、PATCH、HTTP、HEAD、FormUrlEncoded、Headers、Multipart、OPTIONS、Streaming
  • 参数注解:Body、Field、FieldMap、HeaderMap、Part、PartMap、Path、Query、QueryMap、Url

  1. Platform类(判断是属于哪个平台)

Retrofit的介绍说的就是可以在普通的java中使用,也可以在Android中使用,所以里面有一段代码用来判断当前代码是运行在哪个平台上。

private static Platform findPlatform() {
    try {
      Class.forName("android.os.Build");
      if (Build.VERSION.SDK_INT != 0) {
        return new Android();
      }
    } catch (ClassNotFoundException ignored) {
    }
    try {
      Class.forName("java.util.Optional");
      return new Java8();
    } catch (ClassNotFoundException ignored) {
    }
    try {
      Class.forName("org.robovm.apple.foundation.NSObject");
      return new IOS();
    } catch (ClassNotFoundException ignored) {
    }
    return new Platform();
  }

其实就是通过查找有没有平台相关的类来判断具体是哪个平台,像在Android上就找“android.os.Build”;普通java的话,就通过找“java.util.Optional”来判断;但是iOS查找“"org.robovm.apple.foundation.NSObject"”我就不是很明白了,iOS上也可以运行吗?
  这个小技巧感觉可以学习一下。

四、总结

之前使用Retrofit的时候也只是参照官网的例子简单使用了一下,并没有去深究它内部的实现,最近在看自己写的代码的时候,发现每一次http请求都需要在回调里面做好多判断,如:在onResponse()里面要判断http的状态码,进一步又要判断自定义的状态,感觉好臃肿,于是就想到用抽象类来解决。最近刚好有点时间,就阅读了一下源码,发现收获还是挺多的。

  • 通过注解的方式来完成http请求,对于我来说还算是比较新的。
  • 给方法、参数加上自定义注解,来增加一些编译期和运行时的行为。
  • 目前做项目的时候抛出异常的情况还比较少,而如果是设计一个库供他人使用,可能就得像Retrofit一样抛出一些异常,做一些参数检查,来增加代码的健壮性。
  • 使用工厂模式隐藏创建过程的复杂度,
  • 使用Builder模式来避免构造方法里面有一堆的参数。
  • 使用泛型来增加代码的可扩展性,避免写一堆的样板代码。

第一次写博客,以前都是在有道云笔记里面记东西,虽然也记了挺多,但是都比较随意。这一次会想写有两个原因:第一、应工作室同学的邀请;第二、学Android也有一段时间了,也用了不少开源库,看过一部分源码,感觉需要好好总结一下,提升一下自己的技术水平。以前总是想着说自己的水平还不够高,还没到写博客的时候,但是现在想想,其实也没那么绝对,写出来是对自己的一个阶段性总结,对学过的东西的一个巩固,一开始可能写的不好,但写多了,慢慢总会有进步。之前看到过一句话“没看过优秀的源码,是不太容易写好优秀的代码的。”我对此还是比较认同的,开源的世界带来了如此丰富的资源,需要我们好好利用。
  写了一些代码,也看过一些代码,感觉看源码就像在看书一样,也有结构、语法、先后顺序,都是要表达些什么。只不过文字和源代码是从不同方面来理解世界,从不同角度来构筑这个世界。我越发地相信,这个世界有很多东西是相通的,是有联系的。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,394评论 25 707
  • Retrofit已经出来很多时间了,项目中也一直在用,只知道它的功能强大,不知其原理,这不是一个号的开发,所以最近...
    三季人阅读 346评论 1 1
  • 基本用法 上面代码主要创建了Retrofit对象,并且为Retrofit对象分别设置了OkhttpClient对象...
    写代码的阅读 569评论 0 0
  • Retrofit是squareup公司的开源力作,和同属squareup公司开源的OkHttp,一个负责网络调度,...
    蓝灰_q阅读 41,202评论 23 281
  • 今天老师给我们讲ZCS还有整体认读zⅰcⅰsi,课后我还自己画了一幅小房子,科学老师还给我们放了小动画片儿。数学老...
    张雪涵阅读 124评论 0 0