ImageKnifePro 的图片来源不止 HTTP 一种。datashare:// 开头的图库图片、打包进 HAP 的 Resource 资源、应用沙箱里的本地文件、base64 字符串,以及业务方自己注册的 loader,都需要统一接入同一条加载管线。加载拦截器链上默认挂了两个实现——DownloadInterceptorDefault 处理网络下载,ResourceInterceptorDefault 处理本地和资源加载——通过 Resolve() 返回值决定谁来处理。

一、DownloadInterceptorDefault 的 RCP 下载

DownloadInterceptorDefault 在构造函数里创建了一个 Rcp_Session(成员变量 session_),整个应用生命周期内复用。同时维护了一个 std::stack<Rcp_Session*> 作为备用池,GetRcpSession() 先尝试从栈里弹出一个现成的 session,栈空就新建。用完后 RecycleSession() 压回栈,std::mutex 保护。

DownloadInterceptorDefault()
{
    name = "Default DownloadInterceptor";
    uint32_t errorCode = 0;
    session_ = HMS_Rcp_CreateSession(NULL, &errorCode);
    if (errorCode) {
        session_ = nullptr;
    }
}

RCP(Remote Communication Platform)是 HarmonyOS 系统层的网络栈,比 ArkTS 的 @ohos.net.http 少一层 NAPI 跨语言调用开销。在批量加载场景下吞吐量更高。

Resolve() 拿到 URL 后先做排除判断:以 /data/storagefile:data:image/ 开头的直接返回 false,让链上的下一个拦截器接手。空 URL 返回 false 并记录错误。只有标准的 HTTP/HTTPS URL 才进入 LoadImageFromUrl()

bool Resolve(std::shared_ptr<ImageKnifeTask> task) override
{
    std::string url;
    if (task->GetImageSource()->GetString(url)) {
        if (url.find("/data/storage") == 0 || url.find("file:") == 0 || url.find("data:image/") == 0) {
            return false;
        } else if (url.empty()) {
            task->EchoError("Empty Url");
            return false;
        }
        return LoadImageFromUrl(url, taskInternal);
    }
    return false;
}

LoadImageFromUrl() 创建 Rcp_Request,通过 ConfigHttpRequest 配置参数——HTTP headers、GET 方法、连接超时、传输超时、CA 证书路径、DNS 规则、下载进度回调、自动重定向。参数配置完成后调 HMS_Rcp_Fetch() 发起异步请求。

RCP 异步时序图

fetch 返回后立即调 Detach(task),当前线程不再等待下载完成。ResponseCallback 是一个静态函数,由 RCP 在下载完成或失败后回调。它拿到 Rcp_Response 后检查 statusCode,成功则把 response->body.buffer 包装成 shared_ptr<uint8_t[]> 写入 task->product

这里有一个精巧的内存管理细节——shared_ptr 的自定义删除器里调的是 response->destroyResponse(response),buffer 的生命周期由 RCP 管理,ImageKnifePro 只持有一个引用。当引用计数降到 0 时 RCP 才真正释放这块内存,避免了一次内存拷贝。

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

DNS 配置有三层优先级。ConfigDns() 先检查全局动态 DNS 规则(GetGlobalDynamicDnsRule()),有的话直接覆盖。全局动态 DNS 为空时检查请求级别的 DnsOptionInternal。请求级别也为空时用全局 DNS 配置。这三层覆盖关系保证了单个请求可以用独立的 DNS 策略,但又不丢全局默认值。

进度回调通过 Rcp_ConfigurationhttpEventsHandler.onDownloadProgress 注册,参数是 totalSizetransferredSize,只在主图请求时注册。回调函数很简练——transferredSize / totalSize 计算比例后调用用户传入的 progressListener

二、ResourceInterceptorDefault 的多路径读取

ResourceInterceptorDefault 在构造时把 isLoadFromRemote 设为 false,Process 不走 RetryFallbackUrls。它的 Resolve() 按优先级尝试四种来源。

第一种,Resource 对象。通过 task->GetImageSource()->GetResource(resource) 获取。LoadImageFromResource() 内部有三层兜底:先用 OH_ResourceManager_GetMedia() 按 ID 取,失败且 ID 为 -1 时用 GetBufferFromOtherModule() 按名称跨包取,再失败用 GetBufferFromRawfile() 从 rawfile 目录读。

