alibaba / fish-redux

An assembled flutter application framework.
https://github.com/alibaba/fish-redux
Apache License 2.0
7.33k stars 843 forks source link

TickerProviderStateMixin dispose 报错 #461

Closed bitterking closed 5 years ago

bitterking commented 5 years ago

在Lifecycle.dispose 中将正在执行的animationController dispose掉,会报错 image 感觉TickerProviderStateMixin 的dispose会先于ComponentState 中的dispose执行 `Effect buildEffect() { return combineEffects(<Object, Effect>{ Lifecycle.initState: _init, Lifecycle.dispose: _dispose, SeatsAction.volumeChanged: _onVolumeChanged, SeatsAction.selfVolumeChanged: _onSelfVolumeChanged, }); }

void _init(Action action, Context ctx) { final Object tickerProvider = ctx.stfState; ctx.state.volumeAnimationControllers = List.generate(8, (index) { return AnimationController( duration: Duration(milliseconds: 1000), vsync: tickerProvider); }); }

void _dispose(Action action, Context ctx) { ctx.state.volumeAnimationControllers.forEach((controller) { controller.dispose(); }); }`

zjuwjf commented 5 years ago

能提供一个最小的demo么?

bitterking commented 5 years ago

https://github.com/bitterking/redex_sample

@zjuwjf 麻烦大佬看一下,谢谢~~

MMMzq commented 5 years ago

@bitterking 这个问题我好像遇到过

我之前的推断原因是:

flutter执行了"错误"的生命周期导致的:initState->initState->dispose,

导致dispose的是最新的controller。

暂时的解决办法: ctx.extra来保存需要销毁的AnimationController 像这样:

void _initState(Action action, Context<XxxxxState> ctx) {
    ....
    ctx.extra["closeAnimation"] = clone.controller;
    ...
}

void _dispose(Action action, Context<XxxxState> ctx) {
    (ctx.extra["closeAnimation"] as AnimationController)?.dispose();
     ctx.extra["closeAnimation"] = null;
}
zjuwjf commented 5 years ago

感谢,复现了这个问题。

它是由于 flutter 不是最合理的实现导致的

  @override
  void dispose() {
    assert(() {
      if (_tickers != null) {
        for (Ticker ticker in _tickers) {
          if (ticker.isActive) {
            throw FlutterError('$this was disposed with an active Ticker.\n'
                '$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 or after calling super.dispose(). Tickers used by AnimationControllers '
                'should be disposed by calling dispose() on the AnimationController itself. '
                'Otherwise, the ticker will leak.\n'
                'The offending ticker was: ${ticker.toString(debugIncludeStack: true)}');
          }
        }
      }
      return true;
    }());
    super.dispose();
  }
mixin TickerProviderMixin<T> on Component<T> {
  @override
  _TickerProviderStfState<T> createState() => _TickerProviderStfState<T>();
}

class _TickerProviderStfState<T> extends ComponentState<T>
    with TickerProviderStateMixin {}

当 Page mixin TickerProviderMixin 后

class WelcomePage extends Page<WelcomeState, Map<String, dynamic>>
    with TickerProviderMixin<WelcomeState> {}

当执行到dispose 1、执行assert(!ticker.isActive) 2、执行ctx.onLifecycle 3、执行ctx.dispose()

所以还没进入我们的

void _dispose(Action action, Context<WelcomeState> ctx) {
  ctx.state.controller.dispose();
}

前,就先assert报错了。

合理的应该Flutter 应该这个做

  @override
  void dispose() {
    super.dispose();
    assert(() {
      if (_tickers != null) {
        for (Ticker ticker in _tickers) {
          if (ticker.isActive) {
            throw FlutterError('$this was disposed with an active Ticker.\n'
                '$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 or after calling super.dispose(). Tickers used by AnimationControllers '
                'should be disposed by calling dispose() on the AnimationController itself. '
                'Otherwise, the ticker will leak.\n'
                'The offending ticker was: ${ticker.toString(debugIncludeStack: true)}');
          }
        }
      }
      return true;
    }());
  }

目前fish-redux 内部处理了这个case。 下周一会在0.2.6中发布。 目前可以依赖master分支。

zjuwjf commented 5 years ago

@MMMzq @bitterking 你们的问题解决了么?

MMMzq commented 5 years ago

@zjuwjf 那个问题并没有解决 我写了的个example来重现: example

重现步骤 1.进去直接下拉刷新一次 (如果不刷新第一次是正常的) 2.展开ExpansionTile,点击里面的item (发现动画不能正常启动)

初步推断原因: 生命周期函数触发"错误":initState->initState->dispose,

qq329401134 commented 5 years ago

我也是遇到了同样的问题

zjuwjf commented 5 years ago

2.展开ExpansionTile,点击里面的item (发现动画不能正常启动)

你说的问题,我并没有复现, 不断下拉刷新,执行一切符合预期。 initState 两次 是因为有两个item。

我的环境:flutter v1.9.1-hotfixes

MMMzq commented 5 years ago

@zjuwjf

我的版本信息:

Flutter 1.9.1+hotfix.2 • channel unknown • unknown source Framework • revision 2d2a1ffec9 (2 weeks ago) • 2019-09-06 18:39:49 -0700 Engine • revision b863200c37 Tools • Dart 2.5.0

git

zjuwjf commented 5 years ago

@MMMzq 非常感谢你提供的这个case。

为什么通过 在effect-dispose中通过

ctx.state.controller.dispose(); 

会dispose最新的controller, 而非想要的dispose那份老的controller。

而通过

