☰
Retrofit源码中的设计模式解析:动态代理、构建者与适配器实战
2026/10/13 5:49:38 网站建设 项目流程

开始写作

做Android开发的一定绕不开网络请求,而Retrofit作为目前最主流的网络框架,光会用可不够。我最近在刷Retrofit的源码,已经写了四篇系列文章,从基本流程到请求流程、动态代理、并发异步切换都分析了一遍。今天这篇是系列第五篇,重点剖析Retrofit源码中的设计模式。

这篇不讲概念,而是拿真实源码说话,把Retrofit用到的几个核心设计模式(动态代理、构建者、外观、适配器、装饰者、策略)逐一拆开,讲清楚它在源码的哪个位置、为什么这么设计、换一种设计行不行,以及这些模式在业务代码里能怎么借鉴。适合对Retrofit用得熟、但想深入源码一探究竟的开发者,也适合正在准备面试、需要从设计模式角度理解框架的人。

先说一下,这篇文章不是标准的“23种设计模式扫盲”,也不是单讲Retrofit怎么用,而是把两者结合起来,通过源码实例反推设计思路。看完这片,你以后再看其他框架源码,也能带着设计模式的视角去分析。

1. 骨架与入口:动态代理模式

1.1 动态代理在Retrofit中的核心地位

动态代理是Retrofit的灵魂,也是整个框架的入口。你平时定义接口、写注解、调用方法,Retrofit通过动态代理拦截这些接口方法调用,自动转换成HTTP请求。这一步没有代理模式是绝对做不到的。

Retrofit的create方法核心代码大致是:

public <T> T create(final Class<T> service) { return (T) Proxy.newProxyInstance( service.getClassLoader(), new Class<?>[] { service }, new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 拿到接口方法上的注解、参数 ServiceMethod<?> serviceMethod = loadServiceMethod(method); OkHttpCall<Object> okHttpCall = new OkHttpCall<>(serviceMethod, args); return serviceMethod.callAdapter.adapt(okHttpCall); } }); }

我简化了很多代码,但骨架就是这样的。Proxy.newProxyInstance传入三个参数:类加载器、接口数组、InvocationHandler实现。当你调用api.getUser("123")时,实际上并没有直接执行你写的这个接口方法——因为接口根本没有实现类——而是交给了InvocationHandler的invoke方法统一处理。

这里要理解一个关键点:为什么Retrofit不直接让你传一个实现好的类,而非要用动态代理?

答案是灵活性和解耦。接口只是声明约束,注解声明请求类型、参数、路径,真正在调用时通过代理统一解析,这让你在业务层完全不用去关心HTTP细节,只要写好接口就行。如果直接实现类,你每一接口方法都需要手写HTTP请求逻辑,Retrofit就没有存在的必要了。

我在实际调试Retrofit源码时,发现一个容易忽略的细节:create方法里对service接口做了校验,如果不是接口、或者接口里包含继承自其他接口的方法,会直接抛异常。这样设计的原因很好理解——动态代理只代理接口,如果传入的是类,Proxy.newProxyInstance根本工作不了。而限制接口继承,是为了避免复杂的继承关系导致方法冲突,让注解解析的逻辑保持简单可控。

1.2 动态代理的性能与优化思路

用过动态代理的人都会担心一个问题:每次调用接口方法都要经过反射和代理,性能会不会很差?

在实践中积累的经验是,Retrofit在这方面的处理很有一套。它不会每次都重新解析接口方法和注解,而是用ConcurrentHashMap把Method对应的ServiceMethod缓存起来。同一接口的方法第一次调用时解析,后续直接取缓存,这样动态代理的开销只在首次调用时存在。

给我们自己写代码带来的启示是:动态代理不是烂大街的性能杀手,凡是耗时的解析、构建过程,能做成缓存的就做缓存,这和动态代理配合起来性能表现很好。

还有一个小细节。invoke方法中有一个if判断,专门处理Object自带的方法(equals、hashCode、toString)。为什么会需要这个分支?因为动态代理生成的代理对象,调用toString时也会走到invoke里,如果你不特殊处理,把它当成普通接口方法去解析注解,那toString上没有任何HTTP注解,必然抛异常。

这个细节在实际跨系统对接中遇到过:有同事自定义了接口,不小心写了一个默认方法,结果调用时报错,一时没排查出来。最终定位到是动态代理对Object方法和default方法的处理逻辑不同,默认方法会有单独的逻辑,需要额外走一段代码来处理。

