一套阅读工作台能在手机、折叠屏和平板上切换单栏与双栏,并能恢复文章、目录与阅读锚点,还不能直接说明它已经具备长期使用所需的稳定性。列表中有二百篇文章时,快速滑动会不会因为全量创建节点而拖慢交互?连续切换文章后,旧请求会不会把右侧正文覆盖回去?窗口收窄再展开,选择状态和路由栈是否仍然对应?这些问题需要放在同一组回归场景里处理。

前面的内容已经把业务选择、NavPathStack、分栏比例与持久会话分开管理。本篇沿着这条工程主线收束:定义性能预算的观察指标,缩小不必要的状态更新范围,再用可重复的数据与操作序列检验异常路径。性能优化不是简单降低图片质量,而是保证数据变化只影响需要更新的组件,导航变化不改变文章的业务事实。

阅读工作台性能与回归封面

一、把交互问题拆成可观察的阶段

用户从左侧列表点击一篇文章到右侧正文稳定呈现,至少经历列表命中、文章选择、数据请求、状态提交和内容布局五个阶段。每个阶段都有独立的故障模式:点击处理耗时影响操作反馈;数据请求返回顺序错误影响内容一致性;图片解码与节点创建集中发生,会增加布局与渲染压力。只统计一次点击的总耗时,难以知道具体该改哪里。

可以先建立一份简单的排查矩阵。列表滚动关注连续滚动时的卡顿与子节点数量;文章切换关注“最后一次选择获胜”;窗口变化关注当前文章、目录锚点与分栏比例的保留;进程恢复关注持久快照和路由实际状态的匹配。这里的指标是排查对象,而不是预设已经取得的性能成绩。帧耗时需要结合设备刷新率与工具采样条件理解,不能给所有设备套用同一个毫秒门槛。

操作核心观察点需要排除的错误
快速滑动长列表节点构建、滑动帧时间与图片加载过量创建、列表跳位
连续选择不同文章选择版本、请求返回顺序旧正文覆盖新选择
频繁调整左右栏布局计算与阅读锚点每次拖动重建路由
旋转与折叠状态变化布局模式、选中ID、恢复路径重复详情、空白面板
数据源暂时不可用错误态与重试动作无止境重试或状态丢失

二、把长列表加载从“全量”改成“按需”

几十条文章用ForEach足以覆盖常规需求;当列表持续增长且卡片包含摘要、标签和封面时,应该检查节点是否在首屏全部创建。ArkUI的List可与LazyForEach配合,按可见区域加载子组件;cachedCount负责控制可见范围外的预加载数量。缓存不是越大越好,它会以资源占用换取后续滚动时的创建开销。

先给文章列表准备一个具有稳定ID的数据源。下列类放在common/ArticleDataSource.ets,当筛选条件改变时使用全量通知;如果只是一个条目更新,可以扩展为更细粒度的对应事件。

export class ArticleSummary {
  constructor(public id: string,
    public title: string,
    public category: string) {}
}

export class ArticleDataSource implements IDataSource {
  private rows: ArticleSummary[] = []
  private listeners: DataChangeListener[] = []

  totalCount(): number { return this.rows.length }

  getData(index: number): ArticleSummary { return this.rows[index] }

  registerDataChangeListener(listener: DataChangeListener): void {
    if (this.listeners.indexOf(listener) < 0) this.listeners.push(listener)
  }

  unregisterDataChangeListener(listener: DataChangeListener): void {
    const index = this.listeners.indexOf(listener)
    if (index >= 0) this.listeners.splice(index, 1)
  }

  replace(items: ArticleSummary[]): void {
    this.rows = items.slice()
    this.listeners.forEach((listener: DataChangeListener) => {
      listener.onDataReloaded()
    })
  }
}

在阅读列表组件里使用稳定的article.id作为键,不要使用数组下标。分类切换后文章顺序可能改变,若键仍然来自下标,框架会错误地把原来位置上的组件当成新文章,导致高亮和卡片内部状态错位。

@Component
struct ArticleListPanel {
  @Prop selectedId: string
  data: ArticleDataSource = new ArticleDataSource()
  onSelect: (id: string) => void = (_: string): void => {}

  build() {
    List({ space: 8 }) {
      LazyForEach(this.data, (article: ArticleSummary) => {
        ListItem() {
          Row() {
            Text(article.title).fontSize(16).layoutWeight(1)
            if (article.id === this.selectedId) {
              Text('正在阅读').fontColor('#1769E0').fontSize(12)
            }
          }
          .padding(12).backgroundColor('#F6F9FE')
          .borderRadius(10)
          .onClick((): void => this.onSelect(article.id))
        }
      }, (article: ArticleSummary): string => article.id)
    }
    .cachedCount(3)
    .width('100%').height('100%')
  }
}

这段实现把卡片创建与数据源通知分开。cachedCount(3)只是一个初始工程配置,具体值应结合屏幕可见行数、卡片复杂度、内存与滑动轨迹调整。文章中的静态封面可以先占位再按需加载,避免首屏同时解码大量大尺寸图片。

DevEco Studio阅读列表与详情开发环境

三、让异步回调遵守当前阅读选择

上一篇使用请求代际阻断过期结果,本篇增加对加载状态的精确控制。一个容易忽略的细节是:旧请求进入finally后如果直接将loading设为false,可能提前关闭新请求的加载提示。正确做法是每次发起请求时取得新代际,只有仍然是当前代际的请求才允许提交成功、失败或结束状态。

