真懂?Retrofit完整剖析

使⽤示例

以下Retrofit解析版本库均为写作时的最新版本:2.9.0。

先在安卓的构建文件中添加依赖:

 implementation 'com.squareup.retrofit2:retrofit:2.9.0'

使用步骤:

  1. 创建⼀个 interface 作为 Web Service 的请求集合,在⾥⾯⽤注解(Annotation)写⼊需要配置的请求⽅法
public interface GitHubService {
    @GET("/notifications")
    Call<ResponseBody> getNotici();

    @GET("/users/LucasXu01")
    Call<ResponseBody> getUser();

}
  1. 在正式代码⾥⽤ Retrofit 创建出 interface 的实例
Retrofit retrofit = new Retrofit.Builder()
                .baseUrl("https://api.github.com/")
                .build();

GitHubService service = retrofit.create(GitHubService.class);
        
  1. 调⽤创建出的 Service 实例的对应⽅法,创建出相应的可以⽤来发起⽹络请求的Call 对象
 Call<ResponseBody> repos = service.getNotici();
 Call<ResponseBody> user = service.getUser();
  1. 使⽤ Call.execute() 或者 Call.enqueue() 来发起请求

    repos.enqueue(new Callback<ResponseBody>() {
                @Override
                public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response)            {
                   
                }
    
                @Override
                public void onFailure(Call<ResponseBody> call, Throwable t) {
                    
                }
            });
    

实现原理

Retrofit基于OkHttp进行功能细化以更好使用。

以下是Retrofit的整体实现图,对照着图看接下来的源码解析,会不至于嵌套太深而忘记起点,从而达到整体掌握的全局观视角。总而言之:对照着图,往下看源码解析;看下面的源码解析,多返回来看看图,知道自己走到了哪一步。

通过Retrofit.create(Class) ⽅法创建出 Service interface 的实例,从⽽使得 Service 中配置的⽅法变得可⽤,下面这段create()代码是 Retrofit 结构的核⼼;

 public <T> T create(final Class<T> service) {
   //验证接口
    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];

              @Override
              public @Nullable Object invoke(Object proxy, Method method, @Nullable Object[] args)
                  throws Throwable {
                if (method.getDeclaringClass() == Object.class) {
                  return method.invoke(this, args);
                }
                args = args != null ? args : emptyArgs;
                return platform.isDefaultMethod(method)
                    ? platform.invokeDefaultMethod(method, service, proxy, args)
                    : loadServiceMethod(method).invoke(args);
              }
            });
  } 

create()代码主要做两件事:验证接口合法性、动态代理接口。

接口的合规验证

验证服务接口validateServiceInterface(service):

 private void validateServiceInterface(Class<?> service) {
   //service必须是接口
    if (!service.isInterface()) {
      throw new IllegalArgumentException("API declarations must be interfaces.");
    }
// service不能继承泛型接口
    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());
    }

   // 对每个service方法进行初始化加载试错
    if (validateEagerly) {
      Platform platform = Platform.get();
      for (Method method : service.getDeclaredMethods()) {
        if (!platform.isDefaultMethod(method) && !Modifier.isStatic(method.getModifiers())) {
          loadServiceMethod(method);
        }
      }
    }
  }

可以看到,validateServiceInterface(service):验证了需要代理的service必须是接口,且不能继承泛型接口。该方法最后会对每个service方法进行初始化加载试错。

if (!platform.isDefaultMethod(method) && !Modifier.isStatic(method.getModifiers())) {}java接口默认不许有默认实现,但是java8开始可以给接口的一些方法写默认实现了;java接口不允许写静态方法,但java8开始允许接口里写静态方法了。然而Retrofit是不支持的这些的,Retrofit不接纳这两种方式为service接口里的方法。

自定义api的动态代理

有关动态代理的知识可以看这篇文章:Java内功修炼系列:代理模式及动态代理,这里默认读者已有相关知识。

代理:创造一个类并实现了某个接口,这个具体new出来的类就实际上代理这个接口的具体实现,这个类就是代理类。

动态代理:这个代理类在运行时(匿名类都是运行时创建,非编译时)生成。

