Retrofit 源码阅读笔记(三)
第一篇文章中介绍了 Retrofit
的动态代理实现、方法注解解析等等内容:Android Retrofit 源码阅读笔记(一)。
第二篇文章中介绍了 Retrofit
是如何解析方法中的参数的信息:Android Retrofit 源码阅读笔记(二)。
本篇文章是系列文章中的第三篇,介绍 Retrofit
如何构建 Http
请求任务,我个人认为这部分代码是 Retrofit
代码中最复杂的一部分,也是最最核心的一部分,整理好心情准备上车。
Http 请求任务构建
先回顾一下之前的内容,动态代理的方法中都是通过调用 ServiceMethod#invoke()
方法来实现调用的,如果没有缓存的 ServiceMethod
对象,需要通过 ServiceMethod#parseAnnotations()
通过解析方法中的各种注解和方法中的各种参数类型返回值类型构建一个 ServiceMethod
对象,然后供代理方法调用。在 ServiceMethod#parseAnnotations()
方法中,会通过 RequestFactory#parseAnnotations()
方法解析生成一个 RequestFactory
对象,在它里面封装了构建 Http Request
的方法(前面的文章已经分析过了);然后通过 HttpServiceMethod.parseAnnotations()
方法解析生成一个 Http
请求的任务,也就是会返回动态代理中使用的 ServiceMethod
对象,这也是我们今天的主要内容。
在开始之前介绍一下 Kotlin
协程的 suspend
方法,以免后面的源码阅读困难。
假如我定义了这样一个 Kotiln
方法:
suspend fun helloWorld(): String
在经过 Kotlin
编译器处理后,在运行时上面的方法定义变成了如下:
fun helloWorld(continuation: Continuation<String>): Any
方法的返回参数变成了 Object
对象,原来的函数多了一个 Continuation
的参数,它里面的泛型类型就是原来的函数的返回值类型。所以 Retrofit
判断 Kotlin
的 suspend
方法也是通过判断最后一个参数是否是 Continuation
,前面的文章中有讲。
从 HttpServiceMethod#parseAnnotations()
实现开始分析:
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);
}
}
这里理一下上面说到的 adapterType
(有的地方代码也叫 returnType
) 和 responseType
。这里分两种情况,一种是协程方法,一种不是协程方法。我分别举个例子。
协程方法:
interface MyService {
@GET("/foo1")
suspend fun foo1(): String
@GET("/foo2")
suspend fun foo2(): Response<String>
}
协程方法中的 adapterType
固定是 Call
(这里的 Call
是 Retrofit
中定义的,注意和 OkHttp
中的 Call
区分,Retrofit
中的 Call
接口和 OkHttp
中的 Call
差不多,只是简单封装了下)
上面 foo1()
和 foo2()
他们的 responseType
都是 String
,foo1()
就是源码中的不需要 Response
的协程方法,而 foo2()
是需要 Response
的协程方法。
非协程方法:
interface MyService {
@GET("/foo1")
fun foo1(): Call<String>
@GET("/foo2")
fun foo2(): Single<String>
}
foo1()
的 adapterType
就是 Call
,而 foo2()
的 adapterType
就是 Single
,他们的 responseType
都是 String
。
通过上面的例子我相信你已经能够分清不同情况下的 adapterType
和 responseType
了,然后我们分析一下源码中的逻辑。
协程方法:
- 如果返回值为
Response
那么就是需要Response
的协程方法,它的adatperType
固定为Call
,responseType
为Response
中的泛型类型。 - 返回值如果不为
Response
,那么就是不需要Response
的协程方法。adapterType
固定为Call
,responseType
固定为协程方法返回值。 - 协程方法会在原来的注解上添加一个
@SkipCallbackExecutor
注解,这个注解表明后续的回调在主线程(后面我们会看到这个逻辑),如果不加默认在OkHttp
的Dispatcher
中的线程。
非协程方法:
- 方法的返回类型就直接是
adapterType
,返回类型的泛型类型就是responseType
(准确的来说是CallAdapter
返回的,但是几乎都满足这个条件。)
协程方法和非协程方法的通用逻辑:
- 获取
adapterType
对应的CallAdapter
,也就是通过CallAdapter
获取对应adapterType
的实例,这个CallAdapter
就是用来描述如何构建请求任务的,我们可以自定义CallAdapterFactory
来实现自定义的CallAdapter
,来实现自定义的任务封装(比如自定义的RxJava
和LiveData
的任务封装),默认的CallAdapterFactory
就只支持Call
(Retrofit
)。 - 如果是
HEAD
请求,responseType
必须为空。 responseType
不能够是Reponse
。 (包括OkHttp
和Retrofit
中的Response
)- 获取
RequestBody
转responseType
的Converter
。 - 根据不同的方法类型构建不同的
ServiceMethod
实现。普通方法:CallAdapted
;协程需要Resposne
:SuspendForResponse
;协程不需要Response
:SuspendForBody
。
OkHttpCall 的实现
上一节中说到,所有的方法类型的 ServiceMethod
,都是由 CallAdapted
、SuspendForResponse
和 SuspendForResponse
这三个对象实现,而他们的共同的父类就是 HttpServiceMethod
,而 HttpServiceMethod
的父类是 ServiceMethod
。
最终的方法调用是通过 ServiceMethod#invoke()
方法,这是一个抽象方法,实现是 HttpServiceMethod#invoke()
方法,我们先来看看它的实现:
@Override
final @Nullable ReturnT invoke(Object[] args) {
Call<ResponseT> call = new OkHttpCall<>(requestFactory, args, callFactory, responseConverter);
return adapt(call, args);
}
我们看到这个构建了一个 Call
对象,它的实现类是 OkHttpCall
,这个和 OkHttp
中的 ReallCall
是类似的。enqueue()
方法是异步调用,execute()
是同步调用,如果 adapterType
是 Call
,最后返回的 Call
的实现就是 OkHttpCall
,后面我们分析 DefaultCallAdapterFactory
的代码的时候能够看到这部分代码。
OkHttpCall
同步调用
先看看同步调用的方法 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());
}
通过 getRawCall()
方法来构建 OkHttp
中的 Call
,同步执行 Call#execute()
(OkHttp
),然后通过 parseResponse()
方法取解析 OkHttp
中的 Response
为 Retrofit
中的 Response
。
先看看 getRawCall()
方法的源码:
@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;
}
}
创建 OkHttp.Call
是通过 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;
}
上面的 callFactory
的实现就是 OkHttpClient
,其实就是调用了 OkHttpClient#newCall()
方法创建了一个 RealCall
;而 RequestFactory
,就是我们在第一篇文章和第二篇文章中介绍的,解析各种注解后,用来生成 OkHttp.Request
的对象,我们看到它把参数传递给了 RequestFactory#create()
方法,然后生成了 OkHttp.Request
对象,我们在看看它的源码实现:
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();
}
简单描述一下:
- 检查
ParameterHandler
的数量和args
的数量是否一致。 - 构建
RequestBuilder
对象。 - 如果是
suspend
函数,需要把参数的数量减一,原因看前面我对suspend
函数的描述。 - 遍历
ParameterHandler
,填充请求的数据到 RequestBuilder 中。 - 构建
Request
。
再看看解析 OkHttp.Response
的 parseResponse()
方法实现:
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;
}
}
总结一下解析 RepsonseBody
的流程:
- 提取
ResposneBody
,构建一个新的Response
,但是移除ResponseBody
。 Response Code
不为 200+ 都表示请求失败。Response Code
为 204 或者 205,不读ResponseBody
。- 通过
Converter
,将ResponseBody
转换成responseType
。
OkHttpCall
异步调用
直接看 OkHttpCall#enqueue()
的源码:
@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
}
}
});
}
异步调用的代码也是通过 createRawCall()
方法来生成 OkHttp.Call
,也是通过 parseResponse()
方法来解析 OkHttp.Response
,这两个关键方法在同步调用时都分析过了,异步调用的代码也非常简单,大家自己看看就行。
OkHttpCall
总结
OkHttpCall
也就是 Retrofit
中对 Http
请求的核心封装,默认的 DefaultCallAdapterFactory
的实现也是直接返回 OkHttpCall
当 Call
的实现。像其他的任务封装,例如 RxJava
、LiveData
等等,也都是要基于 OkHttpCall
的实现的变形。
CallAdapted 的实现
代理的方法调用通过调用 ServiceMethod#invoke()
方法实现,HttpServiceMethod#invoke()
方法中又会创建一个 OkHttpCall
对象(前面分析了),然后调用 adapt()
方法将这个对象传递给它的实现类,adapt()
是一个抽象方法,CallAdapted
,SuspendForResponse
和 SuspendForBody
他们分别实现了不同的 adapt()
方法。
看看 CallAdapted#adapt()
方法的实现:
@Override
protected ReturnT adapt(Call<ResponseT> call, Object[] args) {
return callAdapter.adapt(call);
}
直接调用 callAdapter
的 adapt()
方法,这个 callAdapter
是根据 adapterType
从 CallAdapterFactory
(它可以在构建 Retrofit
实例的时候自定义) 中获取的。我们的 Call
类型的 adapterType
就是直接从 DefaultCallAdapterFactory
中获取的 callAdapter
。
直接看看 DefaultCallAdapterFactory#get()
方法的实现:
@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);
}
};
}
总结一下上面的逻辑:
adapterType
为Call
表示当前的CallAdapterFactory
能够处理,反之不能处理,返回为空,系统就会查找下一个能够处理的CallAdapterFacotry
。- 获取
Call
的泛型类型作为responseType
。 - 如果有
@SkipCallbackExecutor
注解(讲协程方法时有讲到),表示需要在主线程回调。 - 构建一个匿名的
CallAdapter
对象,其中的adapt()
方法如果需要在主线程回调,就需要用ExecutorCallbackCall
来封装一下OkHttpCall
,反之直接返回OkHttpCall
,adapt()
方法的返回值也是最终ServiceMethod#invoke()
方法的返回值,也是最终代理的方法的返回值。
SuspendForResponse 的实现
我们直接看 SuspendForResponse#adapt()
方法的实现:
@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);
}
}
总结一下上面的代码:
callAdapter
一定是由DefaultCallAdapterFactory
生成的,因为协程方法的adaptType
一定是Call
。- 获取
Continuation
参数,协程方法调用一定要它。 - 通过
KotlinExtensions.awaitResponse()
方法调用协程方法,这是由Kotlin
写的。
继续看看 KotlinExtensions.awaitResponse()
源码:
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)
}
})
}
}
如果不知道 Callback
转 suspend
方法的同学,建议网上去找找资料,直接通过 OkHttpCall#enqueue()
方法触发请求的,然后转化成了协程 suspend
方法,注意这里直接返回的 Response
(Retrofit
)。
SuspendForBody 的实现
SuspendForBody#adapt()
方法的实现:
@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);
}
}
几乎可以说是和 SuspendForResponse
中的代码一模一样,只是加了一个空的判断,最终不为空的协程方法调用是调用的 KotlinExtensions.await()
方法,我们看看它的实现:
@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)
}
})
}
}
实现基本也是和 KotlinExtensions.awaitResponse()
一样,只是最后的返回值是取的 ResponseBody
(转换成 returnType
的 ResponseBody
)。
总结
Retrofit
的 Http Request
的构建和 Http
请求任务的构建两个核心逻辑也都跑通了,基本整个流程也都串起来了,后续准备再写一篇文章来介绍 Moshi
的 ConverterFactory
和 RxJava
的 CallAdapterFactory
是如何工作的,给想自定义 ConverterFactory
与 CallAdapterFactory
的同学一些参考。
最后
如果想要成为架构师或想突破20~30K薪资范畴,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。
如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。
相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照知识体系编排的。
欢迎大家一键三连支持,若需要文中资料,直接扫描文末CSDN官方认证微信卡片免费领取↓↓↓(文末还有ChatGPT机器人小福利哦,大家千万不要错过)
![](https://i-blog.csdnimg.cn/blog_migrate/8f03a8876422c29a00639c993b588a89.jpeg)