Retrofit2源码分析:架构全面解析

目录

 

一、概述

二、Retrofit2源码地址

三、Retrofit2简单使用

四、架构流程

1、流程图

2、流程讲解

五、源码分析

1、Retrofit的内部成员变量

2、Retrofit内部类Builder

3、创建Retrofit实例

4、Builder.build()方法

5、retrofit.create()

6、loadServiceMethod()

7、创建OkHttpCall对象

8、serviceMethod.adapt(okHttpCall)

六、总结


一、概述

    在了解Retrofit2源码架构之前先说明,本文默认读者对Retrofit2框架的使用有一定了解,除此之外还要先熟悉okhttp3框架和Java代理模式中的动态代理,不然后面咱们看Retrofit2源码时会很难理解:

    1.okhttp3框架解析

    2.Java代理模式

    严格地说,Retrofit2并不是一个网络请求交易框架,它只是对网络请求框架的封装。底层把实现交给了okhttp3,由okhttp3做真正的网络请求。Retrofit2框架和其他的用于网络请求的框架流程大体相同,Retrofit2优势在于,它使用了大量的设计模式将功能模块解耦,这样做的好处在于可以让流程更加清晰,可塑性更强。

    注:本文基于Retrofit 2.4.0版本的源码分析。

 

二、Retrofit2源码地址

    Retrofit2源码地址:https://github.com/square/retrofit

 

三、Retrofit2简单使用

    1、首先创建用于网络请求的API接口:

public interface NetService {
    @GET("cozing")
    Call<NetResponse> serviceApi();
}

    2、创建用于接收请求返回的数据接收:

public class NetResponse {
    private int id;
    private String name;
}

    3、创建Retrofit实例并做交易请求:

public class Retrofit2TestForBlogActivity extends BaseActivity{
...
    private void retrofitTest(){
        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl("https://github.com/") //必须以"/"结尾,不然将抛出异常
                .addConverterFactory(GsonConverterFactory.create())
                //.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .build();

        NetService netApi = retrofit.create(NetService.class);
        Call<NetResponse> call = netApi.serviceApi();
        try {
            //① 同步请求
            Response response = call.execute();
        } catch (IOException e) {
            e.printStackTrace();
        }
        //②异步请求
        call.enqueue(new Callback<NetResponse>() {
            @Override
            public void onResponse(Call<NetResponse> call, Response<NetResponse> response) {

            }

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

            }
        });
    }
}

      通过以上代码,咱们完成了请求,其中咱们向:

url = https://github.com/cozing

发送GET请求,在注释①处发起同步请求,在注释②处发送异步请求,交易的结果通过调用返回的Response对象实例的response.body()获取,这个结果就是咱们在构建网络请求时传入的NetResponse数据结构。

接下来看Retrofit2整体架构流程图。

 

四、架构流程

1、流程图

 

2、流程讲解

    1、创建Retrofit实例;

    2、创建网络请求接口和相关属性注解;

    3、通过动态代理解析请求接口的注解,并生成网络请求对象;

    4、通过CallAdapter进行平台适配,平台包括(Android/java8/iOS);

    5、通过OkHttpCall发送网络请求;

    6、通过Converter数据转换适配器转换交易返回的数据;

    7、通过线程切换执行器切换到主线程(仅限异步请求)。

 

五、源码分析

1、Retrofit的内部成员变量

public final class Retrofit {
  private final Map<Method, ServiceMethod<?, ?>> serviceMethodCache = new ConcurrentHashMap<>(); //网络请求缓存,如:请求方法、请求头、请求体,各种适配器等

  final okhttp3.Call.Factory callFactory; //okhttp工厂,真正发送交易的处理类
  final HttpUrl baseUrl; //请求url前半部,基地址
  final List<Converter.Factory> converterFactories; //数据转换器工厂集
  final List<CallAdapter.Factory> adapterFactories; //网络请求适配器工厂集
  final @Nullable Executor callbackExecutor; //异步请求结果线程切换执行器
  final boolean validateEagerly; //标志位、是否马上解析接口方法
...
}

2、Retrofit内部类Builder

