图片加载的后半段是把字节流变成可渲染的 PixelMap。ImageKnifePro 用 C++ 拦截器链驱动解码分发,并通过 dlopen 在运行时动态加载 AVIF 解码器。编译期通过 #ifdef 控制头文件引入,运行时通过 dlopen 检测设备能力,两层防线保证在不支持 AVIF 的环境下安全降级。

一、FileTypeUtil 的文件头识别

解码之前得知道拿到的是什么格式。ImageKnifePro 不依赖文件扩展名,直接读取字节流头部的魔数(magic number)判断。网络请求拿回来的数据不一定带扩展名,缓存文件的命名经常用 hash 替代原名,只有二进制头部的魔数是可靠的。

FileTypeUtilstd::map<ImageFormat, std::vector<std::vector<uint8_t>>> 做签名表。每种格式可以有多条候选签名(比如 AVIF 有 ftyp avifftyp avis 两条,前者是静态 AVIF,后者是序列图/动图)。

static std::map<ImageFormat, std::vector<std::vector<uint8_t>>> fileSignatureMap_ = {
    {ImageFormat::JPG, {{0xFF, 0xD8}}},
    {ImageFormat::PNG, {{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A}}},
    {ImageFormat::GIF, {{0x47, 0x49, 0x46}}},
    {ImageFormat::WEBP, {{0x52, 0x49, 0x46, 0x46, 0, 0, 0, 0, 0x57, 0x45, 0x42, 0x50}}},
    {ImageFormat::HEIC, {{0x00, 0x00, 0x00, 0x18, 0x66, 0x74, 0x79, 0x70, 0x68, 0x65, 0x69, 0x63}, ...}},
    {ImageFormat::AVIF, {{0x00, 0x00, 0x00, 0x1C, 0x66, 0x74, 0x79, 0x70, 0x61, 0x76, 0x69, 0x66},
        {0x00, 0x00, 0x00, 0x20, 0x66, 0x74, 0x79, 0x70, 0x61, 0x76, 0x69, 0x73}}},
    // ...
};

MatchesSignature 方法有两处特殊处理。HEIC 和 AVIF 都基于 ISOBMFF 容器格式,头部结构是一个 Box:前 4 字节是 Box 的 size,后面才是类型标识 ftyp。比对时从第 4 字节开始,跳过 size 字段。WEBP 文件以 RIFF 四字节开头,紧跟 4 字节的 fileSize,然后才是 WEBP 标识。比对逻辑拆成两段:先比前 4 字节 RIFF,跳过 4-8 字节的 size 字段,再从第 8 字节开始比对 WEBP

if (fileType == ImageFormat::WEBP) {
    constexpr int riffEnd = 4;
    for (int i = 0; i < riffEnd; i++) {
        if (data[i] != signature[i]) return false;
    }
    constexpr int typeOffset = 8;
    for (int i = typeOffset; i < signature.size(); i++) {
        if (data[i] != signature[i]) return false;
    }
    return true;
}

CheckImageFormat 入口处有一个前置长度校验:文件字节流不足 32 字节直接返回 UNKNOWN,避免后续比对越界。32 字节足够覆盖所有已知格式的签名长度。

格式识别流程

二、DecodeInterceptorDefault 的标准解码路径

默认解码拦截器 DecodeInterceptorDefault 处理系统 OH_ImageSourceNative 支持的所有格式——JPG、PNG、GIF、WebP、BMP、HEIC、TIFF、ICO 等。Resolve() 开头检查文件格式,遇到 AVIFUNKNOWNCUSTOM_FORMAT 就返回 false,任务自动滑到链上的下一个拦截器。

bool Resolve(std::shared_ptr<ImageKnifeTask> task) override
{
    auto fileTypeInfo = task->GetFileTypeInfo();
    if (fileTypeInfo->format == ImageFormat::UNKNOWN ||
        fileTypeInfo->format == ImageFormat::CUSTOM_FORMAT ||
        fileTypeInfo->format == ImageFormat::AVIF) {
        return false;
    }
    if (taskInternal->IsFrameDecodeMode()) {
        return DecodeFrame(taskInternal);
    } else {
        return Decode(taskInternal);
    }
}

