网络图片加载有三个常见的失败场景:CDN 返回了损坏的图片数据、主域名不可达需要切换备用域名、DNS 解析被劫持或延迟过高。ImageKnifePro 在 C++ 层针对这三个场景分别实现了 CRC32 数据校验、fallbackUrls 多域名自动重试、以及静态/动态两种自定义 DNS 机制。三者的触发点都在 LoadInterceptor 的加载链路上,与 Detach 异步分离机制紧密配合。

一、CRC32 校验——下载数据的完整性验证

用户通过 ImageKnifeOption.crc32 设置一个期望的 CRC32 值。下载完成后,IsImageCrc32Match 函数对图片二进制数据计算 CRC32 并与期望值比较。

bool IsImageCrc32Match(std::shared_ptr<ImageKnifeTaskInternal> taskInternal,
                       std::shared_ptr<ImageKnifeOption> option)
{
    u_int32_t expectedCrc = option->crc32;
    if (expectedCrc == 0) {
        return true;  // 未设置 crc32,直接跳过
    }

    uLong crc = crc32(0L, Z_NULL, 0);
    auto data = taskInternal->product.imageBuffer;
    auto len = taskInternal->product.imageLength;
    if (data != nullptr && len > 0) {
        crc = crc32(crc, reinterpret_cast<const Bytef*>(data.get()), len);
    }
    if (expectedCrc == crc) {
        return true;
    }

    // 校验失败,清空下载数据
    taskInternal->product.imageBuffer = nullptr;
    taskInternal->product.imageLength = 0;
    taskInternal->EchoError("CRC32 Check Failed");
    return false;
}

CRC32 计算使用的是 zlib 库的 crc32 函数。校验的调用时机有两个。同步路径下(未分离),在 LoadInterceptor::ProcessRetryFallbackUrls 中,每次 Resolve 成功后立即校验;异步路径下(已分离),在 LoadInterceptor::OnComplete 中,下载回调完成后校验。

校验失败后有两个去向:如果还有未尝试的 fallbackUrl,继续重试下一个备用地址;如果所有地址都失败了,交由下一个拦截器处理。

二、RetryFallbackUrls——多域名自动重试

fallbackUrls 是一个字符串数组,用户在 ImageKnifeOption 中配置备用下载地址。当主地址下载失败或 CRC32 校验不通过时,框架自动尝试备用地址。

