​如果你的手机突然发烫,电量掉的特别快等情况,多半是有应用在持续吃CPU导致的,也就是发生了应用负载与功耗问题了。那么本篇专题,应该能帮到你。负载问题和上一篇的卡顿问题排查入口不同:卡顿看的单帧超时,负载关注的是持续性。我们直接进入正题:

一、什么是负载与功耗问题

应用跑起来要消耗 CPU,就像燃油车需要发动机一样。正常情况下,应用只在刷新画面、处理交互时消耗 CPU,画面不动时 CPU 应该闲下来。负载异常场景就是CPU一直高占用,而功耗问题是这种高占用导致的发热,耗电等损耗。有点像在等红灯时燃油车应该挂空挡怠速,如果等还在狂踩油门,就会费油和过热…

Flutter鸿蒙应用有两种问题场景,排查难点不同:

场景触发条件排查难点
前台高负载应用在前台时某进程持续占用 CPU找到哪条线程在忙、忙在哪段代码
后台功耗异常应用退到后台后仍频繁唤醒、跑任务抓到后台还在跑什么、多久醒一次

前台高负载和卡顿经常同时出现会导致CPU 被占满,帧画不完,丢帧问题。如果按上一篇的方法排查过丢帧,发现不是 Dart 或 GPU 单帧慢,那就要怀疑是负载问题了。

从现象初判:

看到的现象严重度可能的原因
手机发烫,应用运行一段时间后持续高频重绘、后台轮询
电量掉得快后台任务未暂停、高频定时器
应用越用越卡GC 频繁加 CPU 热点
列表滑动整体偏卡,不是偶发顿挫滑动中持续高 CPU 热路径
静止页面 CPU 仍不回落空闲状态还在重绘、后台线程忙轮询
系统因功耗杀后台后台功耗超标被系统回收

定位可以按照下面步骤开展:

(1)先确认是哪个进程在吃 CPU,用 top -H 找高频线程

(2)再确认是前台还是后台触发,前台看是否持续重绘、重计算,后台看定时器、轮询、动画有没有停

(3)然后用 hiperf 抓指令数采样

(4)生成火焰图,找顶部持续变宽的函数,宽度等于采样时间占比,,越宽越是热点

(5)最后交叉比对 HiTrace、HiLog、HiAppEvent,看有没有伴随丢帧事件和后台 trace

二、采集三件套:top、hiperf、火焰图

2.1 进程和线程信息

先确认目标进程是否持续占用 CPU,指令如下:

# 找到 Flutter 应用进程
hdc shell ps -A | grep flutter
# 按线程看 CPU 占用,进程id 替换为上一步拿到的 PID
hdc shell top -H -p {进程id}

执行效果图:
在这里插入图片描述

字段说明含义
PID线程 ID(TID) ,-H 模式下这里是线程 tid,不是进程 pid
USER所属用户
PR / NI线程调度优先级
VIRT / RES / SHR虚拟内存、物理内存、共享内存
S线程状态:R 运行,S 休眠,D 不可中断睡眠
%CPU该线程 CPU 占比(重点看这个定位高负载线程)
%MEM内存占比
TIME+线程累计 CPU 时间
COMMAND线程名称

看到高频线程后,重点确认四件事:

关注点说明
UI 线程是否被阻塞被阻塞的 UI 线程通常不会高 CPU,但重计算会
Raster / GPU 相关线程是否持续高持续高多半是画面在不停重绘
是否有后台线程忙轮询比如 while(true) 里不停检查状态
是否有线程频繁唤醒但无业务收益定时器、轮询间隔过小,CPU 频繁被唤醒

2.2 CPU 指令数采集工具:hiperf

hiperf 采集单个进程的 CPU 指令数,定位哪些路径在持续消耗 CPU:

# 采集 30 秒指令数采样,期间复现问题
hdc shell "hiperf record -e hw-instructions -p {进程id} -f 1000 -d 30 -o /data/local/tmp/perf.data"
# 把采样数据拉到电脑
hdc file recv /data/local/tmp/perf.data

参数含义
-e hw-instructions以硬件指令数作为采样事件,谁执行的指令多谁就是热点
-p {进程id}只采这个进程
-f 1000采样频率 1000Hz,每秒采 1000 次
-d 30采集时长 30 秒
-o /data/local/tmp/perf.data输出到设备路径

建议在三类场景采集:列表滑动滚动(持续操作)、复杂动画切页(瞬时高负载)、长时间停留页面的空闲状态,第三类是抓功耗的关键,验证空闲是否真的空闲。

2.3 火焰图生成

perf.data 转成堆栈格式再生成火焰图,以 perf / FlameGraph 工具链为例:

perf script -i perf.data > perf.stacks
stackcollapse-perf.pl perf.stacks > perf.folded
flamegraph.pl perf.folded > perf.svg

浏览器打开 perf.svg。火焰图把每个函数的耗时画成一层层火焰:横轴宽度等于该函数被采样到的次数,即耗时占比。越宽的色块就是越大的热点,找到最宽的那个,基本就锁定元凶了。

在这里插入图片描述

2.4 交叉排查

只看 CPU 占用不够,要结合 trace 和日志判断这段时间应用在干什么:

# 抓 30 秒 trace,复现问题场景
hdc shell hitrace --trace_clock boottime -t 30 flutter -o /data/local/tmp/trace.ftrace
hdc file recv /data/local/tmp/trace.ftrace ./trace.ftrace
hdc shell hilog | grep -Ei "jank|frame|flutter|cpu|power"

