Flutter之异常和错误

1,919 阅读2分钟

Flutter异常源码和一些经验总结

Flutter 异常指的是,Flutter 程序中 Dart 代码运行时意外发生的错误事件。我们可以通过与 Java 类似的 try-catch 机制来捕获它。但与 Java 不同的是,Dart 程序不强制要求我们必须处理异常。

这是因为,Dart 采用事件循环的机制来运行任务,所以各个任务的运行状态是互相独立的。也就是说,即便某个任务出现了异常我们没有捕获它,Dart 程序也不会退出,只会导致当前任务后续的代码不会被执行,用户仍可以继续使用其他功能。也因为flutter本身是单线程模型,是一种事件驱动机制而产生的,

Dart 异常,根据来源又可以细分为 App 异常和 Framework 异常。Flutter 为这两种异常提供了不同的捕获方式,接下来我们就一起看看吧。

Flutter异常类型

Flutter异常主要分为了两大类型ExceptionError 我们先来看看定义

1 Exception

abstract class Exception {
  factory Exception([var message]) => _Exception(message);
}

/** Default implementation of [Exception] which carries a message. */
class _Exception implements Exception {
  final dynamic message;

  _Exception([this.message]);

  String toString() {
    Object? message = this.message;
    if (message == null) return "Exception";
    return "Exception: $message";
  }
}

2 Error

class Error {
  Error(); // Prevent use as mixin.
  static String safeToString(Object? object) {
    if (object is num || object is bool || null == object) {
      return object.toString();
    }
    if (object is String) {
      return _stringToSafeString(object);
    }
    return _objectToString(object);
  }

  /** Convert string to a valid string literal with no control characters. */
  external static String _stringToSafeString(String string);

  external static String _objectToString(Object object);

  // 获取堆栈信息
  external StackTrace? get stackTrace;
}

异常产生

Flutter异常产生自定义异常和系统内部产生的异常,都是通过throw抛出来 比如

throw Error()
throw ('异常')
throw Exception('异常')

异常捕获

Flutter异常:基本这种写法能涵盖大部分的类型异常

Future<void> main() async {
  FlutterError.onError = (details) async {
    if (kDebugMode {
      /// 将错误输出到控制台
      FlutterError.dumpErrorToConsole(details);
    } else {
      /// 将Framework的异常转发到当前Zone的onError回调中
      Zone.current.handleUncaughtError(details.exception, details.stack);
    }
  };

  runZoned(() {
    runApp(App()); //应用首页的第一个Widget
  }, onError: (error, stackTrace) async {
    /// 所有的Flutter异常都会在此处统一捕获。有些异常可能debug下无法捕获
    final String crashMessage = error.toString() ?? '';
    final String crashStack = stackTrace.toString();
    final String _crashMessage = crashMessage.toLowerCase();
  });
}

为啥可以重写这个方法呢?答案在framework.dart中的Element中的performRebuild方法 ErrorWidget.builder是一个类的静态方法是可以覆盖的。同样的问题是FlutterError.error也是一个静态方法也是可以覆盖的。所以我们在获取日志和设置统一错误页面的时候原因就找到了根源所在。 但是FlutterError.error仅仅只是捕获由于framework.dart而产生的异常,也就是通常所说的界面异常。另外的比如数组越界,空安全异,空对象方法异常、Future中的异常等,就只能通过Zone进行捕获

因此我们可以统一去处理布局类型产生的异常,而不会出现标红(release模型下会变成灰色很丑)

ErrorWidget.builder = (_) {
    return Text('错误页面');  
};
Framework捕获的异常

我们先来看看framework.dart 抛出来的异常 一般是flutter本身框架而产生的布局页面产生的异常

@override
  void performRebuild() {
  	...
	try {
      built = build();
      /// 会抛出异常就是上面图片飚红的地方
      debugWidgetBuilderValue(widget, built);
      ...
    } catch (e, stack) {
      _debugDoingBuild = false;
      built = ErrorWidget.builder(
        ...
      );
    } finally {
    	/// _dirty标志该组件是否需要重新build
      _dirty = false;
    }
    try {
      _child = updateChild(_child, built, slot);
    } catch (e, stack) {
      built = ErrorWidget.builder(
      	   _debugReportException()
       	...
      );
    }
  }

_debugReportException里面是通过FlutterError.reportError(details)进行异常数据上报的;当然 FlutterError还有其他方法比如

/// 最多的日志条数 设定的默认是100
wrapWidth
/// 输出错误到控制台
dumpErrorToConsole
Zone捕获的异常

Zone是可以理解为是一个盒子的概念,Dart异常捕获都是我们写大dart代码产生的异常。比如说空异常,空安全,数组越界,方法调用等这种类型的异常,目前都是捕获方式有两种方式try...catchcatchError。如果异常被我们自己的代码捕获了是不会往顶层抛的

Dart异常代码捕获:

第一种方式:

test().then((_) {
    print("执行");
}).catchError((e) {
    print("能捕获异常");
});
    
    
Future test() {
    return Future(() {
        throw "error"; 
    });
}

第二种方式:
try {
    await test();
} catch (e) {
    print("捕获异常");
}
    

但是上面的方法只能捕获同步而产生的异常。不能捕获异步产生的异常。引出zone的作用就来了。 zone的作用其实就是一个隔离环境。比如如果你想拦截所有的日志加上特殊的东西就可以通过zoneSpecification ,onError用来收取为捕获的异常比如上面的异步产生的异常

runZoned(() {
  runApp(App()); //应用首页的第一个Widget
},
 zoneSpecification: ZoneSpecification(
    print: (Zone self, ZoneDelegate parent, Zone zone, String line) {
    /// 所有的日志都会在这里出现 在这里你可以自己过滤
  // parent.print(zone, "Intercepted: $line");
}),

典型异常

影响功能的异常

比如界面异常

Text(null);

debug状态下回变红,release模式下会出现灰色屏幕(体验非常不好),那怎么做呢重写ErrorWidget.builder

ErrorWidget.builder = (FlutterErrorDetails errorDetails) {
    return Text('自定义页面')
};

Matrix4.scale() 引起的异常,

这个其实是系统内部的一个bug, 你可以通过修改源码来处理(风险自控)

double sx = 0; 
double sy = 0; 
double sz = 0; 
Provider(Provider._inheritedElementOf)异常

异常信息

NoSuchMethodError: The getter 'widget' was called on null

写一个BaseViewModel基类,利用dispose方法来确保notifyListeners的正确性,让你自己的viewModel继承BaseViewModel即可

/// 解决 notifyListeners 被释放后仍然会抛异常
class BaseViewModel extends ChangeNotifier {
  bool _destroy = false;
  bool get destroy => _destroy;

  @override
  void notifyListeners() {
    if (!_destroy) {
      /// 对象销毁的时候无必要去重新发起 这个异常其实本身是InheritWidget里面抛出来的
      super.notifyListeners();
    }
  }

  @override
  void dispose() {
    super.dispose();
    _destroy = true;
  }
}


setState异常

会有比如在用户在布局未完成的时的时候触发了setState导致的可以加个判断

if(mouted) {
  setState({});
}

方法调用异常

有个安全的做法是类似的

当方法
model.funcA() 方法调用异常有两种办法

第一种:
if(model.funcA != null) {

}
第二种:
model?.funcA?.call()