优化Flutter动画性能损耗

前言:高性能的渲染一直是Flutter引以为傲的地方,通过自行调用Skia引擎绘制,达到媲美原生的渲染效果。然而这种高帧率且无法修改帧率的渲染,却在性能不高的机器上,给我们带来了不少麻烦。今天我们就来讨论下如何在Flutter上,降低动画的帧率。

一、背景

在Flutter Windows的开发过程中,展示动画的时候发现GPU占有很高,比如一个简单的加载框,都会占用很高的GPU资源。比如在NVIDIA Quadro P620的2G独显上展示下图的加载框CircularProgressIndicator,GPU占用就长期在30%以上。这已经是专业的独显了,还这么耗性能!

所以当需要在低性能的设备上长时间的展示动画的时候,这个问题就成为性能瓶颈。比如下面这个场景:

  • 设备的GPU是intel hd 630,属于入门级的低端显卡;
  • 使用AnimatedBuilder展示两个旋转的图片,一个顺时针、一个逆时针,图片尺寸是比较大的;
  • 动画过程中GPU超过100%,而且还会占用CPU进行渲染,消耗了大量的资源,性能隐患很大。

二、孰对孰错

通过对动画的调用速度进行监听,我们发现刷新的间隔稳定在16ms,完全满足60fps。Skia引擎使用GPU加速渲染,尽最大的努力使用硬件的能力,GPU渲染能力不足后,会使用CPU进行软件计算渲染 ,这样的流畅度理论上值得点赞。

无奈的是Flutter的动画刷新速度,开发者目前不能做任何配置,在长期需要显示动画的时候,GPU都会占用的很高,这是很遗憾的现实。

三、Flutter动画的原理

  • Flutter的动画通过AnimationController控制Animation对象,controller会在构造函数中调用vsync.createTicker(_tick);而_tick方法则会调用notifyListeners方法,通知到Animation进行刷新;
dart 复制代码
// animation_controller.dart 236行
AnimationController({
  double? value,
  this.duration,
  this.reverseDuration,
  this.debugLabel,
  this.lowerBound = 0.0,
  this.upperBound = 1.0,
  this.animationBehavior = AnimationBehavior.normal,
  required TickerProvider vsync,
}) : assert(upperBound >= lowerBound),
     _direction = _AnimationDirection.forward {
  // 这里调用TickerProvider.createTicker方法
  _ticker = vsync.createTicker(_tick);
  _internalSetValue(value ?? lowerBound);
}
dart 复制代码
// animation_controller.dart 891行
void _tick(Duration elapsed) {
  _lastElapsedDuration = elapsed;
  final double elapsedInSeconds = elapsed.inMicroseconds.toDouble() / Duration.microsecondsPerSecond;
  assert(elapsedInSeconds >= 0.0);
  _value = clampDouble(_simulation!.x(elapsedInSeconds), lowerBound, upperBound);
  if (_simulation!.isDone(elapsedInSeconds)) {
    _status = (_direction == _AnimationDirection.forward) ?
      AnimationStatus.completed :
      AnimationStatus.dismissed;
    stop(canceled: false);
  }
  // 通知
  notifyListeners();
  _checkStatusChanged();
}
  • AnimationController需要传入TickerProvider,controller在适当的时机调用_startSimulation这个方法,这个方法则是调用了Ticker的start方法;