public final class Retrofit {
...
  public static final class Builder {
    private final Platform platform; //适配平台,通常默认android
    private @Nullable okhttp3.Call.Factory callFactory; //okhttp网络请求工厂,默认okhttp
    private HttpUrl baseUrl; //基地址
    private final List<Converter.Factory> converterFactories = new ArrayList<>(); //数据转换器集,用于生产数据转换器,默认GsonConverterFactory
    private final List<CallAdapter.Factory> adapterFactories = new ArrayList<>(); //网络请求适配器,如RxJava2CallAdapterFactory
    private @Nullable Executor callbackExecutor; //执行异步回调的线程切换
    private boolean validateEagerly; //是否立即解析接口注解方法
  } 
...
}

3、创建Retrofit实例

 Retrofit retrofit = new Retrofit.Builder()
      .baseUrl("https://github.com")
      .addConverterFactory(GsonConverterFactory.create())
      .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
      .build();

内部类Builder实现:

public final class Retrofit {
...
  public static final class Builder {
   ...
    Builder(Platform platform) {
      this.platform = platform;
      converterFactories.add(new BuiltInConverters());//添加默认数据解析器
    }

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

    咱们在创建Builder实例时调用的是Builder的无参构造放法,里面调用了Builder(Platform)这个构造方法,传入的是Platform.get()返回数据,这个方法是获取适配平台,默认是Android,其他平台咱们不做过多分析,直接查看Android:

class Platform {
...
  static class Android extends Platform {
    @Override public Executor defaultCallbackExecutor() {
      return new MainThreadExecutor();
    }

    @Override CallAdapter.Factory defaultCallAdapterFactory(@Nullable Executor callbackExecutor) {
      if (callbackExecutor == null) throw new AssertionError();
      return new ExecutorCallAdapterFactory(callbackExecutor);
    }

    static class MainThreadExecutor implements Executor {
      //默认线程切换是切换到主线程
      private final Handler handler = new Handler(Looper.getMainLooper());

      @Override public void execute(Runnable r) {
        handler.post(r);
      }
    }
  }
...
}

    可以看到在创建Builder实例的设配平台时,将工作线程切换到了主线程,这也是后面可以通过主线程回调返回请求数据的原因。

    接着将会在Builder的有参构造方法中的添加Retrofit默认的数据解析器:

converterFactories.add(new BuiltInConverters());

4、Builder.build()方法

public final class Retrofit {
...
  public static final class Builder {
    ...
    public Retrofit build() {
      if (baseUrl == null) { //对baseurl进行非空判断
        throw new IllegalStateException("Base URL required.");
      }

      //注释①
      okhttp3.Call.Factory callFactory = this.callFactory;
      if (callFactory == null) {
        callFactory = new OkHttpClient(); //创建okhttp客户端
      }
      //注释②
      Executor callbackExecutor = this.callbackExecutor;
      if (callbackExecutor == null) {
        callbackExecutor = platform.defaultCallbackExecutor();
      }

      List<CallAdapter.Factory> adapterFactories = new ArrayList<>(this.adapterFactories);
      adapterFactories.add(platform.defaultCallAdapterFactory(callbackExecutor));

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

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

    首先咱们看注释①处,上面说过Retrofit2框架的底层发送交易其实是提交给了okhttp框架,在okhttp3源码讲解时咱们说过在okhttp中OkhttpClient在整个进程中只创建一次,所以在这里做了相关判断。

    注释②处,可以看到是获取回调执行器,如果没有设置回调执行器,就创建一个默认的主线程回调执行器。

    最后调用Retrofit的构造方法,创建一个Retrofit实例。可以看到整个创建Retrofit过程中,通过构建者模式来执行一系列初始化和对象的创建。

5、retrofit.create()

    接着咱们创建了网络请求接口类NetService.class,在下面的步骤中咱们调用retrofit.create(NetService.class),这个方法是Retrofit的核心,内部采用动态代理,将咱们自定义的网络请求接口转换成一个ServiceMethod对象,ServiceMethod就是咱们Retrofit中的具体请求对象,里面封装了网络请求所必须的全部信息,包括请求方法、url、请求头、请求体等网络配置参数!

    咱们现在看看create()源码:

...
  public <T> T create(final Class<T> service) {
    Utils.validateServiceInterface(service);
    if (validateEagerly) { //创建retrofit实例时提到的标志位
      //注释①
      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, @Nullable Object[] args)
              throws Throwable {
            
            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);
          }
        });
  }
...