export class DetailResult {
  constructor(public id: string, public body: string) {}
}

export class DetailLoadState {
  id: string = ''
  body: string = ''
  loading: boolean = false
  error: string = ''
}

export class DetailRequestGate {
  private epoch: number = 0
  private state: DetailLoadState = new DetailLoadState()

  async open(id: string,
    loader: (id: string) => Promise<DetailResult>,
    publish: (state: DetailLoadState) => void): Promise<void> {
    const ticket: number = ++this.epoch
    this.state = { id: id, body: '', loading: true, error: '' }
    publish({ ...this.state })
    try {
      const result = await loader(id)
      if (ticket !== this.epoch || result.id !== id) return
      this.state = { id: id, body: result.body, loading: false, error: '' }
    } catch (error) {
      if (ticket !== this.epoch) return
      this.state = { id: id, body: '', loading: false,
        error: '文章暂时无法加载,请重试' }
    }
    publish({ ...this.state })
  }

  invalidate(): void { this.epoch++ }
}

组件应只订阅DetailLoadState的快照,而不是把loader函数、窗口布局数据和业务会话混在同一份可观察对象中。布局变化不会触发新的网络请求;文章ID变化才会请求详情。窗口变宽时重新排布当前详情组件,保留已取得的正文内容。

四、记录关键事件,而不是打印整份文章

排错日志要能串起一次用户操作:选择文章、生成请求代际、完成详情提交、变更布局、保存阅读锚点。日志不宜输出文章正文、用户阅读历史或完整个人数据。记录稳定的业务ID与耗时区间即可,且应按构建环境控制日志等级。

import { hilog } from '@kit.PerformanceAnalysisKit'

const DOMAIN: number = 0x0000
const TAG: string = 'ReaderFlow'

export class ReaderTrace {
  private begunAt: number = 0
  begin(articleId: string): void {
    this.begunAt = Date.now()
    hilog.info(DOMAIN, TAG, 'select article: %{public}s', articleId)
  }
  committed(articleId: string): void {
    const spent: number = Date.now() - this.begunAt
    hilog.info(DOMAIN, TAG, 'detail committed: %{public}s, elapsed=%{public}d',
      articleId, spent)
  }
}

这里elapsed是一次业务链路的墙上时钟间隔,包含异步等待,并非ArkUI单帧渲染时间。判断滚动是否丢帧,应结合DevEco Studio Profiler中的帧时间、CPU、内存和具体调用栈查看。若看到列表滑动时JavaScript侧大量构建节点,回头检查列表懒加载与卡片内部的派生计算;如果帧时长主要来自图片相关工作,则检查图片尺寸、解码策略和缓存规模。

五、把异常回归变成确定的操作序列

“点了几下没问题”无法覆盖连续交互中的竞态。可以把最容易出错的路径整理成用例序列,每一步都规定输入和最终应满足的不变量。下面的函数是文章选择回归中的纯业务断言核心,可用于单元测试,也可以嵌入开发时的状态自检;不依赖特定设备窗口。

export class ReaderInvariant {
  static verify(selectedId: string, detailId: string,
    isLoading: boolean, routeCount: number): string[] {
    const problems: string[] = []
    if (routeCount > 1) problems.push('详情路由重复')
    if (!isLoading && detailId.length > 0 && detailId !== selectedId) {
      problems.push('列表选择与详情不一致')
    }
    return problems
  }
}

第一组用例在宽屏中快速点击A、B、C三篇文章,让请求按C、A、B顺序完成。最终选择和正文都应停留在C,等待状态也只能由C请求关闭。第二组从紧凑窗口打开详情,扩大窗口后切换到双栏,再缩回紧凑布局;检查选中ID、返回路径与锚点是否一致。第三组将列表数据从二百条缩减到某个分类的二十条,确认LazyForEach键没有引发内容错配,当前文章被筛选隐藏时界面给出明确提示而不是偷偷切换到另一篇。第四组向详情加载器注入一次失败,确认错误提示对应当前文章,并且重试不会新增重复路由。

性能回归则按固定文章数据量、固定图片样本和固定窗口尺寸收集样本,把优化前后的帧时间分布、CPU占用、内存峰值和交互链路时间放在同一张表里分析。要记录冷启动或热启动状态、刷新率和采样时长,避免由于场景不同得到误导性结论。对于阅读工作台来说,稳定性收益往往来自删除不必要的更新路径,而不是盲目增加缓存。

阅读工作台状态与回归操作界面

六、收束到可维护的阅读组件

回看整个系列,从主从路由、左右推挤、固定导航、断点布局、窗口旋转、会话恢复到应用内分屏,贯穿始终的是同一条边界:文章ID和阅读锚点属于业务状态,路由栈和分栏比例属于呈现方式。性能问题也服从这条边界,只有业务事实改变时才加载新的正文,只有窗口特征改变时才调整布局。长列表按需构建、过期响应过滤、精确加载状态和回归不变量共同构成稳定的工程闭环。

后续维护可以持续扩充回归矩阵:每增加一种新的文章卡片或跨屏布局,就补上其加载与恢复序列;每发现一次真实的竞态,就沉淀成可重复的状态测试。如此一来,界面能力扩张不会破坏已经建立的阅读连续性。

相关开发文档:ArkUI List组件参考、Repeat可复用循环渲染、Navigation页面状态与生命周期、ArkUI页面滑动卡顿排查。

Logo

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

更多推荐