第2章:第一个Flutter应用 —— 2.8 Flutter异常捕获

2.8 Flutter异常捕获

📚 核心知识点

  1. Dart 单线程模型
  2. 消息循环机制(Event Loop)
  3. 微任务队列和事件队列
  4. Flutter 框架异常捕获
  5. 异步异常处理
  6. Zone 的使用
  7. 异常上报机制

💡 Dart 单线程模型

与多线程的区别

flowchart TB subgraph "多线程模型(Java, OC)" A1["线程1"] A2["线程2"] A3["线程3"] A4["任意线程异常
未捕获"] A5["❌ 整个进程崩溃"] A1 & A2 & A3 --> A4 A4 --> A5 end subgraph "单线程模型(Dart, JavaScript)" B1["Event Loop
消息循环"] B2["异常发生"] B3["✅ 只影响当前任务
程序继续运行"] B1 --> B2 B2 --> B3 end style A5 fill:#FFCDD2 style B3 fill:#C8E6C9

关键区别:

  • Java/OC: 任意线程崩溃 → 整个进程终止
  • Dart/JS: 单个任务异常 → 继续处理下一个任务

🔄 Dart 消息循环机制

Event Loop 执行流程

flowchart TB Start["main() 执行"] A["启动 Event Loop"] B{"微任务队列
是否为空?"} B1["执行微任务队列
中的所有任务"] C{"事件队列
是否为空?"} C1["取出一个事件任务"] C2["执行事件任务"] D["Event Loop 结束
程序退出"] Start --> A A --> B B -->|"否"| B1 B1 --> B B -->|"是"| C C -->|"否"| C1 C1 --> C2 C2 --> B C -->|"是"| D style Start fill:#E3F2FD style B1 fill:#FFF9C4 style C2 fill:#C8E6C9 style D fill:#FFCDD2

两个任务队列

队列 优先级 用途 添加方式
Microtask Queue 微任务队列 ⭐⭐⭐ 高 需要尽快执行的任务 scheduleMicrotask()
Event Queue 事件队列 ⭐⭐ 普通 UI事件、I/O、定时器 Future, Timer

执行优先级

dart 复制代码
void main() {
  print('main start');  // 1. 同步代码立即执行
  
  // 添加事件任务
  Future(() {
    print('event 1');    // 5. 事件队列
  });
  
  // 添加微任务
  scheduleMicrotask(() {
    print('microtask 1'); // 3. 微任务优先
  });
  
  // 添加延迟事件
  Future.delayed(Duration(seconds: 1), () {
    print('event 2');     // 6. 延迟事件
  });
  
  // 添加另一个微任务
  scheduleMicrotask(() {
    print('microtask 2'); // 4. 先进先出
  });
  
  print('main end');      // 2. 同步代码立即执行
}

// 输出顺序:
// main start
// main end
// microtask 1
// microtask 2
// event 1
// event 2

🎯 Flutter 异常分类

1. 同步异常

特点: 代码执行过程中立即抛出

dart 复制代码
void syncError() {
  throw Exception('同步异常');  // 立即抛出
}

// 捕获方式
try {
  syncError();
} catch (e) {
  print('捕获到: $e');  // ✅ 可以捕获
}

2. 异步异常(Future)

特点: 在 Future 中抛出

dart 复制代码
void asyncError() {
  Future.delayed(Duration(seconds: 1), () {
    throw Exception('异步异常');
  });
}

// ❌ 错误的捕获方式
try {
  asyncError();
} catch (e) {
  print('捕获到: $e');  // ❌ 捕获不到!
}

// ✅ 正确的捕获方式1:使用 catchError
Future.delayed(Duration(seconds: 1), () {
  throw Exception('异步异常');
}).catchError((e) {
  print('捕获到: $e');  // ✅ 可以捕获
});

// ✅ 正确的捕获方式2:使用 async/await + try/catch
try {
  await Future.delayed(Duration(seconds: 1), () {
    throw Exception('异步异常');
  });
} catch (e) {
  print('捕获到: $e');  // ✅ 可以捕获
}

3. Widget 构建异常

特点: Widget build 过程中抛出

dart 复制代码
class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    String? nullString;
    return Text(nullString!.length.toString());  // ❌ 抛出异常
  }
}

