ImageKnifePro 源码解读(十三):NAPI 桥接——ArkTS 和 C++ 之间的三层绑定
ImageKnifePro 的 Native 侧代码按职责分成三层:最底层的模块注册(napi_init.cpp),中间的 ArkTS 接口绑定(imageknife_napi.cpp),以及独立于 NAPI 之外的纯 C API 封装(imageknifec.cpp)。三层各自解决一个问题——怎么让系统发现模块、怎么把 JS 调用翻译成 C++ 调用、怎么让其他 Native 模块不依赖 NAPI 就能使用 ImageKnife 的能力。 HarmonyOS 加载一个 Native 模块时,需要找到模块的注册函数。napi_init.cpp 只做这一件事:填充 注册函数的名称被改成了 节点相关的接口有四组核心操作: 还有一组自渲染组件接口: 以 这里有个设计取舍:所有参数都在函数调用时同步解析,没有延迟到子线程。原因是 全局配置接口中有几个需要异步执行的操作,比如 类似的模式也用在 imageknifec.cpp 是完全脱离 NAPI 的纯 C API 层。它的存在是为了让其他 C/C++ 模块能调用 ImageKnife 的能力,不需要引入 NAPI 头文件,不需要 接口用 这种设计让 ImageKnifePro 在 HarmonyOS 生态里有了更大的适用面。比如一个用 C 写的音视频处理模块,需要拿到缩略图做封面,直接调用 NAPI 层所有接口最终都汇聚到 这里有个设计细节: NAPI 桥接层不只是单向的"ArkTS 调 C++",还有反向的回调路径。以 ArkTS 侧传入一个 JS 函数,NAPI 层用 这里有个约束: 三层分离的设计并非过度抽象。napi_init.cpp 独立出来,使得模块注册逻辑和业务逻辑完全解耦。如果需要改模块名称或注册方式,只改一个文件。imageknife_napi.cpp 承担了所有 ArkTS-C++ 的类型转换工作,业务逻辑代码不用关心 ImageKnifeInternal 继承 ImageKnife 抽象类的设计也有意义。C API 层调用 以上就是本篇内容的所有了~有什么问题欢迎在评论区提出 项目地址:ImageKnifePro
一、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_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_NodeContentHandle。args[1] 是组件 ID 字符串,用 NapiParser::ParseString 提取。args[2] 是 ImageKnifeOption,被包装成 ImageKnifeOptionNapi 对象——这个包装类在构造函数里遍历 JS 对象的所有属性,把 loadSrc、objectFit、border 等字段逐个解析成 C++ 类型。args[5] 是组件属性对象,ParseComponentAttribute 从中读取 syncLoad、draggable、adaptable、contentTransition 四个字段。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。removeAllFileCache、addSmallEndFileCache 等接口上。取消与重试
Cancel 接口通过 napi_unwrap 取回之前绑定的 ImageKnifeRequestInternal 指针,然后调用 CancelRequest。Preload 在绑定时使用了 napi_wrap,把 C++ 侧的 request 指针关联到 JS 对象上。这样 JS 侧持有的 request 对象就是一个代理,调用 cancel(request) 时能准确定位到 C++ 侧的请求实例。Reload 接口接收一个 requestId 字符串,格式是 "componentId#version"。C++ 侧解析出组件 ID 和版本号,找到对应的 ImageNode,校验版本一致且未完成后才发起重新加载。版本号机制避免了用户对已过期请求的误操作。三、imageknifec.cpp:纯 C API
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 指针传出去,调用方拿到指针后通过配套的释放函数销毁。ImageknifecGetCacheImageByLoadSrc 即可,不用绕道 ArkTS。
四、ImageKnifeInternal:桥接层背后的单例
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
SetGlobalLoadEndCallback 为例: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,会触发运行时错误。PromiseNapi 的 Resolve / Reject 也遵循同样的约束。TaskWorker 的 PushTask 接受两个函数——executeFunc 在子线程运行,completeFunc 自动回到主线程执行,PromiseNapi::Resolve 放在 completeFunc 里就不会有线程问题。六、三层分离的工程价值
napi_value 类型。imageknifec.cpp 让 C API 使用方完全不需要感知 NAPI 的存在。ImageKnife::GetInstance(),NAPI 层调用 ImageKnifeInternal::GetInstance(),后者提供了更多内部方法(如 SetRootNode、GetPreCreatedNode)。外部 C 模块只能通过 ImageKnife 抽象接口调用有限的公开方法,内部 NAPI 层则能使用完整功能。这是一种接口隔离策略。