Retrofit source code reading notes (3)
The first article introduces Retrofit
dynamic proxy implementation, method annotation analysis, etc.: Android Retrofit source code reading notes (1) .
The second article introduces Retrofit
how to parse the parameters in the method : Android Retrofit source code reading notes (2) .
This article is the third in a series of articles. It introduces Retrofit
how to build Http
a request task. I personally think this part of the code is Retrofit
the most complex part of the code and the most core part. Get ready to get on the bus.
Http request task construction
Let's review the previous content. Dynamic proxy methods are all ServiceMethod#invoke()
called by calling the method. If there is no cached ServiceMethod
object, it needs to be ServiceMethod#parseAnnotations()
constructed by parsing various annotations in the method and various parameter types in the method and return value types. An ServiceMethod
object, which is then called by the proxy method. In ServiceMethod#parseAnnotations()
the method, RequestFactory#parseAnnotations()
an object will be generated through method parsing RequestFactory
, which encapsulates the construction Http Request
method (already analyzed in the previous article); then HttpServiceMethod.parseAnnotations()
a requested task will be generated through method parsing , that is, the object Http
used in the dynamic proxy will be returned. ServiceMethod
, which is also our main content today.
Before starting, let me introduce Kotlin
the coroutine suspend
method to avoid difficulties in reading the source code later.
Suppose I define a Kotiln
method like this:
suspend fun helloWorld(): String
After being processed by Kotlin
the compiler, the above method definition becomes as follows at runtime:
fun helloWorld(continuation: Continuation<String>): Any
The return parameter of the method becomes Object
an object. The original function has one more Continuation
parameter, and the generic type in it is the return value type of the original function. Therefore, the method of Retrofit
judgment is also to judge whether the last parameter is , as mentioned in the previous article.Kotlin
suspend
Continuation
HttpServiceMethod#parseAnnotations()
Start analyzing from implementation:
static <ResponseT, ReturnT> HttpServiceMethod<ResponseT, ReturnT> parseAnnotations(
Retrofit retrofit, Method method, RequestFactory requestFactory) {
// 是否是 Kotlin 协程 suspend 方法
boolean isKotlinSuspendFunction = requestFactory.isKotlinSuspendFunction;
// suspend 方法返回值类型是否是 Response
boolean continuationWantsResponse = false;
// suspend 方法返回 Response Body 是否可以为空
boolean continuationBodyNullable = false;
// 获取方法中的注解
Annotation[] annotations = method.getAnnotations();
Type adapterType;
// 如果是 Kotlin suspend 函数
if (isKotlinSuspendFunction) {
Type[] parameterTypes = method.getGenericParameterTypes();
// 获取 Continuation 参数中的泛型的类型,也就是协程方法返回值的类型
Type responseType =
Utils.getParameterLowerBound(
0, (ParameterizedType) parameterTypes[parameterTypes.length - 1]);
// 如果返回值的类型是 Retrofit 中的 Response
if (getRawType(responseType) == Response.class && responseType instanceof ParameterizedType) {
// 获取 Response<T> 中的泛型类型,也就是 Response Body 需要转换的类型
// Unwrap the actual body type from Response<T>.
responseType = Utils.getParameterUpperBound(0, (ParameterizedType) responseType);
// 标记想要 Response
continuationWantsResponse = true;
} else {
// TODO figure out if type is nullable or not
// Metadata metadata = method.getDeclaringClass().getAnnotation(Metadata.class)
// Find the entry for method
// Determine if return type is nullable or not
}
// suspend 方法的 adatperType 统一为 Call
adapterType = new Utils.ParameterizedTypeImpl(null, Call.class, responseType);
// 添加 @SkipCallbackExecutor 注解表明后续 CallBack 回调线程在主线程(后面我们会看到这部分代码)
annotations = SkipCallbackExecutorImpl.ensurePresent(annotations);
} else {
// 非 suspend 的方法
// 直接方法的返回值类型作为 adapterType
adapterType = method.getGenericReturnType();
}
// 获取 adapterType 对应的 CallAdapter,也就是通过 CallAdapterFactory 获取对应 adapterType 的实例
CallAdapter<ResponseT, ReturnT> callAdapter =
createCallAdapter(retrofit, method, adapterType, annotations);
// 通过 CallAdapter 获取 ResponseBody 转换后的类型
Type responseType = callAdapter.responseType();
// Response Type不允许是 OkHttp 中的 Response
if (responseType == okhttp3.Response.class) {
throw methodError(
method,
"'"
+ getRawType(responseType).getName()
+ "' is not a valid response body type. Did you mean ResponseBody?");
}
// Response Type 不允许是 Retrofit 中的 Response
if (responseType == Response.class) {
throw methodError(method, "Response must include generic type (e.g., Response<String>)");
}
// TODO support Unit for Kotlin?
// 如果是 HEAD 请求,Response Type 只能是 Void
if (requestFactory.httpMethod.equals("HEAD") && !Void.class.equals(responseType)) {
throw methodError(method, "HEAD method must use Void as response type.");
}
// 获取 ResponseBody 到 Response Type 的 Converter
Converter<ResponseBody, ResponseT> responseConverter =
createResponseConverter(retrofit, method, responseType);
// 这个 okhttp3.Call.Factory 其实就是 OkhttpClient
okhttp3.Call.Factory callFactory = retrofit.callFactory;
if (!isKotlinSuspendFunction) {
// 非 suspend 函数
return new CallAdapted<>(requestFactory, callFactory, responseConverter, callAdapter);
} else if (continuationWantsResponse) {
//noinspection unchecked Kotlin compiler guarantees ReturnT to be Object.
// 返回值为 Response 的 suspend 函数
return (HttpServiceMethod<ResponseT, ReturnT>)
new SuspendForResponse<>(
requestFactory,
callFactory,
responseConverter,
(CallAdapter<ResponseT, Call<ResponseT>>) callAdapter);
} else {
//noinspection unchecked Kotlin compiler guarantees ReturnT to be Object.
// 返回值不为 Response 的 suspend 函数。
return (HttpServiceMethod<ResponseT, ReturnT>)
new SuspendForBody<>(
requestFactory,
callFactory,
responseConverter,
(CallAdapter<ResponseT, Call<ResponseT>>) callAdapter,
continuationBodyNullable);
}
}
Let’s clarify what was mentioned above adapterType
(some local codes are also called returnType
) and responseType
. There are two situations here, one is a coroutine method, and the other is not a coroutine method. Let me give you an example.
Coroutine method:
interface MyService {
@GET("/foo1")
suspend fun foo1(): String
@GET("/foo2")
suspend fun foo2(): Response<String>
}
The fix in the coroutine method adapterType
is Call
(the here Call
is Retrofit
defined in , pay attention to the difference OkHttp
with Call
the , the interface Retrofit
in is similar to the , but is simply encapsulated)Call
OkHttp
Call
The above foo1()
and foo2()
theirs responseType
are all String
, that is, the coroutine method foo1()
in the source code does not require , but the coroutine method that requires .Response
foo2()
Response
Non-coroutine method:
interface MyService {
@GET("/foo1")
fun foo1(): Call<String>
@GET("/foo2")
fun foo2(): Single<String>
}
foo1()
’s adapterType
is Call
, and foo2()
’ adapterType
s is Single
, theirs responseType
are String
.
Through the above examples, I believe you can already distinguish between adapterType
and in different situations responseType
. Then let's analyze the logic in the source code.
Coroutine method:
- If the return value
Response
is then it isResponse
the coroutine method that requires , and its isadatperType
fixed toCall
,responseType
which isResponse
the generic type in . - If the return value is not
Response
, then it is an unnecessaryResponse
coroutine method.adapterType
Fixed toCall
,responseType
fixed to the coroutine method return value. - The coroutine method will add an annotation to the original annotation
@SkipCallbackExecutor
. This annotation indicates that the subsequent callback is in the main thread (we will see this logic later). If the thread in is not addedOkHttp
by default.Dispatcher
Non-coroutine method:
- The return type of the method is directly
adapterType
, and the generic type of the return type isresponseType
(to be precise, it isCallAdapter
returned, but this condition is almost always met.)
Common logic for coroutine methods and non-coroutine methods:
- Obtain
adapterType
the corresponding instanceCallAdapter
, that is ,CallAdapter
obtain the corresponding instance through , the default only supports ( ).adapterType
CallAdapter
CallAdapterFactory
CallAdapter
RxJava
LiveData
CallAdapterFactory
Call
Retrofit
- Must be empty if
HEAD
requested .responseType
responseType
It can't beReponse
. (Including those inOkHttp
and )Retrofit
Response
- Get
RequestBody
transferred .responseType
_Converter
- Build different
ServiceMethod
implementations based on different method types. Ordinary methods:CallAdapted
; Coroutine requiresResposne
:SuspendForResponse
; Coroutine does not requireResponse
:SuspendForBody
.
Implementation of OkHttpCall
As mentioned in the previous section, all method types ServiceMethod
are implemented by the three objects of CallAdapted
, SuspendForResponse
and , SuspendForResponse
and their common parent class is HttpServiceMethod
, and HttpServiceMethod
the parent class of is ServiceMethod
.
The final method call is through ServiceMethod#invoke()
the method, which is an abstract method, and the implementation is HttpServiceMethod#invoke()
the method. Let's take a look at its implementation first:
@Override
final @Nullable ReturnT invoke(Object[] args) {
Call<ResponseT> call = new OkHttpCall<>(requestFactory, args, callFactory, responseConverter);
return adapt(call, args);
}
We see that this constructs an Call
object, and its implementation class is OkHttpCall
, which is similar to the one OkHttp
in . The method is an asynchronous call or a synchronous call. If yes , the final returned implementation is , and we can see this part of the code when we analyze the code later .ReallCall
enqueue()
execute()
adapterType
Call
Call
OkHttpCall
DefaultCallAdapterFactory
OkHttpCall
Synchronous call
Let’s first look at the implementation of the synchronous call method OkHttpCall#execute()
:
@Override
public Response<T> execute() throws IOException {
okhttp3.Call call;
synchronized (this) {
if (executed) throw new IllegalStateException("Already executed.");
executed = true;
// 构建 OkHttp 中的 Call
call = getRawCall();
}
if (canceled) {
call.cancel();
}
// 解析 OkHttp 的 Response 为 Retrofit 的 Response
return parseResponse(call.execute());
}
Use getRawCall()
the method to build OkHttp
in Call
, execute it synchronously Call#execute()
( OkHttp
), and then use parseResponse()
the method to get the parsed OkHttp
in Response
to be Retrofit
in Response
.
First look at getRawCall()
the source code of the method:
@GuardedBy("this")
private okhttp3.Call getRawCall() throws IOException {
okhttp3.Call call = rawCall;
// 如果已经创建 OkHttp.Call 直接使用。
if (call != null) return call;
// Re-throw previous failures if this isn't the first attempt.
if (creationFailure != null) {
if (creationFailure instanceof IOException) {
throw (IOException) creationFailure;
} else if (creationFailure instanceof RuntimeException) {
throw (RuntimeException) creationFailure;
} else {
throw (Error) creationFailure;
}
}
// Create and remember either the success or the failure.
try {
// 创建 OkHttp.Call
return rawCall = createRawCall();
} catch (RuntimeException | Error | IOException e) {
throwIfFatal(e); // Do not assign a fatal error to creationFailure.
creationFailure = e;
throw e;
}
}
Creation OkHttp.Call
is through createRawCall()
the method, let's continue to see the implementation:
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;
}
callFactory
The implementation of the above is OkHttpClient
actually to call OkHttpClient#newCall()
the method to create one RealCall
; and RequestFactory
, as we introduced in the first and second articles, OkHttp.Request
the objects used to generate after parsing various annotations, we see that it The parameters are passed to RequestFactory#create()
the method, and then the object is generated OkHttp.Request
. Let's take a look at its source code implementation:
okhttp3.Request create(Object[] args) throws IOException {
@SuppressWarnings("unchecked") // It is an error to invoke a method with the wrong arg types.
// 获取所有的参数处理器
ParameterHandler<Object>[] handlers = (ParameterHandler<Object>[]) parameterHandlers;
int argumentCount = args.length;
// 参数的数量不正确,报错。
if (argumentCount != handlers.length) {
throw new IllegalArgumentException(
"Argument count ("
+ argumentCount
+ ") doesn't match expected count ("
+ handlers.length
+ ")");
}
// 构建一个 RequestBuilder
RequestBuilder requestBuilder =
new RequestBuilder(
httpMethod,
baseUrl,
relativeUrl,
headers,
contentType,
hasBody,
isFormEncoded,
isMultipart);
// 如果是协程方法,需要参数数量减一。
if (isKotlinSuspendFunction) {
// The Continuation is the last parameter and the handlers array contains null at that index.
argumentCount--;
}
List<Object> argumentList = new ArrayList<>(argumentCount);
// 遍历参数处理器,填充请求的数据到 RequestBuilder 中。
for (int p = 0; p < argumentCount; p++) {
argumentList.add(args[p]);
handlers[p].apply(requestBuilder, args[p]);
}
// 构建 okHttp.Request
return requestBuilder.get().tag(Invocation.class, new Invocation(method, argumentList)).build();
}
Briefly describe:
- Check whether
ParameterHandler
the quantity of andargs
the quantity of are consistent. - Build
RequestBuilder
object. - If it is
suspend
a function, you need to reduce the number of parameters by one. The reason is as described in my previoussuspend
description of the function. - Traverse
ParameterHandler
and fill the requested data into the RequestBuilder. - Construct
Request
.
Let’s look at the implementation OkHttp.Response
of the parsing parseResponse()
method:
Response<T> parseResponse(okhttp3.Response rawResponse) throws IOException {
// 提取 Response Body
ResponseBody rawBody = rawResponse.body();
// Remove the body's source (the only stateful object) so we can pass the response along.
// 构建新的 Response,但是移除了 ResponseBody
rawResponse =
rawResponse
.newBuilder()
.body(new NoContentResponseBody(rawBody.contentType(), rawBody.contentLength()))
.build();
int code = rawResponse.code();
// 不为 200+,请求都表示失败
if (code < 200 || code >= 300) {
try {
// Buffer the entire body to avoid future I/O.
ResponseBody bufferedBody = Utils.buffer(rawBody);
return Response.error(bufferedBody, rawResponse);
} finally {
rawBody.close();
}
}
// 204,205 RequestBody 固定为空
if (code == 204 || code == 205) {
rawBody.close();
return Response.success(null, rawResponse);
}
// 构建新的 ResponseBody,拦截读取过程中的异常
ExceptionCatchingResponseBody catchingBody = new ExceptionCatchingResponseBody(rawBody);
try {
// 通过 Converter 将 ResponseBody 转换成 responseType
T body = responseConverter.convert(catchingBody);
return Response.success(body, rawResponse);
} catch (RuntimeException e) {
// If the underlying source threw an exception, propagate that rather than indicating it was
// a runtime exception.
// 抛出读取 ResponseBody 时发生的错误。
catchingBody.throwIfCaught();
throw e;
}
}
To summarize the parsing RepsonseBody
process:
- Extract
ResposneBody
, build a new oneResponse
, but remove itResponseBody
. Response Code
If it is not 200+, it means the request failed.Response Code
It is 204 or 205 and cannot be readResponseBody
.- Pass
Converter
,ResponseBody
convert toresponseType
.
OkHttpCall
asynchronous call
Look directly OkHttpCall#enqueue()
at the source code:
@Override
public void enqueue(final Callback<T> callback) {
Objects.requireNonNull(callback, "callback == null");
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 {
// 创建 `OkHttp.Call`
call = rawCall = createRawCall();
} catch (Throwable t) {
throwIfFatal(t);
failure = creationFailure = t;
}
}
}
if (failure != null) {
// 回调 `OkHttp.Call` 创建失败
callback.onFailure(this, failure);
return;
}
if (canceled) {
call.cancel();
}
// OkHttp.Call 的异步调用
call.enqueue(
new okhttp3.Callback() {
@Override
public void onResponse(okhttp3.Call call, okhttp3.Response rawResponse) {
Response<T> response;
try {
// 解析 Response
response = parseResponse(rawResponse);
} catch (Throwable e) {
throwIfFatal(e);
callFailure(e);
return;
}
try {
// 回调成功
callback.onResponse(OkHttpCall.this, response);
} catch (Throwable t) {
throwIfFatal(t);
t.printStackTrace(); // TODO this is not great
}
}
@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) {
throwIfFatal(t);
t.printStackTrace(); // TODO this is not great
}
}
});
}
The code for asynchronous calls is also createRawCall()
generated through the method OkHttp.Call
and parseResponse()
parsed through the method OkHttp.Response
. These two key methods have been analyzed during synchronous calls. The code for asynchronous calls is also very simple. Just take a look at it yourself.
OkHttpCall
Summarize
OkHttpCall
That is, the core encapsulation of requests Retrofit
in , and the default implementation also directly returns the current implementation. Like other task encapsulation, such as , etc., they are also based on the implementation of deformation.Http
DefaultCallAdapterFactory
OkHttpCall
Call
RxJava
LiveData
OkHttpCall
Implementation of CallAdapted
The proxy's method call ServiceMethod#invoke()
is implemented by calling HttpServiceMethod#invoke()
the method, which creates an OkHttpCall
object (analyzed earlier), and then calls adapt()
the method to pass this object to its implementation class, which adapt()
is an abstract method, CallAdapted
and SuspendForResponse
they SuspendForBody
implement different adapt()
methods respectively.
Take a look at CallAdapted#adapt()
the implementation of the method:
@Override
protected ReturnT adapt(Call<ResponseT> call, Object[] args) {
return callAdapter.adapt(call);
}
Directly call callAdapter
the adapt()
method, which callAdapter
is obtained adapterType
from CallAdapterFactory
(it can be Retrofit
customized when building the instance). Our Call
type adapterType
is obtained directly DefaultCallAdapterFactory
from callAdapter
.
Let’s look directly at DefaultCallAdapterFactory#get()
the implementation of the method:
@Override
public @Nullable CallAdapter<?, ?> get(
Type returnType, Annotation[] annotations, Retrofit retrofit) {
// adapterType 必须为 Call
if (getRawType(returnType) != Call.class) {
return null;
}
// Call 中必须有具体的泛型类型
if (!(returnType instanceof ParameterizedType)) {
throw new IllegalArgumentException(
"Call return type must be parameterized as Call<Foo> or Call<? extends Foo>");
}
// 获取 ReponseType
final Type responseType = Utils.getParameterUpperBound(0, (ParameterizedType) returnType);
// 判断是否有 @SkipCallbackExecutor 注解,如果有就表示需要在主线程回调,前面在分析协程那部分是提到过。
final Executor executor =
Utils.isAnnotationPresent(annotations, SkipCallbackExecutor.class)
? null
: callbackExecutor;
// 构建匿名的 CallAdapter 对象
return new CallAdapter<Object, Call<?>>() {
@Override
public Type responseType() {
return responseType;
}
@Override
public Call<Object> adapt(Call<Object> call) {
// 如果不需要在新的线程中回调,直接返回 OkHttpCall,反之用ExecutorCallbackCall 封装一下。
return executor == null ? call : new ExecutorCallbackCall<>(executor, call);
}
};
}
To summarize the above logic:
adapterType
meansCall
that the current oneCallAdapterFactory
can be processed, otherwise it cannot be processed, and if the return is empty, the system will find the next one that can be processedCallAdapterFacotry
.- Get
Call
the generic type of asresponseType
. - If there is
@SkipCallbackExecutor
an annotation (mentioned when talking about coroutine methods), it means that the callback needs to be made on the main thread. - Construct an anonymous
CallAdapter
object.adapt()
If the method needs to be called back in the main thread, it needs to beExecutorCallbackCall
encapsulatedOkHttpCall
. OtherwiseOkHttpCall
, it will be returned directly.adapt()
The return value of the method is alsoServiceMethod#invoke()
the return value of the final method and the return value of the final proxy method.
Implementation of SuspendForResponse
Let’s look directly at SuspendForResponse#adapt()
the implementation of the method:
@Override
protected Object adapt(Call<ResponseT> call, Object[] args) {
// 这个 callAdapter 一定是由 DefaultCallAdapterFactory 生成的,因为协程方法的 adaptType 一定是 Call
call = callAdapter.adapt(call);
//noinspection unchecked Checked by reflection inside RequestFactory.
// 获取协程方法必须的 Continuation 参数
Continuation<Response<ResponseT>> continuation =
(Continuation<Response<ResponseT>>) args[args.length - 1];
// See SuspendForBody for explanation about this try/catch.
try {
// 调用协程方法,这个实现的代码是 Kotlin
return KotlinExtensions.awaitResponse(call, continuation);
} catch (Exception e) {
return KotlinExtensions.suspendAndThrow(e, continuation);
}
}
To summarize the above code:
callAdapter
Must beDefaultCallAdapterFactory
generated by , because the coroutine methodadaptType
must beCall
.- Get
Continuation
the parameters, which are required for coroutine method calls. KotlinExtensions.awaitResponse()
Coroutine methods are called via the method, which isKotlin
written by .
Continue to look at KotlinExtensions.awaitResponse()
the source code:
suspend fun <T> Call<T>.awaitResponse(): Response<T> {
return suspendCancellableCoroutine {
continuation ->
continuation.invokeOnCancellation {
cancel()
}
enqueue(object : Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
continuation.resume(response)
}
override fun onFailure(call: Call<T>, t: Throwable) {
continuation.resumeWithException(t)
}
})
}
}
If you don’t know Callback
the conversion suspend
method, it is recommended to search for information online, OkHttpCall#enqueue()
trigger the request directly through the method, and then convert it into the coroutine method. Pay attention to the ( ) suspend
returned directly here .Response
Retrofit
Implementation of SuspendForBody
SuspendForBody#adapt()
Implementation of method:
@Override
protected Object adapt(Call<ResponseT> call, Object[] args) {
call = callAdapter.adapt(call);
//noinspection unchecked Checked by reflection inside RequestFactory.
Continuation<ResponseT> continuation = (Continuation<ResponseT>) args[args.length - 1];
try {
return isNullable
? KotlinExtensions.awaitNullable(call, continuation)
: KotlinExtensions.await(call, continuation);
} catch (Exception e) {
return KotlinExtensions.suspendAndThrow(e, continuation);
}
}
It can be said that it is almost SuspendForResponse
exactly the same as the code in , except that an empty judgment is added. The coroutine method call that is not empty in the end is KotlinExtensions.await()
the method called. Let's take a look at its implementation:
@JvmName("awaitNullable")
suspend fun <T : Any> Call<T?>.await(): T? {
return suspendCancellableCoroutine {
continuation ->
continuation.invokeOnCancellation {
cancel()
}
enqueue(object : Callback<T?> {
override fun onResponse(call: Call<T?>, response: Response<T?>) {
if (response.isSuccessful) {
continuation.resume(response.body())
} else {
continuation.resumeWithException(HttpException(response))
}
}
override fun onFailure(call: Call<T?>, t: Throwable) {
continuation.resumeWithException(t)
}
})
}
}
The implementation is basically the same KotlinExtensions.awaitResponse()
as , except that the final return value is taken ResponseBody
(converted into returnType
) ResponseBody
.
Summarize
Retrofit
The two core logics of Http Request
the construction of and Http
the construction of the request task have also been run through, and basically the entire process has been strung together. I am going to write another article later to introduce Moshi
how the and ConverterFactory
of work, for those who want to customize and Some references for classmates.RxJava
CallAdapterFactory
ConverterFactory
CallAdapterFactory
at last
If you want to become an architect or want to break through the 20-30K salary range, then don't be limited to coding and business, you must be able to select and expand, and improve your programming thinking. In addition, good career planning is also very important, and learning habits are important, but the most important thing is to be able to persevere. Any plan that cannot be implemented consistently is empty talk.
If you have no direction, here is a set of "Advanced Notes on the Eight Modules of Android" written by a senior architect at Alibaba to help you systematically organize messy, scattered, and fragmented knowledge, so that you can systematically and efficiently Master various knowledge points of Android development.
Compared with the fragmented content we usually read, the knowledge points in this note are more systematic, easier to understand and remember, and are strictly arranged according to the knowledge system.
Welcome everyone to support with one click and three links. If you need the information in the article, just scan the CSDN official certification WeChat card at the end of the article to get it for free↓↓↓ (There is also a small bonus of ChatGPT robot at the end of the article, don’t miss it)