1.3 动态代理源码阅读顺序建议

如果你是第一次阅读这段源码,建议从create方法入手,跟着ServiceMethod的构建流程往下看。可以先不看具体的注解解析细节,先理解动态代理的拦截过程。

我看源码的经验是,先抓住大框架,再填细节。大框架就是:接口方法 → 代理拦截 → 解析ServiceMethod → 构建OkHttpCall → 适配返回类型。这五步就是Retrofit处理一次请求的主干。然后把每个分支细节(解析注解、参数、返回类型适配)作为子模块单独看,逐个击破。

2. 构建者模式:链式调用是怎么设计出来的

2.1 Retrofit.Builder的核心角色

Retrofit有一个对外暴露的Builder静态类,支持链式调用。你在业务代码里经常这么写:

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

这就是构建者模式非常典型的应用。Retrofit本身构造参数非常多:baseUrl、callFactory、converterFactories、callAdapterFactories、callbackExecutor、validateEagerly、platform等等。

如果用一个构造函数把这些参数全部传进去,代码可读性会非常差,而且很多参数有默认值,使用者只需要配置少数几个。构建者模式解决的就是这种“多参数、可选择性配置”的问题。

Builder内部持有和Retrofit一样的字段,通过方法逐一把值设进去,最后在build()方法中进行校验、组装、返回正式的Retrofit实例。

我看过很多源码,这个套路在框架开发中太常见了,比如OkHttp的OkHttpClient.Builder、Glide的GlideBuilder、甚至Android系统API里的AlertDialog.Builder,都是同一个思路。

2.2 build方法的校验逻辑

build()方法不只是简单地把Builder的值拷贝给Retrofit。在正式构建之前,它做了几层校验,这些校验是运行时安全的最后防线:

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

先说baseUrl校验。如果没配置baseUrl,直接抛异常,这是最基础的保护。第二个重要校验是platform的处理,Retrofit通过Platform.get()判断运行在Android还是Java环境,进而决定默认的callbackExecutor——Android上默认使用主线程的Handler来回调。

validateEagerly这个参数值得一提。默认情况下Retrofit是“懒加载”模式,即接口方法第一次被调用时才解析注解。但如果你在构建时设置validateEagerly(true),Retrofit会在build()后立即遍历接口的所有方法,提前解析验证注解是否正确。这样可以把接口配置错误的问题在应用启动阶段就暴露出来,而不是线上跑到某个功能才崩溃。

我在开发中遇到过一娄典型问题:某接口注解写错了,@POST写成了@GET,因为那个功能很少用到,一直没有被发现,上线后用户点击某个入口才闪退。后来我学聪明了,在关键模块初始化Retrofit时开启validateEagerly,这样的低级错误在启动时就能暴露。

Retrofit retrofit = new Retrofit.Builder() .baseUrl("https://api.example.com/") .validateEagerly(true) // 启动时验证所有接口注解 .build();

2.3 构建者模式在业务代码中的借鉴

构建者模式不是框架的专利,你自己的业务代码在参数特别多的时候也可以借鉴。举一个具体例子:创建一个推送配置类,有服务器地址、是否自动重连、心跳间隔、通知栏是否显示、是否只在Wifi下连接等十几个参数。

你要是写一个满参数构造函数,调用方每次都要对着文档填参数,顺序错了还不好排查。改成Builder之后,调用方只需要关心自己要配置的那几项,其他都用默认值。代码的可读性和可维护性会提高一个档次。

不过有个心得:构建者模式不是所有场景都合适,参数少(比如少于5个)的时候反而没必要,直接构造函数更简洁。而且Builder模式下,Builder和Target之间存在重复字段,需要两个类保持同步,代码量会增加不少,项目小的时候强行使用反而得不偿失。

3. 外观模式:Retrofit背后的另一个框架

3.1 外观模式让复杂系统变得易用

Retrofit官网给它的定位就是一个HTTP客户端,但严格意义上说,Retrofit本身并不发起网络请求。真正发送网络请求、管理连接池、处理HTTP协议细节的是底层另一个成熟框架。Retrofit做的事情,是把底层框架复杂的实现细节隐藏起来,只暴露一个简洁的接口和注解方式给开发者。

这就是外观模式。你调用Retrofit,表面上看只需要定义接口、添加注解、调用方法,实际上背后涉及底层指定框架的连接池复用、HTTP头部构建、请求重试、连接超时管理等一系列复杂流程。