    咱们先看注释①处,如果提前验证的标志位为true,将会调用eagerlyValidateMethods(service)方法,咱们来看:

...
  private void eagerlyValidateMethods(Class<?> service) {
    Platform platform = Platform.get();
    for (Method method : service.getDeclaredMethods()) {
      if (!platform.isDefaultMethod(method)) {
        loadServiceMethod(method);
      }
    }
  }
...

    可以看到这个方法内部通过反射机制,来获取咱们自定义的请求接口对象类的内部方法。isDefaultMethod默认为false,因此loadServiceMethod(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();
        serviceMethodCache.put(method, result);
      }
    }
    return result;
  }
...

    其中serviceMethodCache是一个ConcurrentHashMap实例,ConcurrentHashMap是HashMap的一个线程安全的、支持高效并发的版本。继续看代码,创建serviceMethod时并不是每次都创建一个新的实例,而是先去缓存吃中去获取,如果缓存池中没有存储,则会调用builder构建者模式去重新创建method,并将该对象实例存入serviceMethodCache缓存池中。

    接着看注释②,返回了一个动态代理对象,如果不清楚什么是动态代理,请点击这里。熟悉动态代理模式的童鞋应该都知道

真正处理事务逻辑的地方是在代理对象的invoke回调方法内,在Retrofit2代码里,也就是注释③处。

    看注释③的invoke方法,对Retrofit源码理解的重中之重,就是这个方法中的最后三行。接下来咱们将在6、7、8点详细分析这三行代码。

6、loadServiceMethod()

    这个方法用于加载serviceMethod,一个serviceMethod对应咱们定义的网络请求接口中的一个方法,比如栗子中的serviceApi()这个方法。接着来看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的成员变量:

final class ServiceMethod<R, T> {
...
  private final okhttp3.Call.Factory callFactory; //okhttp call的工厂
  private final CallAdapter<R, T> callAdapter; //网络请求适配器

  private final HttpUrl baseUrl; //基地址
  private final Converter<ResponseBody, R> responseConverter; //数据转换器
  private final String httpMethod; //网络请求方法
  private final String relativeUrl; //网络请求相对地址,和基地址拼接成完整地址
  private final Headers headers; //请求头
  private final MediaType contentType; //请求体
  private final boolean hasBody; 
  private final boolean isFormEncoded;
  private final boolean isMultipart;
  private final ParameterHandler<?>[] parameterHandlers; //方法参数处理器

...

}

    咱们开始时候说过,Retrofit2底层是基于okhttp框架来发送请求,可以看到在ServiceMrthod中维护了okhttp用于网络请求的Call工厂,用于生产Call,并且ServiceMrthod声明了网络请求所需元素的所有基本信息。咱们看ServiceMrthod的源码发现,它和Retrofit这个类的初始化非常相似,都是通过构建者模式来创建实例。咱们回过头看loadServiceMethod()方法的:

...
result = new ServiceMethod.Builder<>(this, method).build();
...

这一行,跟进去Builder内部类:

final class ServiceMethod<R, T> {
...
    Builder(Retrofit retrofit, Method method) {
      this.retrofit = retrofit; //retrofit实例
      this.method = method; //网络请求方法
      this.methodAnnotations = method.getAnnotations(); //网络请求方法的注解
      this.parameterTypes = method.getGenericParameterTypes(); //获取网络请求方法里的注解的类型
      this.parameterAnnotationsArray = method.getParameterAnnotations(); //获取网络请求方法里的注解的内容
    }
...
}

    然后调用ServiceMrthod

...
  static final class Builder<T, R> {
  ...
    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);
      }

      ...

      //注释④
      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);
    }
  ...
...

    看注释①的createCallAdapter,创建callAdapter,内部实现:

