一、前言
责任链模式在客户端框架中出镜率很高,但不同框架的实现差异远比"链表依次调用"这个概括要大。OkHttp 用递归 + 索引推进来实现同步洋葱模型,HMRouter 在同步三值返回之上叠加了异步洋葱接口,ImageKnifePro 则用 C++ 虚函数 + 链表指针做了一套 bool 短路链。三者分别面对 HTTP 管线、路由导航、图片加载这三种场景,在"是否需要异步""如何表达终止语义""链的构建方式"上做出了不同取舍。
这篇文章从 OkHttp 的经典实现出发,对照 HMRouter 的 HMInterceptorChainImpl 源码和 ImageKnifePro 的 interceptor.h / interceptor.cpp 源码,梳理三套拦截器链的结构差异与演进脉络。
二、OkHttp Interceptor 的经典设计
OkHttp 的拦截器体系是 Android 社区最广泛引用的责任链实现。它的核心思路可以用三条规则概括:拦截器实现 Interceptor 接口的 intercept(Chain) 方法;Chain 对象持有拦截器列表和当前索引;调用 chain.proceed(request) 推进到下一个拦截器。
kotlin
interface Interceptor {
fun intercept(chain: Chain): Response
interface Chain {
fun request(): Request
fun proceed(request: Request): Response
}
}
RealInterceptorChain 的 proceed() 方法每次调用时把索引加一,构造一个新的 RealInterceptorChain 传给下一个拦截器的 intercept()。这个递归结构天然形成了洋葱模型------proceed() 之前的代码是请求阶段,proceed() 之后的代码是响应阶段。日志拦截器可以在 proceed() 前记录请求信息,proceed() 后记录响应耗时,两段逻辑写在同一个方法里。
kotlin
// 简化后的 proceed 逻辑
fun proceed(request: Request): Response {
val next = RealInterceptorChain(interceptors, index + 1, request)
val interceptor = interceptors[index]
return interceptor.intercept(next)
}
OkHttp 拦截器的几个设计特征值得注意。第一,proceed() 有返回值------它返回 Response,所以整条链是同步的,调用方阻塞等待最终结果。第二,拦截器可以不调用 proceed() 直接返回一个 Response,这就是短路------缓存命中时 CacheInterceptor 直接返回缓存响应,后续的网络拦截器不会执行。第三,拦截器可以修改 request 再传给 proceed(),也可以拿到 response 后做变换再返回,链上的每一环都能对请求和响应做拦截和篡改。
这套设计之所以成为经典,在于它把"推进""短路""修改"三种行为统一到 proceed() 这一个调用上,概念密度低,心智负担轻。
三、HMRouter 的拦截器链:同步三值 + 异步洋葱
HMRouter 是 HarmonyOS 的路由框架,它的拦截器系统经历过一次模型升级。早期版本只有同步拦截器,后来在 1.2.0-beta.0 版本引入了异步拦截器,两套机制共存于同一条链。
3.1 同步拦截器:handle() 三值返回
同步拦截器实现 handle() 方法,接收路由信息,返回一个 HMInterceptorAction 枚举:
typescript
export interface IHMInterceptor {
handle?(info: HMInterceptorInfo): HMInterceptorAction;
intercept?(chain: IHMInterceptorChain): Promise<void>;
}
export enum HMInterceptorAction {
DO_NEXT, // 放行,继续下一个拦截器
DO_REJECT, // 阻止路由跳转,终止链
DO_TRANSITION // 跳过后续拦截器,直接执行跳转
}
与 OkHttp 的 proceed() 返回 Response 不同,HMRouter 的同步拦截器用枚举值显式表达三种意图。DO_NEXT 对应 OkHttp 里调用 proceed() 继续往下走,DO_REJECT 对应不调用 proceed() 直接短路,DO_TRANSITION 则是 OkHttp 里没有的语义------它跳过剩余拦截器但不阻止路由,相当于"加速放行"。
路由场景需要这第三种语义。一个典型用例是:VIP 用户访问付费页面时,跳过后续的权限校验和引导弹窗拦截器,直接跳转。
3.2 异步拦截器:洋葱模型
同步 handle() 无法处理弹窗确认、网络请求这类异步场景。异步拦截器实现 intercept() 方法,接收一个 IHMInterceptorChain 对象:
typescript
export interface IHMInterceptorChain {
onContinue(): Promise<void>; // 继续下一个拦截器
onIntercept(): Promise<void>; // 拦截,不执行路由
onTransition(): Promise<void>; // 跳过后续拦截器,执行路由
getRouterInfo(): HMInterceptorInfo;
getContext(): InterceptorContext | undefined;
}
三个方法对应三种枚举值,语义一致,但执行方式从同步返回变成了异步调用。拦截器内部可以 await 弹窗结果或网络请求,再决定调用哪个方法。
HMInterceptorChainImpl 的调度逻辑是递归的。proceed() 方法取出当前索引处的拦截器,索引加一,然后区分两种接口:
typescript
if (hasIntercept) {
await interceptor.intercept!(this);
} else if (hasHandle) {
const result = interceptor.handle!(this.info);
if (result === HMInterceptorAction.DO_NEXT) {
await this.proceed();
} else if (result === HMInterceptorAction.DO_REJECT) {
this.complete(HMInterceptorAction.DO_REJECT);
} else if (result === HMInterceptorAction.DO_TRANSITION) {
this.complete(HMInterceptorAction.DO_TRANSITION);
}
}
异步拦截器和同步拦截器可以混排在同一条链上。框架先检查拦截器是否实现了 intercept(),如果实现了就走异步路径;否则检查 handle(),走同步路径。两者同时实现时以 intercept() 为准,框架会打印 warn 日志。
3.3 防护机制
HMRouter 在链的安全性上做了几道防线。isCompleted 标志位防止拦截器重复调用 onContinue() / onIntercept() / onTransition(),每个方法入口都调用 checkCompleted() 做检查。start() 方法用 Promise 包装整条链的执行,proceed() 内部 catch 异常后自动执行 DO_REJECT,避免链挂死。这些处理在 OkHttp 的 RealInterceptorChain 中也有对应物------OkHttp 检查 proceed() 的调用次数,防止拦截器多次调用或不调用。
四、ImageKnifePro 的 C++ 拦截器链:Resolve / Cancel / Process
ImageKnifePro 是 HarmonyOS 上的高性能图片加载库,底层用 C++ 实现。它的拦截器链和前两者有本质区别:没有洋葱模型,没有 proceed() 调用,拦截器之间靠链表指针和 bool 返回值驱动。
4.1 Interceptor 基类
cpp
class Interceptor {
public:
std::string name;
virtual bool Resolve(std::shared_ptr<ImageKnifeTask> task) = 0;
virtual void Cancel(std::shared_ptr<ImageKnifeTask> task);
virtual bool Process(std::shared_ptr<ImageKnifeTask> task,
std::function<bool(std::shared_ptr<ImageKnifeTask>)> resolveCallback = nullptr);
virtual ~Interceptor() = default;
protected:
std::shared_ptr<Interceptor> next_ = nullptr;
};
Resolve 是纯虚函数,子类必须实现。返回 true 表示"我处理了,链终止";返回 false 表示"我处理不了,传给下一个"。这是最原始的责任链语义------OkHttp 和 HMRouter 都把"推进"行为交给拦截器主动调用,ImageKnifePro 则把推进逻辑收回框架侧的 Process 方法。
Process 是链的调度核心,逻辑分三步。先做前置检查(致命错误、请求已销毁),然后调用 Resolve(或外部传入的回调),最后根据返回值决定走向:
cpp
bool result = ExecuteResolveFunction(this, taskInternal, resolveFunction);
if (result) {
taskInternal->ClearInterceptorPtr();
return true; // 短路,链终止
} else if (next_ != nullptr) {
return next_->Process(task); // 传递给下一个
} else {
taskInternal->ClearInterceptorPtr();
return false; // 链尾,无人处理
}
拦截器不需要知道链的存在,只需要实现 Resolve 返回 bool。链的遍历和推进由 Process 通过 next_->Process() 递归完成。
4.2 四层子类
ImageKnifePro 按图片加载的阶段划分了四种拦截器子类:MemoryCacheInterceptor、FileCacheInterceptor、LoadInterceptor、DecodeInterceptor。每种子类的 SetNext() 方法限制了链接类型------MemoryCacheInterceptor::SetNext() 只接受 shared_ptr<MemoryCacheInterceptor>,编译期就阻止了跨层级串接。
四层拦截器各自独立成链,运行时可以插入自定义实现。比如要支持 AVIF 解码,写一个 DecodeInterceptorAvif 继承 DecodeInterceptor,实现 Resolve() 判断格式并解码,插入到默认解码拦截器前面即可。
4.3 LoadInterceptor 的异步分离
LoadInterceptor 是四层中最复杂的一层,因为网络下载天然是异步的。ImageKnifePro 没有用 async/await 或 Promise(C++ 也没有这些),而是设计了 Detach / OnComplete 机制。
Detach() 把当前 task 标记为"已分离",Process() 检测到分离状态后直接返回 true,不阻塞当前线程。下载完成后,由外部调用 OnComplete() 回到拦截器链的逻辑------OnComplete 内部做 CRC32 校验、fallback URL 重试、交由下一个拦截器处理,最终通过 FinishLoadChain 完成整个加载流程。
cpp
void LoadInterceptor::Detach(std::shared_ptr<ImageKnifeTask> task) {
auto taskInternal = std::dynamic_pointer_cast<ImageKnifeTaskInternal>(task);
taskInternal->Detach();
}
void LoadInterceptor::OnComplete(std::shared_ptr<ImageKnifeTask> task, bool result) {
// CRC32 校验 → fallback 重试 → next_->Process() → FinishLoadChain
}
这套设计把异步控制权完全交给了拦截器实现者。同步拦截器直接返回 bool,异步拦截器调用 Detach 后在回调里调用 OnComplete,框架侧的 Process 通过 IsDetached() 检查来区分两种路径。与 HMRouter 的 async/await 方案相比,这种方式更底层、更灵活,但对开发者要求也更高------忘记调用 OnComplete 会导致 task 永远挂起。
五、三代拦截器的演进对比
从 OkHttp 到 HMRouter 再到 ImageKnifePro,三套拦截器链在同一个模式上做了不同的设计选择。
推进方式 。OkHttp 由拦截器主动调用 chain.proceed(),HMRouter 由拦截器调用 chain.onContinue(),两者都是"拦截器驱动推进"。ImageKnifePro 的 Resolve 只返回 bool,推进逻辑由框架的 Process 方法控制,是"框架驱动推进"。
终止语义 。OkHttp 用"不调用 proceed"来表达终止,语义隐式。HMRouter 提供了 DO_REJECT / onIntercept() 显式终止,还多了 DO_TRANSITION / onTransition() 这个"跳过但不阻止"的语义。ImageKnifePro 用 return false 表示"我没处理",return true 表示"我处理了、链终止",没有"跳过后续但继续执行"的概念。
异步支持 。OkHttp 的拦截器链是同步的,异步调用通过 enqueue() 在链外处理。HMRouter 在链内支持 async/await,同步和异步拦截器可以混排。ImageKnifePro 通过 Detach/OnComplete 手动管理异步生命周期。
洋葱模型 。OkHttp 和 HMRouter 都支持洋葱模型------拦截器在 proceed() / onContinue() 前后分别执行前置和后置逻辑。ImageKnifePro 的 Resolve 是单向调用,没有洋葱结构。
类型约束 。OkHttp 和 HMRouter 的拦截器链是单一类型列表,所有拦截器共享同一个接口。ImageKnifePro 按阶段分成四条独立链,每条链通过泛型化的 SetNext 方法保证类型安全。
三者的选择与场景密切相关。HTTP 管线的请求/响应天然对称,洋葱模型是最自然的表达。路由拦截需要区分"阻止"和"加速放行",三值语义由此而来。图片加载的阶段是线性的(缓存→下载→解码),不需要对称的前后置处理,bool 短路链足够用。
六、在自己的框架中实现拦截器链
基于上面三个项目的实践,在自定义框架中实现拦截器链时有几个决策点需要提前确定。
第一个决策是推进方式。如果希望拦截器能在推进前后插入逻辑(日志、耗时统计、上下文修改),选择 OkHttp 风格的 proceed() 模型。如果拦截器只需要做判断然后放行或终止,ImageKnifePro 风格的 bool 返回更简单。
第二个决策是异步支持。如果运行环境支持 async/await(TypeScript、Kotlin 协程、Dart),直接把 proceed() 或 intercept() 声明为异步方法即可,HMRouter 的做法可以参考。如果是 C/C++ 环境,Detach/OnComplete 的手动管理方案虽然复杂但足够灵活。
第三个决策是终止语义的丰富程度。最简单的二值(继续/终止)能覆盖大部分场景。HMRouter 的三值增加了"跳过但不阻止"的语义,适合路由和权限校验场景。如果业务上确实需要第三种语义,再加不迟。
下面是一个 TypeScript 实现的最小拦截器链,综合了 OkHttp 的洋葱模型和 HMRouter 的异步支持:
typescript
interface Interceptor {
intercept(chain: Chain): Promise<Response>;
}
class Chain {
private interceptors: Interceptor[];
private index: number;
private request: Request;
constructor(interceptors: Interceptor[], index: number, request: Request) {
this.interceptors = interceptors;
this.index = index;
this.request = request;
}
getRequest(): Request {
return this.request;
}
async proceed(request: Request): Promise<Response> {
if (this.index >= this.interceptors.length) {
throw new Error('no more interceptors');
}
const next = new Chain(this.interceptors, this.index + 1, request);
const interceptor = this.interceptors[this.index];
return interceptor.intercept(next);
}
}
使用时把拦截器按顺序放入数组,构造 Chain 调用 proceed() 即可。每个拦截器在 intercept 内部决定是否调用 chain.proceed() 继续,还是直接返回一个 Response 短路。intercept 是 async 的,所以可以在 proceed() 前后做任何异步操作。
需要三值语义时,可以在 Chain 上增加 abort() 和 skip() 方法,内部通过抛出特定类型的异常或设置标志位来实现,参考 HMRouter 的 onIntercept() / onTransition() 的做法。需要类型安全的多阶段链时,参考 ImageKnifePro 的子类分层,每层链用独立的 SetNext 方法约束类型。
核心原则只有一条:让拦截器只关心自己的业务逻辑,链的推进和终止交给框架。OkHttp 做到了,HMRouter 做到了,ImageKnifePro 也做到了,只是各自选择了不同的表达方式。