Decode 方法的流程分三个阶段。GetImageSourceimageBuffer 创建 OH_ImageSourceNativeConfigDecodeOption 装配 OH_DecodingOptions——设置期望解码尺寸(降采样)、JPEG 的 NV21 像素格式优化、期望动态范围。JPEG NV21 优化的判断条件是:全局开启 jpegOptimizeDecoding 且当前请求没有 multiTransformationtransformation。出于"NV21 格式不支持后续图形变换"的限制,有变换需求时不能走这条优化路径。

bool ConfigDecodeOption(DecodeArgs &args, std::shared_ptr<ImageKnifeTaskInternal> &task)
{
    OH_DecodingOptions_Create(&args.decodeOption);
    Image_Size imageSize = task->GetDesiredImageSize();
    if (imageSize.width != 0 && imageSize.height != 0) {
        OH_DecodingOptions_SetDesiredSize(args.decodeOption, &imageSize);
    }
    if (task->GetFileTypeInfo()->format == ImageFormat::JPG && IsYuvEnable(task)) {
        OH_DecodingOptions_SetPixelFormat(args.decodeOption, PIXEL_FORMAT_NV21);
    }
    OH_DecodingOptions_SetDesiredDynamicRange(args.decodeOption, GetDesiredDynamicRange(task));
    return true;
}

多帧解码走 DecodePixelmapList(),调 OH_ImageSourceNative_CreatePixelmapList() 一次解出所有帧。帧延迟信息优先从 FileTypeInfo 的缓存中获取;缓存没有就重新调 OH_ImageSourceNative_GetDelayTimeList() 获取,这个 fallback 避免了解码阶段因缺少帧时长而失败。

单帧解码走 CreatePixelmapByAllocator(),优先尝试 DMA 内存分配模式。如果指定模式不被支持(返回 IMAGE_SOURCE_UNSUPPORTED_ALLOCATOR_TYPE),自动回退到 IMAGE_ALLOCATOR_TYPE_AUTO。如果封装的 API 函数指针为空(老版本 SDK),走标准的 OH_ImageSourceNative_CreatePixelmap

三、EXIF 方向校正

解码完成后还有一步 EXIF 方向修正。Orientate() 只在主图请求且 option->orientation == AUTO 时执行。它从 OH_ImageSourceNative 读出 Orientation 字符串,传给 PixelmapUtils::Orientate()

void Orientate(std::shared_ptr<ImageKnifeTask> task, OH_ImageSourceNative *source)
{
    if (task->GetImageRequestType() != ImageRequestType::MAIN_SRC) return;
    if (option->orientation != Orientation::AUTO) return;
    std::string keyStr = "Orientation";
    Image_String key = {.data = (char *)keyStr.c_str(), .size = keyStr.length()};
    Image_String value = {.data = nullptr, .size = 0};
    OH_ImageSourceNative_GetImageProperty(source, &key, &value);
    std::string valueStr(value.data, value.size);
    PixelmapUtils::Orientate(task->product.imageData, valueStr);
    free(value.data);
}

EXIF 标准定义了 8 种 Orientation 值,分别对应不同的翻转/旋转组合。PixelmapUtils::Orientate()unordered_map<string, function<void(OH_PixelmapNative*)>> 把每种方向映射到操作函数,对每一帧都执行。Image_String 返回的 data 不包含尾 0,必须用 std::string(value.data, value.size) 构造,并且按接口说明 free(value.data) 释放分配的内存。

四、DecodeInterceptorAvif 的独立拦截器

AVIF 由独立拦截器 DecodeInterceptorAvif 处理。CreateDefaultImageLoader 组装默认链时有条件地添加它:

loader->AddDecodeInterceptor(decodeInterceptor);       // Default 在链头
if (ImageKnifeDecoderAvif::IsAvifEnable()) {
    loader->AddDecodeInterceptor(decodeInterceptorAvif, Position::END); // AVIF 在链尾
}

如果设备不支持 AVIF,这个拦截器压根不会被添加。遇到 AVIF 图片时,默认解码器返回 false,链尾没有下一个节点,整条解码链返回 false,上层报解码失败。格式支持的增减变成了拦截器的有无,不影响已有拦截器的任何代码。

Resolve() 只在 fileTypeInfo->format == ImageFormat::AVIF 时接管。Decode() 方法先尝试从 task 上获取可复用的 ImageKnifeDecoderAvif 实例——AVIF 序列图的逐帧解码场景下同一个 decoder 实例会被多次复用,避免重复解析 AVIF 容器头部。

根据 IsFrameDecodeMode() 的值决定走向。逐帧模式调 DecodeFrame(task->GetDecodeFrameIndex(), &pixelmap) 只解一帧。批量模式分配 OH_PixelmapNative* 数组,循环调用 DecodeFrame(i, &pixelmapList[i])

批量模式有一个防御设计:循环开始前就构造 ImageData 对象持有 pixelmapList 指针。如果中途某一帧解码失败,函数 return false 时 imageData 是局部变量,析构时自动释放已解出的帧,不会泄漏。

auto imageData = std::make_shared<ImageData>(pixelmapList, decoder->GetDelayTimeList(), frameCount);
for (int i = 0; i < frameCount; i++) {
    std::string errorInfo = decoder->DecodeFrame(i, &pixelmapList[i]);
    if (!errorInfo.empty()) {
        task->EchoError(errorInfo);
        return false;  // imageData 析构释放 pixelmapList
    }
}
task->product.imageData = imageData;

解码器类图

五、AvifApi 的 dlopen 动态导入

ImageKnifeDecoderAvif 内部嵌套了一个 AvifApi 单例类,负责在运行时通过 dlopen 加载 libavif.so.16。出于"AVIF 解码依赖系统是否安装了 libavif 动态库"的考虑,不能在编译期硬链接,否则在不带 libavif 的设备上程序直接加载失败。

AvifApi 的构造函数调 dlopen("libavif.so.16", RTLD_NOW)RTLD_NOW 表示立即解析所有符号——如果 so 内部有未解析的依赖,dlopen 阶段就会失败,而不是等到实际调用时才崩溃。dlopen 成功后批量 dlsym 导出 11 个 libavif 的公开接口:

std::vector<std::string> funcNames = {
    "avifDecoderCreate", "avifDecoderDestroy", "avifRGBImageFreePixels",
    "avifDecoderSetIOMemory", "avifDecoderParse", "avifDecoderNthImageTiming",
    "avifDecoderNthImage", "avifRGBImageSetDefaults", "avifRGBImageAllocatePixels",
    "avifImageYUVToRGB", "avifImageScale"
};
for (auto &funcName : funcNames) {
    void *func = dlsym(handle_, funcName.c_str());
    if (func == nullptr) {
        available_ = false;
    } else {
        apiMap_[funcName] = func;
    }
}

函数指针缓存在 apiMap_unordered_map<string, void*>)中。每个包装方法(如 AvifDecoderCreate())内部用 static auto func = GetApiFuncByName<...>(name) 取函数指针。static 局部变量保证每个函数指针只查一次 map,后续调用零开销直接走缓存好的裸指针。

如果 dlsym 有任何一个符号失败,available_ 设 false 并立即 dlclose(handle_) 释放句柄,不让半初始化的 so 占用资源。析构函数对 handle_ 做了空指针检查后再 dlclose,确保正常路径也能正确释放。

六、编译期控制与 mock 实现

