目录

自定义节点常见问题

本文档介绍自定义节点的常见问题并提供参考。

自定义组件的aboutToDisappear回调异常

问题现象

从API version 12开始,自定义节点的子节点在页面退出后未立即回调自定义组件的aboutToDisappear方法。自定义组件的aboutToDisappear通常在其销毁的时候触发,页面销毁后未立即回调则说明该自定义组件在页面销毁后未立即销毁。

可能原因

解决措施

示例代码

下文中,根节点表示BuilderNode的根节点,aboutToDisappear表示BuilderNode中构建的自定义组件(即BuilderNodePage)中的回调。

import { BuilderNode, FrameNode, NodeContent } from '@kit.ArkUI';
import { ArrayList } from '@kit.ArkTS';

const CUSTOM_COMPONENT_CONT: string = 'CustomComponentCont';
AppStorage.setOrCreate<number>(CUSTOM_COMPONENT_CONT, 0);
let globalBuilderNodeList: ArrayList<BuilderNode<[]>> = new ArrayList<BuilderNode<[]>>();

@Component
struct BuilderNodePage {
  aboutToAppear(): void {
    const count: number | undefined = AppStorage.get<number>(CUSTOM_COMPONENT_CONT);
    const current: number = count ? count + 1 : 1;
    AppStorage.setOrCreate<number>(CUSTOM_COMPONENT_CONT, current);
    console.info('BuilderNodePage', 'aboutToAppear ' + AppStorage.get<number>(CUSTOM_COMPONENT_CONT));
  }

  aboutToDisappear(): void {
    setTimeout(() => {
      const count: number | undefined = AppStorage.get<number>(CUSTOM_COMPONENT_CONT);
      console.info('BuilderNodePage', 'aboutToDisappear ' + count);
      const current: number = count ? count - 1 : -1;
      AppStorage.set<number>(CUSTOM_COMPONENT_CONT, current);
      console.info('BuilderNodePage', 'aboutToDisappear ' + AppStorage.get<number>(CUSTOM_COMPONENT_CONT));
    }, 1)
  }

  build() {
    Text('This is a BuilderNode')
  }
}

@Builder
function builderNodeBuilder() {
  BuilderNodePage();
}

@Entry
@Component
struct NavigationExample {
  @Provide('pageInfos') pageInfos: NavPathStack = new NavPathStack()
  private arr: number[] = [1, 2, 3];
  @StorageProp(CUSTOM_COMPONENT_CONT) customComponentCount: number | undefined =
    AppStorage.get<number>(CUSTOM_COMPONENT_CONT);

  @Builder
  pageMap(name: string) {
    if (name === 'NavDestinationTitle1') {
      pageOneTmp();
    } else if (name === 'NavDestinationTitle2') {
      pageTwoTmp();
    } else if (name === 'NavDestinationTitle3') {
      pageThreeTmp();
    }
  }

  onPageShow(): void {
    console.info('NavigationExample ' + this.customComponentCount);
  }

  build() {
    Column() {
      Navigation(this.pageInfos) {
        Text('Number of remaining custom components in BuilderNode ' + this.customComponentCount)
          .width('90%')
          .height(40)
          .backgroundColor('#FFFFFF')
        Button('Remove Global References')
          .onClick(() => {
            // 清除所有全局引用。
            // 可以使用hidumper指令触发GC验证引用关系是否清零。
            globalBuilderNodeList.clear();
          })
        List({ space: 12 }) {
          ForEach(this.arr, (item: number) => {
            ListItem() {
              Text('Page' + item)
                .width('100%')
                .height(72)
                .backgroundColor('#FFFFFF')
                .borderRadius(24)
                .fontSize(16)
                .fontWeight(500)
                .textAlign(TextAlign.Center)
                .onClick(() => {
                  this.pageInfos.pushPath({ name: 'NavDestinationTitle' + item });
                })
            }
          }, (item: number) => item.toString())
        }
        .width('100%')
        .margin({ top: 12 })
      }
      .title('Main Title')
      .mode(NavigationMode.Stack)
      .navDestination(this.pageMap)
    }
    .height('100%')
    .width('100%')
    .backgroundColor('#F1F3F5')
  }
}