Retrofit.create() ⽅法内部,使⽤的是Proxy.newProxyInstance() ⽅法来创建 Service 实例。这个⽅法会为参数中的多个 interface (具体到 Retrofifit 来说,是固定传⼊⼀个 interface)创建⼀个对象,这个对象实现了所有 interface 的每个⽅法,并且每个⽅法的实现都是雷同的:调⽤对象实例内部的⼀个 InvocationHandler 成员变量的invoke() ⽅法,并把⾃⼰的⽅法信息传递进去。这样就在实质上实现了代理逻辑:interface 中的⽅法全部由⼀个另外设定的 InvocationHandler 对象来进⾏代理操作。并且,这些⽅法的具体实现是在运⾏时⽣成 interface 实例时才确定的,⽽不是在编译时(虽然在编译时就已经可以通过代码逻辑推断出来)。这就是⽹上所说的「动态代理机制」的具体含义。

Proxy.newProxyInstance()有三个参数:

1 提供一个类加载器

2 提供代理的接口对象

3 真正代理:实际做操作的对象

由动态代理的知识我们可知,无论哪个service,最终都会调用到invoke方法

public @Nullable Object invoke(Object proxy, Method method, @Nullable Object[] args)
    throws Throwable {
  // 如果方法是Object中的方法,不代理,自己用自己的方法。
  // 确保代理的方法不是Object中的方法
  if (method.getDeclaringClass() == Object.class) {
    return method.invoke(this, args);
  }
  args = args != null ? args : emptyArgs;
  // 不代理java8的默认方法
  return platform.isDefaultMethod(method)
      ? platform.invokeDefaultMethod(method, service, proxy, args)
      : loadServiceMethod(method).invoke(args);
}

platform.isDefaultMethod(method) 判断有没有java8的types, 主要是对不同版本的java进行了处理,使得最终都能得到正确结果。

  boolean isDefaultMethod(Method method) {
    return hasJava8Types && method.isDefault();
  }

loadServiceMethod(method).invoke(args);拆开来看,首先通过loadServiceMethod获得一个HttpServiceMethod的实现类,然后调用它的invoke方法。接下来就看这两个方法。

loadServiceMethod:一个带缓存的加载,核心: result = ServiceMethod.parseAnnotations(this, 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 = ServiceMethod.parseAnnotations(this, method);
        serviceMethodCache.put(method, result);
      }
    }
    return result;
  }

看看result = ServiceMethod.parseAnnotations(this, method)方法:

static <T> ServiceMethod<T> parseAnnotations(Retrofit retrofit, Method method) {
  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);
}

点进HttpServiceMethod.parseAnnotations()中一探究竟:

 static <ResponseT, ReturnT> HttpServiceMethod<ResponseT, ReturnT> parseAnnotations(
      Retrofit retrofit, Method method, RequestFactory requestFactory) {
    ...
    if (!isKotlinSuspendFunction) {
      return new CallAdapted<>(requestFactory, callFactory, responseConverter, callAdapter);
    } else if (continuationWantsResponse) {
      //noinspection unchecked Kotlin compiler guarantees ReturnT to be Object.
      return (HttpServiceMethod<ResponseT, ReturnT>)
          new SuspendForResponse<>(
              requestFactory,
              callFactory,
              responseConverter,
              (CallAdapter<ResponseT, Call<ResponseT>>) callAdapter);
    } else {
      //noinspection unchecked Kotlin compiler guarantees ReturnT to be Object.
      return (HttpServiceMethod<ResponseT, ReturnT>)
          new SuspendForBody<>(
              requestFactory,
              callFactory,
              responseConverter,
              (CallAdapter<ResponseT, Call<ResponseT>>) callAdapter,
              continuationBodyNullable);
    }
  }

可以看到,一般在java下开发的非协程挂起函数,直接new了一个HttpServiceMethod的子类CallAdapted进行返回。

CallAdapted的细致分析后面再深入,走到这步回过头去,再看下invoke方法。

HttpServiceMethod中找到了invoke()方法:

  @Override
  final @Nullable ReturnT invoke(Object[] args) {
    Call<ResponseT> call = new OkHttpCall<>(requestFactory, args, callFactory, responseConverter);
    return adapt(call, args);
  }

