线上反馈说页面偶发掉帧,操作时间长了以后越来越卡,内存一直在涨。本地测试跑了好几遍,复现不出来,设备也发热。这种问题最让人头疼——不是崩溃,不是功能错误,就是慢,而且慢得没有规律。

刚开始我也以为是某个页面写得不对,改了几个列表渲染的地方,结果上线以后问题还是存在。后来才意识到,不拿 Profiler 实际跑一遍,光靠猜是找不到真正瓶颈的。

这篇不讲 Profiler 每个面板怎么用,而是讲遇到"页面卡、内存涨、越用越慢"这类问题时,应该按什么顺序查、看哪些指标、哪些现象容易误判。

一、先搞清楚问题到底出在哪一层

性能问题看似都是"卡",但原因可能完全不同。CPU 跑满、内存泄漏、渲染掉帧、任务阻塞 UI 线程,这四类问题的排查方向完全不一样。如果你一上来就盯着 CPU 曲线看,可能根本不是 CPU 的问题。

我一般先问三个问题:是打开页面就卡,还是操作一段时间以后才卡?是滑动列表卡,还是点击按钮没反应?重启应用以后会不会恢复?

打开就卡,大概率是初始化太重或者渲染层级太深;操作一段时间才卡,可能是内存泄漏或者累积性任务堆积;滑动卡是渲染问题,点击没反应是主线程被阻塞。这三个问题对应 Profiler 里完全不同的面板。

不要一上来就录一段 Profiler 数据然后到处找异常。先根据现象缩小范围,再去对应的面板里看数据,效率高得多。

二、CPU 占用高不一定是真瓶颈

很多人打开 Profiler 第一反应就是看 CPU 曲线,看到某个时间段 CPU 飙到 80% 就觉得找到问题了。但 CPU 高不等于有问题——GC、图片解码、列表复用,这些都会让 CPU 短时间升高。关键要看的是:CPU 高的时候,对应的线程在干什么。

CPU 面板里最有用的不是总占用率,而是各个线程的调用栈。如果主线程在某个函数上停留时间过长,那才是真正需要看的地方。我之前遇到过一次列表滑动卡顿,CPU 曲线看着不高,但主线程每帧都在执行一个字符串格式化函数,调了很多次。总时间加起来虽然不夸张,但每帧都占了不少,累积起来就掉帧了。

这里最容易误判的是:看到 CPU 高就去优化算法,结果真正的问题在渲染层或者内存层。CPU 数据要结合帧率和内存一起看,不能孤立地看。

三、内存持续上涨要分清楚是泄漏还是正常缓存

"操作时间长了内存一直涨"这个现象,要先判断是内存泄漏还是正常的缓存增长。两者的处理方式完全不同。

内存泄漏是指该释放的对象没释放,比如页面退出以后观察者没注销、定时器没清理、全局引用持有了页面上下文。正常缓存增长是指图片、列表数据这些缓存本来就需要保留,用一段时间以后稳定在一个水平。

判断方法是:反复进出同一个页面几次,看内存曲线是阶梯式上涨(每次进页面都涨一点,退了不回落),还是涨上去以后稳定住。阶梯式上涨大概率是泄漏,涨上去以后稳住就是正常缓存。

下面这段代码放在 PerformanceMonitor.ets 里,记录页面进出时的关键资源数量,方便判断是不是泄漏。它在页面 aboutToAppear 和 aboutToDisappear 时分别打一条日志,带上时间戳和当前存活的图片对象数量。

export class PerformanceMonitor {
  private static pageEnterTime: number = 0;
  private static activeImageCount: number = 0;

  static onPageEnter(pageName: string): void {
    this.pageEnterTime = Date.now();
    console.info(`[Perf] ${pageName} enter at ${this.pageEnterTime}, images=${this.activeImageCount}`);
  }

  static onPageLeave(pageName: string): void {
    const duration = Date.now() - this.pageEnterTime;
    console.info(`[Perf] ${pageName} leave, duration=${duration}ms, images=${this.activeImageCount}`);
  }

  static onImageLoaded(): void {
    this.activeImageCount++;
  }

  static onImageReleased(): void {
    if (this.activeImageCount > 0) this.activeImageCount--;
  }
}

这段代码要解决的问题:页面反复进出后内存不回落时,通过对比进出页面时的图片对象数量,判断是不是图片资源没释放。实际使用时要注意,activeImageCount 只是个示例计数,真实项目里需要对接具体的图片加载库,统计它的缓存对象数量。另外,日志里的数值要在 Profiler 里和实际内存增长曲线对照着看,不能只看代码里的数字就下结论。

四、页面掉帧要看 Rendering 面板而不是只看体感

"滑动列表卡"这种问题,靠体感判断太主观了。Profiler 的 Rendering 面板会显示每帧的实际耗时,包括布局、绘制、合成各花了多少时间。一帧超过 16ms 就是掉帧,这个标准是客观的。

看掉帧的时候要注意:是每一帧都慢,还是偶尔某几帧特别慢。偶尔某几帧慢,通常是那个时候做了重活(比如同步解码大图、GC 停顿);每一帧都慢,通常是布局层级太深或者列表复用有问题。

我之前排查过一次列表滑动卡顿,Rendering 面板显示每帧的布局时间都接近 12ms,加上绘制时间就超了 16ms。最后发现是列表项里嵌套了太多层组件,重新调整了组件结构以后,布局时间降下来了,帧耗时也正常了。这种问题光看 CPU 曲线是看不出来的。

五、任务执行时间要拆开来算

有些卡顿不是持续的,而是偶发的——比如点一个按钮,界面转一下才响应。这种问题要看 Task 面板或者主线程 trace,找到那次卡顿期间主线程在执行什么。

关键不是看总耗时,而是把任务拆成几段:网络请求花了多久、数据解析花了多久、UI 更新花了多久。有时候问题根本不在你的代码里,而是系统 GC 恰好发生在那个时间点。GC 停顿是正常的,但如果 GC 太频繁,说明内存压力大,还是要回到 Memory 面板去看。

这里最容易误判的是:把系统自身的开销当成自己代码的问题,花时间去优化一个本来就正常的操作。区分方法是多看几次 trace,如果每次卡顿都出现在同一个函数上,那才是你要改的地方。

六、排查顺序和优化后的验证

总结一下我现在的排查顺序:先根据现象判断是哪一层的问题,然后打开 Profiler 录一段复现操作,先看 Rendering 确认是不是真的掉帧,再看 CPU 主线程调用栈找耗时函数,最后看 Memory 判断有没有泄漏。

优化完以后一定要再录一次 Profiler 对比。不要改完代码觉得"应该好了"就提测,数据不会骗人。对比优化前后的帧率、内存曲线、主线程耗时,确认指标确实改善了,再往下走。

性能排查这件事,最怕的不是找不到问题,而是找到一个看似合理的原因就停下来。真正的瓶颈往往藏在你觉得"应该不是这里"的地方。多录几次、多看几遍数据,比凭经验猜要靠谱得多。

Logo

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

更多推荐