连续打开六个详情页,再一路返回。有人会以为把 NavigationstackSizeLimit 设成 3,第四次入栈时最早的页面就从“返回历史”里消失了。API 26 的实际定义不是这样:限制的是活跃页面节点数量;较早页面节点会按先入先出被销毁,但对应的 NavPathInfo 仍在路由栈中,后续可以重新创建页面。

这个区别会直接影响两件事:返回键还能不能回到早期页面,以及那个页面里临时选中的筛选项、滚动位置该放哪里。只盯 NavPathStack.size(),很容易把“页面重建”误判成“路由丢失”。

路由信息保留与活跃节点限量示意

API 26 到底限制了什么

华为的 Navigation API 参考在 2026 年 9 月 9 日更新了 configuration(config: NavigationConfiguration)。其中 stackSizeLimit 从 API 26.0.0 开始提供:默认值 0;小于等于 0 表示不限制;大于 0 时,活跃页面节点超过上限会按先入先出销毁更早入栈的节点,但其 NavPathInfo 保留,允许之后重建。该接口只用于 Stage 模型。

用法并不复杂,难的是理解它没有替你保存页面组件里的每一份临时状态:

@Entry
@ComponentV2
struct NavigationRoot {
  private stack: NavPathStack = new NavPathStack();

  build() {
    Navigation(this.stack) {
      Button('打开详情').onClick(() => {
        this.stack.pushPath({ name: 'detail', param: { id: 42 } });
      });
    }
    .navDestination(this.pageBuilder)
    .configuration({ stackSizeLimit: 3 }); // API 26.0.0+
  }

  @Builder
  pageBuilder(name: string, param: Object) {
    if (name === 'detail') {
      NavDestination() {
        Text(`详情 ${JSON.stringify(param)}`);
      }
      .title('详情');
    }
  }
}

以上是接口位置示意。detail 页在真实工程中还需要按项目路由组织、类型约束和 SDK 编译结果调整;不要把这段当成已经在设备上跑过的完整工程。更重要的是,stackSizeLimit: 3 不等于“只允许三个历史路由”,也不保证一定节省某个固定数值的内存;是否改善内存应当用 Profiler 实测。

案例一:六次入栈后,为什么还能返回第二页

复现步骤:创建一个 Navigation,将限制设为 3;依次推入详情 1 到详情 6,给每个页面的 aboutToAppear、销毁回调和 NavPathStack 变更打上同一组序号。进入第六页后向前返回,观察第二页是否重新创建。

按文档定义,逻辑上有两层数据。路由信息可以保留 1 到 6;活跃节点上限为 3,较早的节点会被回收。返回早期路由时,系统依据保留的路由信息重新创建页面。配图是这两层的关系示意,不是系统内部对象数量截图。

观察项预期问题应记录的证据
路由信息早期 NavPathInfo 是否仍可返回入栈顺序、返回顺序、路由参数
页面实例早期 NavDestination 是否重建创建/销毁回调次数和页面标识
页面数据筛选、滚动等是否还在重建前后的字段值及其保存位置
内存限额是否真的改善占用相同路线、相同设备上的 Profiler 对比

我先用一段可运行的小模型验证“历史路由”和“活跃实例”不能混为一谈。它不是 ArkUI 的替身,只用于检查业务代码有没有把两个概念写成同一个数组:

export function projectNavigation(routes, stackSizeLimit) {
  if (!Number.isInteger(stackSizeLimit)) throw new TypeError('limit must be an integer');
  const keep = stackSizeLimit <= 0 ? routes.length : stackSizeLimit;
  return {
    retainedRouteInfo: [...routes],
    activePageNames: routes.slice(-keep).map(route => route.name),
    evictedPageNames: routes.slice(0, Math.max(0, routes.length - keep)).map(route => route.name),
  };
}

六条路由、限制 3 的本地测试得到:retainedRouteInfo.length === 6activePageNamespage4/page5/page6,更早的 page1/page2/page3 进入待重建组。真实系统的创建时序和内存变化仍需 API 26 设备验证,不能拿这个数组模型当性能实测。

案例二:返回第二页,筛选条件为什么变回默认值

如果页面把 filter='未读' 和滚动位置只放在页面组件的临时状态里,早期节点被销毁并重建后,组件会重新初始化。文档明确保证保留的是 NavPathInfo,并没有说每个组件字段都自动复原。因此需要把“回去后还必须保持”的数据明确放进路由参数、共享状态或持久化层;不同数据按寿命分层,别全塞进路由参数。

一个可控的做法是路由参数保存稳定业务 ID 和初始条件,短时的筛选/滚动快照存到按“页面名 + 业务 ID”索引的状态表。返回重建时先读快照,没有快照才退回路由初始值:

export function restorePageInput(route, snapshots) {
  const key = `${route.name}:${route.param.id}`;
  return {
    id: route.param.id,
    filter: snapshots.get(key)?.filter ?? route.param.initialFilter ?? 'all',
    scrollIndex: snapshots.get(key)?.scrollIndex ?? 0,
  };
}

这个快照表若只存在进程内,就只解决同一次运行中的页面重建;进程被杀后仍要恢复时,应使用项目已有的持久化方案。还要在业务对象删除、退出该流程或数据版本变化时清掉旧快照,否则会把上次的筛选条件错误套给另一个对象。

我验证了什么,哪些还需要设备验收

上述两段模型代码在本地用 Node.js 跑了三项测试:六条历史路由与三个活跃节点分离;按业务 ID 恢复筛选与滚动快照;限制为 0 或负数时按“不限制”处理。结果 3 pass / 0 fail。这是模型测试,不是 HarmonyOS SDK 编译结果,也不是 Profiler 的内存收益证明。

设备验收建议固定同一套六层路径,分别用默认配置和 stackSizeLimit: 3 测试:记录页面创建/销毁回调、返回后的参数和局部状态,再对比 Profiler 快照。若页面重建成本很高,限制太小可能以更多重建换取较少活跃节点;选 2、3 还是 5,应该由这组数据决定,不宜照搬示例值。

我会把它当成一个“内存与重建成本”的取舍开关,而不是普通的返回栈截断器。先确认页面参数可重建,再确保关键状态有明确归属,最后才调上限。这样返回历史、页面实例和业务状态各负其责。

参考:Navigation API 参考 · Navigation 跳转指导 · 自定义组件生命周期

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