dart 复制代码
// animation_controller.dart 636行
TickerFuture repeat({ double? min, double? max, bool reverse = false, Duration? period }) {
  min ??= lowerBound;
  max ??= upperBound;
  period ??= duration;
  assert(() {
    if (period == null) {
      throw FlutterError(
        'AnimationController.repeat() called without an explicit period and with no default Duration.\n'
        'Either the "period" argument to the repeat() method should be provided, or the '
        '"duration" property should be set, either in the constructor or later, before '
        'calling the repeat() function.',
      );
    }
    return true;
  }());
  assert(max >= min);
  assert(max <= upperBound && min >= lowerBound);
  stop();
  return _startSimulation(_RepeatingSimulation(_value, min, max, reverse, period!, _directionSetter));
}
dart 复制代码
// animation_controller.dart 740行
TickerFuture _startSimulation(Simulation simulation) {
  assert(!isAnimating);
  _simulation = simulation;
  _lastElapsedDuration = Duration.zero;
  _value = clampDouble(simulation.x(0.0), lowerBound, upperBound);
  // 调用Ticker的start方法
  final TickerFuture result = _ticker!.start();
  _status = (_direction == _AnimationDirection.forward) ?
    AnimationStatus.forward :
    AnimationStatus.reverse;
  _checkStatusChanged();
  return result;
}
  • Ticker的Start方法,开始请求帧绘制。注意这里是循环调用的,scheduleTick_tick互相调用;所以一帧调用完毕就会马上调用下一帧;
dart 复制代码
// ticker.dart 243行
void _tick(Duration timeStamp) {
  assert(isTicking);
  assert(scheduled);
  _animationId = null;

  _startTime ??= timeStamp;
  _onTick(timeStamp - _startTime!); // 回调给animationController

  // The onTick callback may have scheduled another tick already, for
  // example by calling stop then start again.
  if (shouldScheduleTick) {
    scheduleTick(rescheduling: true);
  }
}

/// Schedules a tick for the next frame.
///
/// This should only be called if [shouldScheduleTick] is true.
@protected
void scheduleTick({ bool rescheduling = false }) {
  assert(!scheduled);
  assert(shouldScheduleTick);
  // 通过scheduleFrameCallback请求下一帧,然后会回调给_tick处理
  _animationId = SchedulerBinding.instance.scheduleFrameCallback(_tick, rescheduling: rescheduling);
}

从这里就可以看出,AnimationController触发了Ticker的start方法,然后Ticker开始不断请求祯进行刷新。

四、解决方案

熟悉Android的同学应该都知道,Android的属性动画有setFrameDelay这个方法,可以设置动画祯之间的间隙,调整帧率,在不影响动画效果的情况下可以减少CPU资源消耗。

通过上面的分析,其实我们在_tick调用scheduleTick时,进行一下间隙的设置,等于延缓了调用下一帧的速度,这样就能实现调整帧率的问题。

在上面的分析,会涉及到两个文件的改动:

  • flutter\lib\src\widgets\ticker_provider.dart
  • flutter\lib\src\scheduler\ticker.dart
  1. ticker_provider.dart其实不需要改动,但是它应用的Ticker是写死的,我们改动不了。所以只能改下这个文件,复制下来修改
  2. ticker.dart主要是改动_tick方法
dart 复制代码
void _tick(Duration timeStamp) {
  assert(isTicking);
  assert(scheduled);
  _animationId = null;

  _startTime ??= timeStamp;
  _onTick(timeStamp - _startTime!);

  // The onTick callback may have scheduled another tick already, for
  // example by calling stop then start again.
  if (shouldScheduleTick) {
    // 设置刷新间隔,refreshInterval是调用TickerWithInterval的构造函数时传入的参数
    Future.delayed(Duration(milliseconds: refreshInterval), () {
      if (shouldScheduleTick) scheduleTick();
    });
  }
}

分析完代码后,其实改动很简单;这样能完全保留AnimationController的使用习惯,接入没有任何成本。

代码示例

  • ticker_provider.dar
dart 复制代码
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

import 'ticker.dart';

typedef TickerCallback = void Function(Duration elapsed);

