优化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这种接口。

相关推荐
小羊没烦恼!20 小时前
初探性能优化——2个月到4小时的性能提升
java·开发语言·windows·算法·c#
rockmelodies20 小时前
# Windows Server2008 R2 Standard(SP1镜像U盘)重置本地管理员密码【完整详细步骤】
windows·密码破解
千里马学框架20 小时前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台20 小时前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone21 小时前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc21 小时前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo1 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077001 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼1 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone1 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui