ImageKnifePro 的组件层面临一个核心问题:ArkTS 的 Image 组件接收 PixelMap 后需要经过框架层的序列化和渲染调度,这个过程在列表滚动场景下开销明显。ImageKnifePro 的解法是绕过 ArkTS Image,在 C++ 层直接创建和操作 ArkUI 原生节点,再通过 ContentSlot 把原生节点挂载到 ArkTS 组件树上。

一、ContentSlot + nativeNode 的桥接模型

ImageKnifeComponent 是用户直接使用的 ArkTS 组件。它的 build() 方法只有一行:

build() {
    ContentSlot(this.rootSlot)
}

rootSlot 是一个 NodeContent 对象,在组件创建时实例化。ContentSlot 是 ArkUI 框架提供的占位容器,它本身不产生任何可视内容,只是在组件树中预留一个位置,等待 Native 层把真正的节点挂上去。

aboutToAppear 生命周期中,组件通过 nativeNode.createNativeRootnativeNode.connectContentSlot 进入 C++ 层。nativeNode 来自 libimageknifepro.so 的 NAPI 导出。传入的参数包括 rootSlot(NodeContent 句柄)、组件唯一 ID、ImageKnifeOption 配置对象、AnimatorOption 动图控制项以及页面上下文。

C++ 层收到这些参数后,做三件事情。第一,解析 ImageKnifeOption 中的各项配置,构造内部的 ImageKnifeOption C++ 对象。第二,创建 ImageKnifeNodeInternal 的具体子类(ImageKnifeNodeImageImageKnifeNodeAnimator),子类的构造函数中会通过 GetNodeAPI()->createNode(ARKUI_NODE_IMAGE)createNode(ARKUI_NODE_IMAGE_ANIMATOR) 创建原生 ArkUI 节点。第三,把创建好的原生节点通过 OH_ArkUI_NodeContent_AddNode(contentSlot_, handle) 挂载到 ContentSlot 对应的 NodeContent 上。

这个过程中,ArkTS 层只负责声明占位和传递配置,图片的加载、解码、显示全部在 C++ 层闭合完成。

flowchart TD
    A[ArkTS: ImageKnifeComponent] -->|aboutToAppear| B[NAPI: createNativeRoot]
    B --> C[C++: 解析 ImageKnifeOption]
    C --> D[创建 ImageKnifeNodeImage]
    D --> E["createNode(ARKUI_NODE_IMAGE)"]
    E --> F["OH_ArkUI_NodeContent_AddNode(contentSlot, handle)"]
    F --> G[Native 节点挂载到 ContentSlot]
    G --> H[ArkTS 渲染树中显示]
    
    I[ArkTS: ImageKnifeComponent] -->|aboutToDisappear| J[NAPI: destroyNativeRoot]
    J --> K[C++: DisposeNode]
    K --> L["OH_ArkUI_NodeContent_RemoveNode"]
    K --> M["disposeNode(handle)"]

二、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_DRAWON_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

列表滚动场景中,组件创建的耗时会直接影响滑动流畅度。ImageKnifePro 提供了预创建接口来分摊这部分开销。

ImageKnife.getInstance().preCreateImageKnifeComponent(customId, imageKnifeOption);

这个接口在 C++ 层提前创建 ImageKnifeNodeInternal 及其原生节点,但不挂载到任何 ContentSlot。创建好的节点以 customId 为 key 存入内部 map。

ImageKnifeComponentaboutToAppear 执行时,如果传入了 customId,就走 connectContentSlot 路径:从 map 中取出预创建的节点,调用 SetContentSlot 记录 NodeContent 句柄,然后 UploadNodeHandle 把节点挂载到 ContentSlot 上。关联成功后节点从预创建池中移除,转为正常组件管理。

预创建池有最大数量限制(默认 10),通过 setMaxPreCreateNodeCount 调整。当数量超过上限时,按先进先出顺序销毁最早的预创建组件。