@optionalTypeArgs
mixin SingleIntervalTickerProviderStateMixin<T extends StatefulWidget>
    on State<T> implements TickerProvider {
  TickerWithInterval? _ticker;

  @override
  TickerWithInterval createTicker(TickerCallback onTick) {
    assert(() {
      if (_ticker == null) {
        return true;
      }
      throw FlutterError.fromParts(<DiagnosticsNode>[
        ErrorSummary(
            '$runtimeType is a SingleTickerProviderStateMixin but multiple tickers were created.'),
        ErrorDescription(
            'A SingleTickerProviderStateMixin can only be used as a TickerProvider once.'),
        ErrorHint(
          'If a State is used for multiple AnimationController objects, or if it is passed to other '
          'objects and those objects might use it more than one time in total, then instead of '
          'mixing in a SingleTickerProviderStateMixin, use a regular TickerProviderStateMixin.',
        ),
      ]);
    }());
    _ticker = TickerWithInterval(onTick,
        debugLabel: kDebugMode ? 'created by ${describeIdentity(this)}' : null);
    _updateTickerModeNotifier();
    _updateTicker(); // Sets _ticker.mute correctly.
    return _ticker!;
  }

  @override
  void dispose() {
    assert(() {
      if (_ticker == null || !_ticker!.isActive) {
        return true;
      }
      throw FlutterError.fromParts(<DiagnosticsNode>[
        ErrorSummary('$this was disposed with an active Ticker.'),
        ErrorDescription(
          '$runtimeType created a Ticker via its SingleTickerProviderStateMixin, but at the time '
          'dispose() was called on the mixin, that Ticker was still active. The Ticker must '
          'be disposed before calling super.dispose().',
        ),
        ErrorHint(
          'Tickers used by AnimationControllers '
          'should be disposed by calling dispose() on the AnimationController itself. '
          'Otherwise, the ticker will leak.',
        ),
        _ticker!.describeForError('The offending ticker was'),
      ]);
    }());
    _tickerModeNotifier?.removeListener(_updateTicker);
    _tickerModeNotifier = null;
    super.dispose();
  }

  ValueListenable<bool>? _tickerModeNotifier;

  @override
  void activate() {
    super.activate();
    // We may have a new TickerMode ancestor.
    _updateTickerModeNotifier();
    _updateTicker();
  }

  void _updateTicker() {
    if (_ticker != null) {
      _ticker!.muted = !_tickerModeNotifier!.value;
    }
  }

  void _updateTickerModeNotifier() {
    final ValueListenable<bool> newNotifier = TickerMode.getNotifier(context);
    if (newNotifier == _tickerModeNotifier) {
      return;
    }
    _tickerModeNotifier?.removeListener(_updateTicker);
    newNotifier.addListener(_updateTicker);
    _tickerModeNotifier = newNotifier;
  }

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    String? tickerDescription;
    if (_ticker != null) {
      if (_ticker!.isActive && _ticker!.muted) {
        tickerDescription = 'active but muted';
      } else if (_ticker!.isActive) {
        tickerDescription = 'active';
      } else if (_ticker!.muted) {
        tickerDescription = 'inactive and muted';
      } else {
        tickerDescription = 'inactive';
      }
    }
    properties.add(DiagnosticsProperty<TickerWithInterval>('ticker', _ticker,
        description: tickerDescription,
        showSeparator: false,
        defaultValue: null));
  }
}