@Component
export struct pageOneTmp {
  @Consume('pageInfos') pageInfos: NavPathStack;
  private builderNode: BuilderNode<[]> = new BuilderNode(this.getUIContext());
  private content: NodeContent = new NodeContent();

  aboutToAppear(): void {
    console.info('pageOneTmp', 'aboutToAppear')
    this.builderNode.build(wrapBuilder(builderNodeBuilder));
    if (this.builderNode.getFrameNode()) {
      this.content.addFrameNode(this.builderNode.getFrameNode());
    }
    // 添加全局引用,该对象在全局引用移除前无法被GC。
    globalBuilderNodeList.add(this.builderNode);
  }

  aboutToDisappear(): void {
    console.info('pageOneTmp', 'aboutToDisappear')
  }

  build() {
    NavDestination() {
      Column() {
        Text('pageOneTmp')
        ContentSlot(this.content)
      }.width('100%').height('100%')
    }.title('NavDestinationTitle1')
    .onBackPressed(() => {
      const popDestinationInfo = this.pageInfos.pop(); // 弹出路由栈栈顶元素。
      console.info('pop returnValue: ' + JSON.stringify(popDestinationInfo));
      return true;
    })
  }
}

@Component
export struct pageTwoTmp {
  @Consume('pageInfos') pageInfos: NavPathStack;
  private builderNode: BuilderNode<[]> = new BuilderNode(this.getUIContext());
  private content: NodeContent = new NodeContent();

  aboutToAppear(): void {
    console.info('pageTwoTmp', 'aboutToAppear')
    this.builderNode!.build(wrapBuilder(builderNodeBuilder));
    if (this.builderNode!.getFrameNode()) {
      // 将BuilderNode的根节点挂载至NodeContent对象中。
      // 如果要触发builderNode的根节点的析构,需要主动从NodeContent对象中移除该节点,或者等待NodeContent对象被GC。
      // 否则,BuilderNode的根节点无法触发析构。
      this.content.addFrameNode(this.builderNode!.getFrameNode());
    }
  }

  aboutToDisappear(): void {
    console.info('pageTwoTmp', 'aboutToDisappear')
    if (this.builderNode?.getFrameNode()) {
      // 将BuilderNode的根节点从NodeContent对象中移除。
      // 需要在BuilderNode的dispose操作之前执行,否则无法获得该BuilderNode的根节点。
      this.content.removeFrameNode(this.builderNode?.getFrameNode());
    }
    this.builderNode?.dispose();
  }

  build() {
    NavDestination() {
      Column() {
        Text('pageTwoTmp')
        ContentSlot(this.content)
      }.width('100%').height('100%')
    }.title('NavDestinationTitle2')
    .onBackPressed(() => {
      const popDestinationInfo = this.pageInfos.pop(); // 弹出路由栈栈顶元素。
      console.info('pop returnValue: ' + JSON.stringify(popDestinationInfo));
      return true;
    })
  }
}

@Component
export struct pageThreeTmp {
  @Consume('pageInfos') pageInfos: NavPathStack;
  private builderNode: BuilderNode<[]> = new BuilderNode(this.getUIContext());
  private content: NodeContent = new NodeContent();
  private rootNode: FrameNode = new FrameNode(this.getUIContext());

  aboutToAppear(): void {
    console.info('pageThreeTmp', 'aboutToAppear');
    this.builderNode!.build(wrapBuilder(builderNodeBuilder));
    if (this.builderNode!.getFrameNode()) {
      this.content.addFrameNode(this.rootNode);
      // BuilderNode的根节点被挂载至FrameNode对象对应的节点中。
      // BuilderNode的根节点如果要触发析构需要主动从FrameNode对象对应的节点中移除,或者等待FrameNode对象对应的节点析构。
      // 否则,BuilderNode的根节点无法触发析构。
      this.rootNode.appendChild(this.builderNode.getFrameNode());
    }
  }