...
    private CallAdapter<T, R> createCallAdapter() {
      Type returnType = method.getGenericReturnType();
      if (Utils.hasUnresolvableType(returnType)) {
        throw methodError(
            "Method return type must not include a type variable or wildcard: %s", returnType);
      }
      if (returnType == void.class) {
        throw methodError("Service methods cannot return void.");
      }
      Annotation[] annotations = method.getAnnotations();
      try {
        return (CallAdapter<T, R>) retrofit.callAdapter(returnType, annotations);
      } catch (RuntimeException e) { // Wide exception range because factories are user code.
        throw methodError(e, "Unable to create call adapter for %s", returnType);
      }
    }
...

    这个方法主要是根据网络请求接口中的方法和方法内注解的类型,从retrofit中获取网络请求接口适配器。首先从网络请求接口方法中获取注释,然后根据注释和注释返回类型,接着调用retrofit.callAdapter()方法从retrofit中获取适配器。咱们跟着代码会发发现其实最后调用的是retrofit的nextCallAdapter(),来看看它内部实现的主要代码:

...
  public CallAdapter<?, ?> nextCallAdapter(@Nullable CallAdapter.Factory skipPast, Type returnType,
      Annotation[] annotations) {
...

    int start = callAdapterFactories.indexOf(skipPast) + 1;
    for (int i = start, count = callAdapterFactories.size(); i < count; i++) {
      CallAdapter<?, ?> adapter = callAdapterFactories.get(i).get(returnType, annotations, this);
      if (adapter != null) {
        return adapter;
      }
    }
...
  }
...

    在for循环中,根据请求参数在网络适配工厂中创建网络适配器并返回该适配器,

    接着看build()方法中的注释②:

responseConverter = createResponseConverter();

    跟进去:

...
    private Converter<ResponseBody, T> createResponseConverter() {
      Annotation[] annotations = method.getAnnotations();
      try {
        return retrofit.responseBodyConverter(responseType, annotations);
      } catch (RuntimeException e) { // Wide exception range because factories are user code.
        throw methodError(e, "Unable to create converter for %s", responseType);
      }
    }
...

    这个方法是创建网络请求返回数据转换器,和创建网络请求适配器的方式一样,调用的是retrofit的responseBodyConverter()方法,内部真正调用的是nextResponseBodyConverter()方法,nextResponseBodyConverter()的内部主要实现:

...
  public <T> Converter<ResponseBody, T> nextResponseBodyConverter(
  ...
    int start = converterFactories.indexOf(skipPast) + 1;
    for (int i = start, count = converterFactories.size(); i < count; i++) {
      Converter<ResponseBody, ?> converter =
          converterFactories.get(i).responseBodyConverter(type, annotations, this);
      if (converter != null) {
        return (Converter<ResponseBody, T>) converter;
      }
    }
  ...
  }
...

    可以看到,和创建网络请求适配器一样的创建方法,根据响应类型和请求方法注释数值,从数据转换器工厂中创建数据转换器并返回。

    继续看build()方法中的注释③:

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

是解析请求接口中的所有注释。

    接着看注释④的代码:

...
    public ServiceMethod build() {
    ...
      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);
      }
    ...
    }
....

    这部分做的主要是解析网络请求接口的参数,包括自定义请求接口参数和配置的请求参数,其中ParameterHandler类就是接口参数解析器。

    至此,loadServiceMethod()方法的分析完成,这个方法主要是创建并加载ServiceMehod,用于解析网络请求接口参数、网络请求适配器、response数据解析器等工作。

7、创建OkHttpCall对象

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

    咱们前面说过Retrofit2框架的交易发送是由okhttp实现, 所以很容易想到,这个OkHttpCall就是对okhttp的call对象的封装,咱们现在看OkHttpCall的实现:

final class OkHttpCall<T> implements Call<T> {
...
  @GuardedBy("this")
  private @Nullable okhttp3.Call rawCall;
  ...