AvifApi 内部类、avifDecoder_ 指针、avifRGBImage rgbImage_ 等成员全部包裹在 #ifdef IMAGE_KNIFE_ENABLE_AVIF_DECODER 条件编译内。没有这个宏时,头文件中 ImageKnifeDecoderAvif 只剩公开方法声明,没有任何 libavif 类型依赖。

CMakeLists.txt 通过 IMAGEKNIFE_USING_LIBAVIF 选项控制:

if (IMAGEKNIFE_USING_LIBAVIF)
    add_definitions(-DIMAGE_KNIFE_ENABLE_AVIF_DECODER)
    include_directories(imageknifepro PUBLIC
        ${NATIVERENDER_ROOT_PATH}/thirdparty/libavif/${OHOS_ARCH}/include)
    target_sources(imageknifepro PRIVATE decoder/imageknife_decoder_avif.cpp)
else()
    target_sources(imageknifepro PRIVATE decoder/imageknife_decoder_avif_mock.cpp)
endif()

不启用时编译的是 imageknife_decoder_avif_mock.cpp——所有方法都是空实现:IsAvifEnable() 返回 false,Init()DecodeFrame() 返回固定错误字符串 "ImageKnife Avif Decoder Not Enable"GetWidth()/GetHeight()/GetFrameCount() 返回 0。这保证了没有 libavif 头文件和库的环境下也能正常编译链接。

这种双重防线的设计:编译期通过 #ifdef 决定是否引入 avif 头文件和真实实现代码,解决"编译环境没有 libavif SDK"的问题;运行时通过 dlopen 决定是否有可用的 so 库,解决"目标设备没装 libavif 运行时"的问题。即便编译时开启了 AVIF 支持,到了不带 libavif 的设备上,dlopen 返回 nullptr,available_ 设 false,Loader 初始化时不挂载 AVIF 拦截器,整条链路安全降级。

七、DecodeFrame 的单帧解码

ImageKnifeDecoderAvif::DecodeFrame() 的四步流程构成了 AVIF 像素解码的核心。

第一步,AvifDecoderNthImage(decoder_, frameIndex) 定位到指定帧。libavif 的 avifDecoderNthImage 支持随机访问——AVIF 序列图的每一帧都是独立可寻址的,不需要从第 0 帧开始顺序解码。

第二步,检查是否需要缩放。如果 desiredWidth_desiredHeight_ 与原图尺寸不一致且不为 0,调 AvifImageScale() 在 YUV 域完成缩放。YUV 域缩放比先转 RGB 再缩放少一次完整的色彩空间转换,对大尺寸图片能省下可观的计算量。

第三步,YUV 转 RGB。AvifRGBImageSetDefaults() 根据 avifImage 初始化 RGB 参数,AvifRGBImageAllocatePixels() 分配像素缓冲区,AvifImageYUVToRGB() 执行 YUV-to-RGBA 转换。

第四步,CreatePixelmapNative() 把 RGBA 像素数据写进 OH_PixelmapNative。这里有一个位深适配处理:libavif 支持 10bit 和 12bit 图片,以 uint16 存储每个通道,但 OH_PixelmapNativePIXEL_FORMAT_RGBA_8888 只支持 8bit。CovertToRGBA8888() 通过右移操作把每个通道从高位深缩放到 8bit:

size_t byteGap = rgbImage.depth - 8;
outData[pos]     = data[pos]     >> byteGap;  // R
outData[pos + 1] = data[pos + 1] >> byteGap;  // G
outData[pos + 2] = data[pos + 2] >> byteGap;  // B
outData[pos + 3] = data[pos + 3] >> byteGap;  // A

比如 10bit 图片的 depth 是 10,byteGap 为 2,每个 uint16 值右移 2 位截取高 8 位。这种处理会丢失低位精度,但在 8bit 显示设备上差异肉眼不可见。创建 PixelMap 时同样优先尝试 DMA allocator 接口,不支持再回退 AUTO 模式。

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


项目地址:ImageKnifePro

标签: none

添加新评论