前语:高功能的烘托一直是Flutter引以为傲的地方,经过自行调用Skia引擎制作,达到比美原生的烘托作用。但是这种高帧率且无法修改帧率的烘托,却在功能不高的机器上,给咱们带来了不少费事。今天咱们就来讨论下如何在Flutter上,下降动画的帧率。

一、背景

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

优化Flutter动画性能损耗
优化Flutter动画性能损耗

所以当需求在低功能的设备上长时间的展现动画的时分,这个问题就成为功能瓶颈。比方下面这个场景:

  • 设备的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进行刷新;
// 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);
}
// 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办法;
// 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));
}
// 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互相调用;所以一帧调用完毕就会立刻调用下一帧;
// 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是写死的,咱们改动不了。所以只能改下这个文件,复制下来修改
    优化Flutter动画性能损耗
  2. ticker.dart主要是改动_tick办法
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
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
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.';
  }
}
  • 调用方法
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这种接口。