Retrofit源码详解

Retrofit的使用从很早之前就已经开始了, 但是一直没有深入研究为什么使用Retrofit只要定义一个接口, 同时在接口的方法上和方法的参数上加上一些注解就可以完成Http请求了, 也没有研究请求参数和请求结果是如何进行封装的, 所以使用Retrofit一直是处于一知半解的状态, 不知道其内部的原理, 因此花了一点时间看了Retrofit的源码, 对Retrofit的整个请求流程有了一定的理解.

Retrofit核心源码解读

1. 创建Retrofit对象

创建Retrofit对象的时候使用的是Builder模式, 可以在创建Retrofit对象的时候设置RetrofitbaseURL, 添加自己的converterFactory

例子:

Retrofit retrofit = new Retrofit.Builder()
           .baseUrl(GITHUB_API)
           .addConverterFactory(GsonConverterFactory.create())
           .build();

注意点:

  • 在设置baseUrl的时候和Retrofit 1.x版本不同的是url必须以'/'结尾

    源码:

    
    // Retrofit.java#baseUrl method
    
    public Builder baseUrl(HttpUrl baseUrl) {
        ....
       // pathSegments() 返回的是url的查找路径List(查找路径是以'/'分割的字符串, 所以只要split("/")就可以了)
      List<String> pathSegments = baseUrl.pathSegments();
      // 如果List的最后一个元素不是(""), 则说明查找路径不是以'/'结尾的字符串
      if (!"".equals(pathSegments.get(pathSegments.size() - 1))) {
        throw new IllegalArgumentException("baseUrl must end in /: " + baseUrl);
      }
        .... 
    }
    
    
  • 如果不添加自己的转换器, 则返回值只能封装成ResponseBody类型或者没有返回值

    源码:

    // Retrofit.java#Builder
    
    Builder(Platform platform) {
           this.platform = platform;
           // 添加默认的BuiltInConverter
           converterFactories.add(new BuiltInConverters());
        }
    
    // BuiltInConverters.java#responseBodyConverter method
    
     public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit) {
     
         // 如果返回值是ResponseBody类型
        if (type == ResponseBody.class) {
          return Utils.isAnnotationPresent(annotations, Streaming.class)
              ? StreamingResponseBodyConverter.INSTANCE
              : BufferingResponseBodyConverter.INSTANCE;
        }
        
        // 如果返回值类型是Void类型
        if (type == Void.class) {
          return VoidResponseBodyConverter.INSTANCE;
        }
        
        // 如果返回值是其他的类型
        return null;
      }
    
    
  • 创建Retrofit对象时使用了Builder模式, 而使用Builder模式的好处就是创建的对象是不可变对象(因为属性都是private的且没有setter方法), 这样在使用的使用就不用担心对象中属性是否被修改或者不小心修改了对象中的属性了. 如果存在有些请求不能和其他请求共用同一个Retrofit对象, 但是大部分的属性都一样只有少部分的属性不一致, 那么可以用一个Retrofit对象为模版来创建一个新的Retrofit对象

    源码:

    
    // Retrofit.java#Builder
    
    Builder(Retrofit retrofit) {
        // 以retrofit对象为模版生成一个新的Retrofit对象
      this.platform = Platform.get();
      callFactory = retrofit.callFactory;
      baseUrl = retrofit.baseUrl;
      converterFactories.addAll(retrofit.converterFactories);
      adapterFactories.addAll(retrofit.adapterFactories);
      // Remove the default, platform-aware call adapter added by build().
      adapterFactories.remove(adapterFactories.size() - 1);
      callbackExecutor = retrofit.callbackExecutor;
      validateEagerly = retrofit.validateEagerly;
    }
    
  • 这里要额外提一下. 因为Retrofit是支持多个平台(jvm, android, ios), 那么Retrofit是如何分辨当前引用的是什么平台的呢, 实际上Retrofit使用的方法就是查看当前的classpath中是否存在相应平台特有的class来判断当前是在哪个平台. 这个和Spring的@ConditionalOnClass(ClassName.class)来决定是否初始化注解所修饰的bean有异曲同工的味道

    关键代码:

    
    // Platform.java#findPlatform method 
    // 查找当前所在的平台
    private static Platform findPlatform() {
        try {
          // 如果存在"android.os.Build"则是android平台
          Class.forName("android.os.Build");
          if (Build.VERSION.SDK_INT != 0) {
            return new Android();
          }
        } catch (ClassNotFoundException ignored) {
        }
        try {
          // 如果存在"java.util.Optional"则是java8平台
          Class.forName("java.util.Optional");
          return new Java8();
        } catch (ClassNotFoundException ignored) {
        }
        try {
          // 如果存在"org.robovm.apple.foundation.NSObject"则是ios平台
          Class.forName("org.robovm.apple.foundation.NSObject");
          return new IOS();
        } catch (ClassNotFoundException ignored) {
        }
        
        // 默认平台
        return new Platform();
      }
    

