HarmonyOS 5.0.0 后台恢复状态乱了怎么办:请求取消、页面快照和旧结果拦截怎么拆

后台恢复不是“回到页面”这么简单

HarmonyOS 应用从后台回到前台时,页面看起来只是重新显示了一下,但代码里可能同时发生几件事:旧请求还没结束,新请求又开始了;页面快照要恢复,接口数据也要刷新;窗口尺寸可能变了,列表状态还要重新算。

如果这些边界没拆清楚,就会出现很难解释的现象:

现象表面看起来实际可能是
切回来列表变成旧数据缓存没刷新旧请求比新请求晚返回
页面先显示空白再闪一下渲染慢快照恢复和接口刷新互相覆盖
筛选条件丢了状态管理写错后台前后的状态版本没校验
偶尔才复现网络问题生命周期和异步任务时序撞在一起

我会把后台恢复拆成三层:请求取消、页面快照、旧结果拦截。只做其中一个,短期看能好一点,但线上换个网络或设备形态又会出问题。

案例一:旧请求晚回来,把新页面状态覆盖掉

先看一个常见写法。页面进来就请求数据,切后台回来再请求一次。

type PageData = {
  keyword: string;
  rows: string[];
  updatedAt: number;
};

async function loadPageData(keyword: string): Promise<PageData> {
  await new Promise<void>((resolve) => setTimeout(resolve, 120));
  return {
    keyword,
    rows: ['item-a', 'item-b', 'item-c'],
    updatedAt: Date.now()
  };
}

let pageData: PageData | undefined;

async function refresh(keyword: string): Promise<void> {
  pageData = await loadPageData(keyword);
}

这段代码的问题不在请求本身,而在“谁回来都能写”。用户先搜索 arkui,马上切后台,再回来搜索 rdb。如果 arkui 那次请求最后才返回,它就会把 rdb 的新结果覆盖掉。

我更稳的做法是给每次刷新加一个版本号,旧版本结果直接丢弃。

class RequestVersion {
  private value = 0;

  next(scene: string): number {
    this.value += 1;
    console.info('[request-version] scene=' + scene + ', value=' + this.value);
    return this.value;
  }

  isCurrent(version: number): boolean {
    return version === this.value;
  }
}

const requestVersion = new RequestVersion();

async function refreshSafely(keyword: string, scene: string): Promise<void> {
  const version = requestVersion.next(scene);
  const result = await loadPageData(keyword);

  if (!requestVersion.isCurrent(version)) {
    console.warn('[request-version] ignore stale result, keyword=' + keyword);
    return;
  }

  pageData = result;
}

这一步很关键。它不是为了让请求变快,而是为了保证只有最新场景能写页面。后台恢复、搜索切换、分页加载、本地缓存回填,都可以用同一套判断。

案例二:页面快照和接口刷新抢状态,导致界面先旧后新再旧

第二个问题更像线上偶发。为了让用户切回来时不空白,我们通常会保存一份页面快照。但如果快照恢复和接口刷新都能写同一个状态,就可能互相打架。

我会把状态来源分清楚:快照只负责“先恢复可看内容”,接口刷新负责“确认最新内容”。快照不能覆盖刷新后的数据。

type PageSnapshot = {
  keyword: string;
  scrollY: number;
  rows: string[];
  savedAt: number;
};

class SnapshotStore {
  private snapshot: PageSnapshot | undefined;

  save(data: PageData, scrollY: number): void {
    this.snapshot = {
      keyword: data.keyword,
      scrollY,
      rows: data.rows,
      savedAt: Date.now()
    };
  }

  restore(maxAgeMs: number): PageSnapshot | undefined {
    if (!this.snapshot) {
      return undefined;
    }
    if (Date.now() - this.snapshot.savedAt > maxAgeMs) {
      return undefined;
    }
    return this.snapshot;
  }
}

后台恢复时,先恢复快照,再启动一次受版本控制的刷新。

const snapshotStore = new SnapshotStore();

async function onPageHide(scrollY: number): Promise<void> {
  if (pageData) {
    snapshotStore.save(pageData, scrollY);
  }
}

async function onPageShow(): Promise<void> {
  const snapshot = snapshotStore.restore(5 * 60 * 1000);
  if (snapshot) {
    pageData = {
      keyword: snapshot.keyword,
      rows: snapshot.rows,
      updatedAt: snapshot.savedAt
    };
    console.info('[snapshot] restored keyword=' + snapshot.keyword);
  }

  await refreshSafely(pageData?.keyword ?? '', 'page-show-refresh');
}

这里的重点是顺序:快照只在回来时先顶一下,后续刷新仍然走版本校验。旧请求不能写,新请求才能写。

请求取消要不要做

版本号能挡住旧结果,但旧请求本身还在跑。如果请求很重、会占资源,最好再加一层取消。

class CancelBox {
  private cancelled = false;

  cancel(): void {
    this.cancelled = true;
  }

  get isCancelled(): boolean {
    return this.cancelled;
  }
}

let currentCancelBox: CancelBox | undefined;

async function refreshWithCancel(keyword: string): Promise<void> {
  currentCancelBox?.cancel();
  const cancelBox = new CancelBox();
  currentCancelBox = cancelBox;

  const result = await loadPageData(keyword);
  if (cancelBox.isCancelled) {
    console.warn('[request-cancel] cancelled keyword=' + keyword);
    return;
  }

  pageData = result;
}

如果项目里请求层支持真正的 abort,那就用真实取消;如果暂时没有,至少也要做结果拦截。不能因为取消能力不完整,就让旧结果随便写页面。

三种处理方式对比

方案能解决什么不足
只在 onPageShow 重新请求数据可能更新旧请求仍然可能覆盖新状态
只保存页面快照返回时不空白快照可能盖掉新结果
快照恢复 + 请求版本 + 旧结果拦截可看、可刷新、可防旧写入代码多一点,但回归最稳

我会选第三种。后台恢复的问题本质是“时序不确定”,不是某一个 API 调一次就完事。只要时序不确定,就要有版本、来源和写入规则。

回归时我会看哪些日志

type ResumeProbe = {
  scene: string;
  version: number;
  keyword: string;
  source: 'snapshot' | 'network' | 'ignored';
  costMs: number;
};

function reportResumeProbe(record: ResumeProbe): void {
  console.info('[resume-probe] ' + JSON.stringify(record));
}

回归动作我会这样设计:

动作预期
搜索 A 后马上搜索 BA 的旧结果不会覆盖 B
搜索后切后台再回来先恢复快照,再刷新最新数据
弱网下连续切前后台日志能看到旧请求被忽略
快照超过有效期不恢复过期快照,直接刷新
重复三轮页面关键状态不会反复跳回旧值

后台恢复写稳以后,用户感知会很明显:页面回来不空白,数据不会莫名倒退,列表位置也更容易保持。后续扩到多窗口、折叠屏、平板和鸿蒙电脑时,这套“状态来源 + 版本校验 + 旧结果拦截”的结构还能继续复用。

Logo

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

更多推荐