ImageKnifePro 源码解读(九):组件设计——ContentSlot 包裹 Native 节点
ImageKnifePro 的组件层面临一个核心问题:ArkTS 的 C++ 层收到这些参数后,做三件事情。第一,解析 这个过程中,ArkTS 层只负责声明占位和传递配置,图片的加载、解码、显示全部在 C++ 层闭合完成。 构造函数接收三个参数: 事件注册采用了一个巧妙的设计。不再使用 组件尺寸变化时通过 显示图片时通过 动图播放的逻辑值得注意。 列表滚动场景中,组件创建的耗时会直接影响滑动流畅度。ImageKnifePro 提供了预创建接口来分摊这部分开销。 这个接口在 C++ 层提前创建 当 预创建池有最大数量限制(默认 10),通过 除了 两者的关键差异在于渲染方式。 从性能角度看, 当 判断的核心是 属性更新也做了精细的判断。border 变化需要特别处理——对于 Image 节点,border 更新后需要 组件销毁时调用 原生 ArkUI handle 的销毁必须在主线程执行。 如果析构发生在子线程(比如请求对象的引用计数归零),就把 handle 的 dispose 操作抛到主线程异步执行。 子类通过 以上就是本篇内容的所有了~有什么问题欢迎在评论区提出 项目地址:ImageKnifeProImage 组件接收 PixelMap 后需要经过框架层的序列化和渲染调度,这个过程在列表滚动场景下开销明显。ImageKnifePro 的解法是绕过 ArkTS Image,在 C++ 层直接创建和操作 ArkUI 原生节点,再通过 ContentSlot 把原生节点挂载到 ArkTS 组件树上。一、ContentSlot + nativeNode 的桥接模型
ImageKnifeComponent 是用户直接使用的 ArkTS 组件。它的 build() 方法只有一行:build() {
ContentSlot(this.rootSlot)
}rootSlot 是一个 NodeContent 对象,在组件创建时实例化。ContentSlot 是 ArkUI 框架提供的占位容器,它本身不产生任何可视内容,只是在组件树中预留一个位置,等待 Native 层把真正的节点挂上去。aboutToAppear 生命周期中,组件通过 nativeNode.createNativeRoot 或 nativeNode.connectContentSlot 进入 C++ 层。nativeNode 来自 libimageknifepro.so 的 NAPI 导出。传入的参数包括 rootSlot(NodeContent 句柄)、组件唯一 ID、ImageKnifeOption 配置对象、AnimatorOption 动图控制项以及页面上下文。ImageKnifeOption 中的各项配置,构造内部的 ImageKnifeOption C++ 对象。第二,创建 ImageKnifeNodeInternal 的具体子类(ImageKnifeNodeImage 或 ImageKnifeNodeAnimator),子类的构造函数中会通过 GetNodeAPI()->createNode(ARKUI_NODE_IMAGE) 或 createNode(ARKUI_NODE_IMAGE_ANIMATOR) 创建原生 ArkUI 节点。第三,把创建好的原生节点通过 OH_ArkUI_NodeContent_AddNode(contentSlot_, handle) 挂载到 ContentSlot 对应的 NodeContent 上。二、ImageKnifeNodeInternal——组件的核心基类
ImageKnifeNodeInternal 继承了两个父类:ImageKnifeNode(公开接口类)和 ArkUINode(封装 ArkUI C-API 的节点操作)。它管理着组件的完整生命周期,包括创建、更新、上树、清空、销毁。ImageKnifeOption 配置、ArkUI_NodeHandle 原生节点句柄和 customMeasure 是否自定义测算。内部调用 Init 完成初始化:创建 ImageKnifeRequestInternal 请求对象、记录边框配置、注册事件监听器、绑定组件 ID。setUserData 绑定指针——因为该接口可能被调用者用 handle 覆盖造成冲突。取而代之的是在注册事件时传入一个固定的事件 ID(IMAGE_KNIFE_EVENT_ID,由 "ImageKnifePro" 各字符 ASCII 值相加得到)和 this 指针。收到事件时先检查事件 ID 是否匹配,不匹配说明这个事件不是 ImageKnifePro 注册的,直接丢弃。void ImageKnifeNodeInternal::RegisterNodeEvent(ArkUI_NodeEventType eventType)
{
GetNodeAPI()->registerNodeEvent(handle, eventType, IMAGE_KNIFE_EVENT_ID, this);
}NODE_ON_SIZE_CHANGE(API 21+)或 NODE_EVENT_ON_AREA_CHANGE(低版本兼容)事件获取最新宽高。尺寸发生变化后,把 vp 转为 px 设置到 request 上,然后重新入队主图加载。如果组件还没完成首次测算,SVG 图片的解码会被延迟到测算完成后再发起。三、三种节点子类的分工
ImageKnifeNodeImage——标准图片节点
ImageKnifeNodeImage 创建 ARKUI_NODE_IMAGE 类型的原生节点。构造时设置了百分比宽高为 1(撑满父容器)、系统 Image 同步加载、禁用拖拽。它支持静态图和动图——动图播放完全由 C++ 层控制帧切换,不依赖系统 ImageAnimator 组件。DrawableDescriptor 把 PixelMap 传递给 Image 节点:ArkUI_AttributeItem item = {.object = data->GetDrawableDescriptorList()[frameCount]};
GetNodeAPI()->setAttribute(handle, NODE_IMAGE_SRC, &item);DisplayAnimation 记录首帧、尾帧、当前帧索引,然后通过 DisplayNextFrame 驱动帧循环。每帧显示后,通过 TaskWorker::ToMainThreadWithDelay 投递延时任务,延时值取自帧间隔时间(最小 40ms)。播放状态由 AnimatorOption 控制,组件不可见时自动暂停,恢复可见时继续播放。ImageKnifeNodeCustom——自绘制节点
ImageKnifeNodeCustom 创建 ARKUI_NODE_CUSTOM 类型的原生节点,继承自 ImageKnifeNodeImage。它注册了 ON_DRAW 和 ON_MEASURE 两个自定义事件。OnMeasure 回调中拿到布局约束的 maxWidth 和 maxHeight(单位 px),设置为组件尺寸。当 adaptable 属性开启时,组件宽高会适配图片宽高比例——计算图片和布局的宽高比,以较窄的一边为基准等比缩放。这个能力用于超长图片浏览,定宽不定高或定高不定宽的布局场景。OnDraw 回调中通过 ImageKnifeRender::Drawing 直接用 Drawing API 把 PixelMap 绘制到画布上,跳过了系统 Image 组件的渲染管线。ImageFit 的适配、ColorFilter 的应用、Border 的绘制都在这个绘制回调中完成。ImageKnifeView 组件使用的就是这种节点。ImageKnifeNodeAnimator——系统动图节点
ImageKnifeNodeAnimator 创建 ARKUI_NODE_IMAGE_ANIMATOR 类型的原生节点,利用系统 ImageAnimator 组件做动图播放。它通过 NODE_IMAGE_ANIMATOR_IMAGES 属性一次性把所有帧的 FrameInfo 列表传给系统组件。注释中明确标注了它的局限:不支持逐帧解码模式、不支持 HDR、无法使用 border 圆角、ImageFit、拖拽、ColorFilter 等。这个组件目前标注为"不推荐使用"。四、预创建机制——preCreateImageKnifeComponent
ImageKnife.getInstance().preCreateImageKnifeComponent(customId, imageKnifeOption);ImageKnifeNodeInternal 及其原生节点,但不挂载到任何 ContentSlot。创建好的节点以 customId 为 key 存入内部 map。ImageKnifeComponent 的 aboutToAppear 执行时,如果传入了 customId,就走 connectContentSlot 路径:从 map 中取出预创建的节点,调用 SetContentSlot 记录 NodeContent 句柄,然后 UploadNodeHandle 把节点挂载到 ContentSlot 上。关联成功后节点从预创建池中移除,转为正常组件管理。setMaxPreCreateNodeCount 调整。当数量超过上限时,按先进先出顺序销毁最早的预创建组件。五、ImageKnifeView——自绘制组件的另一条路径
ImageKnifeComponent,ImageKnifePro 还提供了 ImageKnifeView 组件。它的用法和 ImageKnifeComponent 几乎一样,但内部创建的是 ImageKnifeNodeCustom(自绘制节点)而非 ImageKnifeNodeImage(系统 Image 节点)。ImageKnifeNodeImage 把 PixelMap 通过 NODE_IMAGE_SRC 属性交给系统 Image 组件渲染;ImageKnifeNodeCustom 在 OnDraw 回调中通过 Drawing API 直接把 PixelMap 画到画布上。自绘制的好处是可以完全控制绘制过程——ImageFit 适配、ColorFilter 应用、Border 圆角裁切都在一次 Draw 调用中完成,不需要依赖系统 Image 组件的属性设置。ImageKnifeView 有一个独有的 adaptable 属性。开启后组件的测算宽高会适配图片的宽高比例,适用于超长图片浏览、定宽不定高的流式布局。开启 adaptable 时降采样会退化为 DEFAULT 策略,因为组件尺寸取决于图片尺寸而非反过来。ImageKnifeView 的 aboutToAppear 调用 nativeNode.createNativeImageView,传入参数中多了 adaptable 标记。C++ 层根据这个标记决定在 OnMeasure 中是用布局约束还是图片宽高比来计算最终尺寸。ImageKnifeView 的自绘制路径在某些场景下比 ImageKnifeComponent 更快——系统 Image 组件内部也有一套属性更新和布局计算逻辑,自绘制跳过了这些中间步骤。但 ImageKnifeView 暂不支持系统 Image 组件的隐式动画和 ResizeableOption 能力。六、Update——组件属性更新的决策逻辑
ImageKnifeOption 的 @Watch 触发时,ArkTS 层调用 nativeNode.updateNativeRoot,最终到达 ImageKnifeNodeInternal::Update。这个方法需要判断是否需要重新加载图片,还是只更新组件属性。IsUpdateNecessary。它生成当前配置的 memoryKey(包含图片源、变换信息、降采样参数、签名等),与上一次加载成功时记录的 mainSrcKey_ 比较。如果 key 相同,说明图片没变,只需更新 objectFit、border、colorFilter 等属性;如果 key 不同,需要取消当前加载请求,创建新的请求并发起加载。RefreshImage 清空重设图片资源(这是 ArkUI Image 组件的一个行为特点);对于 Custom 节点则不需要,RefreshImage 被覆盖为空实现。七、DisposeNode——析构的线程安全
DisposeNode,它需要处理多个清理事务:取消加载请求、从 ContentSlot 移除节点、注销事件监听、终止动图播放、清理子类注册的 ArkUI 事务。ImageKnifeNodeInternal 的析构函数中做了线程判断:if (TaskWorker::GetInstance()->IsMainThread()) {
GetNodeAPI()->disposeNode(handle);
} else {
TaskWorker::GetInstance()->ToMainThread([](void *handle) {
GetNodeAPI()->disposeNode(static_cast<ArkUI_NodeHandle>(handle));
}, handle);
}disposeHandle_ 标志位允许调用者选择自己管理 handle 的销毁,避免重复释放。RegisterDisposeIssue 注册清理函数,这些函数在 DisposeNode 中按注册顺序执行,此时对象尚未析构,所有成员仍然可用。这种机制把子类特有的清理逻辑与基类的通用清理流程解耦开来。