【一起读源码】Retrofit源码探秘

原载于个人博客Retrofit源码探秘, 转载请注明出处, 谢谢.
本文讲Retrofit. 总的来说它是一个面向业务流程的网络请求库, 内部默认使用面向网络协议的库OkHttp来打出成吨伤害.

截至我写文章为止, Retrofit更新到了2.2.0版本. 据说1.x2.0+差异巨大, 这里我们就直接拥抱变化来看最新代码了.

Retrofit的使用

添加依赖和添加网络权限我就不说了. 假设我们要访问网络获取一个Json格式的响应, 来看代码怎么写:

Retrofit retrofit = new Retrofit.Builder()
                    .baseUrl("http://url.of.yourserver/")
                    .addConverterFactory(GsonConverterFactory.create())
                    .build(); // 创建全局管理
HttpService httpService = retrofit.create(YourHttpService.class); // 创建访问接口实例
Call<Json> call = httpService.getJson(); // 为要访问的接口创建Call
call.enqueue(new YourCallbackForThisCall(){...}); // Call入队列并添加回调

非常简单对不对, 而且YourHttpService.class甚至可能根据某种规范直接从服务端导出, 需要自己手写的代码几乎可以忽略不计, 出bug的几率就更小了.
这里的Callback经我验证是在UI线程执行的, 所以可以直接在onResponse里更新UI. 如果想要换到其他线程处理, 需要设置callbackExecutor, 默认是主线程.

Retrofit源码探秘

根据我们前面分析过的OkHttp源码可以知道, Retrofit背后的请求发送和接收都是通过它来完成的, 自己几乎没做什么. Retrofit能实现这么简单的调用方式, 其魔法就在它的抽象架构上. Retrofit通过定义和解析注解的方式生成Request, 又在原本OkHttpCall(Retrofit定义的OkHttp的代理)的回调之中加了一层, 用于处理响应出错情况和把响应结果包装成ExceptionCatchingRequestBody, 最后用ServiceMethod.toResponse使用Converter完成响应解析, 使用解析结果通过CallAdapter构造一个自定义的Response<T>并返回. 再在外层回调我们设置的Callback<T>.
真正的网络请求的事情包括连接和缓存的维护都交给OkHttpClient完成了, 因此关键就在于Retrofit从注解到Call的过程和从ResponseResponse<T>的过程.

Retrofit注解定义和解析

在我之前的某篇文章中曾经解析过JavaAnnotation机制, 这里就不再解释了, 我们已经知道, Java的注解是通过注解处理器Processor进行处理的, Retrofit应该也是实现了类似的机制去处理它自己的注解. 注解都是编译器完成处理的, 其处理后的代码的生命周期根据它的Retention不同而不同, RetentionRUNTIME的注解在编译器处理之后存在于.class文件中并且还被虚拟机载入, 所以可供后续处理. 接下来我们就来看看Retrofit中的注解定义和解析器的定义, 我们将会发现它所有的注解都是RUNTIME的.
注解都在http包下, 从名字也看得出来基本上对应了http协议中的各个实体. 我们只看其中GET Headers Streaming三个.
选这三个的原因是三个在http协议中分别代表三种类型: 请求方法, 结构参数, 传输标记. 如果不记得http协议的同学可以看我另一篇博客Android源码解析-网络架构.
三个注解的完整代码如下:

@Target(METHOD)
@Retention(RUNTIME)
public @interface GET {
  /**
   * A relative or absolute path, or full URL of the endpoint. This value is optional if the first
   * parameter of the method is annotated with {@link Url @Url}.
   * <p>
   * See {@linkplain retrofit2.Retrofit.Builder#baseUrl(HttpUrl) base URL} for details of how
   * this is resolved against a base URL to create the full endpoint URL.
   */
  String value() default "";
}

/**
 * Adds headers literally supplied in the {@code value}.
 * <pre><code>
 * @Headers("Cache-Control: max-age=640000")
 * @GET("/")
 * ...
 *
 * @Headers({
 *   "X-Foo: Bar",
 *   "X-Ping: Pong"
 * })
 * @GET("/")
 * ...
 * </code></pre>
 * <strong>Note:</strong> Headers do not overwrite each other. All headers with the same name will
 * be included in the request.
 *
 * @see Header
 * @see HeaderMap
 */
@Documented
@Target(METHOD)
@Retention(RUNTIME)
public @interface Headers {
  String[] value();
}

/**
 * Treat the response body on methods returning {@link okhttp3.Response Response} as is,
 * i.e. without converting {@link okhttp3.Response#body() body()} to {@code byte[]}.
 */
@Documented
@Target(METHOD)
@Retention(RUNTIME)
public @interface Streaming {
}

