Retrofit2.0源码解析

欢迎访问我的个人博客 ,原文链接:http://wensibo.net/2017/09/05/retrofit/ ,未经允许不得转载!

今天是九月的第四天了,学校也正式开学,趁着大学最后一年的这大好时光,抓紧时间赶快学习新知识吧!今天想要与大家一起分享的是Retrofit,由于网上已经有许多讲解Retrofit使用的文章了,本篇文章只会给一个小小的示例,以这个示例作为入口分析其源码,同样也会贴上流程图,以免迷路。话不多说,我们开始吧!!!

关于Retrofit

简介

Retrofit是近来十分火热的一个网络请求开源库,Android开发者使用的网络请求开源库从最早的HttpClient与HttpURLConnection到2013年Google官方推出的Volley,接着就到了现在很火的OKHttp,最后才到了Retrofit。网络请求开源库的演变也正是移动互联网下用户对网络需求的真实写照。有哪个用户不想使用APP的时候网络加载速度更快,更省流量,更加安全呢?也就是基于用户的这些需求,才有了许多开源库的不断迭代,而Retrofit可以说正是当下最适合开发者使用的网络请求开源库之一。
何出此言呢?首先它是由大名鼎鼎的square公司出品的,或许你不知道square公司,但你应该认识Jake Wharton,不过他最近已经到谷歌去了,倘若你连他都不知道,那你应该使用过他开发的这些开源库:OkHttp,picasso,butterknife,RxAndroid等等,可以说Retrofit是由一个十分厉害的公司开发和维护的,所以你大可以放心地在你的项目中使用。

什么场景下适合使用呢?

尽管Retrofit十分强大,但是他却不一定适合所有场景,正所谓术业有专攻,我们也不必大材小用,如果是一些频繁但是访问量很小的网络请求,那么Volley就足以对付了,接下来我列举一下Retrofit普遍的使用场景。

  • 服务器后台遵循RESTful API的设计风格。如果你对这种风格不熟悉,建议你看看阮一峰大神的这篇文章,或者向你的后台小伙伴请教一番。
  • 项目中使用了RxJava。如果你的项目中使用了RxJava,那么使用Retrofit绝对会让你的开发效率翻倍。
  • 项目中的网络数据请求量比较大。如果你的应用经常会有数据量比较大的网络请求,那么使用Retrofit也会很有效,因为Retrofit底层的实现是使用OKHttp,而OKHttp就是适用于这种场景的。

如果你符合以上三种情况,当然是选择Retrofit啦!
Retrofit绿

一个简单的栗子

说了这么多,我们就通过下面这个栗子来看看他究竟好在哪里?
需要说明的是:这个例子是用来获取干货集中营API上面的数据
1、首先定义一个常量用来描述要访问的服务器主机的地址

public class GankConfig {
    public static final String HOST = "http://gank.io/api/";
}

2、定义返回数据的bean类

public class GankData {
    public List<String> category;
    public Result results;

    public class Result{
            @SerializedName("Android")
            public List<Gank> androidList;
            @SerializedName("休息视频")
            public List<Gank> restVideoList;
            @SerializedName("iOS")
            public List<Gank> iosList;
            @SerializedName("福利")
            public List<Gank> meiZiList;
            @SerializedName("拓展资源")
            public List<Gank> extendResourceList;
            @SerializedName("瞎推荐")
            public List<Gank> suggestionList;
            @SerializedName("App")
            public List<Gank> appList;
            @SerializedName("前端")
            public List<Gank> webList;
    }
}

3、定义要访问的接口

public interface GankRetrofit {

    //这里以获取指定日期的内容为例子
    @GET("day/{year}/{month}/{day}")
    GankData getDailyData(@Path("year") int year, @Path("month") int month, @Path("day") int day);

}

4、用单例模式创建一个Retrofit客户端

public class GankRetrofitClient {
        private volatile static GankRetrofit gankRetrofit;
        private static Retrofit retrofit;

        private GankRetrofitClient(){}

        static{
                Gson date_gson = new GsonBuilder().setDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'").create();
                retrofit = new Retrofit.Builder()
                        .baseUrl(GankConfig.HOST)
                        .addConverterFactory(GsonConverterFactory.create(date_gson))//添加一个转换器,将gson数据转换为bean类
                        .addCallAdapterFactory(RxJava2CallAdapterFactory.create())//添加一个适配器,与RxJava配合使用
                        .build();
        }

        public static GankRetrofit getGankRetrofitInstance() {
                if (gankRetrofit==null){
                        synchronized (GankRetrofitClient.class){
                                if (gankRetrofit==null){
                                        gankRetrofit=retrofit.create(GankRetrofit.class);
                                }
                        }
                }
                return gankRetrofit;
        }
}