@optionalTypeArgs
mixin IntervalTickerProviderStateMixin<T extends StatefulWidget> on State<T>
    implements TickerProvider {
  Set<TickerWithInterval>? _tickers;

  @override
  TickerWithInterval createTicker(TickerCallback onTick) {
    if (_tickerModeNotifier == null) {
      // Setup TickerMode notifier before we vend the first ticker.
      _updateTickerModeNotifier();
    }
    assert(_tickerModeNotifier != null);
    _tickers ??= <_WidgetTicker>{};
    final _WidgetTicker result = _WidgetTicker(onTick, this,
        debugLabel: kDebugMode ? 'created by ${describeIdentity(this)}' : null)
      ..muted = !_tickerModeNotifier!.value;
    _tickers!.add(result);
    return result;
  }

  void _removeTicker(_WidgetTicker ticker) {
    assert(_tickers != null);
    assert(_tickers!.contains(ticker));
    _tickers!.remove(ticker);
  }

  ValueListenable<bool>? _tickerModeNotifier;

  @override
  void activate() {
    super.activate();
    // We may have a new TickerMode ancestor, get its Notifier.
    _updateTickerModeNotifier();
    _updateTickers();
  }

  void _updateTickers() {
    if (_tickers != null) {
      final bool muted = !_tickerModeNotifier!.value;
      for (final TickerWithInterval ticker in _tickers!) {
        ticker.muted = muted;
      }
    }
  }

  void _updateTickerModeNotifier() {
    final ValueListenable<bool> newNotifier = TickerMode.getNotifier(context);
    if (newNotifier == _tickerModeNotifier) {
      return;
    }
    _tickerModeNotifier?.removeListener(_updateTickers);
    newNotifier.addListener(_updateTickers);
    _tickerModeNotifier = newNotifier;
  }

  @override
  void dispose() {
    assert(() {
      if (_tickers != null) {
        for (final TickerWithInterval ticker in _tickers!) {
          if (ticker.isActive) {
            throw FlutterError.fromParts(<DiagnosticsNode>[
              ErrorSummary('$this was disposed with an active Ticker.'),
              ErrorDescription(
                '$runtimeType created a Ticker via its TickerProviderStateMixin, but at the time '
                'dispose() was called on the mixin, that Ticker was still active. All Tickers must '
                'be disposed before calling super.dispose().',
              ),
              ErrorHint(
                'Tickers used by AnimationControllers '
                'should be disposed by calling dispose() on the AnimationController itself. '
                'Otherwise, the ticker will leak.',
              ),
              ticker.describeForError('The offending ticker was'),
            ]);
          }
        }
      }
      return true;
    }());
    _tickerModeNotifier?.removeListener(_updateTickers);
    _tickerModeNotifier = null;
    super.dispose();
  }

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(DiagnosticsProperty<Set<TickerWithInterval>>(
      'tickers',
      _tickers,
      description: _tickers != null
          ? 'tracking ${_tickers!.length} ticker${_tickers!.length == 1 ? "" : "s"}'
          : null,
      defaultValue: null,
    ));
  }
}

class _WidgetTicker extends TickerWithInterval {
  _WidgetTicker(super.onTick, this._creator, {super.debugLabel});

  final IntervalTickerProviderStateMixin _creator;

  @override
  void dispose() {
    _creator._removeTicker(this);
    super.dispose();
  }
}
  • ticker.dart
ini 复制代码
import 'dart:async';

import 'package:flutter/foundation.dart';
import 'package:flutter/scheduler.dart';

class TickerWithInterval implements Ticker {
  TickerWithInterval(this._onTick,
      {this.refreshInterval = 30, this.debugLabel}) {
    assert(() {
      _debugCreationStack = StackTrace.current;
      return true;
    }());
  }

  final int refreshInterval;

  MyTickerFuture? _future;

  @override
  bool get muted => _muted;
  bool _muted = false;

  set muted(bool value) {
    if (value == muted) {
      return;
    }
    _muted = value;
    if (value) {
      unscheduleTick();
    } else if (shouldScheduleTick) {
      scheduleTick();
    }
  }

  @override
  bool get isTicking {
    if (_future == null) {
      return false;
    }
    if (muted) {
      return false;
    }
    if (SchedulerBinding.instance.framesEnabled) {
      return true;
    }
    if (SchedulerBinding.instance.schedulerPhase != SchedulerPhase.idle) {
      return true;
    } // for example, we might be in a warm-up frame or forced frame
    return false;
  }

  @override
  bool get isActive => _future != null;

  Duration? _startTime;

  @override
  TickerFuture start() {
    assert(() {
      if (isActive) {
        throw FlutterError.fromParts(<DiagnosticsNode>[
          ErrorSummary('A ticker was started twice.'),
          ErrorDescription(
              'A ticker that is already active cannot be started again without first stopping it.'),
          describeForError('The affected ticker was'),
        ]);
      }
      return true;
    }());
    assert(_startTime == null);
    _future = MyTickerFuture._();
    if (shouldScheduleTick) {
      scheduleTick();
    }
    if (SchedulerBinding.instance.schedulerPhase.index >
            SchedulerPhase.idle.index &&
        SchedulerBinding.instance.schedulerPhase.index <
            SchedulerPhase.postFrameCallbacks.index) {
      _startTime = SchedulerBinding.instance.currentFrameTimeStamp;
    }
    return _future!;
  }