🛡️ Flutter 框架异常捕获

FlutterError.onError

Flutter 框架会捕获 Widget 构建、布局、绘制过程中的异常。

dart 复制代码
void main() {
  // 设置 Flutter 框架异常处理
  FlutterError.onError = (FlutterErrorDetails details) {
    // 打印错误详情
    print('Flutter异常: ${details.exception}');
    print('堆栈: ${details.stack}');
    
    // 上报到服务器
    reportError(details);
  };
  
  runApp(MyApp());
}

FlutterErrorDetails 包含的信息

dart 复制代码
class FlutterErrorDetails {
  final dynamic exception;      // 异常对象
  final StackTrace? stack;       // 堆栈信息
  final String library;          // 发生异常的库
  final DiagnosticsNode? context; // 上下文信息
  final InformationCollector? informationCollector; // 额外信息收集器
  // ...
}

默认错误处理

dart 复制代码
// Flutter 默认的错误处理
FlutterError.onError = (FlutterErrorDetails details) {
  FlutterError.dumpErrorToConsole(details);  // 打印到控制台
};

🌐 Zone 异常捕获

什么是 Zone?

Zone 是 Dart 的一个执行环境,可以理解为一个代码沙箱

功能:

  • ✅ 捕获未处理的异步异常
  • ✅ 拦截 print 输出
  • ✅ 拦截 Timer 创建
  • ✅ 存储私有数据

runZonedGuarded 基本用法

dart 复制代码
void main() {
  runZonedGuarded(
    () {
      // 在这个 Zone 中运行的代码
      runApp(MyApp());
    },
    (Object error, StackTrace stack) {
      // 捕获未处理的异步异常
      print('捕获到异常: $error');
      print('堆栈: $stack');
    },
  );
}

Zone 配置(ZoneSpecification)

dart 复制代码
runZonedGuarded(
  () => runApp(MyApp()),
  (error, stack) {
    print('异常: $error');
  },
  zoneSpecification: ZoneSpecification(
    // 拦截 print
    print: (Zone self, ZoneDelegate parent, Zone zone, String line) {
      // 收集日志
      collectLog(line);
      // 继续输出
      parent.print(zone, '拦截: $line');
    },
    
    // 拦截 Timer 创建(可选)
    createTimer: (Zone self, ZoneDelegate parent, Zone zone,
                   Duration duration, void Function() callback) {
      print('创建了一个定时器: $duration');
      return parent.createTimer(zone, duration, callback);
    },
  ),
);

🎯 完整的异常处理方案

最佳实践代码

dart 复制代码
void main() {
  // 1. 捕获 Flutter 框架异常
  FlutterError.onError = (FlutterErrorDetails details) {
    // 开发环境:打印到控制台
    if (kDebugMode) {
      FlutterError.presentError(details);
    } else {
      // 生产环境:上报到服务器
      Zone.current.handleUncaughtError(details.exception, details.stack!);
    }
  };

  // 2. 捕获未处理的异步异常
  runZonedGuarded(
    () {
      runApp(MyApp());
    },
    (Object error, StackTrace stack) {
      // 收集错误信息
      _reportError(error, stack);
    },
    zoneSpecification: ZoneSpecification(
      // 拦截 print 输出
      print: (Zone self, ZoneDelegate parent, Zone zone, String line) {
        _collectLog(line);  // 收集日志
        parent.print(zone, line);
      },
    ),
  );
}

// 错误上报
void _reportError(Object error, StackTrace stack) {
  print('========== 错误上报 ==========');
  print('错误: $error');
  print('堆栈: $stack');
  print('时间: ${DateTime.now()}');
  print('设备: ${Platform.operatingSystem}');
  print('==============================');
  
  // 调用实际的上报 API
  // 例如:Sentry.captureException(error, stackTrace: stack);
}

// 日志收集
final List<String> _logs = [];

void _collectLog(String message) {
  _logs.add('${DateTime.now()}: $message');
  
  // 限制日志数量
  if (_logs.length > 100) {
    _logs.removeAt(0);
  }
}