5、使用Retrofit进行网络请求

GankData data= GankRetrofitClient.getGankRetrofitInstance().getDailyData(2017, 9, 1);

源码解析

从Builder模式创建实例开始看起

首先我们先从上面的第4步开始解析源码,有下面这段代码:

retrofit = new Retrofit.Builder()
            .baseUrl(GankConfig.HOST)
            .addConverterFactory(GsonConverterFactory.create(date_gson))//添加一个转换器,将gson数据转换为bean类
            .addCallAdapterFactory(RxJava2CallAdapterFactory.create())//添加一个适配器,与RxJava配合使用
            .build();

很明显这个是使用了Builder模式,接下来我们一步一步来看里面做了什么?首先是Builder()。

public Builder() {
      this(Platform.get());
}

Builder(Platform platform) {
      this.platform = platform;
      //添加转换器,请见下面关于addConverterFactory()的讲解
      converterFactories.add(new BuiltInConverters());
    }

构造方法中的参数是Platform的静态方法get(),接下来就看看get()。

private static final Platform PLATFORM = findPlatform();

  static Platform get() {
    return PLATFORM;
  }

  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) {
    }
    return new Platform();
  }
}

可以看到,Retrofit支持多平台,包括Android与JAVA8,它会根据不同的平台设置不同的线程池。
先来看看到目前为止我们分析到哪里了

接下来看一下baseUrl()方法。

public Builder baseUrl(String baseUrl) {
      checkNotNull(baseUrl, "baseUrl == null");
      HttpUrl httpUrl = HttpUrl.parse(baseUrl);
      if (httpUrl == null) {
        throw new IllegalArgumentException("Illegal URL: " + baseUrl);
      }
      return baseUrl(httpUrl);
    }

很容易理解,baseUrl()是配置服务器的地址的,如果为空,那么就会抛出异常。

接着是addConverterFactory()

private final List<Converter.Factory> converterFactories = new ArrayList<>(); 

public Builder addConverterFactory(Converter.Factory factory) {
      converterFactories.add(checkNotNull(factory, "factory == null"));
      return this;
}

大家是不是还记得刚才在Builder()方法初始化的时候,有这样一行代码:

converterFactories.add(new BuiltInConverters());

可以看到,converterFactories在初始化的时候就已经添加了一个默认的Converter,那我们手动添加的这个GsonConverter是干什么用的呢?

public final class GsonConverterFactory extends Converter.Factory {
 
  public static GsonConverterFactory create() {
    return create(new Gson());
  }

  
  public static GsonConverterFactory create(Gson gson) {
    return new GsonConverterFactory(gson);
  }

  private final Gson gson;

  private GsonConverterFactory(Gson gson) {
    if (gson == null) throw new NullPointerException("gson == null");
    this.gson = gson;
  }

  @Override
  public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations,
      Retrofit retrofit) {
    TypeAdapter<?> adapter = gson.getAdapter(TypeToken.get(type));
    return new GsonResponseBodyConverter<>(gson, adapter);
  }

  @Override
  public Converter<?, RequestBody> requestBodyConverter(Type type,
      Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit) {
    TypeAdapter<?> adapter = gson.getAdapter(TypeToken.get(type));
    return new GsonRequestBodyConverter<>(gson, adapter);
  }
}

其实这个Converter主要的作用就是将HTTP返回的数据解析成Java对象,我们常见的网络传输数据有Xml、Gson、protobuf等等,而GsonConverter就是将Gson数据转换为我们的Java对象,而不用我们重新去解析这些Gson数据。

接着看addCallAdapterFactory()

private final List<CallAdapter.Factory> adapterFactories = new ArrayList<>();

public Builder addCallAdapterFactory(CallAdapter.Factory factory) {
      adapterFactories.add(checkNotNull(factory, "factory == null"));
      return this;
}

可以看到,CallAdapter同样也被一个List维护,也就是说用户可以添加多个CallAdapter,那Retrofit总得有一个默认的吧,默认的是什么呢?请看接下来的build()。

最后看一下build()

public Retrofit build() {
  //检验baseUrl
  if (baseUrl == null) {
    throw new IllegalStateException("Base URL required.");
  }

  //创建一个call,默认情况下使用okhttp作为网络请求器
  okhttp3.Call.Factory callFactory = this.callFactory;
  if (callFactory == null) {
    callFactory = new OkHttpClient();
  }

  Executor callbackExecutor = this.callbackExecutor;
  if (callbackExecutor == null) {
    callbackExecutor = platform.defaultCallbackExecutor();
  }

  List<CallAdapter.Factory> adapterFactories = new ArrayList<>(this.adapterFactories);
  //添加一个默认的callAdapter
  adapterFactories.add(platform.defaultCallAdapterFactory(callbackExecutor));

  List<Converter.Factory> converterFactories = new ArrayList<>(this.converterFactories);

  return new Retrofit(callFactory, baseUrl, converterFactories, adapterFactories,
      callbackExecutor, validateEagerly);
    }