  @Override public void enqueue(final Callback<T> callback) {
    call.enqueue(new okhttp3.Callback() {
      @Override public void onResponse(okhttp3.Call call, okhttp3.Response rawResponse) {
        Response<T> response;
        try {
          response = parseResponse(rawResponse);
        } catch (Throwable e) {
          callFailure(e);
          return;
        }

        try {
          callback.onResponse(OkHttpCall.this, response);
        } catch (Throwable t) {
          t.printStackTrace();
        }
      }

      @Override public void onFailure(okhttp3.Call call, IOException e) {
        callFailure(e);
      }

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

  ...

  @Override public Response<T> execute() throws IOException {
    okhttp3.Call call;

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

      if (creationFailure != null) {
        if (creationFailure instanceof IOException) {
          throw (IOException) creationFailure;
        } else if (creationFailure instanceof RuntimeException) {
          throw (RuntimeException) creationFailure;
        } else {
          throw (Error) creationFailure;
        }
      }

      call = rawCall;
      if (call == null) {
        try {
          call = rawCall = createRawCall();
        } catch (IOException | RuntimeException | Error e) {
          throwIfFatal(e); //  Do not assign a fatal error to creationFailure.
          creationFailure = e;
          throw e;
        }
      }
    }

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

    return parseResponse(call.execute());
  }
...
  }

    代码有点多,但是逻辑还是很清晰的,内部维护了一个okhttp3.Call rawCall的对象,接着看下面两个方法:enqueue()和execute(),可以看到内部调用的是rawCall的对应方法,也就是调用okhttp3.Call的enqueue()和execute()。

8、serviceMethod.adapt(okHttpCall)

    这个方法很显然使用适配器模式,将某些咱们需要处理的事件,适配成其他平台可以使用的类型,并在该平台使用。这里调用serviceMethod的adapt适配方法,传入了上面步骤7中创建的OkHttpCall实例,并将适配对象返回。adapt()方法源码:

final class ServiceMethod<R, T> {
...
  T adapt(Call<R> call) {
    return callAdapter.adapt(call);
  }
...
}

    可以看到内部调用的是callAdapter.adapter(call),而callAdapter对象实例是在loadServiceMethod()方法中调用ServiceMethod构建者模式创建实例时候创建的:

final class ServiceMethod<R, T> {
  ...
  static final class Builder<T, R> {
    ...
    CallAdapter<T, R> callAdapter;
        public ServiceMethod build() {
            ...
            callAdapter = createCallAdapter();
            ...
        }
  }
...
}

    在调用callAdapter的adapter()方法,其实就是把一个个Retrofit中的Call适配成其他平台也能使用的Call类型,比如咱们栗子中使用的是RxJava2CallAdapterFactory.create()来创建了一个RxJava2CallAdapter的适配器来适配RxJava2平台,适配成RxJava2平台能使用的Call。

    咱们接下来看Retrofit的CallAdapter接口中的Rxjava2CallAdapter实现类的adapter()方法:

final class RxJava2CallAdapter implements CallAdapter<Object> {
...
  @Override public <R> Object adapt(Call<R> call) {
    Observable<Response<R>> responseObservable = new CallObservable<>(call);
    Observable<?> observable;
    ...
    return observable;
  }
...
}

    可以看到,该适配方法返回了一个Observable被观察者对象。所以在咱们栗子中最后创建Retrofit的Call接口的实例对象时,也就创建了一个被观察者对象。

    最后,通过Retrofit的Call实现类,调用execute()方法,也就是调用okhttp3.Call的execute()方法来发送同步请求,请求结果同步返回;调用enqueue()方法,也就是调用okhttp3.Call的enqueue()方法来发送异步请求,请求结果通过Callback接口对象回调返回。

    至此,有关Retrofit2的源码分析结束。

 

六、总结

    咱们通过观察Retrofit2的源码发现Retrofit2代码量其实不多,但是有些童鞋看完整篇文章可能还一脸懵逼,这TM也太复杂了!其实不然,因为Retrofit2内部采用了大量的设计模式,导致咱们分析源码时候需要经常跳转着看源码,所以建议咱们在分析Retrofit2源码时候,一定要跟着流程仔细阅读源码,如果读懂了Retrofit2的流程,就会发现其实Retrofit2的整体框架还是挺简单的。

    最后再说一句,Retrofit2采用大量的设计模是Retrofit2的精髓所在,完全可以借鉴到咱们的日常开发中,这才是咱们分析框架源码的初衷!

                    

 

    关注我的博客和github,有大量福利哟~

    csdn博客:https://blog.csdn.net/qq_29152241

    github:https://github.com/cozing

 

 

  • 6
    点赞
  • 35
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值