2.8 Flutter异常捕获
📚 核心知识点
- Dart 单线程模型
- 消息循环机制(Event Loop)
- 微任务队列和事件队列
- Flutter 框架异常捕获
- 异步异常处理
- Zone 的使用
- 异常上报机制
💡 Dart 单线程模型
与多线程的区别
flowchart TB
subgraph "多线程模型(Java, OC)"
A1["线程1"]
A2["线程2"]
A3["线程3"]
A4["任意线程异常<br/>未捕获"]
A5["❌ 整个进程崩溃"]
A1 & A2 & A3 --> A4
A4 --> A5
end
subgraph "单线程模型(Dart, JavaScript)"
B1["Event Loop<br/>消息循环"]
B2["异常发生"]
B3["✅ 只影响当前任务<br/>程序继续运行"]
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{"微任务队列<br/>是否为空?"}
B1["执行微任务队列<br/>中的所有任务"]
C{"事件队列<br/>是否为空?"}
C1["取出一个事件任务"]
C2["执行事件任务"]
D["Event Loop 结束<br/>程序退出"]
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 |
执行优先级
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. 同步异常
特点: 代码执行过程中立即抛出
void syncError() {
throw Exception('同步异常'); // 立即抛出
}
// 捕获方式
try {
syncError();
} catch (e) {
print('捕获到: $e'); // ✅ 可以捕获
}
2. 异步异常(Future)
特点: 在 Future 中抛出
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 过程中抛出
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
String? nullString;
return Text(nullString!.length.toString()); // ❌ 抛出异常
}
}
🛡️ Flutter 框架异常捕获
FlutterError.onError
Flutter 框架会捕获 Widget 构建、布局、绘制过程中的异常。
void main() {
// 设置 Flutter 框架异常处理
FlutterError.onError = (FlutterErrorDetails details) {
// 打印错误详情
print('Flutter异常: ${details.exception}');
print('堆栈: ${details.stack}');
// 上报到服务器
reportError(details);
};
runApp(MyApp());
}
FlutterErrorDetails 包含的信息
class FlutterErrorDetails {
final dynamic exception; // 异常对象
final StackTrace? stack; // 堆栈信息
final String library; // 发生异常的库
final DiagnosticsNode? context; // 上下文信息
final InformationCollector? informationCollector; // 额外信息收集器
// ...
}
默认错误处理
// Flutter 默认的错误处理
FlutterError.onError = (FlutterErrorDetails details) {
FlutterError.dumpErrorToConsole(details); // 打印到控制台
};
🌐 Zone 异常捕获
什么是 Zone?
Zone 是 Dart 的一个执行环境,可以理解为一个代码沙箱。
功能:
- ✅ 捕获未处理的异步异常
- ✅ 拦截 print 输出
- ✅ 拦截 Timer 创建
- ✅ 存储私有数据
runZonedGuarded 基本用法
void main() {
runZonedGuarded(
() {
// 在这个 Zone 中运行的代码
runApp(MyApp());
},
(Object error, StackTrace stack) {
// 捕获未处理的异步异常
print('捕获到异常: $error');
print('堆栈: $stack');
},
);
}
Zone 配置(ZoneSpecification)
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);
},
),
);
🎯 完整的异常处理方案
最佳实践代码
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["同步异常<br/>(已 try/catch)"]
E1["FlutterError.onError<br/>捕获"]
E2["runZonedGuarded<br/>捕获"]
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(推荐)
# pubspec.yaml
dependencies:
sentry_flutter: ^7.0.0
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
# pubspec.yaml
dependencies:
firebase_crashlytics: ^3.0.0
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(腾讯)
# pubspec.yaml
dependencies:
flutter_bugly: ^0.3.0
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: 不能!
// ❌ 捕获不到异步异常
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 判断
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:
// 方法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:
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:实现基本的异常捕获 ⭐⭐
目标: 捕获并显示应用中的异常
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:实现错误日志上报 ⭐⭐⭐
目标: 将错误信息格式化并模拟上报
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,
});
}