ImageKnifePro 的 Native 侧代码按职责分成三层:最底层的模块注册(napi_init.cpp),中间的 ArkTS 接口绑定(imageknife_napi.cpp),以及独立于 NAPI 之外的纯 C API 封装(imageknifec.cpp)。三层各自解决一个问题——怎么让系统发现模块、怎么把 JS 调用翻译成 C++ 调用、怎么让其他 Native 模块不依赖 NAPI 就能使用 ImageKnife 的能力。

NAPI三层绑定架构

一、napi_init.cpp:模块注册入口

HarmonyOS 加载一个 Native 模块时,需要找到模块的注册函数。napi_init.cpp 只做这一件事:填充 napi_module 结构体,在动态库加载时把模块注册到运行时。

static napi_module demoModule = {
    .nm_version = 1,
    .nm_flags = 0,
    .nm_filename = nullptr,
    .nm_register_func = init,
    .nm_modname = "imageknifepro",
    .nm_priv = ((void*)0),
    .reserved = { 0 },
};

extern "C" __attribute__((constructor)) void NapiRegisterModuleImageKnifePro(void)
{
    napi_module_register(&demoModule);
}

__attribute__((constructor)) 标记让这个函数在 .so 加载时自动执行,执行时机早于应用代码。nm_modname"imageknifepro",对应 ArkTS 侧 import nativeNode from 'libimageknifepro.so' 时的模块名。nm_register_func 指向 init 函数,内部只调了一行:

static napi_value init(napi_env env, napi_value exports)
{
    ImageKnifePro::ImageKnife::GetInstance().InitImageKnifeArkTs(env, exports);
    return exports;
}

InitImageKnifeArkTs 内部触发两件事——Init() 调用 TaskWorker::GetInstance()->RecordMainThreadId() 记录当前线程 ID(后续判断跨线程回调需要用到),然后 ImageKnifeNapi::Init(env, exports) 把所有 C++ 函数绑定到 exports 对象上,使得 ArkTS 侧能通过属性名直接调用。

注册函数的名称被改成了 NapiRegisterModuleImageKnifePro,而不是系统默认的名字。代码注释里写了原因:避免和其他模块的注册函数产生符号冲突。在一个 APP 同时链接多个 Native 模块的场景下,如果大家都使用默认名称,链接器会报重复符号错误。这个改名对多模块共存至关重要。

二、imageknife_napi.cpp:主接口层

ImageKnifeNapi 是整个桥接层中代码量最大的部分。它的 Init 方法向 exports 注册了超过 40 个函数,分成两组:一组处理组件节点的生命周期(在 DefineNodeAPI 中注册),另一组处理全局配置和缓存管理。

节点生命周期接口

节点相关的接口有四组核心操作:createNativeRoot / updateNativeRoot / clearNativeRoot / destroyNativeRoot,分别对应静态图组件的创建、更新、清理和销毁。动图组件有独立的 createAnimatorImage / updateAnimatorImage / controlAnimatorImage / clearAnimatorImage / destroyAnimatorImage 系列,额外支持了动画控制。

还有一组自渲染组件接口:createNativeImageView 创建基于 CustomNode 的自渲染组件,updateNodeAdaptable 控制组件是否自适应图片宽高比。以及一组属性更新接口:updateNodeDraggable 控制图片拖拽,updateNodeContentTransition 设置内容过渡效果。

NAPI接口分组与调用流程

参数解析:从 napi_value 到 C++ 对象

CreateNativeRoot 为例,ArkTS 侧传入 6 个参数,NAPI 层逐个转换成 C++ 类型:

napi_value ImageKnifeNapi::CreateNativeRoot(napi_env env, napi_callback_info info)
{
    const size_t argSize = 6;
    size_t argc = argSize;
    napi_value args[argSize] = {nullptr};
    napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);

    ArkUI_NodeContentHandle contentHandle;
    OH_ArkUI_GetNodeContentFromNapiValue(env, args[0], &contentHandle);

    std::string componentId = NapiParser::ParseString(env, args[1]);
    auto imageKnifeOption = std::make_shared<ImageKnifeOptionNapi>(env, args[2]);
    auto animatorOption = AnimatorOptionNapi::CreateAnimatorOptionNapi(env, args[3]);
    imageKnifeOption->ParseContext(env, args[4]);

    auto imageKnifeNode = ImageKnifeNode::CreateImageKnifeNode(
        componentId, contentHandle, imageKnifeOption, animatorOption);
    ParseComponentAttribute(env, args[5], imageKnifeNode);
    imageKnifeNode->Execute();
    return nullptr;
}