  @override
  DiagnosticsNode describeForError(String name) {
    return DiagnosticsProperty<Ticker>(name, this,
        description: toString(debugIncludeStack: true));
  }

  @override
  void stop({bool canceled = false}) {
    if (!isActive) {
      return;
    }

    final MyTickerFuture localFuture = _future!;
    _future = null;
    _startTime = null;
    assert(!isActive);

    unscheduleTick();
    if (canceled) {
      localFuture._cancel(this);
    } else {
      localFuture._complete();
    }
  }

  final TickerCallback _onTick;

  int? _animationId;

  @override
  bool get scheduled => _animationId != null;

  @override
  bool get shouldScheduleTick => !muted && isActive && !scheduled;

  void _tick(Duration timeStamp) {
    assert(isTicking);
    assert(scheduled);
    _animationId = null;

    _startTime ??= timeStamp;
    _onTick(timeStamp - _startTime!);

    // The onTick callback may have scheduled another tick already, for
    // example by calling stop then start again.
    if (shouldScheduleTick) {
      // 设置刷新间隔,refreshInterval是调用TickerWithInterval的构造函数时传入的参数
      Future.delayed(Duration(milliseconds: refreshInterval), () {
        if (shouldScheduleTick) scheduleTick();
      });
    }
  }

  @override
  void scheduleTick({bool rescheduling = false}) {
    assert(!scheduled);
    assert(shouldScheduleTick);
    _animationId = SchedulerBinding.instance
        .scheduleFrameCallback(_tick, rescheduling: rescheduling);
  }

  @override
  void unscheduleTick() {
    if (scheduled) {
      SchedulerBinding.instance.cancelFrameCallbackWithId(_animationId!);
      _animationId = null;
    }
    assert(!shouldScheduleTick);
  }

  @override
  void absorbTicker(Ticker originalTicker) {
    originalTicker as TickerWithInterval;
    assert(!isActive);
    assert(_future == null);
    assert(_startTime == null);
    assert(_animationId == null);
    assert(
        (originalTicker._future == null) == (originalTicker._startTime == null),
        'Cannot absorb Ticker after it has been disposed.');
    if (originalTicker._future != null) {
      _future = originalTicker._future;
      _startTime = originalTicker._startTime;
      if (shouldScheduleTick) {
        scheduleTick();
      }
      originalTicker._future =
          null; // so that it doesn't get disposed when we dispose of originalTicker
      originalTicker.unscheduleTick();
    }
    originalTicker.dispose();
  }

  @override
  void dispose() {
    if (_future != null) {
      final MyTickerFuture localFuture = _future!;
      _future = null;
      assert(!isActive);
      unscheduleTick();
      localFuture._cancel(this);
    }
    assert(() {
      _startTime = Duration.zero;
      return true;
    }());
  }

  @override
  final String? debugLabel;
  late StackTrace _debugCreationStack;

  @override
  String toString({bool debugIncludeStack = false}) {
    final StringBuffer buffer = StringBuffer();
    buffer.write('${objectRuntimeType(this, 'Ticker')}(');
    assert(() {
      buffer.write(debugLabel ?? '');
      return true;
    }());
    buffer.write(')');
    assert(() {
      if (debugIncludeStack) {
        buffer.writeln();
        buffer.writeln(
            'The stack trace when the $runtimeType was actually created was:');
        FlutterError.defaultStackFilter(
                _debugCreationStack.toString().trimRight().split('\n'))
            .forEach(buffer.writeln);
      }
      return true;
    }());
    return buffer.toString();
  }
}

class MyTickerFuture implements TickerFuture {
  MyTickerFuture._();

  MyTickerFuture.complete() {
    _complete();
  }

  final Completer<void> _primaryCompleter = Completer<void>();
  Completer<void>? _secondaryCompleter;
  bool?
      _completed; // null means unresolved, true means complete, false means canceled

