• Flutter状态管理 — 探索Flutter中的状态


    前言

    随着响应式编程的理念&Flutter被大众所了解以来,状态管理一直是一个引人深思的话题。如果想要学习好Flutter这样的响应式的编程框架就一定是离不开状态管理的。我遇到过很多没有了解过响应式编程框架的,或者从事后端开发,自己想用Flutter写个app玩玩的朋友,一上来,不管在哪里都用setState,我问为啥不用状态管理,大部分都回了一句:啥是状态管理?当然,曾经的我也一样,啥也不懂,一上来就用上了GetX这个“大杀器”从而导致走了许多弯路。

    ps:都2023年了,已经有那么多的大佬写了与状态管理相关的文章,为啥我还要“炒冷饭”?因为一句话:GetX你害得我好惨!同样,不同的人对于状态管理有不同的看法,一千个读者眼中就会有一千个哈姆雷特。我希望能通过几篇文章来帮助和我类似经历的朋友,更好的明白,啥是状态管理。

    需要我们管理的状态有哪些

    在一个应用中,存在着大量的状态,例如某个组件的动画状态、界面的外观效果、字体…当然,很多的状态并不需要我们自己去做管理,框架本身就已经做了这部分的工作了,例如:将Widget树转换为底层的图像或纹理、无需手动跟踪和管理动画状态,以及处理布局,包括大小、位置和约束,无需手动计算和管理布局状态。这才可以让我们开发者更专注于构建用户界面和交互逻辑,而无需过多关注底层的状态细节。而需要我们开发者去管理的状态可以分为两类:

    • 短时状态

    你可以简单的将其理解为:某些数据状态只需要在当前的Widget中使用,不需要将这些数据和状态共享给其他组件或者页面。通常这样的情况,你不需要用到一些provider、GetX这样的状态管理框架,你仅仅需要一个StatefulWidget,依靠其自身的State管理即可,这种状态也不会以复杂的方式而改变。像一个提示消息状态(Toast)、页面切换时的一些动画(淡入淡出效果)、动画状态(一个淡入淡出的动画或一个旋转动画都是短时状态)。

    class _MyHomePageState extends State {
      Color _buttonColor = Colors.blue; // 初始颜色
      void _changeButtonColor() {
        setState(() {
          _buttonColor = Colors.green; // 更新颜色为绿色
        });
    
        // 等待1秒后恢复原样
        Future.delayed(Duration(seconds: 1), () {
          setState(() {
            _buttonColor = Colors.blue; // 恢复原样
          });
        });
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text('短时状态'),
          ),
          body: Center(
            child: ElevatedButton(
              onPressed: _changeButtonColor,
              style: ElevatedButton.styleFrom(
                backgroundColor: _buttonColor, // 使用当前状态中的颜色
                padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10),
              ),
              child: const Text(
                'Change Color',
                style: TextStyle(fontSize: 18),
              ),
            ),
          ),
        );
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    当用户点击 “Change Color” 按钮时,按钮的颜色会在一秒钟内变为绿色,然后恢复为蓝色。这里的按钮颜色变化过程就是一个短时状态,因为它只在特定的时间段内有效,不需要长期存储,也不需要共享。

    • 应用状态

    当数据或状态需要被共享时,就是当一个组件的状态发生变化后,其他的组件也会跟着发生变化,这就是应用状态。建议通过状态管理库(如Provider)来管理。应用状态的生命周期更长,通常会影响应用全局行为,例如用户身份、主题、国际化等。

    状态提升

    在使用状态管理之前,我们还需学习下状态提升,它能使我们更好的理解到状态管理的作用!话不多说,直接上案例:

    可以从图中发现,第一个页面的值与第二个页面的值是不同步的。在很多业务场景下,都需要将二级的详情页面的值与一级页面的值同步,例如文章的点赞数。那么这个时候会有一种简单的方法,就是将这个值提取到父级页面,由父级页面把状态传递下去。

    我们抽离出一个Count类,并在父级界面实例化一个Count类,通过这种方式,第一个页面和第二个页面将共享同一个计数器对象,因此无论在哪个页面中增加计数器的值,都会同步地影响另一个页面中的计数器。这就实现了状态的提升和同步。不过,我们虽然通过状态提升的方法做到了状态同步这一点,但是,仅仅是一个Count数字的改变,却需要将第一个页面和第二个页面上的所有组件进行重新build,简单的页面当然看不出什么,一旦页面复杂,渲染的成本实在是太高,也违背了我们使用状态管理的一个初衷:控制组件局部刷新。为了能更好的解决这样的问题,我们需要探索新的方法,这时,状态管理就起到作用了!

    Flutter中有哪些可以做到状态管理

    1.setState

    我相信无论是新手还是老手,只要是体验过Flutter框架99%的朋友都知道setState,大部分情况只需要在改变UI数据的逻辑外面套上一个setState就可以实现对界面的更新。在本文中我们不过多讨论setState的使用方式以及原理,我们聊聊它的优点和缺点。

    • 优点

    相信大家在各种Flutter交流群中应该都有遇到过群友问性能相关的问题。而一些新手朋友这个时候看的多了就会有一个疑惑:我写的一些页面也卡卡的,帧率只有30几帧,会不会是因为我使用了setState去刷新页面啊?其实不然,setState不但不会造成性能困扰,它反而在帮你!最关键的一点,是因为Flutter在底层实现了一些机制来减少不必要的重绘。

    具体一点来说,当去调用setState时,Flutter会将新的状态放入队列中,并计划在下一个绘制周期(Frame)中更新UI。在下一个绘制周期到来之前,Flutter会执行一些优化步骤:

      • 差异校验:Flutter会比较前后两个状态,确定实际上哪些部分需要更新。这种差异校验能够有效地减少不必要的布局、绘制和合成操作。例如:
      • 合并多个setState调用:如果在同一个绘制周期内连续调用了多次setState,Flutter会将它们合并成一个,以避免不必要的重复工作。setState只是调用了_element的markNeedsBuild方法,以标记这个元素需要在下一个绘制周期中进行重建。这个标记会将新的状态放入队列中,并在下一个帧(绘制周期)中触发重建。
      • 重绘策略:Flutter会尽量减少需要重新绘制的部分。如果某个部分没有发生变化,那么不会重新绘制它,从而节省CPU和GPU资源。将组件单独抽离为一个widget,就可以通过setState来更新局部状态,实现局部刷新。

    所以,只要能正确的使用setStatesetState的节点越远离根部,那么布局、绘制渲染的开销就会越少。(使用绝对宽高也可以减少开销哦~)

    • 缺点

    聊完了setState的优点,那么我们再来聊聊它的缺点:

      • **无法做到跨组件共享数据
        **setStateState的函数,一般我们会将State的子类设置为私有,所以无法做到让别的组件调用StatesetState函数来刷新。
      • 维护成本极高
        在文章的开头,就提到过,一些新手写Flutter时,不管在哪里都用setState,搞的哪哪都是。随着页面状态的增多,调用setState的地方会越来越多,不能统一管理。难以维护。
      • **状态和UI耦合
        **使用状态管理很重要的一点就是解耦,而随意使用setState会导致数据逻辑和视图混合在一起,比如数据库的数据取出来setState到ui上,这样编写代码,会导致状态和UI耦合在一起,不利于测试,不利于复用。
      • 实现局部刷新复杂度高
        setState是整个Widget重新构建(子Widget也会跟着销毁重建),如果页面足够复杂,就会导致非常严重的性能损耗。而如果想要通过setState进行局部刷新,就需要对组件进行提取,如果每个组件都要封装提取一下,这个工作量太多了!
    2.ChangeNotifier

    为了解决setState带来的一些问题,可以通过ChangeNotifier作为状态管理的方案。 ChangeNotifier可以将状态逻辑和UI逻辑分开,从而使得状态管理更加集中和可控。通过创建一个单独的ChangeNotifier类来管理某一部分状态,然后在需要使用这些状态的地方进行订阅。

    class CounterModel extends ChangeNotifier {
      int _counter = 0;
    
      int get counter => _counter;
    
      void increment() {
        _counter++;
        notifyListeners();
      }
    }
    
    class MyApp extends StatelessWidget {
      final CounterModel _counterModel = CounterModel();
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          home: Scaffold(
            appBar: AppBar(title: Text('ChangeNotifier Example')),
            body: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                CounterDisplay(_counterModel),
                CounterControl(_counterModel),
              ],
            ),
          ),
        );
      }
    }
    
    class CounterDisplay extends StatefulWidget {
      final CounterModel counterModel;
    
      CounterDisplay(this.counterModel);
    
      @override
      _CounterDisplayState createState() => _CounterDisplayState();
    }
    
    class _CounterDisplayState extends State {
      @override
      void initState() {
        super.initState();
        widget.counterModel.addListener(_update);
      }
    
      @override
      void dispose() {
        widget.counterModel.removeListener(_update);
        super.dispose();
      }
    
      void _update() {
        setState(() {});
      }
    
      @override
      Widget build(BuildContext context) {
        return Center(
          child: Text('Value: ${widget.counterModel.counter}'),
        );
      }
    }
    
    class CounterControl extends StatelessWidget {
      final CounterModel counterModel;
    
      const CounterControl(this.counterModel, {Key? key}) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return Center(
          child: ElevatedButton(
            onPressed: () {
              counterModel.increment();
            },
            child: Text('+'),
          ),
        );
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82

    不过,虽然ChangeNotifier解决了一些问题,但在大型应用中,仍然可能遇到一些挑战。例如,当应用状态较为复杂时,可能需要多个ChangeNotifier,从而导致状态分散和层级复杂。

    3.ChangeNotifier+InheritedWidget

    在前面我们提到可以通过状态提升的方法,实现跨组件之间的数据传递。但是一些简单的小项目都会有很多很多的组件,组件之间的继承关系会很复杂,如果只是使用状态提升,然后通过传参和回调函数这样的方式,那么你会发现,在继承关系中间的一些组件需要传入大量的参数,相当麻烦!那怎么让底层的子组件直接去访问被我们提升到父级的状态呢?Flutter已经帮我们解决了这个问题,就是使用InheritedWidget,它可以高效快捷的实现共享数据的跨组件传递。一些初学者可能会觉得InheritedWidget很陌生,但是,你一定使用过InheritedWidget传递状态的场景:

    Theme.of(context).primaryColor //获取主题色
    MediaQuery.of(context).size.width;  // 获取屏幕宽度
    
    • 1
    • 2

    点击他们的实现源码就可以看到使用了context.dependOnInheritedWidgetOfExactType。使用inheritedWidget的时候,会有一个取数据的过程,这个时候就会通过子节点的BuildContext使用context.getElementForInheritedWidgetOfExactTypecontext.dependOnInheritedWidgetOfExactType来获取到这个widget(element),从而获取到数据。

    InheritedWidget通常用于子组件共享父组件中的数据,但它不具备修改更新父组件中数据的能力。它解决了访问状态和根据状态更新的问题,但是没有改变状态的能力,所以通常会把InheritedWidgetChangeNotifier结合一起使用,通过ChangeNotifier去跟踪变化的数据。

    class CounterModel extends ChangeNotifier {
      int _counter = 0;
    
      int get counter => _counter;
    
      void increment() {
        _counter++;
        notifyListeners(); // Notify listeners when the state changes
      }
    }
    
    class CounterProvider extends InheritedWidget {
      final CounterModel counterModel;
      final Widget child;
    
      CounterProvider({required this.counterModel, required this.child})
          : super(child: child);
    
      static CounterProvider? of(BuildContext context) {
        return context.dependOnInheritedWidgetOfExactType();
      }
    
      @override
      bool updateShouldNotify(CounterProvider oldWidget) {
        return counterModel != oldWidget.counterModel;
      }
    }
    
    class MyApp extends StatelessWidget {
      final count = CounterModel();
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          home: Scaffold(
            appBar: AppBar(title: Text('ChangeNotifier + InheritedWidget')),
            body: CounterProvider(
                counterModel: count,
                child: Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    CounterDisplay(),
                    CounterControl(),
                  ],
                )),
          ),
        );
      }
    }
    
    class CounterDisplay extends StatefulWidget {
      @override
      _CounterDisplayState createState() => _CounterDisplayState();
    }
    
    class _CounterDisplayState extends State {
      late CounterModel counterModel;
      late VoidCallback listener;
    
      @override
      void didChangeDependencies() {
        super.didChangeDependencies();
        counterModel = CounterProvider.of(context)!.counterModel;
        listener = () {
          if (mounted) setState(() {});
        };
        counterModel.addListener(listener);
      }
    
      @override
      void dispose() {
        counterModel.removeListener(listener);
        super.dispose();
      }
    
      @override
      Widget build(BuildContext context) {
        return Center(
          child: Text('Counter Value: ${counterModel.counter}'),
        );
      }
    }
    
    class CounterControl extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        final counterModel = CounterProvider.of(context)!.counterModel;
        return Center(
          child: GestureDetector(
            onTap: () {
              counterModel.increment();
            },
            child: Text('+'),
          ),
        );
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97

    通过这样一个简单的例子来体验InheritedWidget结合ChangeNotifier实现状态管理的功能。不过InheritedWidget也存在缺点,InheritedWidget的通知机制是基于它的数据发生变化时触发,而不是针对特定的子小部件,无法定向通知,这可能导致不必要的刷新,尤其是在大型小部件树中。它会触发整个子小部件树的重建,即使只有一部分子小部件受到了影响,这也有可能导致性能问题,尤其是在需要精细控制刷新的情况下。当然,在后续的文章中,我们也会详细分析InheritedWidget的实现机制,来帮助更好的理解整套流程。同时,考虑到这些缺点,在后续的文章也会分析更好的状态管理解决方案。

    4.Notification

    Notification是一种用于在小部件树中传递信息的机制,它可以用于实现子树中的特定部分之间的通信。Notification并不像状态管理或全局状态传递那样普遍,它主要用于特定场景下的通信,比如当某个事件发生时,需要在小部件树的各个部分之间传递消息。Notification的工作方式是通过Notification对象在小部件树中传递,然后从父级小部件开始逐级向上冒泡,直到找到一个处理该通知的小部件为止。每个处理通知的小部件可以根据需要执行特定的操作。你可以把InheritedWidget 理解为从上到下传递、共享的方式,而Notification则是从下往上。Notification它提供了dispatch方法,沿着context对应的Element节点向上逐层发送通知。

    class MyNotification extends Notification {
      final String message;
    
      MyNotification(this.message);
    }
    
    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          home: Scaffold(
            appBar: AppBar(title: Text('Notification')),
            body: Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  NotificationListener(
                    onNotification: (notification) {
                      print(notification.message);
                      return true;
                    },
                    child: ChildWidget(),
                  ),
                ],
              ),
            ),
          ),
        );
      }
    }
    
    class ChildWidget extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return ElevatedButton(
          onPressed: () {
            MyNotification('Hello 这里是子Widget!').dispatch(context);
          },
          child: Text('ChildWidget'),
        );
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    当点击按钮后,就会向上传递消息,被NotificationListener所监听,在控制台打印出消息。你可以尝试InheritedWidget + Notification来控制状态之间的访问。

    5.Stream

    Stream是一种用于在应用程序中管理状态和数据流的重要工具。Stream是异步数据流的抽象表示,它可以在应用程序中传递和监听数据的变化。但是它和Flutter关系并不大,它是通过纯dart去实现的。你可以理解为flutter只是通过StreamBuilder去构建了一个Stream通道。它的使用其实也并没有复杂太多,通常只需要创建StreamController,然后去监听控制器(可以直接去监听StreamController,然后通过setState更新UI,也可以通过StreamBuilder),最后将更新后的数据通过Stream的sink属性添加到Stream中即可。知名的状态管理库Bloc,就是基于Stream的封装。

    class Todo {
      final String text;
      bool isCompleted;
    
      Todo(this.text, this.isCompleted);
    }
    
    class _TodoAppState extends State {
      final _controller = StreamController>();
      List _todos = [];
    
      @override
      void initState() {
        super.initState();
        // 初始化Stream,将空列表添加到Stream中
        _controller.sink.add(_todos);
      }
    
      @override
      void dispose() {
        _controller.close(); // 关闭StreamController以释放资源
        super.dispose();
      }
    
      void _addTodo(String text) {
        // 添加新的Todo项并将更新后的列表添加到Stream中
        final newTodo = Todo(text, false);
        _todos.add(newTodo);
        _controller.sink.add(_todos);
      }
    
      void _toggleTodoCompletion(int index) {
        // 切换指定Todo项的完成状态并将更新后的列表添加到Stream中
        _todos[index].isCompleted = !_todos[index].isCompleted;
        _controller.sink.add(_todos);
      }
    
      void _deleteTodo(int index) {
        // 删除指定Todo项并将更新后的列表添加到Stream中
        _todos.removeAt(index);
        _controller.sink.add(_todos);
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text('备忘录 —— Stream'),
          ),
          body: Column(
            children: [
              Padding(
                padding: EdgeInsets.all(16.0),
                child: TextField(
                  onSubmitted: (text) {
                    if (text.isNotEmpty) {
                      _addTodo(text);
                    }
                  },
                  decoration: InputDecoration(
                    labelText: '添加任务',
                    border: OutlineInputBorder(),
                  ),
                ),
              ),
              Expanded(
                child: StreamBuilder>(
                  stream: _controller.stream,
                  builder: (context, snapshot) {
                    if (snapshot.hasData) {
                      return ListView.builder(
                        itemCount: snapshot.data!.length,
                        itemBuilder: (context, index) {
                          final todo = snapshot.data![index];
                          return ListTile(
                            title: Text(todo.text),
                            trailing: Checkbox(
                              value: todo.isCompleted,
                              onChanged: (_) => _toggleTodoCompletion(index),
                            ),
                            onLongPress: () => _deleteTodo(index),
                          );
                        },
                      );
                    } else {
                      return Center(child: CircularProgressIndicator());
                    }
                  },
                ),
              ),
            ],
          ),
        );
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95

    Stream的缺点也同样是它的优点:

    • 缺点:需要对其定制化,才能满足更复杂的场景。
    • 优点:可以针对业务进行定制化,足够灵活,可以基于它做一个好的设计。

    总结

    看完这篇文章后,相信您对Flutter中的状态应该有了一个更深刻的记忆。对Flutter自带的状态管理的方式也有了一定的了解,那么在后续的文章中,我们将进一步深入学习ChangeNotifier+InheritedWidget的一些机制,以及主流的状态管理框架。在文章的最后,我想引用一下flutter.cn的一句话:状态管理是一个相当复杂的话题。如果您在浏览后发现一些问题并未得到解答,或者并不适用于您的具体需求场景,自信些,您的实现就是对的。

    参考

    状态 (State) 管理参考 —— flutter.cn

    Flutter 工程化框架选择 — 状态管理何去何从 —— 恋猫de小郭

    Flutter 对状态管理的认知与思考 —— 小呆呆666

    关于我

    Hello,我是Taxze,如果您觉得文章对您有价值,希望您能给我的文章点个❤️,有问题需要联系我的话:我在这里 。如果您觉得文章还差了那么点东西,也请通过关注督促我写出更好的文章~万一哪天我进步了呢?😝

  • 相关阅读:
    Python——LeetCode刷题——【36. 有效的数独】(字节二面)
    Gartner发布中国人工智能软件市场指南,激烈竞争下走向差异化
    使用 NVIDIA NIM 部署生成式 AI 的简单指南
    const和volatile和restict
    基于Docker搭建Redis集群并进行扩容、缩容教程
    CentOS7 二进制安装 zabbix_agent 5.0
    web3调研:Iron fish调研
    Websocket搭建(Vue+Springboot)
    3.3 使用广播信道的数据链路层
    SpringCloud-5-基础工程-创建服务服务消费者
  • 原文地址:https://blog.csdn.net/txaz6/article/details/132655165