bool Resolve(std::shared_ptr<ImageKnifeTask> task) override
{
    Resource resource;
    if (task->GetImageSource()->GetResource(resource)) {
        return LoadImageFromResource(resource, task);
    }
    std::string filePath = task->GetImageSource()->ToString();
    if (filePath.find("/data/storage") == 0) {
        return ReadFromLocal(task, filePath);
    }
    if (filePath.find("file:") == 0) {
        // file: URI 转本地路径
        char *pathResult = nullptr;
        OH_FileUri_GetPathFromUri(filePath.c_str(), filePath.size(), &pathResult);
        std::string localPath(pathResult);
        free(pathResult);
        return ReadFromLocal(task, localPath);
    }
    if (filePath.find("data:image/") == 0) {
        return DecodeBase64(task, filePath);
    }
    return false;
}

GetBufferFromOtherModule() 里的文件名提取逻辑是从 resource.param 字符串的最后一个 . 往后截取。resource.param 的格式是 [module].media.xxx,截取后得到资源名用于 OH_ResourceManager_GetMediaByNameGetBufferFromRawfile()OH_ResourceManager_OpenRawFile64 读取 rawfile 目录下的文件,适合图片资源没有注册到资源管理系统而是直接放在 rawfile 目录的情况。

第二种,本地沙箱路径。/data/storage 开头的路径走 ReadFromLocal(),用标准 C 的 stat/fopen/fread/fclose 读取。先用 stat 获取文件大小校验合法性,再 malloc 分配 buffer,fread 一次读完。buffer 用 shared_ptr 包装,自定义删除器里调 free

bool ReadFromLocal(std::shared_ptr<ImageKnifeTask> task, std::string &filePath)
{
    struct stat fileStat;
    if (stat(filePath.c_str(), &fileStat) == -1 || fileStat.st_size <= 0) {
        return false;
    }
    FILE *fd = fopen(filePath.c_str(), "r");
    if (fd == nullptr) return false;
    uint8_t *buffer = static_cast<uint8_t *>(malloc((fileStat.st_size + 1) * sizeof(uint8_t)));
    int64_t length = fread(buffer, sizeof(uint8_t), fileStat.st_size, fd);
    // ...
    task->product.imageBuffer = std::shared_ptr<uint8_t[]>(buffer, [](void *ptr) {
        free((uint8_t*)ptr);
    });
    task->product.imageLength = length;
    fclose(fd);
    return true;
}

第三种,file: URI。图库图片、跨应用共享的文件通常用这种协议。OH_FileUri_GetPathFromUri() 将 URI 转成本地路径后走 ReadFromLocal。这里 pathResult 指针必须用 free() 释放——HarmonyOS NDK 文档里明确说明了这一点,如果用 delete 会导致内存管理不匹配。

第四种,base64 字符串。data:image/ 开头,调 DecodeBase64()。解码过程:找到 ;base64, 分隔符位置,计算 base64 编码数据长度,分配输出 buffer(大小为 length * 3 / 4 + 1,因为 base64 每 4 个字符编码 3 个字节),调 libbase64 库的 base64_decode() 完成解码。

三、加载链上的两个拦截器如何协作

CreateDefaultImageLoader()DownloadInterceptorDefaultResourceInterceptorDefault 串在同一条加载链上:

loader->AddLoadInterceptor(downloadInterceptor);
loader->AddLoadInterceptor(resourceInterceptor);

downloadInterceptor 先添加所以在链头,resourceInterceptor 在链尾。请求进来后先走 DownloadInterceptorDefault。如果 URL 以 /data/storagefile:data:image/ 开头,Resolve 直接返回 false 跳过,基类 Processnext_ 自动把任务传给 ResourceInterceptorDefault。如果是 HTTP URL,走下载流程。下载失败时同样经过 next_ 传递,ResourceInterceptorDefault 会再尝试本地读取。