换成另一个角度理解:你不需要知道底层的具体连接细节,你只需要告诉Retrofit你要发什么样的请求,剩下的事情它帮你搞定。这就像你去餐厅吃饭,你只需要看着菜单点菜,不需要知道后厨怎么洗菜、怎么切菜、怎么炒菜。

Retrofit中关于这个模式的体现非常直观。你在Retrofit.Builder配置了client(okHttpClient),也可以在没配置的时候用Platform相关默认逻辑生成一个默认的请求客户端。真正访问时,OkHttpCall内部持有这个请求客户端并执行网络请求。对上层业务开发者,所有底层逻辑都是被封装起来的黑盒。

3.2 Retrofit如何充当门面

具体来说,Retrofit这个“门面”背后帮我们做了以下事情。

首先是请求层的封装,包括URL解析、Query参数拼接、请求体序列化、请求头设置。你定义接口时写@POST("user/login")和@Body LoginRequest,Retrofit把这些注解解析成真实的HTTP请求,然后交给底层执行。

其次是返回层的封装,包括响应体的解析、错误码的处理、转换器调用链的选择。服务器返回的JSON字符串,通过GsonConverterFactory转换成Java对象,这一层对外部也是透明的。

还有一个容易被忽略的点是线程切换。Retrofit的设计方式比较特殊,它的同步请求在调用线程直接执行,异步请求通过ExecutorCallbackCall包装后在Executor回调。Android上的默认回调是在主线程执行的,这个细节业务层往往感知不到,因为框架都已经在你配置Builder时自动设定好了。

这个门面模式的应用,让Retrofit非常适合做项目中的网络层抽象。团队里可以基于Retrofit再封装一层更贴合业务的网络工具类,屏蔽掉一些Retrofit本身的配置细节,提供统一的上报、鉴权、日志打印能力,这类实践在稍大的业务中很有用。

3.3 外观模式的边界与“泄露”

这里说一个重要的经验。外观模式虽然是做封装,但架不住某些情况下具体细节的“泄露”。Retrofit并不把所有底层行为都重写,它默认让你传入一个网络请求客户端实例。这意味着你可以在请求链路的底层进行拦截、自定义DNS、自定义TLS握手等操作。这对有特殊网络需求的团队是好事,但也侧面说明外观模式不等于“包括一切”,有些扩展点仍然需要暴露。

我在项目里遇到过一次连接超时问题,排查了很久没有结论。后来检查底层请求客户端的配置,发现自定义的客户端没有设置读超时,默认设置不匹配业务预期。所以,你以为你在和Retrofit打交道,实际上底层客户端的配置是另一套体系,这个边界如果不清晰,容易出现定位半天找不到问题的情况。

使用Retrofit封装网络层时,我建议把底层请求客户端的配置也纳入你的“关注范围”,不要觉得Retrofit已经都帮你搞定了,否则出问题时你会无从下手。

4. 适配器模式与装饰者模式:返回结果如何变出花样

4.1 CallAdapter先适配再返回

Retrofit把每次请求封装成了Call<T>,但开发中我们经常会希望接口返回的不是Call<T>,而是Observable<T>(配合RxJava)、Deferred<T>(配合协程)、或者直接就是T本身。

这个转换能力就是通过适配器模式实现的。Retrofit抽象出一个接口:

public interface CallAdapter<R, T> { Type responseType(); T adapt(Call<R> call); }

比如RxJava2CallAdapterFactory返回的CallAdapter,它的adapt方法把Call<T>转换成Observable<T>。当你在接口方法里声明返回类型是Observable<User>时,Retrofit会从callAdapterFactories里找能处理这个返回类型的CallAdapter,然后用它的adapt方法把Call进行转换。

这里“适配器”的含义要理解准确。它不是把一个类强转成另一个类,而是让两种不兼容的接口协同工作。Call<T>是一种数据结构,Observable<T>是另一种数据流结构,有一次性的Call,有可订阅的流,它们在语义上是不同的东西,适配器做的就是搭桥的工作。

我在实现自定义CallAdapter时,感受到的难点在于两个地方。一个是responseType()的确定,需要从方法泛型里解析出真实的响应类型。比如返回Observable<List<User>>,那responseType应该是List<User>而不是Observable<List<User>>。Retrofit内部有Utils工具类专门处理这种泛型解析,自己实现时这一点要格外小心,不然解析出错后续的Converter会找不到合适的转换器去反序列化。

