Retrofit 源码阅读笔记(一)

Retrofit 源码阅读笔记(一)

Retrofit 相信每一个 Android 开发者都对它非常熟悉,它使我们调用 Http 请求变得非常的简单(内部实现是 OkHttp ),我们只需要定义一个接口,接口的每一个方法就表示一个 Http 请求,方法的注解,参数的注解和参数他们共同来描述了这一个 Http 请求的 Request,而方法的返回值描述了如何处理 HttpResponse。通过 Retrofit#create() 方法就能够生成一个上面接口的实现类(通过动态代理实现),通过调用这个实现类的方法就能够完成一次 Http 请求,一次网络请求就好像调用了一次我们的本地方法一样。刚开始接触 Retrofit 的我,感觉这个真的是太神奇,工作多年后渐渐地我也知道了它的原理,所以这次文章来基于 Retrofit 的源码来记录一下它的实现方式。

本篇文章是系列文章的第一篇,我阅读的 Retrofit 源码版本是 2.9.0

动态代理创建接口实现类

创建我们定义的接口实现类通过 Retrofit#create() 方法传一个接口的 Class 对象。我们来看看它的源码:

Java 复制代码
  @SuppressWarnings("unchecked") // Single-interface proxy creation guarded by parameter safety.
  public <T> T create(final Class<T> service) {
    // 校验 Class 对象是否合法
    validateServiceInterface(service);
    // 构建动态代理对象
    return (T)
        Proxy.newProxyInstance(
            service.getClassLoader(),
            new Class<?>[] {service},
            new InvocationHandler() {
              private final Platform platform = Platform.get();
              private final Object[] emptyArgs = new Object[0];
             
              // inovke() 方法会在每一个方法的调用时,调用这个方法,对应被调用的对象就是 proxy,方法是 method,方法的参数是 args。
              @Override
              public @Nullable Object invoke(Object proxy, Method method, @Nullable Object[] args)
                  throws Throwable {
                // If the method is a method from Object then defer to normal invocation.
                // 如果是 Object 中的方法,直接通过反射调用 Object 中对应的方法。
                if (method.getDeclaringClass() == Object.class) {
                  return method.invoke(this, args);
                }
                args = args != null ? args : emptyArgs;
                // 判断是否是接口中的 default 方法,如果是通过 platform 对象调用,如果不是通过 loadServiceMethod() 方法获取一个对象,然后调用 invoke() 方法。
                return platform.isDefaultMethod(method)
                    ? platform.invokeDefaultMethod(method, service, proxy, args)
                    : loadServiceMethod(method).invoke(args);
              }
            });
  }

上面代码做了两件事,通过 validateServiceInterface() 方法检查 Class 对象是否合法,然后通过动态代理构建一个代理对象(如果对动态代理没有一点了解的同学,建议先去查查相关资料)。

代理对象的每一个方法调用都会触发 InvocationHandler#invoke() 的方法,proxy 是代理的对象,method 是对应的方法的 Method(常写反射的同学可能很熟悉),args 就是对应方法的参数。invoke() 方法的处理主要做了以下事情:

  1. 判断是否是 Object 中的方法,如果是直接通过反射的方式调用对应的方法。
  2. 判断是否是接口中的 default 方法,如果是直接通过 Platform#invokeDefaultMethod() 方法去调用。
  3. 如果第一点和第二点的条件都不满足,通过 loadServiceMethod() 方法加载一个对象,然后调用它的 invoke() 方法来执行调用。(这也是我们分析的主要逻辑)

validateServiceInterface()

我们简单介绍一下是如何校验 Class 对象的:

Java 复制代码
  private void validateServiceInterface(Class<?> service) {
    // 判断是否是接口
    if (!service.isInterface()) {
      throw new IllegalArgumentException("API declarations must be interfaces.");
    }

    Deque<Class<?>> check = new ArrayDeque<>(1);
    check.add(service);
    // 接口不能够有泛型的参数
    while (!check.isEmpty()) {
      Class<?> candidate = check.removeFirst();
      if (candidate.getTypeParameters().length != 0) {
        StringBuilder message =
            new StringBuilder("Type parameters are unsupported on ").append(candidate.getName());
        if (candidate != service) {
          message.append(" which is an interface of ").append(service.getName());
        }
        throw new IllegalArgumentException(message.toString());
      }
      Collections.addAll(check, candidate.getInterfaces());
    }
    
    // 是否提前加载所有的方法
    if (validateEagerly) {
      Platform platform = Platform.get();
      for (Method method : service.getDeclaredMethods()) {
        if (!platform.isDefaultMethod(method) && !Modifier.isStatic(method.getModifiers())) {
          loadServiceMethod(method);
        }
      }
    }
  }