为避免各位说我凑篇幅, 请各位仔细看上文代码的注释部分和注解部分, 我们可以看到这几个注解的设计思路, 基本上是按照http协议的基本部分来分解的, 并且都有明确的作用对象和作用范围.
对这三种类型的注解的处理自然也是不太一样, 接下来我们就看看Retrofit为它的这些注解编写了怎样的处理器. 注解的转存在ServiceMethod.Builder()中, 处理则在ServiceMethod.Builder.build()中, 如下:

    public ServiceMethod build() {
      ...
      // Method级别的Annotation, 主要是GET/POST等方法
      for (Annotation annotation : methodAnnotations) {
        parseMethodAnnotation(annotation);
      }
      ...
      // Parameter级别的Annotation, 主要是Path等
      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);
      }
     ...
    }
    
    // 处理Method级别Annotation
    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) {
        String[] headersToParse = ((retrofit2.http.Headers) annotation).value();
        if (headersToParse.length == 0) {
          throw methodError("@Headers annotation is empty.");
        }
        headers = parseHeaders(headersToParse);
      } else if (annotation instanceof Multipart) {
        if (isFormEncoded) {
          throw methodError("Only one encoding annotation is allowed.");
        }
        isMultipart = true;
      } else if (annotation instanceof FormUrlEncoded) {
        if (isMultipart) {
          throw methodError("Only one encoding annotation is allowed.");
        }
        isFormEncoded = true;
      }
    }
    
    // 处理Parameter级别Annotation
    private ParameterHandler<?> parseParameter(
        int p, Type parameterType, Annotation[] annotations) {
      ParameterHandler<?> result = null;
      for (Annotation annotation : annotations) {
        ParameterHandler<?> annotationAction = parseParameterAnnotation(
            p, parameterType, annotations, annotation);

        ...// handleException

        result = annotationAction;
      }
      ...// handleException

      return result;
    }
    
    // 具体的处理Annotation的实现, 方法颇长就不贴详情了
    private ParameterHandler<?> parseParameterAnnotation(
        int p, Type type, Annotation[] annotations, Annotation annotation) {
      if (annotation instanceof Url) { // handle @Url
        ...// throw Exception if necessary

        gotUrl = true;

        if (type == HttpUrl.class
            || type == String.class
            || type == URI.class
            || (type instanceof Class && "android.net.Uri".equals(((Class<?>) type).getName()))) {
          return new ParameterHandler.RelativeUrl();
        } else {
          ...// throw Exception
        }

      } else if (annotation instanceof Path) { // handle @Path
        ...// throw Exception if necessary
        
        gotPath = true;

        Path path = (Path) annotation;
        String name = path.value();
        validatePathName(p, name);

        Converter<?, String> converter = retrofit.stringConverter(type, annotations); // 见Retrofit.stringConverter()
        return new ParameterHandler.Path<>(name, converter, path.encoded()); // 见ParameterHandler.Path<>

      } else if (annotation instanceof Query) { // handle @Query
      } else if (annotation instanceof QueryName) { // handle @QueryName
      } else if (annotation instanceof Header) { // handle @Header
      } else if (annotation instanceof FieldMap) { // handle @FieldMap
      } else if (annotation instanceof Part) { // handle @Part
      } else if (annotation instanceof PartMap) { // handle @PartMap
      } else if (annotation instanceof Body) { // handle @Body
      }
      return null; // Not a Retrofit annotation.
    }

可见Retrofit对注解的处理分为两个部分: 一个是Method级别的Annotation, 所有http协议方法都被定义为该级别的Annotation, 在ServiceMethod.parseHttpMethodAndPath中处理(注意到, 基本方法DELETE GET HEAD POST等应该是互斥的, 但代码中没有进行判断, 而MultipardFormUrlEncoded之间却进行了判断, 至于代码中没有对Streaming的处理, 是因为逻辑上它不在这一层, 而是在响应结果的解析, 所以其实是在retrofit2/BuiltInConverters.java中进行了处理); 另一个是Parameter级别的, 大多数是在进行基本预处理之后就委托给ParameterHandler接口的实现类进行处理.

通过对某个方法的注解的处理, 在ServiceMethod.Builder.build()完成后ServiceMethod就获取到了调用该方法发起网络请求所需要的信息, 可以开始构造Call了.

Retrofit的Call与Response

RetrofitCallResponse是对OkHttpCallResponse的封装, 为什么要进行这么一层封装呢? 接下来我们就看看它的源码.
我们选择从核心方法Retrofit.create(Class)开始.

