MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

剖析 Flutter 中异步操作的内存管理

2021-06-066.8k 阅读

异步操作基础

在 Flutter 开发中,异步操作无处不在。无论是从网络获取数据,读取本地存储,还是执行耗时的计算任务,异步操作都能确保应用的 UI 保持响应,不会因为长时间等待某个操作完成而冻结。最常见的异步操作方式是使用 asyncawait 关键字。

async 用于标记一个函数为异步函数,异步函数会返回一个 Future 对象。await 只能在 async 函数内部使用,它会暂停当前函数的执行,直到 await 后面的 Future 完成(resolved),然后返回 Future 的结果。

Future<String> fetchData() async {
  // 模拟异步操作,比如网络请求
  await Future.delayed(Duration(seconds: 2));
  return 'Data fetched successfully';
}

void main() async {
  String result = await fetchData();
  print(result);
}

在上述代码中,fetchData 是一个异步函数,它使用 await Future.delayed 模拟了一个耗时 2 秒的异步操作。main 函数也是异步的,通过 await 获取 fetchData 的结果并打印。

Future 生命周期与内存管理

  1. 创建阶段:当我们创建一个 Future 对象时,内存中会为其分配空间。Future 对象包含了一些状态信息,例如是否已完成,以及完成时的结果(如果有)。
Future<int> createFuture() {
  return Future<int>.value(42);
}

在这个例子中,Future<int>.value(42) 创建了一个已经完成的 Future,它的值为 42。此时,内存中为这个 Future 对象分配了空间来存储这些信息。

  1. 执行阶段:如果 Future 是通过异步操作创建的,比如 Future.delayed 或者网络请求,在操作执行过程中,相关的资源也会被占用。例如,Future.delayed 会占用一定的 CPU 资源来进行计时。
Future<void> delayedFuture() async {
  await Future.delayed(Duration(seconds: 3));
  print('Delayed operation completed');
}

在这个例子中,从 await Future.delayed 开始到操作完成的 3 秒内,系统资源会被占用以维持这个计时操作。

  1. 完成阶段:当 Future 完成时,它所占用的部分资源会被释放。但是,如果 Future 被其他对象持有,比如存储在一个列表或者作为类的成员变量,即使它已经完成,相关的内存可能不会立即被回收。
class FutureHolder {
  Future<int>? myFuture;

  void startFuture() {
    myFuture = Future<int>.delayed(Duration(seconds: 1), () => 10);
  }
}

void main() {
  FutureHolder holder = FutureHolder();
  holder.startFuture();
  // 这里即使 Future 完成了,由于 myFuture 持有它,内存不会立即回收
}

Stream 的内存管理

  1. Stream 基本概念Stream 用于处理异步数据流,它可以在一段时间内多次返回数据。与 Future 不同,Future 只返回一个结果,而 Stream 可以返回多个。
Stream<int> numberStream() async* {
  for (int i = 0; i < 5; i++) {
    await Future.delayed(Duration(seconds: 1));
    yield i;
  }
}

在上述代码中,numberStream 是一个异步生成器,它每秒生成一个数字,共生成 5 个数字。

  1. 订阅与资源占用:当我们订阅一个 Stream 时,内存中会为订阅者和相关的回调函数分配空间。如果订阅者没有正确取消订阅,即使 Stream 已经不再发送数据,相关的资源也不会被释放。
void main() {
  StreamSubscription<int>? subscription;
  Stream<int> stream = numberStream();
  subscription = stream.listen((data) {
    print(data);
  });

  // 一段时间后取消订阅
  Future.delayed(Duration(seconds: 6), () {
    subscription?.cancel();
  });
}

在这个例子中,我们通过 stream.listen 订阅了 numberStream。如果不调用 subscription.cancel(),即使 numberStream 已经完成发送数据,订阅者相关的资源(包括回调函数占用的内存)也不会被释放。

  1. 背压处理:在 Stream 处理中,背压是一个重要的概念。当 Stream 生成数据的速度比订阅者处理数据的速度快时,就会产生背压。如果不处理背压,可能会导致内存不断增加,因为数据会在缓冲区中堆积。
import 'dart:async';

void main() {
  Stream<int> fastStream() async* {
    for (int i = 0; i < 10000; i++) {
      yield i;
    }
  }

  StreamSubscription<int>? subscription;
  Stream<int> stream = fastStream();
  subscription = stream.listen((data) {
    // 模拟较慢的处理
    Future.delayed(Duration(milliseconds: 100), () {
      print(data);
    });
  }, onDone: () {
    subscription?.cancel();
  }, cancelOnError: true);
}

在这个例子中,fastStream 快速生成 10000 个数字,而订阅者处理每个数字需要 100 毫秒。如果不进行背压处理,数据会在缓冲区中堆积,可能导致内存溢出。可以通过 StreamTransformer 或者 StreamController 来处理背压。

异步操作中的闭包与内存管理

  1. 闭包的概念:在 Dart 中,闭包是一个函数对象,它可以访问其词法作用域之外的变量。在异步操作中,闭包经常被使用,尤其是在 FutureStream 的回调函数中。