数据源看什么交叉比对结论
top -H哪条线程 CPU 高锁定线程
hiperf 火焰图该线程的热点函数锁定代码路径
HiTrace这段时间有没有帧在画有帧是前台重绘,无帧是后台偷跑
HiLog有没有丢帧、纹理事件伴随 OTHER_JANK 说明已影响流畅度

三、怎么解读

3.1 火焰图看什么

关注点说明怎么看
哪个函数在顶部持续变宽顶部是实际在执行的函数最宽的色块就是热点
哪些栈路径反复出现反复出现等于同一类操作被高频执行看是否有重复的调用栈模式
是否同一类操作被反复执行比如 build/layout 被反复触发顺栈往下找根因

3.2 功耗异常的典型信号

信号怎么发现通俗解释
页面静默状态下依然频繁重绘火焰图里有持续的 build/paint,trace 里帧没停画面明明没变,引擎还在不停画
存在后台任务没有暂停退后台后 top -H 仍见高频线程应用都退到后台了,还在干活
有持续轮询或高频定时器火焰图里反复出现 timer/poll 栈隔几十毫秒就醒一次,CPU 没法深度睡眠

四、Flutter 侧常见热点

4.1 Dart 侧高负载(UI 线程)

热点通俗解释怎么发现
build 里做重计算、对象创建每帧都在干重活火焰图顶部持续变宽指向 build
列表项构建过多、频繁重建一滚动就重建几百个 WidgetTrack Widget Builds
UI 线程里 JSON 解析、图片处理、复杂布局测量同步重活阻塞主线程火焰图出现 jsonDecode / decodeImage 等

build 里做重计算的对照修复:

// 错误:build 里做重计算,每帧都跑一遍
Widget build(BuildContext context) {
  final result = heavyCalc(data);
  return Text('$result');
}
// 正确:initState 里算一次,缓存结果
class _MyState extends State<MyWidget> {
  late final int _result;
  @override
  void initState() {
    super.initState();
    _result = heavyCalc(data);  
// 只算一次
  }
  @override
  Widget build(BuildContext context) => Text('$_result');
}

4.2 渲染与绘制热点(Raster 线程)

热点通俗解释怎么发现
CustomPainter / Canvas 过于复杂每帧画的东西太多太复杂Raster 线程 CPU 持续高
过度重绘、过度遮盖、过多图层同一像素被画了好几遍Repaint Rainbow 满屏闪,见上一篇
动画频繁触发重绘动画一直在跑,画面一直在重画火焰图里持续出现 paint 栈

4.3 图片、纹理与资源管理

热点通俗解释
大图未经缩放缓存直接解码4K 图塞进 200px 卡片,解码炸
同一图片重复解码或频繁加载没走缓存,每帧都重新解码
页面切换时纹理未及时释放离开页面后视频、相机纹理还在产帧

五、对症优化

5.1 降低无效渲染

无效渲染等于画面没变却在不停重画,是前台高负载最常见的原因:

// 错误:静态子树每帧跟着父级重建
Widget build(_) => Column(children: [Header(), ...]);
// 正确:const 构造,Flutter 直接复用实例
Widget build(_) => Column(children: [const Header(), ...]);

手段作用
const 构造函数静态子树不重建
ValueKey精确控制哪些 Widget 需要更新
RepaintBoundary隔离高频重绘区域,防止拖全页
列表 itemExtent / prototypeItem减少滚动中的复杂绘制和重新布局
高频动画轻量化别用 Opacity 动画,用 FadeTransition

5.2 降低后台和空闲功耗

后台功耗的核心原则:应用不可见时,该停的都要停。监听前后台切换,及时暂停和恢复:

class _MyPageState extends State<MyPage> with WidgetsBindingObserver {
  Timer? _timer;
  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
    _timer = Timer.periodic(const Duration(seconds: 5), (_) => _poll());
  }
  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    if (state == AppLifecycleState.paused) {
      _timer?.cancel();  
// 退后台就停
    } else if (state == AppLifecycleState.resumed) {
      _timer = Timer.periodic(const Duration(seconds: 5), (_) => _poll());
    }
  }
  @override
  void dispose() {
    _timer?.cancel();
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }
  void _poll() { /* ... */ }
}

手段通俗解释
页面不可见时暂停动画和资源刷新没人看就别画,paused 时停动画
后台切换时释放不必要的纹理和图形资源视频离开页面要停,见 系列纹理篇 第 6 节
避免定时器、轮询在后台持续工作paused 时 timer.cancel(),resumed 时再建
对 WebView / PlatformView 做可见性控制嵌入的网页也要跟着停,监听可见区域回调

5.3 优化图片和资源加载

手段作用代码示例
使用适当尺寸的图片别让引擎解码超大图Image.network(url, cacheWidth: 200, cacheHeight: 200)
大图做缓存和预加载避免重复解码precacheImage
重解码移出 UI 线程解码移到 isolatecompute(_decode, data)
释放不再使用的图片和纹理防止内存和 CPU 双泄漏离开页面 dispose,清理 ImageCache

5.4 降低跨语言调用开销

合并频繁的插件调用,十次 MethodChannel 调用合成一次批量:

// 错误:列表里每个 item 都单独调一次原生
for (final e in items) {
  await platform.invokeMethod('fetch', e.id);
}
// 正确:一次性批量请求
final all = await platform.invokeMethod('fetchBatch', items.map((e) => e.id).toList());


以上就是本次要分享的内容了,希望能帮到大家~ 同时大家也持续关注!

小伙伴们记得点赞+关注

关注 CPF-Flutter 社区

“AI再牛,技术不能丢”

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