2. 创建发送请求的接口

包含发送请求方法必须是一个接口, 同时这个接口是不能继承其他的接口的

例子:

    public interface GitApi {
        @GET("/users/{user}")
        Call<GitModel> getFeed(@Path("user") String user);
    }

注意点:

  • 包含请求方法必须是一个接口同时不能继承其他的接口的

    源码:

    static <T> void validateServiceInterface(Class<T> service) {
    
         // class必须是一个接口
        if (!service.isInterface()) {
          throw new IllegalArgumentException("API declarations must be interfaces.");
        }
        
        // 接口不能继承其他的接口
        if (service.getInterfaces().length > 0) {
          throw new IllegalArgumentException("API interfaces must not extend other interfaces.");
        }
    }
    
  • 为什么只要定一个接口和一个请求方法, Retrofit就可以发送Http请求了呢? 因为Retrofit使用了代理, 其实在创建接口的对象的时候返回的是一个代理对象, 这个代理其实也是Retrofit的核心.

    源码:

        // 创建接口的实现对象, 返回的是GitApi接口的代理对象
       GitApi git = retrofit.create(GitApi.class);
       
       // 关键代码: create方法
       public <T> T create(final Class<T> service) {
       
            // 校验service是否是一个没有继承其他接口的接口
            Utils.validateServiceInterface(service);
            
            // 如果validateEagerly==true, 则先将接口中的方法全部都放到serviceMethodCache中,
            // 这样在之后的调用过程中就不需要走loadServiceMethod的流程, 而是直接走的缓存, 这样子可以加快访问的速度,
            // 但是这样子也存在一定的坏处, 因为会造成内存占用量变大而且可能有些方法不会被调用都被放到缓存中了
            if (validateEagerly) {
              eagerlyValidateMethods(service);
            }
            
            // 代理模式,
            // 使用代理拦截接口中所有的方法, 从而解析方法上的注解, 方法参数上的注解, 返回值等, 这也是retrofit可以实现面向接口和注解编程的关键
            // service: 真实对象, proxy: 代理对象, method: 调用的方法, args: 方法参数
            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 {
                    
                    // 如果调用的是Object对象中方法则直接返回调用结果
                    if (method.getDeclaringClass() == Object.class) {
                      return method.invoke(this, args);
                    }
                    // 如果调用的是接口中的默认方法则直接返回对应平台对调用默认方法的处理
                    if (platform.isDefaultMethod(method)) {
                      return platform.invokeDefaultMethod(method, service, proxy, args);
                    }
                    
                    // 获取或者创建ServiceMethod对象
                    ServiceMethod<Object, Object> serviceMethod =
                        (ServiceMethod<Object, Object>) loadServiceMethod(method);
                   // 创建OkHttpCall对象用于发送请求和解析结果 
                    OkHttpCall<Object> okHttpCall = new OkHttpCall<>(serviceMethod, args);
                    return serviceMethod.callAdapter.adapt(okHttpCall);
                  }
                });
            }
    
  • 上面很明显可以看到Retrofit实际上使用了jdkProxy动态代理技术使得可以定义一个接口加上注解就完成http请求. 由于使用了Proxy所以必须定义成一个接口而不是一个抽象类的原因也就显而易见了, 因为jdkproxy只能生成接口的代理对象. 如果这里使用的是cglib库的话那也可以定义成抽象类

3. 发送Http请求

要发送http请求就离不开ServiceMethod类和OkHttpCall类, 他们是发送Http请求的核心类. 首先会从Map中获取和当前请求方法相关联的ServiceMethod, 如果找到了(说明之前这个方法已经被调用过了)就直接使用找到的ServiceMethod, 如果没有找到, 则创建一个新的ServiceMethod并且和当前的请求方法相关联putMap中. 最后调用OkHttpCall.execute发送请求.

源码:

  // 查找或者判断和请求的方法相关联的ServiceMethod
  
    ServiceMethod<?, ?> loadServiceMethod(Method method) {
        // 缓存技术
        ServiceMethod<?, ?> result = serviceMethodCache.get(method);
        if (result != null) return result;
    
        // 使用了两次判断(两段锁)
        synchronized (serviceMethodCache) {
          // 这一次从缓存中取是有必要的且非常重要, 如果没有这一次则有可能下面的代码会被重复执行, 同一个key也可能被重复赋值
          result = serviceMethodCache.get(method);
          if (result == null) {
            result = new ServiceMethod.Builder<>(this, method).build();
            serviceMethodCache.put(method, result);
          }
        }
        return result;
    }
  • 创建ServiceMethod也使用了Build模式, 在build方法中对请求的方法上的注解, 方法参数, 返回值进行了解析