public <T> T create(final Class<T> service) {
  Utils.validateServiceInterface(service); // #1
  if (validateEagerly) {
    eagerlyValidateMethods(service); // #2
  }
  
  // #3 below
  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<Object, Object> serviceMethod =
              (ServiceMethod<Object, Object>) loadServiceMethod(method); // #4
          OkHttpCall<Object> okHttpCall = new OkHttpCall<>(serviceMethod, args); // #5
          return serviceMethod.callAdapter.adapt(okHttpCall); // #6
        }
      });
}

这里面一共有三个步骤, 其中前两步是用来检查接口和提前载入方法的, 第三步是关键, 生成并返回了一个代理. 实例化代理时传入了三个参数, 第三个是InvocationHandler的实现, 我们可以看到里面最关键的步骤是#4 #5 #6, 分别完成方法载入(Method->ServiceMethod), Call构造和返回Call代理. 这里ProxyInvocationHandler都是Java中就已经存在并引入了Android中的机制, 即动态代理机制. 关于动态代理机制, 思想成熟战法犀利, 是相当有意思的一个东西, 可以参见这篇文章
总之要使用动态代理呢, 委托类必须要实现某个接口, 而Proxy创建的代理类通过实现InvocationHandler来调用委托类的方法, 完成代理. 所有这一切都跟反射密不可分, 所以理所当然Proxy是在反射包下的.
从上面代码可以看出, Platform.isDefaultMethod()Object的方法将被InvocationHandler直接代理执行, 而其他方法则被包装成OkHttpCall交给ServiceMethodCallAdapter去执行. 这里也可以看出对CallResponse进行封装代理的作用: 屏蔽底层实现, 统一处理非核心逻辑, 统一接口, 令底层替换成其他网络访问库成为可能.

下面我们来看ServiceMethod的构造过程, OkHttpCall的构造过程以及默认的CallAdapter的代码.
还记得在create里传入loadServiceMethod的是Method method, 它是代理方法, 通过它去访问委托(被代理)方法, 代码如下:

ServiceMethod<?, ?> loadServiceMethod(Method method) {
  ServiceMethod<?, ?> result = serviceMethodCache.get(method);
  if (result != null) return result;

  synchronized (serviceMethodCache) {
    result = serviceMethodCache.get(method);
    if (result == null) {
      result = new ServiceMethod.Builder<>(this, method).build(); // 传入Retrofit, Method实例, 构造代理包装类ServiceMethod实例
      serviceMethodCache.put(method, result); // 缓存代理方法-代理包装实例映射
    }
  }
  return result;
}

然后根据传入的方法及其参数, 构造一个合适的Call, 这个CallOkHttpCall的代理, 所以Retrofit中它的类是OkHttpCall. 下面我们看它是怎么构造和代理的, 代码如下:

final class OkHttpCall<T> implements Call<T> {
    OkHttpCall(ServiceMethod<T, ?> serviceMethod, Object[] args) {
      this.serviceMethod = serviceMethod;
      this.args = args;
    }
  
