ImageKnifePro 源码解读(四):拦截器链——四层责任链的设计与扩展
ImageKnifePro 的加载引擎围绕拦截器-责任链模式构建。缓存、加载、解码各自独立成链,每一层拦截器只做一件事,做不到就传给链上的下一个。运行时随时可以插入、替换、重排。这篇从源码看四层拦截器的内部结构,它们怎么串联、怎么短路、怎么处理异步下载的线程分离,以及自定义拦截器怎么塞进去。 MemoryCacheInterceptor 是内存缓存层。默认实现 FileCacheInterceptor 是文件缓存层。 LoadInterceptor 是加载层,四类中逻辑最复杂的一个。它比基类多了三样东西: 默认加载链上挂了两个拦截器: DecodeInterceptor 是解码层。默认链上有两个拦截器: 网络下载是异步操作,如果让下载线程阻塞等待 RCP 回调,线程池会很快耗尽。 分离后的任务由 RCP 的回调线程通过 API 版本低于 13 的设备不支持 Detach。 每次重试中间都检查了 CRC32 校验用 zlib 的 四条链的 loader 既可以全局默认应用,也可以针对特定请求单独配置。每个 四层拆分的收益体现在三个维度上。 职责边界。 扩展方式。新增加载源只需要继承对应子类、实现 失败处理。链式调用天然支持 fallback: 以上就是本篇内容的所有了~有什么问题欢迎在评论区提出 项目地址:ImageKnifePro一、Interceptor 基类
Interceptor 定义在 include/interceptor.h 里,核心结构精简到三样东西:一个 Resolve 纯虚函数、一个 Process 虚方法、一个指向下一个拦截器的 next_ 智能指针。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 表示"让下一个来"。Cancel 是可选的取消接口,基类给了空实现,只有需要中断异步操作的拦截器才需要覆写。Process 的执行逻辑分四步。第一步做前置检查:向下转型为 ImageKnifeTaskInternal,检查致命错误或请求已被销毁,任何一个条件成立直接返回 false。第二步 SetInterceptor(this) 把自己的裸指针记录在 task 上,后续的 Cancel 操作能找到正在执行的拦截器。第三步调用 Resolve(或外部传入的 resolveCallback),通过 ExecuteResolveFunction 包装执行,负责写 HiTrace 追踪标记和日志输出。第四步根据返回值决定走向:bool result = ExecuteResolveFunction(this, taskInternal, resolveFunction);
if (taskInternal->IsDetached() && IsLoadInterceptor(this)) {
return true;
}
if (result) {
taskInternal->ClearInterceptorPtr();
return true; // 短路
} else if (next_ != nullptr) {
return next_->Process(task); // 传递
} else {
taskInternal->ClearInterceptorPtr();
return false; // 链尾
}name 字段用于日志和 HiTrace 追踪。每个默认拦截器在构造函数里取名("Default DownloadInterceptor"、"Default DecodeInterceptor Avif" 等),ExecuteResolveFunction 用 name 拼接缓存操作类型(Read/Write)作为 trace 名称。在海量并发请求中定位单条请求的路径变得可行。二、四类拦截器子类
Interceptor 派生出四个抽象子类,分别对应图片加载流水线的四个阶段。每个子类都把 Process 标记为 final——开发者写自定义拦截器时只需要实现 Resolve。
MemoryCacheInterceptorDefault 的 Resolve 根据 cacheTask.type 走读或写分支。读操作用 cacheKey 去 MemoryCache 单例查找,命中就把 imageDataCache 塞进 task 的 product 里返回 true。写操作在缓存里不存在对应 key 时放入解码后的 ImageData。读和写复用同一个拦截器实例,通过 cacheTask.type 区分——同一条内存缓存链既服务于加载前的"查缓存",也服务于解码后的"写缓存"。bool Read(std::shared_ptr<ImageKnifeTask> task)
{
if (MemoryCache::GetInstance()->Contains(task->cacheTask.cacheKey)) {
task->product.imageDataCache = MemoryCache::GetInstance()->Get(task->cacheTask.cacheKey);
if (task->product.imageDataCache == nullptr) {
task->EchoError("Empty ImageData");
return false;
}
return true;
}
return false;
}FileCacheInterceptorDefault 采用单例模式,因为磁盘缓存需要全局统一管理容量。它内部维护一个主缓存实例"大端"和一个 fileCacheMap_"小端"集合。Resolve 的第一件事是确定用哪个 FileCache 实例——如果请求类型是主图且 fileCacheName 在 fileCacheMap_ 里能找到,就用对应的小端;否则用大端。读操作调 GetImageFromDisk,写操作调 SaveImageToDisk,支持文件后缀感知:开启 EnableExtension 后用 FileTypeUtil::CheckImageFormat 检测 buffer 头部魔数,给缓存文件加上 .jpg、.png 等后缀。Detach 异步分离方法、OnComplete 回调归队方法、isLoadFromRemote 布尔标志。isLoadFromRemote 决定了 Process 的行为差异。DownloadInterceptorDefault 默认为 true,Process 会启用 RetryFallbackUrls 作为 resolveCallback,激活多域名重试和 CRC32 校验逻辑。ResourceInterceptorDefault 设为 false,直接走基类的普通链式调用,不做重试。bool LoadInterceptor::Process(std::shared_ptr<ImageKnifeTask> task,
std::function<bool(std::shared_ptr<ImageKnifeTask>)> resolveCallback)
{
if (!isLoadFromRemote || task->GetImageRequestType() != ImageRequestType::MAIN_SRC) {
return Interceptor::Process(task);
}
return Interceptor::Process(task, [this](std::shared_ptr<ImageKnifeTask> t) {
return RetryFallbackUrls(t, this);
});
}DownloadInterceptorDefault 排在前面,ResourceInterceptorDefault 排在后面。网络下载失败时,基类 Process 的 next_ 调用自然地把任务传给后者,不需要额外的 fallback 分支。DecodeInterceptorDefault 在前,DecodeInterceptorAvif 在后(条件添加)。DecodeInterceptorDefault 遇到 AVIF、UNKNOWN 或 CUSTOM_FORMAT 就返回 false,任务自动滑到下一个。三、Detach 分离机制
Detach 在发起异步请求后立即释放当前工作线程。void LoadInterceptor::Detach(std::shared_ptr<ImageKnifeTask> task)
{
auto taskInternal = std::dynamic_pointer_cast<ImageKnifeTaskInternal>(task);
taskInternal->Detach();
}OnComplete 重新接管。OnComplete 内部的流程比较复杂:先做 CRC32 校验(如果配置了),校验失败则清空 imageBuffer 并标记错误。如果下载失败且配置了 fallbackUrls,会在当前线程中重试下一个 URL,重试过程中可能再次 Detach。全部 URL 耗尽仍然失败,且 next_ 非空时,构造新的 ImageKnifeTaskInternal(因为分离后的 task 不能复用),调 next_->Process 把任务传给 ResourceInterceptorDefault。最终通过 FinishLoadChain 将任务推回 TaskWorker 队列。IsDownloadDetachEnabled() 做了检测,低版本回退到 std::promise/future 方案:ResponseCallback 不调 OnComplete,而是用 data->waitDownload.set_value(result) 通知等待线程。四、RetryFallbackUrls 多域名重试
ImageKnifeOption 可以配置 fallbackUrls 备用地址列表。RetryFallbackUrls 从 fallbackUrlIndex 开始逐个尝试。bool RetryFallbackUrls(std::shared_ptr<ImageKnifeTask> task, LoadInterceptor *downloadInterceptor)
{
for (int i = taskInternal->fallbackUrlIndex; i < (int)option->fallbackUrls.size(); i++) {
if (request->IsDestroy() || taskInternal->IsFatalErrorHappened()) {
return false;
}
if (taskInternal->fallbackUrlIndex > -1) {
taskInternal->SetFallbackUrl(option->fallbackUrls[i]);
}
taskInternal->fallbackUrlIndex++;
bool result = downloadInterceptor->Resolve(task);
if (taskInternal->IsDetached()) {
return true;
}
if (result && IsImageCrc32Match(taskInternal, option)) {
return true;
}
}
taskInternal->ResetFallbackUrl();
return false;
}fallbackUrlIndex 初始值是 -1,表示首次使用原始地址。第一轮循环时 SetFallbackUrl 不会被调用,Resolve 用的是原始 URL。原始 URL 失败后 index 递增到 0,开始使用 fallbackUrls[0]。IsDetached()——如果某个备用地址的下载也走了 Detach,当前循环中断,后续重试由 OnComplete 回调继续驱动。重试全部失败后 ResetFallbackUrl 恢复到初始状态,确保链上下一个拦截器拿到的是干净的原始请求。crc32() 函数对整个 imageBuffer 计算校验值。crc32 设为 0 表示跳过校验。校验发生在两条路径上:同步路径在 RetryFallbackUrls 的每次 Resolve 成功后调用;异步分离路径在 OnComplete 回调中对远端加载结果调用。两条路径最终汇入同一个 IsImageCrc32Match 函数,校验逻辑保持一致。五、自定义拦截器的插入
ImageKnifeLoader 为四条链各提供一个 Add 方法,参数是拦截器实例和插入位置。Position 枚举只有 START 和 END 两个值。void ImageKnifeLoaderInternal::AddLoadInterceptor(
std::shared_ptr<LoadInterceptor> interceptor, Position position)
{
if (loadInterceptorHead_ == nullptr) {
loadInterceptorHead_ = loadInterceptorTail_ = interceptor;
return;
}
if (position == Position::START) {
interceptor->SetNext(loadInterceptorHead_);
loadInterceptorHead_ = interceptor;
} else {
loadInterceptorTail_->SetNext(interceptor);
loadInterceptorTail_ = interceptor;
}
}Add 方法代码结构完全一致,只是类型参数不同。写一个自定义加载拦截器:继承对应子类,实现 Resolve,构造时给 name 赋值用于追踪,调 Add 方法指定位置。比如想在网络下载前先查一层自研的 P2P 缓存,写一个 P2PCacheInterceptor : public LoadInterceptor,命中返回 true 短路跳过网络请求,未命中返回 false 让 Process 的 next_ 自动传给 DownloadInterceptorDefault。Position::START 保证 P2P 拦截器在默认下载拦截器之前执行。ImageKnifeOption 可以绑定不同的 loader 实例,实现"不同业务场景用不同的拦截器组合"。CreateEmptyImageLoader 创建四条链全空的 loader,完全由开发者自行填充。CreateDefaultImageLoader 则预装全套默认拦截器,开箱即用。
六、责任链模式的实际收益
DownloadInterceptorDefault 只管发 HTTP 请求拿到 buffer,缓存读写由 FileCacheInterceptor 链负责,解码由 DecodeInterceptor 链负责。改动缓存策略不会碰到下载代码。Resolve、调 AddLoadInterceptor 插入链上,不碰任何现有代码。DecodeInterceptorAvif 的条件添加就是这个思路的实际案例——CreateDefaultImageLoader 中用 if (ImageKnifeDecoderAvif::IsAvifEnable()) 决定是否把 AVIF 拦截器挂到解码链。DownloadInterceptorDefault 返回 false,基类 Process 的 next_ 自动把任务传给 ResourceInterceptorDefault,后者尝试本地路径。这个流转完全由基类驱动,业务拦截器不需要知道自己的前后是谁。