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

157 阅读3分钟

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["任意线程异常<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.onErrorrunZonedGuarded
捕获范围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,
  });
}

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