阅读 422

Dart语言async函数不正确的异常处理引发的异常

本文介绍了当你使用了Dart异步函数并进行try-catch异常处理的时候,很容易陷入的一个坑。并且它可能有助于分析并解决一些跟异步调用有关的Dart崩溃问题。

小测试

这样写platform channel调用并处理异常的代码,有啥问题?

const MethodChannel channel = MethodChannel('my_platform_channel');
try {
  channel.invokeMethod('my_method');
} catch (e) {
  debugPrint('Caught an exception: $e'); 
}
复制代码

结果往下看:

代码这样直接用try-catch是无效的,不会走到catch块里!且日志里直接显示Unhandled Exception。

因为channel.invokeMethod是一个异步函数,它的返回值是Future:

// 省略巨长的官方函数文档

Future<T?> invokeMethod<T>(String method, [ dynamic arguments ]) {
  return _invokeMethod<T>(method, missingOk: false, arguments: arguments);

}
复制代码

更多的分析和正确的写法,请看下文。

最佳实践

  1. 如果使用了await调用了异步函数(返回Future的函数)并且需要做异常处理,在await外面直接try-catch即可
  2. 如果没有使用await调用了异步函数(返回Future的函数)并且需要做异常处理,千万不要直接try-catch(否则起不到捕获作用,代码会继续误导下一个人),需要使用onError或者catchError做对应的处理或者re-throw。

背景

先说下这个问题的背景是,调查了一个webview_flutter和一个settings插件的platform调用异常问题,两个异常很类似:

有如下异常:

MissingPluginException(No implementation found for method evaluateJavascript on channel plugins.flutter.io/webview_0)
#0      MethodChannel._invokeMethod (package:flutter/src/services/platform_channel.dart:253)
<asynchronous suspension>
// 然后就没有然后了,没有调用者的行号
复制代码

这应该是一个类似于上面提到的 channel.invokeMethod('evaluateJavascript') 的调用。在查看webview plugin部分出现的invokeMethod的dart代码后,看到代码是这样写的:

// js_bridge.dart
            """ // 省略一些js代码
            }
            return result;
        })($s)""";
  try {
    _webViewController.evaluateJavascript(js);
  } catch(e) {
  }
}
复制代码

evaluateJavascript里面的实现是这样

@override
Future<String> evaluateJavascript(String javascriptString) {
  return _channel.invokeMethod<String>(
      'evaluateJavascript', javascriptString);
}
复制代码

第一反应,🤔 try-catch 为什么没有catch住异常呢?

做了5个测试

看下面的5种情况的异常处理的测试代码,前提是:getPlatformVersion故意弄成了一个没有注册过的channel method的名字,一定会抛出MissingPluginException异常。

那么猜想5种写法分别会得到什么结果?

class FlutterPluginDemo {
  static const MethodChannel _channel =
      const MethodChannel('flutter_plugin_demo');

  static Future<String> get platformVersion async {
    var future;
    // 第1种错误处理: 错误
    try {
      future = _channel.invokeMethod('getPlatformVersion');
      print('1. future.type: ${future.runtimeType}'); // invoke的返回值是个Future
    } catch (e) {
      // 所以这里基本永远不会走到
      print('1. caught a exception: $e');
    }

    // 第2种错误处理: 正确
    try {
      var result = await future;
      print('2. result.type: ${result.runtimeType}');
    } catch (e) {
      print('2. caught a exception by try-catch: $e'); // 这里可以正确catch住上面第1个future的异常
    }

    // 第3种错误处理: 正确
    _channel.invokeMethod('getPlatformVersion').onError(
        (error, stackTrace) => print('3. caught a exception by onError: $error'));

    // 第4种:不处理,也不用await,会抛出异常,但是<asynchronous suspension>找不到调用来源代码的行号信息
    _channel.invokeMethod('getPlatformVersion');

    // 第5种: 默认不处理异常,但是用await,并且<asynchronous suspension>可以找到来源行号信息
    await _channel.invokeMethod('getPlatformVersion');
    return "test";
  }
}
复制代码

结果是(看图):

img

也就是:

  • 第1种,是错误的,catch块永远不会调用到,且future变量的type是Future<dynamic>
  • 第2种,是正确的,拿到future对象并对它做了await操作,此时catch可以正常捕获
  • 第3种,是正确的,onError是Future的一个方法,可以在内部抛出异常的时候,转成异常处理的lambda函数,并且此时这个异常从unhandled转换成handled状态
  • 第4种,是不推荐的,不用await且不做异常处理,直接在日志中抛出如下异常:
com.eggfly.flutter_plugin_demo_example E/flutter: [ERROR:flutter/lib/ui/ui_dart_state.cc(186)] Unhandled Exception: MissingPluginException(No implementation found for method getPlatformVersion on channel flutter_plugin_demo)
    #0      MethodChannel._invokeMethod (package:flutter/src/services/platform_channel.dart:156:7)
    <asynchronous suspension>
复制代码

并且注意到,这种异常的里面,是没有调用来源信息的(没有调用地方的行号,只有platform_channel.dart的行号)

img

  • 第5种,是不推荐的,使用了await但是没有onError或者try-catch做异常处理,遇到异常可能日志会出现如下异常信息:
com.eggfly.flutter_plugin_demo_example E/flutter: [ERROR:flutter/lib/ui/ui_dart_state.cc(186)] Unhandled Exception: MissingPluginException(No implementation found for method getPlatformVersion on channel flutter_plugin_demo)

    #0      MethodChannel._invokeMethod (package:flutter/src/services/platform_channel.dart:156:7)

    <asynchronous suspension>

    #1      FlutterPluginDemo.platformVersion (package:flutter_plugin_demo/flutter_plugin_demo.dart:35:5)

    <asynchronous suspension>

    #2      _MyAppState.initPlatformState (package:flutter_plugin_demo_example/main.dart:30:25)

    <asynchronous suspension>
复制代码

img

注意到这种使用了await的情况下,里面是有嵌套的异步调用信息的。这里对于解决Dart异常会有很大帮助。

解决方法

  • webview_flutter的Dart异常可以改成:
// try catch cannot catch exceptions using Future
_webViewController.evaluateJavascript(js).catchError((e) {});
复制代码

注:在evaluateJavascript的这里的场景下catchError暂时不做其他处理;

在其他场景下,可以根据具体exception的类型做区分,并相应处理或者re-throw出来。

参考 & read more:

文章分类
前端
文章标签