第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节

相关推荐
猪哥帅过吴彦祖2 小时前
Flutter 系列教程:应用导航 - Navigator 1.0 与命名路由
android·flutter·ios
旧时光_2 小时前
第2章:第一个Flutter应用 —— 2.7 调试Flutter应用
flutter
鹏多多4 小时前
flutter图片选择库multi_image_picker_plus和image_picker的对比和使用解析
android·flutter·ios
GISer_Jing4 小时前
Flutter架构解析:从引擎层到应用层
前端·flutter·架构
lqj_本人4 小时前
Flutter与鸿蒙EventChannel事件流通信详解
flutter
lpfasd1234 小时前
Flutter持续健康发展的多维度分析
flutter
GISer_Jing4 小时前
Flutter开发全攻略:从入门到精通
android·前端·flutter
默默_david1 天前
14.5 绘制(一)绘制原理及Layer——问答
flutter