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" 等),ExecuteResolveFunctionname 拼接缓存操作类型(Read/Write)作为 trace 名称。在海量并发请求中定位单条请求的路径变得可行。

二、四类拦截器子类

Interceptor 派生出四个抽象子类,分别对应图片加载流水线的四个阶段。每个子类都把 Process 标记为 final——开发者写自定义拦截器时只需要实现 Resolve

拦截器类图

MemoryCacheInterceptor 是内存缓存层。默认实现 MemoryCacheInterceptorDefaultResolve 根据 cacheTask.type 走读或写分支。读操作用 cacheKeyMemoryCache 单例查找,命中就把 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;
}

FileCacheInterceptor 是文件缓存层。FileCacheInterceptorDefault 采用单例模式,因为磁盘缓存需要全局统一管理容量。它内部维护一个主缓存实例"大端"和一个 fileCacheMap_"小端"集合。Resolve 的第一件事是确定用哪个 FileCache 实例——如果请求类型是主图且 fileCacheNamefileCacheMap_ 里能找到,就用对应的小端;否则用大端。读操作调 GetImageFromDisk,写操作调 SaveImageToDisk,支持文件后缀感知:开启 EnableExtension 后用 FileTypeUtil::CheckImageFormat 检测 buffer 头部魔数,给缓存文件加上 .jpg.png 等后缀。

LoadInterceptor 是加载层,四类中逻辑最复杂的一个。它比基类多了三样东西: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 排在后面。网络下载失败时,基类 Processnext_ 调用自然地把任务传给后者,不需要额外的 fallback 分支。

DecodeInterceptor 是解码层。默认链上有两个拦截器:DecodeInterceptorDefault 在前,DecodeInterceptorAvif 在后(条件添加)。DecodeInterceptorDefault 遇到 AVIFUNKNOWNCUSTOM_FORMAT 就返回 false,任务自动滑到下一个。

三、Detach 分离机制

网络下载是异步操作,如果让下载线程阻塞等待 RCP 回调,线程池会很快耗尽。Detach 在发起异步请求后立即释放当前工作线程。

void LoadInterceptor::Detach(std::shared_ptr<ImageKnifeTask> task)
{
    auto taskInternal = std::dynamic_pointer_cast<ImageKnifeTaskInternal>(task);
    taskInternal->Detach();
}

分离后的任务由 RCP 的回调线程通过 OnComplete 重新接管。OnComplete 内部的流程比较复杂:先做 CRC32 校验(如果配置了),校验失败则清空 imageBuffer 并标记错误。如果下载失败且配置了 fallbackUrls,会在当前线程中重试下一个 URL,重试过程中可能再次 Detach。全部 URL 耗尽仍然失败,且 next_ 非空时,构造新的 ImageKnifeTaskInternal(因为分离后的 task 不能复用),调 next_->Process 把任务传给 ResourceInterceptorDefault。最终通过 FinishLoadChain 将任务推回 TaskWorker 队列。

API 版本低于 13 的设备不支持 Detach。IsDownloadDetachEnabled() 做了检测,低版本回退到 std::promise/future 方案:ResponseCallback 不调 OnComplete,而是用 data->waitDownload.set_value(result) 通知等待线程。

四、RetryFallbackUrls 多域名重试

ImageKnifeOption 可以配置 fallbackUrls 备用地址列表。RetryFallbackUrlsfallbackUrlIndex 开始逐个尝试。

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 校验用 zlib 的 crc32() 函数对整个 imageBuffer 计算校验值。crc32 设为 0 表示跳过校验。校验发生在两条路径上:同步路径在 RetryFallbackUrls 的每次 Resolve 成功后调用;异步分离路径在 OnComplete 回调中对远端加载结果调用。两条路径最终汇入同一个 IsImageCrc32Match 函数,校验逻辑保持一致。

五、自定义拦截器的插入

ImageKnifeLoader 为四条链各提供一个 Add 方法,参数是拦截器实例和插入位置。Position 枚举只有 STARTEND 两个值。

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 让 Processnext_ 自动传给 DownloadInterceptorDefaultPosition::START 保证 P2P 拦截器在默认下载拦截器之前执行。

loader 既可以全局默认应用,也可以针对特定请求单独配置。每个 ImageKnifeOption 可以绑定不同的 loader 实例,实现"不同业务场景用不同的拦截器组合"。CreateEmptyImageLoader 创建四条链全空的 loader,完全由开发者自行填充。CreateDefaultImageLoader 则预装全套默认拦截器,开箱即用。

请求流程图

六、责任链模式的实际收益

四层拆分的收益体现在三个维度上。

职责边界DownloadInterceptorDefault 只管发 HTTP 请求拿到 buffer,缓存读写由 FileCacheInterceptor 链负责,解码由 DecodeInterceptor 链负责。改动缓存策略不会碰到下载代码。

扩展方式。新增加载源只需要继承对应子类、实现 Resolve、调 AddLoadInterceptor 插入链上,不碰任何现有代码。DecodeInterceptorAvif 的条件添加就是这个思路的实际案例——CreateDefaultImageLoader 中用 if (ImageKnifeDecoderAvif::IsAvifEnable()) 决定是否把 AVIF 拦截器挂到解码链。

失败处理。链式调用天然支持 fallback:DownloadInterceptorDefault 返回 false,基类 Processnext_ 自动把任务传给 ResourceInterceptorDefault,后者尝试本地路径。这个流转完全由基类驱动,业务拦截器不需要知道自己的前后是谁。

以上就是本篇内容的所有了~有什么问题欢迎在评论区提出


项目地址:ImageKnifePro

标签: none

添加新评论