bool RetryFallbackUrls(std::shared_ptr<ImageKnifeTask> task, LoadInterceptor *downloadInterceptor)
{
    // index初始为-1,即没有使用过备用地址
    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,表示首次下载使用原始 URL。每次失败后 index 递增,取 fallbackUrls[i] 作为新的下载地址。整个循环中,每次 Resolve 之前都检查请求是否已取消或出现致命错误,支持中途中断。

重试逻辑在同步和异步两条路径上的行为有区别。同步路径下(Process 方法),RetryFallbackUrls 作为回调传入 Interceptor::Process,替代默认的 Resolve 函数。异步路径下(OnComplete 回调),下载分离后回来发现失败,需要构造新的 ImageKnifeTaskInternal 对象来进行重试——因为已分离的 task 不能标记为未分离状态。

flowchart TD
    A[发起下载: 原始 URL] --> B{下载成功?}
    B -->|否| C{有 fallbackUrl?}
    B -->|是| D{CRC32 校验}
    D -->|通过| E[加载成功]
    D -->|失败| C
    C -->|是| F["切换到 fallbackUrls[i]"]
    F --> G{下载成功?}
    G -->|是| H{CRC32 校验}
    H -->|通过| E
    H -->|失败| I{还有更多 fallbackUrl?}
    G -->|否| I
    I -->|是| F
    I -->|否| J[交由下一个拦截器]
    C -->|否| J

三、Detach 交互——异步分离与加载链回归

LoadInterceptorDetach 机制允许网络下载异步化而不阻塞当前工作线程。DownloadInterceptorDefault 在调用 HMS_Rcp_Fetch 发起异步下载后,如果设备支持分离(API > 13),就调用 Detach(task) 把 task 标记为已分离状态。

if (IsDownloadDetachEnabled()) {
    Detach(task);
} else {
    // 低版本ROM不支持分离,用promise同步等待
    bool res = data->waitDownload.get_future().get();
    delete data;
    return res;
}

低版本设备不支持分离时,使用 std::promise/future 把异步下载转为同步阻塞——回调中 set_value,调用处 get 等待。

分离后,下载完成的回调 ResponseCallback 在 RCP 线程中触发。回调中调用 OnComplete,这个方法承担了重新回归加载链的职责:执行 CRC32 校验、进行 fallbackUrl 重试、如果全部失败则交由下一个拦截器。最终无论成功还是失败,都通过 FinishLoadChain 把结果推回 ImageKnifeDispatcher 的主流程。

取消请求时需要特别小心竞态。Cancel 方法先获取 task 的 cancelMutex_,标记 rcpRequestCanceled = true 并取出 rcpRequest 指针,然后释放锁后再调用 HMS_Rcp_CancelRequestLoadImageFromUrl 中在 fetch 完成后也需要获取同一把锁来记录 request 并检查是否已被取消。两把锁(sessionLock_ 保护全局 session、cancelMutex_ 保护单个 task)分开操作,避免死锁。

四、DnsOption 静态 DNS——编译时 host-to-IP 映射

静态 DNS 通过 DnsOption 类设置。用户创建 StaticDnsRuleItem 列表,每项包含 host、port 和 ip 地址数组:

struct StaticDnsRuleItem {
    std::string host;
    uint16_t port = 80;
    std::vector<std::string> ipAddresses;
};

DnsOptionInternal 继承 DnsOption,在构造函数中调用 CovertRcpStaticDnsRulevector<StaticDnsRuleItem> 转换为 RCP 需要的 Rcp_StaticDnsRule 链表结构。每个 Rcp_StaticDnsRule 节点包含一个 Rcp_StaticDnsRuleItem(host、port 和 ip 地址链表),通过 next 指针串联。

转换过程中需要分配内存并拷贝字符串——host 通过 memcpy_s 拷贝到固定长度的 char[RCP_HOST_MAX_LEN] 数组,ip 地址拷贝到 char[RCP_IP_MAX_LEN] 数组。析构函数负责按链表顺序释放所有动态分配的节点和 ip 地址。

配置路径有两条:单请求级别通过 ImageKnifeOption.dnsOption 设置;全局级别通过 ImageKnife.setGlobalDnsOption(dnsOption) 设置。DownloadInterceptorDefault::ConfigDns 中的优先级为:全局动态 DNS > 请求级别静态 DNS > 全局静态 DNS。

sequenceDiagram
    participant App as 应用层
    participant IK as ImageKnife
    participant DI as DownloadInterceptor
    participant RCP as HMS_Rcp

    App->>IK: setGlobalDnsOption(dnsOption)
    Note over IK: 存储全局 DnsOptionInternal

    App->>IK: 加载图片(option)
    IK->>DI: ConfigHttpRequest(task, rcpRequest)
    DI->>DI: ConfigDns(config, option)
    alt 全局动态DNS已设置
        DI->>DI: 使用全局动态DNS规则
    else 请求级别DNS已设置
        DI->>DI: 使用请求级别静态DNS
    else 全局静态DNS已设置
        DI->>DI: 使用全局静态DNS
    end
    DI->>RCP: config->dnsConfiguration.dnsRules = ...
    RCP->>RCP: 按DNS规则解析host

五、DnsDynamic 异步动态 DNS——运行时 host 解析

动态 DNS 通过回调函数实现,允许应用在运行时动态解析域名。ArkTS 层设置一个返回 Promise<Array<string>> 的回调函数:

ImageKnife.getInstance().setGlobalDynamicDns(
    (host: string, port: number) => Promise<Array<string>>,
    timeout  // 超时时间,默认 50ms
);

C++ 层的 DnsDynamic 类包装了这个回调。当 RCP 需要解析域名时,调用 DnsDynamic::Execute

Rcp_IpAddress *DnsDynamic::Execute(const char *host, uint16_t port)
{
    auto future = callback_(host, port);
    auto status = future.wait_for(std::chrono::milliseconds(timeout_));
    if (status != std::future_status::ready) {
        IMAGE_KNIFE_LOG(LOG_WARN, "Dynamic Dns Callback Reach Timeout");
        return nullptr;
    }

    std::vector<std::string> result = future.get();
    // 将 vector<string> 转换为 Rcp_IpAddress 链表
    Rcp_IpAddress *head = nullptr;
    Rcp_IpAddress *point = nullptr;
    for (auto &ipAddress : result) {
        Rcp_IpAddress *address = (Rcp_IpAddress *)malloc(sizeof(Rcp_IpAddress));
        memcpy_s(address->ipAddress, RCP_IP_MAX_LEN, ipAddress.c_str(), ipAddress.size() + 1);
        // ... 链表串联
    }
    return head;
}

关键设计是超时机制。wait_for 等待指定毫秒数,如果回调没有在超时时间内返回结果,直接返回 nullptr 让 RCP 走默认的 DNS 解析。默认超时 50ms,这个值考虑了用户自定义 DNS 解析通常只是查本地缓存或内网 DNS 服务器的场景。如果用户的回调涉及外部网络请求,需要适当调大超时值。

Rcp_IpAddress 链表的内存由 RCP 框架负责释放——这与 DnsOptionInternal 中静态规则由 ImageKnifePro 自己释放不同。

动态 DNS 设置后会覆盖所有静态 DNS 配置。ConfigDns 方法中,如果全局动态 DNS 规则不为空,直接使用并 return,不再检查静态规则。

六、DownloadInterceptorDefault 的整体请求配置

网络请求的配置在 ConfigHttpRequest 中完成。除了 DNS,还包括以下几项。

HTTP Headers:优先使用请求级别的 ImageKnifeOption.httpHeaders,如果为空则使用全局 headers。Session 池:DownloadInterceptorDefault 维护一个 stack<Rcp_Session *> 做连接池复用,GetRcpSession 取出、RecycleSession 回收。超时配置:connectTimeoutreadTimeout 分别设置连接超时和传输超时。CA 证书:通过 httpOption.caPath 设置自定义证书路径。下载进度:如果设置了 progressListener,通过 RCP 的 onDownloadProgress 回调实时报告下载进度百分比。

请求完成后,ResponseCallback 检查 HTTP 状态码。状态码不是 200 时记录错误并销毁 response。成功时用 shared_ptr 包装 response body 的 buffer,设置自定义删除器来确保 response->destroyResponse 被正确调用——避免手动释放下载数据 buffer 导致的内存错误。

auto deleter = [response](void *) {
    response->destroyResponse(response);
};
task->product.imageBuffer = std::shared_ptr<uint8_t[]>(
    (uint8_t *)response->body.buffer, deleter);

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


项目地址:ImageKnifePro

标签: none

添加新评论