可以看到,因为上面返回的是一个HttpServiceMethod的子类CallAdapted,所以这里的adapt(call, args)执行的是类CallAdapted中的adapt()方法:

  static final class CallAdapted<ResponseT, ReturnT> extends HttpServiceMethod<ResponseT, ReturnT> {
    private final CallAdapter<ResponseT, ReturnT> callAdapter;

    CallAdapted(
        RequestFactory requestFactory,
        okhttp3.Call.Factory callFactory,
        Converter<ResponseBody, ResponseT> responseConverter,
        CallAdapter<ResponseT, ReturnT> callAdapter) {
      super(requestFactory, callFactory, responseConverter);
      this.callAdapter = callAdapter;
    }

    @Override
    protected ReturnT adapt(Call<ResponseT> call, Object[] args) {
      return callAdapter.adapt(call);
    }
  }

可以看到adapter的实现就一行return callAdapter.adapt(call)。这个⽅法会使⽤⼀个 CallAdapter 对象来把 OkHttpCall 对象进⾏转换,⽣成⼀个新的对象,默认情况下,该方法返回的是⼀个 ExecutorCallbackCall对象 ,它的主要作⽤是把操作切回主线程后再交给 Callback 。

另外,如果有⾃定义的 CallAdapter,这⾥也可以⽣成别的类型的对象,例如RxJava 的 Observable ,来让 Retrofit 可以和 RxJava 结合使⽤。

如何创建的OKHttp

上面说到,在HttpServiceMethod中的invoke()方法创建了OkHttpCallnew OkHttpCall<>(requestFactory, args, callFactory,responseConverter)

OkHttpCall 是 retrofit.Call 的⼦类。这⾏代码负责将ServiceMethod 解读到的信息( RequestFactory 、OkHttpClient 和ResponseConverter )封装进 OkHttpCall ;⽽这个对象可以在需要的时候(例如它的 enqueue() ⽅法被调⽤的时候),利⽤ RequestFactory 和 OkHttpClient 来创建⼀个 okhttp3.Call对象,并调⽤这个 okhttp3.Call 对象来进⾏⽹络请求的发起,然后利⽤ResponseConverter 对结果进⾏预处理之后,交回给 Retrofifit 的Callback 。