这个职责划分通过 URL 前缀硬编码实现。可以用更通用的 scheme 注册机制替代 if-else,但在图片加载场景下来源类型是有限且稳定的(HTTP、Resource、file、base64、sandbox),硬编码带来的确定性——不需要查注册表、不存在注册遗漏——比泛化更重要。

加载策略选择流程

四、进度回调的实现

进度回调只在网络下载场景有意义。ConfigHttpRequest 中检查 option->progressListener != nullptrtask->GetImageRequestType() == ImageRequestType::MAIN_SRC 时才注册。占位图和错误图的加载不需要进度通知。

if (option->progressListener != nullptr && task->GetImageRequestType() == ImageRequestType::MAIN_SRC) {
    config->tracingConfiguration.httpEventsHandler.onDownloadProgress = {
        .callback = OnProgressCallback,
        .usrObject = &(option->progressListener)
    };
}

OnProgressCallback 是一个静态函数,参数是 totalSizetransferredSize。RCP 在传输过程中持续调用它,频率取决于网络栈的内部缓冲策略。回调内容很简单——计算 transferredSize / totalSize 的比例,调用用户传入的 std::function<void(double)>

进度值是 0 到 1 之间的 double,而不是百分比整数。这样做的好处是精度更高,UI 层可以自己决定怎么展示——圆形进度条可能需要 0-360 度的角度,百分比文字可能需要 0-100 的整数,都从 0-1 转换即可。

五、RegisterLoader——自定义加载管线

RegisterLoader() 提供了比单个拦截器更粗粒度的定制能力:

void ImageKnifeInternal::RegisterLoader(std::string name,
                                         std::shared_ptr<ImageKnifeLoader> loader) {
    if (loader != nullptr) {
        loaderMap_[name] = loader;
    }
}

注册进 loaderMap_ 的是一个完整的 ImageKnifeLoader 对象,包含自己的内存缓存拦截器、文件缓存拦截器、下载拦截器和解码拦截器——四条链可以完全独立配置。ArkTS 层的 ImageKnifeOption 通过字符串指定要用哪个 loader,NAPI 解析层调 GetRegisterLoader(name) 从 map 中查找。

这种方式允许注册多个不同用途的 loader。一个走 CDN 下载并使用 WebP 解码,另一个走内网专线并使用自研格式解码,在请求级别按名称切换,互不干扰。和单个拦截器的插入相比,RegisterLoader 适合需要完全控制加载流程的场景——比如某个业务线的图片需要经过特殊的鉴权、解密、解码全套流程,和默认管线没有任何交集。

六、取消机制的竞态处理

取消操作在 Detach 场景下需要处理竞态。DownloadInterceptorDefault::Cancel() 先加 cancelMutex_ 锁把 rcpRequestCanceled 标记为 true,取出 rcpRequest 指针后解锁,再通过 HMS_Rcp_CancelRequest() 中断请求。

void Cancel(std::shared_ptr<ImageKnifeTask> task) override
{
    auto taskInternal = std::static_pointer_cast<ImageKnifeTaskInternal>(task);
    taskInternal->cancelMutex_.lock();
    taskInternal->rcpRequestCanceled = true;
    auto rcpRequest = taskInternal->rcpRequest;
    taskInternal->rcpRequest = nullptr;
    taskInternal->cancelMutex_.unlock();
    if (rcpRequest != nullptr && session_ != nullptr) {
        HMS_Rcp_CancelRequest(session_, rcpRequest);
    }
}

LoadImageFromUrl() 里 fetch 和 task->rcpRequest = rcpRequest 赋值之间存在时间窗口——如果 Cancel 在这个窗口内被调用,因为 rcpRequest 还是 null,Cancel 无法中断请求。所以赋值后会再检查一次 rcpRequestCanceled,如果为 true 就补调 Cancel。两把锁分开操作——sessionLock_ 保护全局 session,cancelMutex_ 保护单个任务状态——避免嵌套加锁导致的死锁风险。

ResponseCallback 中也做了对称处理:加锁取出 rcpRequest 并置空,如果请求被取消(rcpRequest 为 null),由取消方负责销毁;否则由回调方销毁。无论哪条路径,DestroyConfigurationHMS_Rcp_DestroyRequest 都只会被调用一次。

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


项目地址:ImageKnifePro

标签: none

添加新评论