  void _complete() {
    assert(_completed == null);
    _completed = true;
    _primaryCompleter.complete();
    _secondaryCompleter?.complete();
  }

  void _cancel(Ticker ticker) {
    assert(_completed == null);
    _completed = false;
    _secondaryCompleter?.completeError(TickerCanceled(ticker));
  }

  void whenCompleteOrCancel(VoidCallback callback) {
    void thunk(dynamic value) {
      callback();
    }

    orCancel.then<void>(thunk, onError: thunk);
  }

  Future<void> get orCancel {
    if (_secondaryCompleter == null) {
      _secondaryCompleter = Completer<void>();
      if (_completed != null) {
        if (_completed!) {
          _secondaryCompleter!.complete();
        } else {
          _secondaryCompleter!.completeError(const TickerCanceled());
        }
      }
    }
    return _secondaryCompleter!.future;
  }

  @override
  Stream<void> asStream() {
    return _primaryCompleter.future.asStream();
  }

  @override
  Future<void> catchError(Function onError, {bool Function(Object)? test}) {
    return _primaryCompleter.future.catchError(onError, test: test);
  }

  @override
  Future<R> then<R>(FutureOr<R> Function(void value) onValue,
      {Function? onError}) {
    return _primaryCompleter.future.then<R>(onValue, onError: onError);
  }

  @override
  Future<void> timeout(Duration timeLimit,
      {FutureOr<void> Function()? onTimeout}) {
    return _primaryCompleter.future.timeout(timeLimit, onTimeout: onTimeout);
  }

  @override
  Future<void> whenComplete(dynamic Function() action) {
    return _primaryCompleter.future.whenComplete(action);
  }

  @override
  String toString() =>
      '${describeIdentity(this)}(${_completed == null ? "active" : _completed! ? "complete" : "canceled"})';
}

class MyTickerCanceled implements TickerCanceled {
  const MyTickerCanceled([this.ticker]);

  final Ticker? ticker;

  @override
  String toString() {
    if (ticker != null) {
      return 'This ticker was canceled: $ticker';
    }
    return 'The ticker was canceled before the "orCancel" property was first used.';
  }
}
  • 调用方式
dart 复制代码
class DemoAnimation extends StatefulWidget {
  const DemoAnimation({Key? key}) : super(key: key);

  @override
  State<DemoAnimation> createState() => _DemoAnimationState();
}

class _DemoAnimationState extends State<DemoAnimation>
    with SingleIntervalTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _animation;
  int? mill;

  @override
  void initState() {
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 8),
    );

总结

通过分析动画的源码,加入帧延时,实现对动画帧率的控制;在硬件达标的时候,这篇文章一定能帮到你;通过此方案,文章开头中的动画,同一个机器下,GPU的占用由34%降为12%

我也把这个方案提在issus上了,希望官方能尽快支持类似Android setFrameDelay这种接口。

相关推荐
Python私教36 分钟前
Flutter组件化开发
flutter
bcdaren1 小时前
《Windows PE》4.2 绑定导入表
c语言·汇编·windows·pe
芯的一天1 小时前
windows下DockerDesktop命令行方式指定目录安装
windows·docker
沐言人生1 小时前
Android10 Framework—Init进程-9.服务端属性值初始化
android·android studio·android jetpack
localbob2 小时前
uniapp超全user-agent判断 包括微信开发工具 hbuilder mac windows 安卓ios端及本地识别
windows·macos·uni-app·user-agent
追光天使2 小时前
【Mac】和【安卓手机】 通过有线方式实现投屏
android·macos·智能手机·投屏·有线
科研小达人2 小时前
ChatGPT进行文本分类
windows·chatgpt
helloxmg2 小时前
鸿蒙harmonyos next flutter混合开发之开发FFI plugin
flutter
小雨cc5566ru2 小时前
uniapp+Android智慧居家养老服务平台 0fjae微信小程序
android·微信小程序·uni-app
一切皆是定数3 小时前
Android车载——VehicleHal初始化(Android 11)
android·gitee