args[0] 是 ArkTS 侧的 NodeContent 对象,通过 OH_ArkUI_GetNodeContentFromNapiValue 转换成 C 侧的 ArkUI_NodeContentHandleargs[1] 是组件 ID 字符串,用 NapiParser::ParseString 提取。args[2]ImageKnifeOption,被包装成 ImageKnifeOptionNapi 对象——这个包装类在构造函数里遍历 JS 对象的所有属性,把 loadSrcobjectFitborder 等字段逐个解析成 C++ 类型。args[5] 是组件属性对象,ParseComponentAttribute 从中读取 syncLoaddraggableadaptablecontentTransition 四个字段。

这里有个设计取舍:所有参数都在函数调用时同步解析,没有延迟到子线程。原因是 napi_value 的生命周期绑定在当前的 napi_env 上,不能跨线程使用。所以 NAPI 层的职责就是"尽快把 JS 对象翻译成 C++ 对象,然后交给 ImageKnifeInternal 去异步处理"。

异步操作与 Promise

全局配置接口中有几个需要异步执行的操作,比如 initFileCache。文件缓存初始化涉及磁盘扫描,不能阻塞主线程:

napi_value ImageKnifeNapi::InitFileCache(napi_env env, napi_callback_info info)
{
    auto promise = std::make_shared<PromiseNapi<void>>();
    // ... 解析参数 ...
    auto initFunc = [fileDir, fileSize, memorySize, path](void* data) {
        ImageKnifeInternal::GetInstance().InitFileCache(fileDir, fileSize, memorySize, path);
    };
    bool res = TaskWorker::GetInstance()->PushTask(
        initFunc, [promise](void*) { promise->Resolve(); },
        nullptr, "InitFileCache", ImageKnifeQOS::USER_INITIATED);
    if (res) {
        return promise->GetPromise();
    }
    return promise->Reject("Push Thread Failed");
}

PromiseNapi<void> 在构造时通过 napi_create_promise 创建一个 JS Promise,在 Resolve() 调用时通过 napi_resolve_deferred 兑现。这样 ArkTS 侧调用 await ImageKnife.getInstance().initFileCache(context) 就是标准的异步写法。TaskWorker 把初始化任务推到子线程执行,完成后回到主线程 Resolve Promise。

类似的模式也用在 removeAllFileCacheaddSmallEndFileCache 等接口上。

取消与重试

Cancel 接口通过 napi_unwrap 取回之前绑定的 ImageKnifeRequestInternal 指针,然后调用 CancelRequestPreload 在绑定时使用了 napi_wrap,把 C++ 侧的 request 指针关联到 JS 对象上。这样 JS 侧持有的 request 对象就是一个代理,调用 cancel(request) 时能准确定位到 C++ 侧的请求实例。

Reload 接口接收一个 requestId 字符串,格式是 "componentId#version"。C++ 侧解析出组件 ID 和版本号,找到对应的 ImageNode,校验版本一致且未完成后才发起重新加载。版本号机制避免了用户对已过期请求的误操作。

三、imageknifec.cpp:纯 C API

imageknifec.cpp 是完全脱离 NAPI 的纯 C API 层。它的存在是为了让其他 C/C++ 模块能调用 ImageKnife 的能力,不需要引入 NAPI 头文件,不需要 napi_env

extern "C" {
ImageknifecError ImageknifecGetCacheImageByLoadSrc(
    const char *loadSrc,
    ImageknifecGetCacheImageCallbackObject *onComplete,
    ImageknifecCacheStrategy cacheType,
    const char *signature)
{
    if (loadSrc == nullptr || onComplete == nullptr) {
        return IMAGE_KNIFE_ERROR_BAD_PARAMETER;
    }
    auto option = std::make_shared<ImageKnifeOption>();
    option->loadSrc.SetString(loadSrc);
    if (signature != nullptr) {
        option->signature = signature;
    }
    auto callback = onComplete->callback;
    void *usrCtx = onComplete->usrCtx;
    auto funcInner = [callback, usrCtx](std::shared_ptr<ImageData> imageData) {
        if (callback == nullptr) return;
        if (imageData == nullptr) {
            callback(usrCtx, nullptr);
            return;
        }
        callback(usrCtx, reinterpret_cast<ImageknifecImageData *>(
            new ImageKnifeWrapper<ImageData>(imageData)));
    };
    ImageKnife::GetInstance().GetCacheImage(option, funcInner, static_cast<CacheStrategy>(cacheType));
    return IMAGE_KNIFE_SUCCESS;
}
}

接口用 extern "C" 包裹,函数名采用 Imageknifec 前缀以区分 NAPI 层接口。入参全是 C 类型(const char *、结构体指针),返回值是错误码枚举。内部通过 ImageKnifeWrapper 模板把 shared_ptr<ImageData> 包装成不透明的 C 指针传出去,调用方拿到指针后通过配套的释放函数销毁。