源码:

public ServiceMethod build() {
 // 创建callAdapter对象(默认使用的是DefaultCallAdapterFactory), callAdapter对象用于.
 // callAdapter对象的主要作用就是返回一个CallAdapter接口的实例用于调用底层okhttp的方法发送请求和解析返回值(DefaultCallAdapterFactory中使用OkHttpCall.execute来发送请求和解析结果). 
 // 默认的请求方法返回值都是Call<T>, 可以继承CallAdapter.Factory来实现自定义的返回值类型
 callAdapter = createCallAdapter();
 
 ...
 other code
 ...
 
 // 创建返回值解析器(默认只有BuiltInConverters), 可以在创建Retrofit的时候使用addConverterFactory加入其他的converters
 responseConverter = createResponseConverter();

    // 解析方法上的注解
 for (Annotation annotation : methodAnnotations) {
   parseMethodAnnotation(annotation);
 }

 ...
 other code
 ...
 
 // 解析参数
 int parameterCount = parameterAnnotationsArray.length;
 parameterHandlers = new ParameterHandler<?>[parameterCount];
 for (int p = 0; p < parameterCount; p++) {
   Type parameterType = parameterTypes[p];
   if (Utils.hasUnresolvableType(parameterType)) {
     throw parameterError(p, "Parameter type must not include a type variable or wildcard: %s",
         parameterType);
   }

   Annotation[] parameterAnnotations = parameterAnnotationsArray[p];
   if (parameterAnnotations == null) {
     throw parameterError(p, "No Retrofit annotation found.");
   }

   parameterHandlers[p] = parseParameter(p, parameterType, parameterAnnotations);
 }

 ...
 一些错误情况的处理
 ...

 return new ServiceMethod<>(this);
}
  • Retrofit.java#createproxy中调用请求方法最后返回的是 return serviceMethod.callAdapter.adapt(okHttpCall);, 默认情况下返回的就是OkHttpCall, 所以最后调用的就是OkHttpCall.execute()方法发送请求和解析返回结果

关键代码:

    @Override public Response<T> execute() throws IOException {
    okhttp3.Call call;
    
    synchronized (this) {
      
      ...
        
      call = rawCall;
      if (call == null) {
        try {
          // 创建Okhttp的call方法
          call = rawCall = createRawCall();
        } catch (IOException | RuntimeException e) {
          ...
        }
      }
    }
        
     ....
    
     // call.execute: 调用okhttp发送真正的请求
     // parseResponse: 对返回的结果进行解析, 方法就是调用创建retrofit时加入的所有converterFactory, 知道找到一个converter可以处理返回值
    return parseResponse(call.execute());
  }

总结

至此, Retrofit发送请求的整个流程就已经讲解完毕了, 实际上整个流程中关键点就只有几个, 比如:

  • Retrofit.java#create方法, 这个方法的作用就是使用Proxy创建用户定义的接口的实现, 从而实现使用时只要创建接口, 创建方法, 添加相关注释三个步骤就可以完成一个http请求的关键

  • ServiceMethod.java#build方法, 这个方法的作用是对请求方法上的注解, 方法的参数, 方法的返回值进行解析, 同时创建callAdapter对象, 这是Retrofit留下的一个'插口', 只要我们实现CallAdapter.Factory就可以处理自定义的方法返回值类型, 方法不一定要返回Call<T>类型

  • OkHttpCall.java#execute方法, 这个方法的作用是调用底层的okhttp发送真正的http请求 , 然后对返回结果进行解析. 解析返回结果是的方法就是遍历Retrofit中的converterFactories, 知道找到一个可以解析返回结果的对象. 这事Retrofit留下的另一个'插口', 只要我们实现Converter.Factory就可以处理自定义的返回结果, 而不一定只能是Call<ResponseBody>类型

另外, Retrofit提供了

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 (primitives, boxed, and String): com.squareup.retrofit2:converter-scalars

这写转换器, 应该可以满足日常开发需要了.

从上面可以看到, Retrofit可以允许我们自定义返回类型, 返回结果同时屏蔽掉了底层的OkHttp的复杂性, 使得我们只要定义一个接口就完成Http请求的发送, 这对于使用者来说是非常友好的. 易于上手和高度的定制性是现如今Retrofit如此流行的关键

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

推荐阅读更多精彩内容