2.8 Flutter异常捕获
📚 核心知识点
- Dart 单线程模型
- 消息循环机制(Event Loop)
- 微任务队列和事件队列
- Flutter 框架异常捕获
- 异步异常处理
- Zone 的使用
- 异常上报机制
💡 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
未捕获"] 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
是否为空?"} 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
(已 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,
});
}