sequenceDiagram
    participant App as 应用代码
    participant IK as ImageKnife
    participant Pool as 预创建池
    participant Comp as ImageKnifeComponent
    participant Slot as ContentSlot

    App->>IK: preCreateImageKnifeComponent("card_1", option)
    IK->>Pool: 创建节点, 存入 map["card_1"]
    Note over Pool: 节点已创建但未挂载

    App->>Comp: 渲染 ImageKnifeComponent(customId="card_1")
    Comp->>Comp: aboutToAppear()
    Comp->>Pool: connectContentSlot("card_1", rootSlot)
    Pool->>Slot: OH_ArkUI_NodeContent_AddNode(rootSlot, handle)
    Pool->>Pool: 从 map 中移除 "card_1"
    Note over Slot: 预创建节点挂载到组件树

五、ImageKnifeView——自绘制组件的另一条路径

除了 ImageKnifeComponent,ImageKnifePro 还提供了 ImageKnifeView 组件。它的用法和 ImageKnifeComponent 几乎一样,但内部创建的是 ImageKnifeNodeCustom(自绘制节点)而非 ImageKnifeNodeImage(系统 Image 节点)。

两者的关键差异在于渲染方式。ImageKnifeNodeImage 把 PixelMap 通过 NODE_IMAGE_SRC 属性交给系统 Image 组件渲染;ImageKnifeNodeCustomOnDraw 回调中通过 Drawing API 直接把 PixelMap 画到画布上。自绘制的好处是可以完全控制绘制过程——ImageFit 适配、ColorFilter 应用、Border 圆角裁切都在一次 Draw 调用中完成,不需要依赖系统 Image 组件的属性设置。

ImageKnifeView 有一个独有的 adaptable 属性。开启后组件的测算宽高会适配图片的宽高比例,适用于超长图片浏览、定宽不定高的流式布局。开启 adaptable 时降采样会退化为 DEFAULT 策略,因为组件尺寸取决于图片尺寸而非反过来。

ImageKnifeViewaboutToAppear 调用 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 不同,需要取消当前加载请求,创建新的请求并发起加载。

属性更新也做了精细的判断。border 变化需要特别处理——对于 Image 节点,border 更新后需要 RefreshImage 清空重设图片资源(这是 ArkUI Image 组件的一个行为特点);对于 Custom 节点则不需要,RefreshImage 被覆盖为空实现。

七、DisposeNode——析构的线程安全

组件销毁时调用 DisposeNode,它需要处理多个清理事务:取消加载请求、从 ContentSlot 移除节点、注销事件监听、终止动图播放、清理子类注册的 ArkUI 事务。

原生 ArkUI handle 的销毁必须在主线程执行。ImageKnifeNodeInternal 的析构函数中做了线程判断:

if (TaskWorker::GetInstance()->IsMainThread()) {
    GetNodeAPI()->disposeNode(handle);
} else {
    TaskWorker::GetInstance()->ToMainThread([](void *handle) {
        GetNodeAPI()->disposeNode(static_cast<ArkUI_NodeHandle>(handle));
    }, handle);
}

如果析构发生在子线程(比如请求对象的引用计数归零),就把 handle 的 dispose 操作抛到主线程异步执行。disposeHandle_ 标志位允许调用者选择自己管理 handle 的销毁,避免重复释放。

flowchart TD
    A[DisposeNode 调用] --> B[取消加载请求]
    B --> C{节点已上树?}
    C -->|是| D["OH_ArkUI_NodeContent_RemoveNode"]
    C -->|否| E[跳过]
    D --> F[释放 SVG 延迟解码任务]
    E --> F
    F --> G[注销尺寸变化事件]
    G --> H[移除事件接收器]
    H --> I[终止动图播放]
    I --> J[执行子类 DisposeIssue]
    J --> K[从全局管理器中移除]
    K --> L{析构时所在线程?}
    L -->|主线程| M["disposeNode(handle)"]
    L -->|子线程| N["抛到主线程执行 disposeNode"]

子类通过 RegisterDisposeIssue 注册清理函数,这些函数在 DisposeNode 中按注册顺序执行,此时对象尚未析构,所有成员仍然可用。这种机制把子类特有的清理逻辑与基类的通用清理流程解耦开来。

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


项目地址:ImageKnifePro

标签: none

添加新评论