上面主要验证两点:

  1. Class 对象必须是接口。
  2. Class 对象和其对应的父类接口都不能有泛型参数。

后续还通过 validateEagerly 参数来判断是否要提前加载对应的请求 Method,默认是不提前加载,只有在方法调用时才会去判断加载。

loadServiceMethod()

加载方法的实现是通过 loadServiceMethod(),看看代码实现:

Java 复制代码
  ServiceMethod<?> loadServiceMethod(Method method) {
    // 从缓存中去获取 ServiceMethod
    ServiceMethod<?> result = serviceMethodCache.get(method);
    if (result != null) return result;
    
    // 缓存中获取失败,创建一个新的
    synchronized (serviceMethodCache) {
      result = serviceMethodCache.get(method);
      if (result == null) {
        // 创建一个新的
        result = ServiceMethod.parseAnnotations(this, method);
        
        // 添加到缓存中
        serviceMethodCache.put(method, result);
      }
    }
    return result;
  }

请求方法解析完成后使用 ServiceMethod 类封装,首先从本地缓存中去获取 ServiceMethod 对象,如果没有再通过 ServiceMethod#parseAnnotaions() 方法去创建,然后存放在缓存中,下次再使用就不需要再创建。

Java 复制代码
  static <T> ServiceMethod<T> parseAnnotations(Retrofit retrofit, Method method) {
    // 构建 http 请求
    RequestFactory requestFactory = RequestFactory.parseAnnotations(retrofit, method);
    // 校验方法的返回值类型
    Type returnType = method.getGenericReturnType();
    // 返回值中不能有不确定的泛型
    if (Utils.hasUnresolvableType(returnType)) {
      throw methodError(
          method,
          "Method return type must not include a type variable or wildcard: %s",
          returnType);
    }
    // 返回值不能为空
    if (returnType == void.class) {
      throw methodError(method, "Service methods cannot return void.");
    }
    
    // 构建请求任务
    return HttpServiceMethod.parseAnnotations(retrofit, method, requestFactory);
  }

上面的代码主要做了以下事情:

  1. 通过 RequestFactory.parseAnnotations() 方法来解析方法的注解和参数的注解来构建 Http 的请求。
  2. 校验方法返回值的类型,返回类型中不能够有不确定的泛型,也不能够是返回空。
  3. 通过 HttpServiceMethod.parseAnnotations() 方法来构建 Http 的请求任务。

RequestFactory.parseAnnotations()HttpServiceMethod.parseAnnotations() 这两个方法可以说是核心代码中的核心,后续的分析也都从它们两个开始。

方法注解解析

我们接着看前面一节提到的 RequestFactory.parseAnnotations() 方法:

Java 复制代码
static RequestFactory parseAnnotations(Retrofit retrofit, Method method) {  
   return new Builder(retrofit, method).build();  
}

朴实无华的代码,直接创建一个 Builder() 对象,然后调用其 build() 方法,我们看看 它的构造函数和 build() 方法的源码实现:

Java 复制代码
    Builder(Retrofit retrofit, Method method) {
      this.retrofit = retrofit;
      this.method = method;
      // 获取方法的注解
      this.methodAnnotations = method.getAnnotations();
      // 获取参数的类型
      this.parameterTypes = method.getGenericParameterTypes();
      // 获取参数的注解
      this.parameterAnnotationsArray = method.getParameterAnnotations();
    }
    
    RequestFactory build() {
      // 方法注解解析
      for (Annotation annotation : methodAnnotations) {
        parseMethodAnnotation(annotation);
      }
      
      // 如果不知道 Http 请求的类型报错
      if (httpMethod == null) {
        throw methodError(method, "HTTP method annotation is required (e.g., @GET, @POST, etc.).");
      }
      
      // 如果 Multipart 和 FormEncoded 没有 Body 报错。
      if (!hasBody) {
        if (isMultipart) {
          throw methodError(
              method,
              "Multipart can only be specified on HTTP methods with request body (e.g., @POST).");
        }
        if (isFormEncoded) {
          throw methodError(
              method,
              "FormUrlEncoded can only be specified on HTTP methods with "
                  + "request body (e.g., @POST).");
        }
      }
      
      int parameterCount = parameterAnnotationsArray.length;
      parameterHandlers = new ParameterHandler<?>[parameterCount];
      // 解析参数的注解,不同的参数注解处理方式通过 ParameterHandler 类来封装
      for (int p = 0, lastParameter = parameterCount - 1; p < parameterCount; p++) {
        parameterHandlers[p] =
            parseParameter(p, parameterTypes[p], parameterAnnotationsArray[p], p == lastParameter);
      }
      // 如果没有获取到 URL 和 请求的 Path,报错。
      if (relativeUrl == null && !gotUrl) {
        throw methodError(method, "Missing either @%s URL or @Url parameter.", httpMethod);
      }
      // 不允许有 Body 的请求,但是有 Body,报错
      if (!isFormEncoded && !isMultipart && !hasBody && gotBody) {
        throw methodError(method, "Non-body HTTP method cannot contain @Body.");
      }
      // FormEncoded 请求没有获取到 Field, 报错
      if (isFormEncoded && !gotField) {
        throw methodError(method, "Form-encoded method must contain at least one @Field.");
      }
      // Multipart 请求,没有获取到 Part Body,报错
      if (isMultipart && !gotPart) {
        throw methodError(method, "Multipart method must contain at least one @Part.");
      }

      return new RequestFactory(this);
    }

构造函数中首先获取到方法的注解(可以有多个注解,所以是一个数组);获取方法中参数的类型;获取方法参数中的注解(因为有多个参数,一个参数又可以有多个注解,所以是一个二维数组)。

build() 方法中主要做了两件事,通过 parseMethodAnnotation() 来解析方法的注解;通过 parseParameter() 来解析参数和对应的注解,解析成功后对应的处理方式用 ParameterHandler 类来封装。

build() 函数中还判断了各种请求参数是否正确,有以下几点:

  1. 如果不知道 Http 请求的类型(就是 GETPOST 啥的没有明确)报错。
  2. 如果 MultipartFormEncoded 没有 Body 报错。
  3. 如果没有获取到 Url 和 请求的 Path,报错 (也就是 Url 和 请求的 Path 取其一即可) 。
  4. 不允许有 Body 的请求,但是有 Body,报错(比如 GET 请求就不允许有 Body)。
  5. FormEncoded 请求没有获取到 Field, 报错。
  6. Multipart 请求,没有获取到 Part Body,报错。

我们看看 parseMethodAnnotation() 方法的源码:

Java 复制代码
    private void parseMethodAnnotation(Annotation annotation) {
      // 首先通过 parseHttpMethodAndPath() 方法取解析 Http 请求的方法和请求的相对路径
      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);
      } 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) {
        // 解析 Headers
        String[] headersToParse = ((retrofit2.http.Headers) annotation).value();
        if (headersToParse.length == 0) {
          throw methodError(method, "@Headers annotation is empty.");
        }
        headers = parseHeaders(headersToParse);
      } else if (annotation instanceof Multipart) {
        // Multipart
        // 和 FormUrlEncoded 冲突
        if (isFormEncoded) {
          throw methodError(method, "Only one encoding annotation is allowed.");
        }
        isMultipart = true;
      } else if (annotation instanceof FormUrlEncoded) {
        // FormUrlEncoded
        // 和 Multipart 冲突
        if (isMultipart) {
          throw methodError(method, "Only one encoding annotation is allowed.");
        }
        isFormEncoded = true;
      }
    }

如果是请求方法类的注解通过 parseHttpMethodAndPath() 方法去解析它的请求方法和解析请求的相对路径,请求方法类的注解包括 @DELETE@GET@HEAD@PATCH@POST@PUT@OPTIONS、 和 @HTTP 等等。
@Headers 注解表示请求的 Header,通过 parseHeaders() 方法完成解析。
@Multipart 标记请求 BodyMultipart,它和 FormUrlEncoded 冲突。
@FormUrlEncoded 标记请求 BodyForm 表单,它和 Multipart 冲突。

我们看看 parseHttpMethodAndPath() 方法的实现:

Java 复制代码
    private void parseHttpMethodAndPath(String httpMethod, String value, boolean hasBody) {
      // 多次设置 HttpMethod 报错
      if (this.httpMethod != null) {
        throw methodError(
            method,
            "Only one HTTP method is allowed. Found: %s and %s.",
            this.httpMethod,
            httpMethod);
      }
      this.httpMethod = httpMethod;
      this.hasBody = hasBody;
      
      // 相对路径可以为空,为空直接返回
      if (value.isEmpty()) {
        return;
      }

      // Get the relative URL path and existing query string, if present.
      // 如果在请求测 Query 参数后添加参数化的 Path 报错。
      int question = value.indexOf('?');
      if (question != -1 && question < value.length() - 1) {
        // Ensure the query string does not have any named parameters.
        String queryParams = value.substring(question + 1);
        Matcher queryParamMatcher = PARAM_URL_REGEX.matcher(queryParams);
        if (queryParamMatcher.find()) {
          throw methodError(
              method,
              "URL query string \"%s\" must not have replace block. "
                  + "For dynamic query parameters use @Query.",
              queryParams);
        }
      }
      // 记录相对路径 Path
      this.relativeUrl = value;
      // 获取参数化的 Path 的名字。
      this.relativeUrlParamNames = parsePathParameters(value);
    }

这里主要做了以下的事情:

  1. 如果已经设置过了 HttpMethod,报错。
  2. 如果 Query 参数后有参数化的 Path 设置,报错。
  3. 记录相对路径 Path 和解析 Path 参数化的名字。

可能你有点忘了什么是参数化的 Path,我这里简单介绍一下,parseHttpMethodAndPath() 中只能处理相对路径 Path,比如 /a/b/c 就是一个相对路径,参数化就是比如我定义的 Path 是这样的 /a/{name}/c,其中的 {name} 就是一个占位符,可以通过参数中的注解 @Path 来替换它(后续的文章中会看到它),比如我的参数 nametans,那么请求是的 Path 就是 /a/tans/c

上面的 Path 是可以为空的,如果为空就需要参数的注解中有 @Url(后续的文章中会看到它),它是来表示一个绝对路径的,比如 https://www.tans.com/a/b/c。相对路径和绝对路径只能有一个存在。

我们再看看 parseHeaders() 方法的实现:

Java 复制代码
    private Headers parseHeaders(String[] headers) {
      Headers.Builder builder = new Headers.Builder();
      for (String header : headers) {
        int colon = header.indexOf(':');
        if (colon == -1 || colon == 0 || colon == header.length() - 1) {
          throw methodError(
              method, "@Headers value must be in the form \"Name: Value\". Found: \"%s\"", header);
        }
        // 获取 Header 的名字和对应的 Value
        String headerName = header.substring(0, colon);
        String headerValue = header.substring(colon + 1).trim();
        // 如果是 Content-Type 记录一下
        if ("Content-Type".equalsIgnoreCase(headerName)) {
          try {
            contentType = MediaType.get(headerValue);
          } catch (IllegalArgumentException e) {
            throw methodError(method, e, "Malformed content type: %s", headerValue);
          }
        } else {
          // 添加到 Header.Builder 中。 
          builder.add(headerName, headerValue);
        }
      }
      return builder.build();
    }

Http 协议的 HeaderNameValue 都是以 : 分割开,上面的解析代码也是非常的简单,就不多说了。

最后

本篇文章中介绍了 Retrofit 的动态代理实现、方法注解解析等等内容,后续的文章还会继续介绍方法参数和参数注解的解析,请求任务的解析等等逻辑。

相关推荐
m0_548514772 小时前
2024.12.10——攻防世界Web_php_include
android·前端·php
凤邪摩羯2 小时前
Android-性能优化-03-启动优化-启动耗时
android
凤邪摩羯2 小时前
Android-性能优化-02-内存优化-LeakCanary原理解析
android
喀什酱豆腐3 小时前
Handle
android
m0_748232924 小时前
Android Https和WebView
android·网络协议·https
m0_748251725 小时前
Android webview 打开本地H5项目(Cocos游戏以及Unity游戏)
android·游戏·unity
m0_748254666 小时前
go官方日志库带色彩格式化
android·开发语言·golang
zhangphil6 小时前
Android使用PorterDuffXfermode模式PorterDuff.Mode.SRC_OUT橡皮擦实现“刮刮乐”效果,Kotlin(2)
android·kotlin
爱学测试的李木子7 小时前
从0到1搭建 Android 自动化 python+appium 环境
android·软件测试·python·测试工具·自动化
咸芝麻鱼7 小时前
Android Studio | 连接手机设备后,启动App时出现:Waiting For DebuggerApplication (App名)...
android·adb·智能手机·android studio