    @Override public void enqueue(final Callback<T> callback) {
      // 检查参数

      okhttp3.Call call;
      Throwable failure;

      synchronized (this) {
        if (executed) throw new IllegalStateException("Already executed.");
        executed = true;

        call = rawCall;
        failure = creationFailure;
        if (call == null && failure == null) {
          try {
            call = rawCall = createRawCall();
          } catch (Throwable t) {
            failure = creationFailure = t;
          }
        }
      }

      if (failure != null) {
        callback.onFailure(this, failure);
        return;
      }

      if (canceled) {
        call.cancel();
      }

      call.enqueue(new okhttp3.Callback() { // okhttp3.Callback反向代理retrofit2.Callback
        @Override public void onResponse(okhttp3.Call call, okhttp3.Response rawResponse)
            throws IOException {
          Response<T> response;
          try {
            response = parseResponse(rawResponse); // 解析响应
          } catch (Throwable e) {
            callFailure(e); // 回调自身callFailure
            return;
          }
          callSuccess(response); // 回调自身callSuccess
        }

        @Override public void onFailure(okhttp3.Call call, IOException e) {
          try {
            callback.onFailure(OkHttpCall.this, e);
          } catch (Throwable t) {
            t.printStackTrace();
          }
        }

        private void callFailure(Throwable e) {
          try {
            callback.onFailure(OkHttpCall.this, e);
          } catch (Throwable t) {
            t.printStackTrace();
          }
        }

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

结构清晰, 基本可以当做OkHttpCall来对待.

接下来通过ServiceMethod.callAdapter.adapt(OkHttpCall)OkHttpCall转换为Call<T>. ServiceMethod的建造模式如下:

    Builder(Retrofit retrofit, Method method) {
      this.retrofit = retrofit;
      this.method = method;
      this.methodAnnotations = method.getAnnotations(); # 获取代理方法的方法注解
      this.parameterTypes = method.getGenericParameterTypes(); # 获取泛型参数类型
      this.parameterAnnotationsArray = method.getParameterAnnotations(); # 获取参数注解
    }

    public ServiceMethod build() {
      callAdapter = createCallAdapter(); # 根据泛型返回类型和方法注解, 通过retrofit.callAdapter(returnType, annotations)寻找适配器实例. Retrofit实例化时默认添加`DefaultCallAdapterFactory`工厂
      responseType = callAdapter.responseType(); # 获取返回类型
      if (responseType == Response.class || responseType == okhttp3.Response.class) {
        throw methodError("'"
            + Utils.getRawType(responseType).getName()
            + "' is not a valid response body type. Did you mean ResponseBody?"); # 不允许以`Response`作为返回类型
      }
      responseConverter = createResponseConverter();

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

      if (httpMethod == null) {
        throw methodError("HTTP method annotation is required (e.g., @GET, @POST, etc.).");
      }

      ...// 判断方法注解合法性

      int parameterCount = parameterAnnotationsArray.length;
      parameterHandlers = new ParameterHandler<?>[parameterCount];
      for (int p = 0; p < parameterCount; p++) {
        Type parameterType = parameterTypes[p];
        // 解析参数注解
        Annotation[] parameterAnnotations = parameterAnnotationsArray[p];
        ...
        parameterHandlers[p] = parseParameter(p, parameterType, parameterAnnotations);
      }

      ...// 判断参数合法性

      return new ServiceMethod<>(this);
    }
    
    ServiceMethod(Builder<R, T> builder) {
      this.callFactory = builder.retrofit.callFactory();
      this.callAdapter = builder.callAdapter;
      this.baseUrl = builder.retrofit.baseUrl();
      this.responseConverter = builder.responseConverter;
      this.httpMethod = builder.httpMethod;
      this.relativeUrl = builder.relativeUrl;
      this.headers = builder.headers;
      this.contentType = builder.contentType;
      this.hasBody = builder.hasBody;
      this.isFormEncoded = builder.isFormEncoded;
      this.isMultipart = builder.isMultipart;
      this.parameterHandlers = builder.parameterHandlers;
    }

始终别忘了以上内容都是通过Java自带的Proxy机制调用InvocationHandler来代理的, 所以create传入的是什么类, 返回的就是什么类的代理, 访问这个代理的方法时, 实际调用的是InvocationHandler. 通过ServiceMethod.callAdapter.adapt()可以将传入的方法适配为你自己定义的返回类型Call<T>的实例. 我们来看看默认情况下添加的DefaultCallAdapterFactory会生成什么CallAdapter:

public CallAdapter<?, ?> get(Type returnType, Annotation[] annotations, Retrofit retrofit) { // 工厂方法, 传入返回类型, 注解数组, retrofit实例
  if (getRawType(returnType) != Call.class) { // 返回类型如果不是Call则直接返回null
    return null;
  }

  final Type responseType = Utils.getCallResponseType(returnType); // 获取ReturnType<T>中泛型参数T的上限类型
  return new CallAdapter<Object, Call<?>>() {
    @Override public Type responseType() {
      return responseType;
    }

    @Override public Call<Object> adapt(Call<Object> call) {
      return call; // 默认返回传入参数
    }
  };
}

显然默认情况下这个适配器什么也不做, 直接把传入的OkHttpCall返回给你.

Retrofit的Converter与CallAdapter: 开始使用RxJava

上文已经暗示了, 对结果的解析器Converter可以替换, 其实发送请求的代理Call也可以被替换, 只要换掉CallAdapter就可以了, 网上都说RxJava+Retrofit可以打出成吨输出, 原因就在于CallAdapter可以换成一个返回Observable类型的适配器, 这使得在Retrofit.create()之后可以直接以RxJava的风格做链式调用.
实现起来也非常简单, 只要.addConverterFactory() .addCallAdapterFactory()即可, 因为Retrofit对外提供了抽象类Converter.Factory CallAdapter.Factory, 并且实例全部使用泛型来表示, 通过对外提供统一接口, 只要外部可以继承抽象工厂类并返回实现了统一接口的实例类, 就可以在不改变Retrofit一行代码的情况下接入其他库, 或者原有逻辑代码变动不大的情况下接入Retrofit.

是时候上一波图了:

Retrofit, RxJava 和 OkHttp之间的关系

上面这张图是我基于Stay的这篇文章改的, 荣耀归他, 另外由于图滞后于Retrofit版本, 所以如果有不一致的地方锅也归他(目前没看见不一致).

参考

这篇文章分析了Retrofit的实现, 其优缺点即使不是一目了然, 也是足够清晰了, 希望能够帮助到正在选择网络请求框架的各位.
本文参考如下:

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

推荐阅读更多精彩内容