ImageKnifePro 源码解读(五):五种图片来源的加载策略
ImageKnifePro 的图片来源不止 HTTP 一种。 RCP(Remote Communication Platform)是 HarmonyOS 系统层的网络栈,比 ArkTS 的 fetch 返回后立即调 这里有一个精巧的内存管理细节—— DNS 配置有三层优先级。 进度回调通过 第一种,Resource 对象。通过 第二种,本地沙箱路径。 第三种, 第四种,base64 字符串。 这个职责划分通过 URL 前缀硬编码实现。可以用更通用的 scheme 注册机制替代 if-else,但在图片加载场景下来源类型是有限且稳定的(HTTP、Resource、file、base64、sandbox),硬编码带来的确定性——不需要查注册表、不存在注册遗漏——比泛化更重要。 进度回调只在网络下载场景有意义。 进度值是 0 到 1 之间的 double,而不是百分比整数。这样做的好处是精度更高,UI 层可以自己决定怎么展示——圆形进度条可能需要 0-360 度的角度,百分比文字可能需要 0-100 的整数,都从 0-1 转换即可。 注册进 这种方式允许注册多个不同用途的 loader。一个走 CDN 下载并使用 WebP 解码,另一个走内网专线并使用自研格式解码,在请求级别按名称切换,互不干扰。和单个拦截器的插入相比, 取消操作在 Detach 场景下需要处理竞态。 以上就是本篇内容的所有了~有什么问题欢迎在评论区提出 项目地址:ImageKnifeProdatashare:// 开头的图库图片、打包进 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;
}
}@ohos.net.http 少一层 NAPI 跨语言调用开销。在批量加载场景下吞吐量更高。Resolve() 拿到 URL 后先做排除判断:以 /data/storage、file: 或 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() 发起异步请求。
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;ConfigDns() 先检查全局动态 DNS 规则(GetGlobalDynamicDnsRule()),有的话直接覆盖。全局动态 DNS 为空时检查请求级别的 DnsOptionInternal。请求级别也为空时用全局 DNS 配置。这三层覆盖关系保证了单个请求可以用独立的 DNS 策略,但又不丢全局默认值。Rcp_Configuration 的 httpEventsHandler.onDownloadProgress 注册,参数是 totalSize 和 transferredSize,只在主图请求时注册。回调函数很简练——transferredSize / totalSize 计算比例后调用用户传入的 progressListener。二、ResourceInterceptorDefault 的多路径读取
ResourceInterceptorDefault 在构造时把 isLoadFromRemote 设为 false,Process 不走 RetryFallbackUrls。它的 Resolve() 按优先级尝试四种来源。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_GetMediaByName。GetBufferFromRawfile() 用 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 会导致内存管理不匹配。data:image/ 开头,调 DecodeBase64()。解码过程:找到 ;base64, 分隔符位置,计算 base64 编码数据长度,分配输出 buffer(大小为 length * 3 / 4 + 1,因为 base64 每 4 个字符编码 3 个字节),调 libbase64 库的 base64_decode() 完成解码。三、加载链上的两个拦截器如何协作
CreateDefaultImageLoader() 把 DownloadInterceptorDefault 和 ResourceInterceptorDefault 串在同一条加载链上:loader->AddLoadInterceptor(downloadInterceptor);
loader->AddLoadInterceptor(resourceInterceptor);downloadInterceptor 先添加所以在链头,resourceInterceptor 在链尾。请求进来后先走 DownloadInterceptorDefault。如果 URL 以 /data/storage、file: 或 data:image/ 开头,Resolve 直接返回 false 跳过,基类 Process 的 next_ 自动把任务传给 ResourceInterceptorDefault。如果是 HTTP URL,走下载流程。下载失败时同样经过 next_ 传递,ResourceInterceptorDefault 会再尝试本地读取。
四、进度回调的实现
ConfigHttpRequest 中检查 option->progressListener != nullptr 且 task->GetImageRequestType() == ImageRequestType::MAIN_SRC 时才注册。占位图和错误图的加载不需要进度通知。if (option->progressListener != nullptr && task->GetImageRequestType() == ImageRequestType::MAIN_SRC) {
config->tracingConfiguration.httpEventsHandler.onDownloadProgress = {
.callback = OnProgressCallback,
.usrObject = &(option->progressListener)
};
}OnProgressCallback 是一个静态函数,参数是 totalSize 和 transferredSize。RCP 在传输过程中持续调用它,频率取决于网络栈的内部缓冲策略。回调内容很简单——计算 transferredSize / totalSize 的比例,调用用户传入的 std::function<void(double)>。五、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 中查找。RegisterLoader 适合需要完全控制加载流程的场景——比如某个业务线的图片需要经过特殊的鉴权、解密、解码全套流程,和默认管线没有任何交集。六、取消机制的竞态处理
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),由取消方负责销毁;否则由回调方销毁。无论哪条路径,DestroyConfiguration 和 HMS_Rcp_DestroyRequest 都只会被调用一次。