首先Retrofit会新建一个call,其实质就是OKHttp,作用就是网络请求器;接着在上一点中我们困惑的callAdapter也已经能够得到解决了,首先Retrofit有一个默认的callAdapter,请看下面这段代码:

adapterFactories.add(platform.defaultCallAdapterFactory(callbackExecutor));

CallAdapter.Factory defaultCallAdapterFactory(Executor callbackExecutor) {
    if (callbackExecutor != null) {
      return new ExecutorCallAdapterFactory(callbackExecutor);
    }
    return DefaultCallAdapterFactory.INSTANCE;
  }

final class ExecutorCallAdapterFactory extends CallAdapter.Factory {
  final Executor callbackExecutor;

  ExecutorCallAdapterFactory(Executor callbackExecutor) {
    this.callbackExecutor = callbackExecutor;
  }

  @Override
  public CallAdapter<?, ?> get(Type returnType, Annotation[] annotations, Retrofit retrofit) {
    if (getRawType(returnType) != Call.class) {
      return null;
    }
    final Type responseType = Utils.getCallResponseType(returnType);
    return new CallAdapter<Object, Call<?>>() {
      @Override public Type responseType() {
        return responseType;
      }

      @Override public Call<Object> adapt(Call<Object> call) {
        return new ExecutorCallbackCall<>(callbackExecutor, call);
      }
    };
  }

  static final class ExecutorCallbackCall<T> implements Call<T> {
    final Executor callbackExecutor;
    final Call<T> delegate;

    ExecutorCallbackCall(Executor callbackExecutor, Call<T> delegate) {
      this.callbackExecutor = callbackExecutor;
      this.delegate = delegate;
    }

    @Override public void enqueue(final Callback<T> callback) {
      if (callback == null) throw new NullPointerException("callback == null");

      delegate.enqueue(new Callback<T>() {
        @Override public void onResponse(Call<T> call, final Response<T> response) {
          callbackExecutor.execute(new Runnable() {
            @Override public void run() {
              if (delegate.isCanceled()) {
                // Emulate OkHttp's behavior of throwing/delivering an IOException on cancellation.
                callback.onFailure(ExecutorCallbackCall.this, new IOException("Canceled"));
              } else {
                callback.onResponse(ExecutorCallbackCall.this, response);
              }
            }
          });
        }

        @Override public void onFailure(Call<T> call, final Throwable t) {
          callbackExecutor.execute(new Runnable() {
            @Override public void run() {
              callback.onFailure(ExecutorCallbackCall.this, t);
            }
          });
        }
      });
    }

    @Override public boolean isExecuted() {
      return delegate.isExecuted();
    }

    @Override public Response<T> execute() throws IOException {
      return delegate.execute();
    }

    @Override public void cancel() {
      delegate.cancel();
    }

    @Override public boolean isCanceled() {
      return delegate.isCanceled();
    }

    @SuppressWarnings("CloneDoesntCallSuperClone") // Performing deep clone.
    @Override public Call<T> clone() {
      return new ExecutorCallbackCall<>(callbackExecutor, delegate.clone());
    }

    @Override public Request request() {
      return delegate.request();
    }
  }
}

可以看到默认的callAdapter是ExecutorCallAdapterFactory。callAdapter其实也是运用了适配器模式,其实质就是网络请求器Call的适配器,而在Retrofit中Call就是指OKHttp,那么CallAdapter就是用来将OKHttp适配给不同的平台的,在Retrofit中提供了四种CallAdapter,分别如下:

  • ExecutorCallAdapterFactory(默认使用)
  • GuavaCallAdapterFactory
  • Java8CallAdapterFactory
  • RxJavaCallAdapterFactory

为什么要提供如此多的适配器呢?首先是易于扩展,例如用户习惯使用什么适配器,只需要添加即可使用;再者RxJava如此火热,因为其切换线程十分的方便,不需要手动使用handler切换线程,而Retrofit使用了支持RxJava的适配器之后,功能也会更加强大。

综上我们已经将使用Builder模式创建出来的Retrofit实例分析完毕了,我们只需要对相关的功能进行配置即可,Retrofit负责接收我们配置的功能然后进行对象的初始化,这个也就是Builder模式屏蔽掉创建对象的复杂过程的好处。现在我们再次用流程图来梳理一下刚才的思路。