📊 异常处理流程图

flowchart TB Start["应用启动"] A["设置 FlutterError.onError"] B["设置 runZonedGuarded"] C["运行应用代码"] D{"异常类型"} D1["Widget 构建异常"] D2["未捕获的异步异常"] D3["同步异常
(已 try/catch)"] E1["FlutterError.onError
捕获"] E2["runZonedGuarded
捕获"] E3["不需要处理"] F["收集上下文信息"] G["上报到服务器"] Start --> A A --> B B --> C C --> D D --> D1 D --> D2 D --> D3 D1 --> E1 D2 --> E2 D3 --> E3 E1 --> F E2 --> F F --> G style E1 fill:#FFF9C4 style E2 fill:#C8E6C9 style E3 fill:#E3F2FD style G fill:#FFCDD2

🔧 常用错误上报服务

1. Sentry(推荐)

yaml 复制代码
# pubspec.yaml
dependencies:
  sentry_flutter: ^7.0.0
dart 复制代码
import 'package:sentry_flutter/sentry_flutter.dart';

Future<void> main() async {
  await SentryFlutter.init(
    (options) {
      options.dsn = 'YOUR_DSN_HERE';
    },
    appRunner: () => runApp(MyApp()),
  );
}

2. Firebase Crashlytics

yaml 复制代码
# pubspec.yaml
dependencies:
  firebase_crashlytics: ^3.0.0
dart 复制代码
import 'package:firebase_crashlytics/firebase_crashlytics.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  
  FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterError;
  
  runZonedGuarded(
    () => runApp(MyApp()),
    FirebaseCrashlytics.instance.recordError,
  );
}

3. Bugly(腾讯)

yaml 复制代码
# pubspec.yaml
dependencies:
  flutter_bugly: ^0.3.0
dart 复制代码
import 'package:flutter_bugly/flutter_bugly.dart';

void main() {
  FlutterBugly.init(androidAppId: 'YOUR_APP_ID', iOSAppId: 'YOUR_APP_ID');
  runApp(MyApp());
}

📝 常见问题

Q1: try/catch 能捕获所有异常吗?

A: 不能!

dart 复制代码
// ❌ 捕获不到异步异常
try {
  Future.delayed(Duration(seconds: 1), () {
    throw Exception('异步异常');
  });
} catch (e) {
  print('捕获到: $e');  // ❌ 不会执行
}

// ✅ 需要使用 await
try {
  await Future.delayed(Duration(seconds: 1), () {
    throw Exception('异步异常');
  });
} catch (e) {
  print('捕获到: $e');  // ✅ 可以捕获
}

Q2: FlutterError.onError 和 runZonedGuarded 有什么区别?

A:

特性 FlutterError.onError runZonedGuarded
捕获范围 Flutter 框架内的异常 未捕获的 Dart 异常
使用场景 Widget 构建/布局/绘制 异步异常、Timer等
是否必须 推荐设置 推荐设置

两者配合使用才能完整覆盖!

Q3: 如何在开发和生产环境使用不同的处理方式?

A: 使用 kDebugMode 判断

dart 复制代码
import 'package:flutter/foundation.dart';

void main() {
  FlutterError.onError = (details) {
    if (kDebugMode) {
      // 开发环境:打印详细错误
      FlutterError.presentError(details);
    } else {
      // 生产环境:上报到服务器
      reportError(details.exception, details.stack);
    }
  };
  
  runApp(MyApp());
}

Q4: 如何测试异常处理是否生效?

A:

dart 复制代码
// 方法1:手动抛出异常
ElevatedButton(
  onPressed: () {
    throw Exception('测试异常');
  },
  child: Text('触发异常'),
)

// 方法2:访问空对象
ElevatedButton(
  onPressed: () {
    String? nullString;
    print(nullString!.length);  // 抛出空指针异常
  },
  child: Text('触发空指针异常'),
)

// 方法3:Future 异常
ElevatedButton(
  onPressed: () {
    Future.delayed(Duration(seconds: 1), () {
      throw Exception('异步异常');
    });
  },
  child: Text('触发异步异常'),
)

Q5: 异常上报时应该包含哪些信息?

