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

相关推荐
网安Ruler29 分钟前
代码审计-PHP专题&原生开发&SQL注入&1day分析构造&正则搜索&语句执行监控&功能定位
android
paid槮2 小时前
MySql基础:数据类型
android·mysql·adb
用户2018792831673 小时前
AMS和app通信的小秘密
android
用户2018792831673 小时前
ThreadPoolExecutor之市场雇工的故事
android
诺诺Okami3 小时前
Android Framework-Launcher-InvariantDeviceProfile
android
Antonio9155 小时前
【音视频】Android NDK 与.so库适配
android·音视频
sun00770013 小时前
android ndk编译valgrind
android
AI视觉网奇15 小时前
android studio 断点无效
android·ide·android studio
jiaxi的天空15 小时前
android studio gradle 访问不了
android·ide·android studio
No Silver Bullet16 小时前
android组包时会把从maven私服获取的包下载到本地吗
android