网络请求接口的创建

我最初使用Retrofit的时候觉得有一个地方十分神奇,如下:

GankRetrofit gankRetrofit=retrofit.create(GankRetrofit.class);
GankData data= gankRetrofit.getDailyData(2017, 9, 1);

要想解惑,首先得对动态代理有所了解,如果你对动态代理还不是很清楚,请点击这里了解动态代理的原理,之后再接着往下看。
前方高能预警

我们就以这里为切入点开始分析吧!首先是create()

public <T> T create(final Class<T> 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<Object, Object> serviceMethod =
                (ServiceMethod<Object, Object>) loadServiceMethod(method);

            //下一小节讲到哦
            OkHttpCall<Object> okHttpCall = new OkHttpCall<>(serviceMethod, args);
            
            //下两个小节讲哦
            return serviceMethod.callAdapter.adapt(okHttpCall);
          }
        });
  }

我们主要看Proxy.newProxyInstance方法,它接收三个参数,第一个是一个类加载器,其实哪个类的加载器都无所谓,这里为了方便就选择了我们所定义的借口的类加载器;第二个参数是我们定义的接口的class对象,第三个则是一个InvocationHandler匿名内部类。
那大家应该会有疑问了,这个newProxyInstance到底有什么用呢?其实他就是通过动态代理生成了网络请求接口的代理类,代理类生成之后,接下来我们就可以使用ankRetrofit.getDailyData(2017, 9, 1);这样的语句去调用getDailyData方法,当我们调用这个方法的时候就会被动态代理拦截,直接进入InvocationHandler的invoke方法。下面就来讲讲它。

invoke方法
它接收三个参数,第一个是动态代理,第二个是我们要调用的方法,这里就是指getDailyData,第三个是一个参数数组,同样的这里就是指2017, 9, 1,收到方法名和参数之后,紧接着会调用loadServiceMethod方法来生产过一个ServiceMethod对象,这里的一个ServiceMethod对象就对应我们在网络接口里定义的一个方法,相当于做了一层封装。接下来重点来看loadServiceMethod方法。

loadServiceMethod方法

  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();
        serviceMethodCache.put(method, result);
      }
    }
    return result;
  }

它调用了ServiceMethod类,而ServiceMethod也使用了Builder模式,直接先看Builder方法。

    Builder(Retrofit retrofit, Method method) {
      this.retrofit = retrofit;
      //获取接口中的方法名
      this.method = method;

      //获取方法里的注解
      this.methodAnnotations = method.getAnnotations();

      //获取方法里的参数类型 
      this.parameterTypes = method.getGenericParameterTypes();

      //获取接口方法里的注解内容 
      this.parameterAnnotationsArray = method.getParameterAnnotations();
    }

再来看build方法

public ServiceMethod build() {
      callAdapter = createCallAdapter();
      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?");
      }
      responseConverter = createResponseConverter();

      for (Annotation annotation : methodAnnotations) {
        parseMethodAnnotation(annotation);
      }

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

      if (!hasBody) {
        if (isMultipart) {
          throw methodError(
              "Multipart can only be specified on HTTP methods with request body (e.g., @POST).");
        }
        if (isFormEncoded) {
          throw methodError("FormUrlEncoded can only be specified on HTTP methods with "
              + "request body (e.g., @POST).");
        }
      }

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

      if (relativeUrl == null && !gotUrl) {
        throw methodError("Missing either @%s URL or @Url parameter.", httpMethod);
      }
      if (!isFormEncoded && !isMultipart && !hasBody && gotBody) {
        throw methodError("Non-body HTTP method cannot contain @Body.");
      }
      if (isFormEncoded && !gotField) {
        throw methodError("Form-encoded method must contain at least one @Field.");
      }
      if (isMultipart && !gotPart) {
        throw methodError("Multipart method must contain at least one @Part.");
      }

      return new ServiceMethod<>(this);
    }

代码稍微有点长,但是思路很清晰,主要的工作有
1、首先对注解的合法性进行检验,例如,HTTP的请求方法是GET还是POST,如果不是就会抛出异常;
2、根据方法的返回值类型和方法注解从Retrofit对象的的callAdapter列表和Converter列表中分别获取到该方法对应的callAdapter和Converter;
3、将传递进来的参数与注解封装在parameterHandlers中,为后面的网络请求做准备。

先用流程图梳理一下刚才的思路:

分析到这里,我们总算是明白了最初的两行代码原来干了这么多事情,J神真的是流弊啊!接下来我们就来看一下网络请求部分。

使用OkHttpCall进行网络请求

回头看一下上一小节讲解create方法时我们有这一行代码:

OkHttpCall<Object> okHttpCall = new OkHttpCall<>(serviceMethod, args);

他将我们刚才得到的serviceMethod与我们实际传入的参数传递给了OkHttpCall,接下来就来瞧瞧这个类做了些什么?

final class OkHttpCall<T> implements Call<T> {
  private final ServiceMethod<T, ?> serviceMethod;
  private final Object[] args;

  private volatile boolean canceled;

  // All guarded by this.
  private okhttp3.Call rawCall;
  private Throwable creationFailure; // Either a RuntimeException or IOException.
  private boolean executed;

  OkHttpCall(ServiceMethod<T, ?> serviceMethod, Object[] args) {
    this.serviceMethod = serviceMethod;
    this.args = args;
  }
}

很可惜,我们好像没有看到比较有用的东西,只是将传进来的参数进行了赋值,那我们就接着看create方法中的最后一行吧!

callAdapter的使用

create方法的最后一行是这样的:

return serviceMethod.callAdapter.adapt(okHttpCall);

最后是调用了callAdapter的adapt方法,上面我们讲到Retrofit在决定使用什么callAdapter的时候是看我们在接口中定义的方法的返回值的,而在我们的例子中使用的是RxJava2CallAdapter,因此我们就直接看该类中的adapt方法吧!

@Override 
public Object adapt(Call<R> call) {
    Observable<Response<R>> responseObservable = isAsync
        ? new CallEnqueueObservable<>(call)
        : new CallExecuteObservable<>(call);

    Observable<?> observable;
    if (isResult) {
      observable = new ResultObservable<>(responseObservable);
    } else if (isBody) {
      observable = new BodyObservable<>(responseObservable);
    } else {
      observable = responseObservable;
    }

    if (scheduler != null) {
      observable = observable.subscribeOn(scheduler);
    }

    if (isFlowable) {
      return observable.toFlowable(BackpressureStrategy.LATEST);
    }
    if (isSingle) {
      return observable.singleOrError();
    }
    if (isMaybe) {
      return observable.singleElement();
    }
    if (isCompletable) {
      return observable.ignoreElements();
    }
    return observable;
  }

首先在adapt方法中会先判断是同步请求还是异步请求,这里我们以同步请求为例,直接看CallExecuteObservable。

final class CallExecuteObservable<T> extends Observable<Response<T>> {
  private final Call<T> originalCall;

  CallExecuteObservable(Call<T> originalCall) {
    this.originalCall = originalCall;
  }

  @Override protected void subscribeActual(Observer<? super Response<T>> observer) {
    // Since Call is a one-shot type, clone it for each new observer.
    Call<T> call = originalCall.clone();
    observer.onSubscribe(new CallDisposable(call));

    boolean terminated = false;
    try {
      //重点看这里
      Response<T> response = call.execute();
      if (!call.isCanceled()) {
        observer.onNext(response);
      }
      if (!call.isCanceled()) {
        terminated = true;
        observer.onComplete();
      }
    } catch (Throwable t) {
      Exceptions.throwIfFatal(t);
      if (terminated) {
        RxJavaPlugins.onError(t);
      } else if (!call.isCanceled()) {
        try {
          observer.onError(t);
        } catch (Throwable inner) {
          Exceptions.throwIfFatal(inner);
          RxJavaPlugins.onError(new CompositeException(t, inner));
        }
      }
    }
  }

  private static final class CallDisposable implements Disposable {
    private final Call<?> call;

    CallDisposable(Call<?> call) {
      this.call = call;
    }

    @Override public void dispose() {
      call.cancel();
    }

    @Override public boolean isDisposed() {
      return call.isCanceled();
    }
  }
}

在subscribeActual方法中去调用了OKHttpCall的execute方法开始进行网络请求,网络请求完毕之后,会通过RxJava的操作符对返回来的数据进行转换,并进行线程的切换,至此,Retrofit的一次使用也就结束了。最后我们再用一张完整的流程图总结上述的几个过程。

后记

相信通过上面的详解,大家对Retrofit应该有了一个比较全面的认识,与其说它是一个网络请求框架不如说他做了一层封装,使得我们能够更方便的间接使用了RxJava与OkHttp。从某种意义上来讲我们从源码中更应该学习其对设计模式的正确运用,使得整个框架的耦合度大大降低,调用者也使用得更加简洁。最后希望这篇文章能够对大家的面试有所帮助!

posted @ 2017-09-05 13:57  温斯渤  阅读(2676)  评论(0编辑  收藏  举报