ctx.extra["controller''].dispose(); 

是正确的。

这引导我们去思考一个问题🤔: 状态的生命周期问题。

ctx.state 是一个函数,它总会根据固定的方法(connector)去获取自己最新的状态。在绝大多数场景(slots)下,它work的很好,但是在DynamicFlowAdapter场景下,情况会变的复杂。

一般而言,我们都说,组件是有生命周期的,状态是无生命周期的。

但是在@MMMzq 的case中,列表中的每一项状态,都是需要有生命周期的。

DynamicFlowAdapter的列表下 每一个组件如何获取它的最新state, 特别是 ListView 刷新下, 存在组件的复用和不复用的case下,组件如何获取它的state。

上面 @MMMzq 提到的问题就是,当为每一个组件设置一个唯一Key的时候,也就是每一个组件都不复用,那如何让刷新前的第1项的组件获得的state是刷新前的state,而让刷新后的第1项的组件获得的state是刷新后的state,尽管他们都是通过index来获取状态的。

再考虑这样的场景,当List下的某一个Item组件,发出一个action,被自己的reducer所处理,如何让列表下的组件合理的获取自己最新的state?

再考虑一个场景,当List 被插入一行数据的时候,状态和widget又是如何在被复用的?

下面解释下这个过程: 关键代码: 0.2.6

/// Define itemBean how to get state with connector
///
/// [_isSimilar] return true just use newState after reducer safely
/// [_isSimilar] return false we should use cache state before reducer invoke.
/// for reducer change state immediately but sub component will refresh on next
/// frame. in this time the sub component will use cache state.
Get<Object> _subGetter(Get<List<ItemBean>> getter, int index) {
  final List<ItemBean> curState = getter();
  final Object subCache = curState[index].data;
  return () {
    final List<ItemBean> newState = getter();

    /// Either all sub-components use cache or not.
    if (_isSimilar(curState, newState)) {
      return newState[index].data;
    } else {
      return subCache;
    }
  };
}

/// Judge [oldList] and [newList] is similar
///
/// if true: means the list size and every itemBean type & data.runtimeType
/// is equal.
bool _isSimilar(
  List<ItemBean> oldList,
  List<ItemBean> newList,
) {
  if (oldList != newList &&
      oldList?.length == newList.length &&
      Collections.isNotEmpty(newList)) {
    bool isEvery = true;
    for (int i = 0; i < newList.length; i++) {
      if (oldList[i].type != newList[i].type ||
          oldList[i].data.runtimeType != newList[i].data.runtimeType) {
        isEvery = false;
        break;
      }
    }
    return isEvery;
  }
  return false;
}

最新的

/// Define itemBean how to get state with connector
///
/// [_isSimilar] return true just use newState after reducer safely
/// [_isSimilar] return false we should use cache state before reducer invoke.
/// for reducer change state immediately but sub component will refresh on next
/// frame. in this time the sub component will use cache state.
Get<Object> _subGetter(Get<List<ItemBean>> getter, int index) {
  final List<ItemBean> curState = getter();
  ItemBean cacheItem = curState[index];

  return () {
    final List<ItemBean> newState = getter();

    /// Either all sub-components use cache or not.
    if (newState != null && newState.length > index) {
      final ItemBean newItem = newState[index];
      if (_couldReuse(cacheItem, newItem)) {
        cacheItem = newItem;
      }
    }

    return cacheItem.data;
  };
}

bool _couldReuse(ItemBean beanA, ItemBean beanB) {
  if (beanA.type != beanB.type) {
    return false;
  }

  final Object dataA = beanA.data;
  final Object dataB = beanB.data;
  if (dataA.runtimeType != dataB.runtimeType) {
    return false;
  }

  final Object keyA = dataA is StateKey ? dataA.key() : null;
  final Object keyB = dataB is StateKey ? dataB.key() : null;
  return keyA == keyB;
}

修改后, Item状态的获取是否是最新的state,还是沿用上一份state, 完全取决于组件是否被复用,和ListView 下 Widget的复用机制吻合。

同时将Key的表达,推荐,迁移到State中来,让框架能感知到它。

class SuperItemState implements Cloneable<SuperItemState>, StateKey {
  AnimationController controller;
  Animation<Color> animationColor;

  UniqueKey uniqueKey = UniqueKey();

  SuperItemState();

  @override
  SuperItemState clone() {
    return SuperItemState()
      ..controller = controller
      ..animationColor = animationColor
      ..uniqueKey = uniqueKey;
  }

  @override
  Object key() {
    return uniqueKey;
  }
}

/// 同时不在需要在Component组合中提供 key
class SuperItemComponent extends Component<SuperItemState>
    with
        PrivateReducerMixin<SuperItemState>,
        TickerProviderMixin<SuperItemState> {
  SuperItemComponent()
      : super(
          effect: buildEffect(),
          reducer: buildReducer(),
          view: buildView,
          // key: (state) => state.key(),
        );
}
MMMzq commented 5 years ago

@zjuwjf

非常感谢,问题解决了😀.

但是我还有一个问题就是:为什么现在还是initState->initState->dispose这个流程呢🤔? 这个流程很反直觉,正常来讲不应该是initState->dispose->initState这个过程的么?

zjuwjf commented 5 years ago

这个是flutter内部的处理流程, 从我理解,比较符合预期,先创建再释放。

一个listview下有2个item

initState item0 initState item1 下拉刷新 initState item2 initState item3 dispose 1 dispose 0

MMMzq commented 5 years ago

@zjuwjf 请问一下什么时候可以把这个改动发版?

zjuwjf commented 5 years ago

@zjuwjf 请问一下什么时候可以把这个改动发版?

@MMMzq 已发布 0.2.7