A:

dart 复制代码
Map<String, dynamic> buildErrorReport(Object error, StackTrace? stack) {
  return {
    // 基本信息
    'error': error.toString(),
    'stackTrace': stack.toString(),
    'timestamp': DateTime.now().toIso8601String(),
    
    // 设备信息
    'platform': Platform.operatingSystem,
    'version': Platform.version,
    
    // 应用信息
    'appVersion': '1.0.0',  // 从配置读取
    'buildNumber': '100',
    
    // 用户信息(注意隐私)
    'userId': getCurrentUserId(),
    
    // 额外上下文
    'logs': recentLogs,  // 最近的日志
    'route': currentRoute,  // 当前路由
  };
}

🎓 跟着做练习

练习1:实现基本的异常捕获 ⭐⭐

目标: 捕获并显示应用中的异常

dart 复制代码
void main() {
  // 存储异常列表
  final List<String> errors = [];
  
  FlutterError.onError = (details) {
    errors.add('Flutter异常: ${details.exception}');
  };
  
  runZonedGuarded(
    () => runApp(MyApp()),
    (error, stack) {
      errors.add('Dart异常: $error');
    },
  );
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: Text('异常列表')),
        body: ListView.builder(
          itemCount: errors.length,
          itemBuilder: (context, index) {
            return ListTile(
              leading: Icon(Icons.error, color: Colors.red),
              title: Text(errors[index]),
            );
          },
        ),
      ),
    );
  }
}

练习2:实现错误日志上报 ⭐⭐⭐

目标: 将错误信息格式化并模拟上报

dart 复制代码
class ErrorReporter {
  // 错误队列
  final List<ErrorReport> _errorQueue = [];
  
  // 收集错误
  void reportError(Object error, StackTrace? stack) {
    final report = ErrorReport(
      error: error.toString(),
      stackTrace: stack.toString(),
      timestamp: DateTime.now(),
      deviceInfo: _getDeviceInfo(),
    );
    
    _errorQueue.add(report);
    
    // 达到一定数量或时间间隔后上报
    if (_errorQueue.length >= 10) {
      _uploadErrors();
    }
  }
  
  // 上传错误
  Future<void> _uploadErrors() async {
    if (_errorQueue.isEmpty) return;
    
    try {
      // 模拟 HTTP 请求
      print('上报 ${_errorQueue.length} 个错误到服务器...');
      
      for (var error in _errorQueue) {
        print('- ${error.error}');
      }
      
      // 实际项目中应该调用 API
      // await http.post('https://api.example.com/errors', body: ...);
      
      _errorQueue.clear();
      print('上报成功!');
    } catch (e) {
      print('上报失败: $e');
    }
  }
  
  Map<String, String> _getDeviceInfo() {
    return {
      'platform': Platform.operatingSystem,
      'version': Platform.version,
    };
  }
}

class ErrorReport {
  final String error;
  final String stackTrace;
  final DateTime timestamp;
  final Map<String, String> deviceInfo;
  
  ErrorReport({
    required this.error,
    required this.stackTrace,
    required this.timestamp,
    required this.deviceInfo,
  });
}

参考: 《Flutter实战·第二版》2.8节

相关推荐
程序员Ctrl喵16 小时前
异步编程:Event Loop 与 Isolate 的深层博弈
开发语言·flutter
前端不太难17 小时前
Flutter 如何设计可长期维护的模块边界?
flutter
小蜜蜂嗡嗡18 小时前
flutter列表中实现置顶动画
flutter
始持19 小时前
第十二讲 风格与主题统一
前端·flutter
始持19 小时前
第十一讲 界面导航与路由管理
flutter·vibecoding
始持19 小时前
第十三讲 异步操作与异步构建
前端·flutter
新镜19 小时前
【Flutter】 视频视频源横向、竖向问题
flutter
黄林晴20 小时前
Compose Multiplatform 1.10 发布:统一 Preview、Navigation 3、Hot Reload 三箭齐发
android·flutter
Swift社区20 小时前
Flutter 应该按功能拆,还是按技术层拆?
flutter
肠胃炎21 小时前
树形选择器组件封装
前端·flutter