另一个是取消机制的处理。Call本身有enqueue、execute、isCanceled、cancel等方法。RxJava的Observable有dispose的概念。适配器需要把Observable被订阅和取消的事件与Call的执行和取消关联起来,保证界面销毁时网络请求能及时取消。这个细节有些朋友在自定义适配器时会漏掉,导致请求泄漏。

一套完整的自定义CallAdapter需要做的事情相对多一些,但这也是理解Retrofit那个阶段能学到最多东西的练习。我会在后面的实操部分用一个简单案例演示。

4.2 装饰者模式:ExecutorCallbackCall的妙处

提到装饰者模式,Retrofit源码里有一个非常典型的实现:ExecutorCallbackCall。

先看一下它的整体逻辑。如果你没有自定义CallAdapterFactory,Retrofit默认使用BuiltInConverters和DefaultCallAdapterFactory或者ExecutorCallAdapterFactory(不同版本略有差异)。以ExecutorCallAdapterFactory为例,它的adapt方法返回了一个ExecutorCallbackCall,这个类实现了Call接口,内部持有另一个Call和一个Executor。

static final class ExecutorCallbackCall<T> implements Call<T> { private final Call<T> delegate; private final Executor callbackExecutor; ExecutorCallbackCall(Call<T> delegate, Executor callbackExecutor) { this.delegate = delegate; this.callbackExecutor = callbackExecutor; } @Override public void enqueue(final Callback<T> callback) { delegate.enqueue(new Callback<T>() { @Override public void onResponse(Call<T> call, Response<T> response) { callbackExecutor.execute(() -> { if (delegate.isCanceled()) return; callback.onResponse(ExecutorCallbackCall.this, response); }); } // ... }); } }

这个模式的设计很典型。原先底层的Call是一个普通的网络Call,它在网络库的回调线程中执行回调。但Android开发要求在UI线程更新界面,所以Retrofit用一个装饰类把原Call包了一层,在回调时把逻辑切换到主线程执行。原Call不需要关心线程调度,它只负责网络请求本身,装饰者(ExecutorCallbackCall)负责线程调度。

装饰者模式和适配器模式容易搞混。装饰者的核心是“增强”,在不改变原来接口的前提下添加新功能;适配器的核心是“转换”,把一种接口转换成另一种接口。ExecutorCallbackCall实现的还是Call接口,只是在原来基础上增加了线程切换的功能,所以是装饰者模式。

4.3 从源码中挖掘设计模式的分界

我从这个例子中获得的经验是:看源码时,要时时问自己“这段代码是让原功能变强了,还是把原功能换了一种形式”。如果是前者,那是装饰者;如果是后者,那是适配器。

Retrofit的CallAdapter体系如果作为一个整体看,几乎就是一个“适配器工厂模式”。每个CallAdapter.Factory都是一个工厂,负责生产适配器。Retrofit持有这些工厂列表,在需要的时候逐个询问“你能处理这个返回类型吗”。这其实也有一点责任链的味道,每一个工厂都有机会处理对应的类型,但谁先处理完谁就负责到底。

在面试的时候,如果能把ExecutorCallbackCall这个装饰者案例和CallAdapter这个适配器案例讲清楚,面试官会认为你有真正的源码阅读能力,而不是只是背诵标题和结论。

5. 策略模式与观察者模式:转化器与回调机制

5.1 Converter的精准策略

Retrofit把请求体的序列化和响应体的反序列化抽象成Converter接口:

public interface Converter<F, T> { T convert(F value) throws IOException; abstract class Factory { public Converter<?, RequestBody> requestBodyConverter(...) { return null; } public Converter<ResponseBody, ?> responseBodyConverter(...) { return null; } // ... } }

Converter.Factory是策略的提供者。GsonConverterFactory、MoshiConverterFactory、FastJsonConverterFactory等第三方库都实现了这个接口。你在Builder里添加哪个工厂,Retrofit在转换时就会使用对应的策略。

这正符合策略模式的定义——定义一系列的算法族,分别封装起来,让它们之间可以相互替换。Retrofit不需要在代码里硬编码“用Gson还是Moshi”,而是通过策略接口动态组合。

既然说到了策略模式,我补充一些实际使用时的选型建议。Gson的反射适配能力强,对复杂泛型和特殊类型都有不错的兼容,但性能相对一般。Moshi在Kotlin支持下性能更好,而且能够处理不可变对象。FastJson反序列化速度快,但历史上有一些安全方面的争议。实际项目的选择,要结合团队的Kotlin使用比例和性能要求来决定。

这里有一个我在项目中切转换策略时踩过的坑:在更新converterFactories时,Retrofit的查找顺序是“从第一个开始匹配,谁先返回非null就用谁”。如果你在列表前面加了自定义的Converter.Factory,后面再加GsonConverterFactory,那所有类型都会被前面的工厂先匹配,如果前面的工厂没有明确的不支持判断逻辑,可能会拦截掉原本Gson可以处理的类型,形成隐藏的Bug。所以添加多个ConverterFactory时,顺序是非常重要的。

5.2 回调机制中的观察者模式

Retrofit源码中还体现了一个设计思路:回调机制。虽然它不是传统意义上通过java.util.Observer这种形式实现的观察者模式,但Callback接口定义的两个回调方法,本质上是观察者思想的变体。网络请求发起后,通过Callback通知调用方“我成功了”或者“我失败了”。

有人认为这是回调模式,而不是观察者模式,但从发布订阅的角度来看,请求方是被观察者,回调方法是订阅者,网络请求是一个事件流,两者是观察者模式的灵活运用。

我也注意到在结合RxJava时订阅关系对应得更明显:Observable被订阅,然后产生网络请求,发送onNext和onError。如果你用协程,await会挂起等待结果,本质上也类似于注册一个回调在恢复时唤醒协程。

如果你要深入理解这个观察者机制的实现,可以重点看Retrofit如何管理Callback与线程切换的配合,这会帮助你更灵活地处理复杂的并发场景。

5.3 混合模式的组合拳

我经常给团队内部分享的一句话是:设计模式很少单独出现,越成熟的框架,越是多个模式混合在一起工作。Retrofit就是最好的例子。

拿一个简单的接口调用梳理一下:

  • 你定义接口@GET("user/{id}") fun getUser(@Path("id") id: String): Observable<User>
  • Retrofit通过动态代理拦截这次方法调用
  • 通过ServiceMethod的构建,解析这个方法的注解和泛型返回值
  • 通过CallAdapter.Factory找到能把Call<User>转成Observable<User>的适配器
  • 通过Converter.Factory找到能把ResponseBody转成User的转换器
  • 执行请求时通过装饰者和回调机制实现线程切换和异步通知

这整个流程,每个环节都能映射到一个设计模式。所以你问Retrofit用了什么设计模式,答案是它不是“用了什么”,而是“把多种模式编排成了一个完整的流水线”。设计模式只有组合起来才能发挥真正的威力。

6. 实操:从自定义CallAdapter开始写一个简化流程

6.1 怎么写一个CustomCallAdapter

理解了设计模式,如果不亲手写一遍,总感觉是纸上学来的。下面我用一个最简化的自定义CallAdapter来演示适配器模式在真实场景中的落地。这里假设我们要让Retrofit的接口方法直接返回一个叫HttpResult<T>的包装类。

先定义包装类:

public class HttpResult<T> { public int code; public String message; public T data; public static <T> HttpResult<T> success(T data) { HttpResult<T> result = new HttpResult<>(); result.code = 200; result.message = "ok"; result.data = data; return result; } }

然后实现CallAdapter:

public class HttpResultCallAdapter<R> implements CallAdapter<R, HttpResult<R>> { private Type responseType; public HttpResultCallAdapter(Type responseType) { this.responseType = responseType; } @Override public Type responseType() { return responseType; } @Override public HttpResult<R> adapt(Call<R> call) { try { Response<R> response = call.execute(); if (response.isSuccessful()) { R body = response.body(); return HttpResult.success(body); } HttpResult<R> result = new HttpResult<>(); result.code = response.code(); result.message = response.message(); return result; } catch (IOException e) { HttpResult<R> result = new HttpResult<>(); result.code = -1; result.message = e.getMessage(); return result; } } }

再定义工厂:

public class HttpResultCallAdapterFactory extends CallAdapter.Factory { @Override public CallAdapter<?, ?> get(Type returnType, Annotation[] annotations, Retrofit retrofit) { if (getRawType(returnType) != HttpResult.class) { return null; } Type responseType = getParameterUpperBound(0, (ParameterizedType) returnType); return new HttpResultCallAdapter<>(responseType); } }

最后把它注册进Retrofit:

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

接口定义就可以写成:

public interface ApiService { @GET("user/{id}") HttpResult<User> getUser(@Path("id") String id); }

6.2 实操中的教训

这个Demo看起来简单,但实操的时候有几个小细节值得留意。

第一个是getRawType(returnType) != HttpResult.class的判断,这个必须放在最前面。如果返回类型不是HttpResult,直接返回null表示这个CallAdapter处理不了,让后面的工厂继续尝试。

第二个是getParameterUpperBound(0, (ParameterizedType) returnType)。这里假设了HttpResult只有一个泛型参数,下标0取的就是这个泛型。如果你的包装类有多个泛型参数,需要根据实际下标取。这里的细节容易出现越界异常,调试时要逐个确认。

第三个是在adapt方法里,我偷懒直接用call.execute()同步请求。如果接口方法返回类型是HttpResult<T>,在Android里这样做会导致网络请求发生在主线程,Android不允许主线程网络请求,会直接抛异常。这就是为什么真正的框架实现会做线程切换,使用异步回调而不是同步执行。我在写Demo时只关注了适配器怎么转换类型,却忽略了线程模型,这是一个典型的反面教材。

正确的做法是把自定义CallAdapter和装饰者模式结合,包装成异步调用的形式,在回调里处理线程切换。这也是为什么框架里CallAdapter的实现会和ExecutorCallbackCall结合得那么紧密。

所以结论是:光会适配器模式还不够,要考虑线程模型、请求生命周期、取消机制、异常处理,一个完整的网络框架容易出现问题的点往往就在这些细节处。这也是读源码时最有价值的地方——你看到的不只是一个模式,而是一整套工程化方案。

7. 常见问题与排查技巧实录

7.1 接口方法声明了HttpResult却返回null

有段时间我以为是自己写错了,排查了半天。后来发现是getRawType(returnType) != HttpResult.class这个判断没有提前返回null。当前的Factory先处理了不是HttpResult的返回类型,并且强行创建了CallAdapter。由于Factory处理顺序是靠前的,所以Retrofit优先使用了它,结果转换逻辑不正确,返回了无法处理的数据。这个问题的排查思路是打印各个Factory返回的CallAdapter是谁,看哪一层在拦截。

做法很简单:在Factory的get方法里打印returnType,在adapt方法里打印实际返回的类,一步就能定位。

7.2 Retrofit Builder调用顺序的影响

Builder的调用顺序不会影响最终构建结果,build()方法会把所有字段统一组装。这一点和OkHttp的Builder类似,顺序无关,重点在于每个字段是否有效。所以不要担心链式调用顺序写错,真正需要注意的是addConverterFactory和addCallAdapterFactory的先后关系,它们存储到列表中的顺序决定了优先匹配的顺序,这和使用顺序无关,但和添加次数、类型有关。

7.3 装饰者模式引发的回调多次调用

如果在自定义CallAdapter以及装饰者中不小心调用了callback.onResponse多次,业务层会出现重复更新UI的问题。框架本身保证了回调只触发一次,但自己扩展时容易破坏这个保证。排查这个问题的方法是,在回调方法入口打印堆栈,查看触发来源。从我的经验来看,自定义装饰Call时一定要保持“每个事件只上报一次”的严谨风格。

7.4 自定义Converter优先级的坑

这个我在第五节提过,但还是要单独拎出来说。多个ConverterFactory按顺序匹配时,谁先返回非null用谁。如果你在Gson之前先添加了自定义转换器,且这个转换器对所有ResponseBody都返回一个非null的Converter,那Gson就永远不会工作。

有一种容易被忽略的情况是:自定义Converter只判断了特定类型(比如只处理String),其他类型返回null,那没问题;但如果你一股脑全部接管,后面的Gson就形同虚设。我的建议是,自定义Converter的responseBodyConverter方法里,一定要妥善判断,不是自己负责的类型就直接返回null。

8. 写在最后的体会

源码分析是一个细水长流的过程,我写这个系列的时候,每一篇都逼着自己从源码推演设计思路,收获确实很大。

每次回顾都会发现以前没有注意到的细节。比如动态代理的invoke方法对Object方法的特殊处理、装饰者模式在回调线程切换中的重要作用、策略模式在Converter选择上的精妙设计——这些都是用Retrofit时根本感知不到的。

如果你也想系统学习源码中的设计模式,我建议以Retrofit源码为起点,因为它的代码量不大、设计清晰、依赖相对少,每个模式都能对应有名字可查的实现,非常适合训练“源码+设计模式”的分析能力。

最后再分享一个小技巧:读源码时不要只是看,最好能把核心流程画出来,再把每个模式标注在对应位置。我习惯用笔记软件记录这些分析过程,等技术积累更深了,回头看会非常有收获。无论是面准备,还是真正的架构设计能力提升,这种方式都值得尝试。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询