如下代码所示,在repos.enqueue(new Callback<ResponseBody>() {}时,执行的是OkHttpCallenqueue方法:

  @Override
  public void enqueue(final Callback<T> callback) {
		...
    okhttp3.Call call;

    synchronized (this) {
			...
      call = rawCall = createRawCall();
    }
    ...
    call.enqueue(
        new okhttp3.Callback() {
          @Override
          public void onResponse(okhttp3.Call call, okhttp3.Response rawResponse) {
            Response<T> response;
            // 注释1
            response = parseResponse(rawResponse);
            callback.onResponse(OkHttpCall.this, response);
          }

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

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

可以看到,真正执行call.enqueue的是okhttp3的call,它在OkHttpCall内部生成,主要通过call = rawCall = createRawCall();:

private okhttp3.Call createRawCall() throws IOException {
    okhttp3.Call call = callFactory.newCall(requestFactory.create(args));
    if (call == null) {
      throw new NullPointerException("Call.Factory returned null.");
    }
    return call;
  }

可以看到OkHttpCall中通过callFactory创建,我们知道OkHttp3的的call的创建如下代码所示: client.newCall(request).enqueue(new Callback()),它需要一个OkHttp3中的Request对象,它在Retrofit中通过RequestFactory进行构建,并传给callFactory创造出OkHttp3的Call对象。

对照着前文的结构图看,否则容易迷失。callFactory对象在HttpServiceMethod中的invoke()方法中创建OkHttpCall时被赋值,它的创建在HttpServiceMethodparseAnnotations()方法中:

 okhttp3.Call.Factory callFactory = retrofit.callFactory;

可以看到,这个用来创建OkHttp3的Call的工厂来自于Retrofit,在Retrofit中的构建者模式中可以找到:

   public Retrofit build() {
      okhttp3.Call.Factory callFactory = this.callFactory;
      if (callFactory == null) {
        callFactory = new OkHttpClient();
      }
     ..
      return new Retrofit(
          callFactory,
          baseUrl,
          unmodifiableList(converterFactories),
          unmodifiableList(callAdapterFactories),
          callbackExecutor,
          validateEagerly);
    }
  }

真相大白了!原来Retrofit用来创建OkHttp3的Call的工厂就是OkHttp3的OkHttpClient:callFactory = new OkHttpClient();

如何解析返回结果

回看前结构图,可以知道请求结果的返回,在前一章节:如何创建的OKHttp 的注释1处:

response = parseResponse(rawResponse);

查看OkHttpCall的parseResponse方法:

 Response<T> parseResponse(okhttp3.Response rawResponse) throws IOException {
    ResponseBody rawBody = rawResponse.body();
   	...	

    ExceptionCatchingResponseBody catchingBody = new ExceptionCatchingResponseBody(rawBody);
    try {
      
      T body = responseConverter.convert(catchingBody);
      return Response.success(body, rawResponse);
      
    } catch (RuntimeException e) {
      
    }
  }

可以看到,网络请求的结果最终会被一个返回结果转换器进行转换之后再返回,不难猜测,这个responseConverter转换器的功能,就是将网络返回的不易看懂的数据转换为我们需要的自定义的Object接收对象,如UserBean、XXListBean等。现在我们已经有了原始的返回对象,接下只需要知道这个responseConverter对象是什么以及它的convert()方法即可。

还是在HttpServiceMethodinvoke()方法中寻找OkHttpCall的创建过程,responseConverter作为构建参数被传递进来。而它又是在HttpServiceMethodparseAnnotations()方法中被create:

Converter<ResponseBody, ResponseT> responseConverter =
    createResponseConverter(retrofit, method, responseType);

createResponseConverter()方法

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

很容易又继续追到retrofit.responseBodyConverter(responseType, annotations)方法:

  public <T> Converter<ResponseBody, T> responseBodyConverter(Type type, Annotation[] annotations) {
    return nextResponseBodyConverter(null, type, annotations);
  }

继续追到Retrofit里的nextResponseBodyConverter(null, type, annotations);

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

  ...
  }

converterFactories.get(i).responseBodyConverter(type, annotations, this);可以看到,这个返回结果转换器responseConverter来自于converterFactories,在Retrofit中寻找converterFactories,在Retrofit的建造者Builder的build()方法中我们找到:

 public Retrofit build() {
      ...
      // ensures correct behavior when using converters that consume all types.
      converterFactories.add(new BuiltInConverters());
      converterFactories.addAll(this.converterFactories);
      converterFactories.addAll(platform.defaultConverterFactories());
      ...
    }

第一行和第三行被add的converter不是我们自定义的,暂不去看,去寻找第二行addAll(this.converterFactories)中的converterFactories,看它是哪里被赋值构建的,我们于是又在Retrofit中找到了addConverterFactory()方法:

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

这个方法是不是很眼熟,回忆一下我们平时是怎么使用Retrofit的:

        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl("https://api.github.com/")
                .addConverterFactory(gsonConvertFactory)
                .build();

这不就是我们在一开始在创建Retrofit时常用的添加Gson转换器的地方嘛!原来这个Gson转化器在Retrofit里是最终被用到OkHttpCall的parseResponse()中的responseConverter.convert(catchingBody);啊!原来Retrofit内部的网络结果转换器是这样工作的!

返回的UI线程切换原理

相比OkHttp3,Retrofit在使用时一个很明显的方便之处就是在 Call.execute() 或者 Call.enqueue() 来发起请求后的返回结果事件中,不需要再切换线程,因为此刻,它已经在安卓的UI主线程当中了。

repos.enqueue(new Callback<ResponseBody>() {
            @Override
            public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response)            {
               
            }

            @Override
            public void onFailure(Call<ResponseBody> call, Throwable t) {
                
            }
        });

在在执行retrofit2.Callenqueue()方法时,我们需要注意,这个Call对象我们刚刚是在HttpServiceMethod中的invoke()方法创建的OkHttpCallnew OkHttpCall<>(requestFactory, args, callFactory,responseConverter),但是这个OkHttpCall会经过adapt转为真正进行请求的另一个类ExecutorCallbackCall,它里面会持有OkHttpCall并在执行enqueue方法时去执行OkHttpCallenqueue方法。

我们其实可以顺着HttpServiceMethod中的CallAdapted子类找到callAdapter.adapt(call),顺着这个adapt(call)方法不断溯源,可以找到这个callAdapter的赋值过程实际上是在Retrofit中的build函数中,再在Retrofit中的build()方法中不断溯源,最终可以找到它在Retrofit中赋值如下所示:

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

可以看到,callAdapter最终取自 callAdapterFactories,它是定义在Retrofit中的callAdapter的工厂List,具体每一个callAdapter由这个List中的某一个工厂的get()方法进行创建。

那么问题来了, callAdapterFactories这个List中的工厂又在哪里来的呢?再去寻找 callAdapterFactories,如下所示, callAdapterFactories中添加的是platform.defaultCallAdapterFactories(callbackExecutor)对象:

 public Retrofit build() {
      ...
      Executor callbackExecutor = this.callbackExecutor;
      if (callbackExecutor == null) {
        //注释2
        callbackExecutor = platform.defaultCallbackExecutor();
      }

      List<CallAdapter.Factory> callAdapterFactories = new ArrayList<>(this.callAdapterFactories);
      callAdapterFactories.addAll(platform.defaultCallAdapterFactories(callbackExecutor));
			...
    }

那么问题来了:

  1. platform.defaultCallAdapterFactories()这个方法干嘛的?

  2. callbackExecutor哪里来的,有什么用?

首先看platform.defaultCallAdapterFactories()方法:

List<? extends CallAdapter.Factory> defaultCallAdapterFactories(
      @Nullable Executor callbackExecutor) {
    DefaultCallAdapterFactory executorFactory = new DefaultCallAdapterFactory(callbackExecutor);
    return hasJava8Types
        ? asList(CompletableFutureCallAdapterFactory.INSTANCE, executorFactory)
        : singletonList(executorFactory);
  }

该方法主要创建了DefaultCallAdapterFactory类,这里需要中断注意下,刚刚说了,具体每一个callAdaptercallAdapterFactories这个List中的某一个工厂的get()方法进行创建的,所以,我们此刻应该去DefaultCallAdapterFactory类中看看它的get()方法做了什么:

public @Nullable CallAdapter<?, ?> get(...) {
   ...
    return new CallAdapter<Object, Call<?>>() {
      @Override
      public Type responseType() {
        return responseType;
      }

      @Override
      public Call<Object> adapt(Call<Object> call) {
        return executor == null ? call : new ExecutorCallbackCall<>(executor, call);
      }
    };
  }

来了!get()方法它创建了一个CallAdapter进行返回,这不就是在HttpServiceMethodinvoke()方法返回的那个adapt(),我们再看一下:

  @Override
  final @Nullable ReturnT invoke(Object[] args) {
    Call<ResponseT> call = new OkHttpCall<>(requestFactory, args, callFactory, responseConverter);
    return adapt(call, args);
  }

对了!是它了,HttpServiceMethodinvoke()方法里执行的adapt()就是return executor == null ? call : new ExecutorCallbackCall<>(executor, call);这个了!它把OkHttpCall传给了ExecutorCallbackCall,通过前面的分析不难想象,最终的请求ExecutorCallbackCall还是通过这个OkHttpCall来进行的,那么,第二个问题还没解决,这个executor是什么?干嘛用的?

回到callAdapterFactories添加platform.defaultCallAdapterFactories(callbackExecutor)对象那里,可以看到注释2:

 callbackExecutor = platform.defaultCallbackExecutor();

这个就是哪个executor!看它的实现:

    public Executor defaultCallbackExecutor() {
      return new MainThreadExecutor();
    }

MainThreadExecutor():

 static final class MainThreadExecutor implements Executor {
      private final Handler handler = new Handler(Looper.getMainLooper());

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

这不就是在切线程!而且拿的是Looper.getMainLooper(),难不成这就是Retrofit返回直接在UI主线程的原因嘛?汇过去看这个executor的使用地方,也就是ExecutorCallbackCall去执行enqueue()进行网络请求时的使用情况:

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

        public void enqueue(final Callback<T> callback) {
            
            this.delegate.enqueue(new Callback<T>() {
                public void onResponse(Call<T> call, Response<T> response) {
                    ExecutorCallbackCall.this.callbackExecutor.execute(() -> {
                          ...
                         callback.onResponse(ExecutorCallbackCall.this, response);

                    });
                }

                public void onFailure(Call<T> call, Throwable t) {
                    ExecutorCallbackCall.this.callbackExecutor.execute(() -> {
                        callback.onFailure(ExecutorCallbackCall.this, t);
                    });
                }
            });
        }

       
       
    }

由上面的分析可知,ExecutorCallbackCall中的delegate就是 adapt()传过去的OkHttpCallcallbackExecutor就是我们刚刚看到的MainThreadExecutor,ExecutorCallbackCall在处理返回结果时,使用的是MainThreadExecutor,而MainThreadExecutor又将这个线程切换到了UI主线程中,至此,完成了网络请求过程子线程个UI主线程的切换。

参考

https://square.github.io/retrofit/

评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

许进进

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值