void outerFunction() {
  int counter = 0;
  Future<void> innerFuture() async {
    counter++;
    print('Counter in inner future: $counter');
  }

  innerFuture();
}

在这个例子中,innerFuture 是一个闭包,它可以访问 outerFunction 中的 counter 变量。

  1. 闭包与内存泄漏:如果闭包持有对外部对象的引用,并且这个闭包在异步操作完成后仍然存活,可能会导致外部对象无法被垃圾回收,从而产生内存泄漏。
class BigObject {
  // 假设这是一个占用大量内存的对象
  List<int> largeData = List.generate(1000000, (index) => index);
}

void main() {
  BigObject bigObject = BigObject();
  Future<void>.delayed(Duration(seconds: 5), () {
    print('Big object data length: ${bigObject.largeData.length}');
  });
  // 这里即使 5 秒后 Future 完成,由于闭包持有 bigObject 的引用,bigObject 可能不会被回收
}

为了避免这种情况,可以在异步操作完成后手动释放对外部对象的引用,或者使用弱引用(WeakReference)。

异步任务队列与内存管理

  1. 微任务与事件循环:在 Dart 中,异步操作是基于事件循环的。事件循环处理两种类型的任务队列:微任务队列和事件队列。微任务队列具有更高的优先级,在事件循环的每次迭代中,微任务队列会被优先处理,直到队列为空,然后才处理事件队列。
void main() {
  print('Start of main');
  Future.delayed(Duration.zero).then((_) {
    print('Future in event queue');
  });
  scheduleMicrotask(() {
    print('Microtask');
  });
  print('End of main');
}

在这个例子中,输出顺序会是 “Start of main”,“Microtask”,“End of main”,“Future in event queue”。因为微任务在事件队列之前执行。

  1. 内存影响:过多的微任务可能会导致事件队列中的任务(比如 UI 更新相关的任务)被延迟处理,从而影响应用的响应性。此外,如果微任务中创建了大量的临时对象,并且这些微任务持续执行,可能会导致内存压力增大。
void main() {
  for (int i = 0; i < 10000; i++) {
    scheduleMicrotask(() {
      List<int> tempList = List.generate(1000, (index) => index);
      // 这里创建了大量临时对象,如果持续执行可能导致内存问题
    });
  }
}

最佳实践与优化

  1. 及时取消异步操作:对于 Future,如果不再需要其结果,应该及时取消相关的异步操作。对于 Stream,确保在不需要数据时及时取消订阅。
Future<void> longRunningFuture() async {
  await Future.delayed(Duration(seconds: 10));
  print('Long running future completed');
}

void main() {
  Future<void> future = longRunningFuture();
  Future.delayed(Duration(seconds: 5), () {
    future.cancel();
  });
}
  1. 使用弱引用:当异步操作中的闭包需要引用外部对象时,考虑使用弱引用,这样当外部对象不再被其他地方引用时,可以被垃圾回收。
import 'dart:weak';

class BigObject {
  List<int> largeData = List.generate(1000000, (index) => index);
}

void main() {
  BigObject bigObject = BigObject();
  WeakReference<BigObject> weakRef = WeakReference(bigObject);
  Future<void>.delayed(Duration(seconds: 5), () {
    BigObject? obj = weakRef.target;
    if (obj != null) {
      print('Big object data length: ${obj.largeData.length}');
    } else {
      print('Big object has been garbage collected');
    }
  });
  bigObject = null; // 释放对 bigObject 的强引用
}
  1. 优化异步任务队列:避免在微任务队列中执行过多的复杂操作,尽量将耗时操作放入事件队列。同时,合理控制异步任务的数量,避免内存过度占用。
void main() {
  for (int i = 0; i < 10000; i++) {
    Future.delayed(Duration.zero).then((_) {
      List<int> tempList = List.generate(1000, (index) => index);
      // 这里使用 Future 而不是 scheduleMicrotask,将任务放入事件队列
    });
  }
}
  1. 背压处理:在处理 Stream 时,确保正确处理背压。可以使用 StreamTransformer 来缓冲或丢弃数据,以避免内存堆积。
import 'dart:async';

void main() {
  Stream<int> fastStream() async* {
    for (int i = 0; i < 10000; i++) {
      yield i;
    }
  }

  StreamTransformer<int, int> bufferTransformer = StreamTransformer.fromHandlers(
    handleData: (data, sink) {
      // 简单的缓冲区处理,只保留最近的 10 个数据
      List<int> buffer = [];
      buffer.add(data);
      if (buffer.length > 10) {
        buffer.removeAt(0);
      }
      sink.add(buffer.last);
    },
  );

  StreamSubscription<int>? subscription;
  Stream<int> stream = fastStream().transform(bufferTransformer);
  subscription = stream.listen((data) {
    print(data);
  }, onDone: () {
    subscription?.cancel();
  }, cancelOnError: true);
}

通过以上对 Flutter 中异步操作内存管理的剖析,我们了解了异步操作在不同阶段的内存使用情况,以及如何通过最佳实践来优化内存使用,避免内存泄漏和性能问题。在实际开发中,需要根据具体的业务场景,合理运用这些知识,确保应用的高效运行。