  aboutToDisappear(): void {
    console.info('pageThreeTmp', 'aboutToDisappear');
    if (this.builderNode?.getFrameNode()) {
      // 将BuilderNode的根节点从FrameNode对象对应的节点中移除。
      // 需要在BuilderNode的dispose操作以及FrameNode对象dispose之前执行,否则无法获得他们对应的节点。
      this.rootNode.removeChild(this.builderNode?.getFrameNode());
    }
    this.builderNode?.dispose();
  }

  build() {
    NavDestination() {
      Column() {
        Text('pageThreeTmp')
        ContentSlot(this.content)
      }.width('100%').height('100%')
    }.title('NavDestinationTitle3')
    .onBackPressed(() => {
      const popDestinationInfo = this.pageInfos.pop(); // 弹出路由栈栈顶元素。
      console.info('pop returnValue: ' + JSON.stringify(popDestinationInfo));
      return true;
    })
  }
}

BuilderNode前后端循环引用导致的内存泄漏问题

问题现象

使用BuilderNode创建自定义组件节点时,有可能出现前后端(ArkTS UI层与Native UI引擎层)之间的循环引用,使得自定义节点无法被销毁,进而引发内存泄漏。

可能原因

解决措施

示例代码

如下示例中,将BuilderNode前端对象作为参数传递给了自定义组件,构造了前后端循环引用的场景。

下文中,aboutToDisappear表示BuilderNode中构建的自定义组件(即TestComponent)析构时的回调。

import { FrameNode, NodeController, BuilderNode } from '@kit.ArkUI';
import { hilog } from '@kit.PerformanceAnalysisKit';

// 定义传递参数的接口
interface ParamsInterface {
  builderRootNode: BuilderNode<[ParamsInterface]> | null;
}

// 自定义组件
@Component
struct TestComponent {
  builderRootNode: BuilderNode<[ParamsInterface]> | null = null;
  build() {
    Column() {
      Text('This is a BuilderNode.')
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
    }
    .width('100%')
    .backgroundColor(Color.Gray)
  }

  // 自定义组件实例创建时的回调
  aboutToAppear() {
    hilog.info(0x0000, 'testTag', 'aboutToAppear');
  }

  // 自定义组件实例析构时的回调
  aboutToDisappear() {
    hilog.info(0x0000, 'testTag', 'aboutToDisappear');
  }
}

@Builder
function buildComponent(params: ParamsInterface) {
  TestComponent(params)
}

// 继承NodeController实现自定义UI控制器
class MyNodeController extends NodeController {
  private builderNode: BuilderNode<[ParamsInterface]> | null = null;

  makeNode(uiContext: UIContext): FrameNode | null {
    this.builderNode = new BuilderNode(uiContext);

    // 将builderNode自身作为参数传递给自定义组件,产生前后端循环引用场景
    this.builderNode.build(new WrappedBuilder(buildComponent), {builderRootNode: this.builderNode});

    return this.builderNode.getFrameNode();
  }

  // 解除当前builderNode与后端实体的引用关系,并置空为null
  dispose() {
    if (this.builderNode !== null) {
      this.builderNode.dispose();
      this.builderNode = null;
    }
  }

  // 清理builderNode对象持有的参数,解除参数对象对builderNode对象的引用
  clearParams() {
    this.builderNode?.update({builderRootNode: null} as ParamsInterface)
  }
}

@Entry
@Component
struct Index {
  @State myNodeController: MyNodeController | undefined = new MyNodeController();
  build() {
    Column({ space: 4 }) {
      NodeContainer(this.myNodeController)
      Button('Destroy')
        .onClick(() => {
          this.myNodeController?.clearParams();
          // 通过将传入NodeContainer的NodeController置为undefined,使BuilderNode节点下树
          this.myNodeController = undefined;
        })
        .width('100%')
      Button('Destroy with dispose')
        .onClick(() => {
          this.myNodeController?.clearParams();
          this.myNodeController?.dispose();
          this.myNodeController = undefined;
        })
        .width('100%')
      Button('Create')
        .onClick(() => {
          if (this.myNodeController === undefined) {
            this.myNodeController = new MyNodeController();
          }
        })
        .width('100%')
    }
  }
}