这种设计让 ImageKnifePro 在 HarmonyOS 生态里有了更大的适用面。比如一个用 C 写的音视频处理模块,需要拿到缩略图做封面,直接调用 ImageknifecGetCacheImageByLoadSrc 即可,不用绕道 ArkTS。

三层绑定的完整调用链

四、ImageKnifeInternal:桥接层背后的单例

NAPI 层所有接口最终都汇聚到 ImageKnifeInternal 这个单例上。它继承自 ImageKnife(对外的抽象接口),实现了所有虚函数。ImageKnife::GetInstance() 返回的是 ImageKnifeInternal 的静态实例:

ImageKnife &ImageKnife::GetInstance()
{
    static ImageKnifeInternal imageKnifeInternal;
    return imageKnifeInternal;
}

ImageKnifeInternal 持有几个核心状态:imageNodeMap_ 存储所有活跃的组件节点,preCreatedNodeMap_preCreatedNodeIdList_ 管理预创建节点池,loaderMap_transformationMap_ 存储注册的自定义加载器和图形变换,httpHeaders_ 存全局 HTTP 头,onLoadEnd_ 存全局加载完成回调。

Execute 方法是请求执行的入口。它先通过 MarkExecuted 防止重复执行,再通过组件 ID 查找对应的 ImageNode(先查 imageNodeMap_,再查 preCreatedNodeMap_),把 Node 的弱引用设置到 request 上(避免子线程竞争 NodeMap),最后把 request 推入 Dispatcher 队列。

这里有个设计细节:SetImageNode 用的是弱引用而不是强引用。request 执行周期可能跨越多次组件复用,如果持有强引用,已被回收的 Node 就释放不了。用弱引用在回调时 lock 一下,如果 Node 已经销毁就跳过显示,这是典型的异步生命周期管理手法。

五、回调机制:从 C++ 回到 ArkTS

NAPI 桥接层不只是单向的"ArkTS 调 C++",还有反向的回调路径。以 SetGlobalLoadEndCallback 为例:

ArkTS 侧传入一个 JS 函数,NAPI 层用 napi_create_reference 创建持久引用保存下来。当 C++ 侧某个请求完成时,OnImageLoadEnd 被调用,它构造一个 LoadEndInfo 结构体,然后调用 onLoadEnd_->Execute(info)LoadEndCallbackNapi::Execute 内部通过 napi_get_reference_value 取回 JS 函数,再用 napi_call_function 把参数传过去。

void LoadEndCallbackNapi::CallJsFunction(const LoadEndInfo &info)
{
    ImageKnifeHandleScope scope(env_);
    napi_value src = ConvertStringToJsValue(env_, info.src, false);
    napi_value isSuccess;
    napi_get_boolean(env_, info.isSuccess, &isSuccess);
    napi_value timeInfo = ConvertTimeInfo(env_, info);
    // ... 构造其他参数 ...
    napi_value funcValue = nullptr;
    napi_get_reference_value(env_, jsCallback_, &funcValue);
    napi_call_function(env_, nullptr, funcValue, argCount, args, nullptr);
}

这里有个约束:napi_call_function 必须在创建 napi_env 的线程(即主线程)上调用。ImageKnifePro 通过 TaskWorker 的 ToMainThread 机制确保回调在主线程执行。如果直接在解码线程调用 napi_call_function,会触发运行时错误。

PromiseNapiResolve / Reject 也遵循同样的约束。TaskWorker 的 PushTask 接受两个函数——executeFunc 在子线程运行,completeFunc 自动回到主线程执行,PromiseNapi::Resolve 放在 completeFunc 里就不会有线程问题。

六、三层分离的工程价值

三层分离的设计并非过度抽象。napi_init.cpp 独立出来,使得模块注册逻辑和业务逻辑完全解耦。如果需要改模块名称或注册方式,只改一个文件。imageknife_napi.cpp 承担了所有 ArkTS-C++ 的类型转换工作,业务逻辑代码不用关心 napi_value 类型。imageknifec.cpp 让 C API 使用方完全不需要感知 NAPI 的存在。

ImageKnifeInternal 继承 ImageKnife 抽象类的设计也有意义。C API 层调用 ImageKnife::GetInstance(),NAPI 层调用 ImageKnifeInternal::GetInstance(),后者提供了更多内部方法(如 SetRootNodeGetPreCreatedNode)。外部 C 模块只能通过 ImageKnife 抽象接口调用有限的公开方法,内部 NAPI 层则能使用完整功能。这是一种接口隔离策略。

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


项目地址:ImageKnifePro

标签: none

添加新评论