Dio:

前言

热知识:Dio是国人开发的网络请求库,所以中文文档很完善。

又:目前最新版本为dio: ^4.0.5-beta1,在其github的develop分支,不过并不建议使用。以下基于master的dio: ^4.0.3 / dio: ^4.0.4大致看了一下改变并不大。基本直接搬运github文档,免得有同学上不去github。

总而言之跟okhttp挺像的。

添加依赖

1
2
dependencies:
dio: ^4.0.3

如果你是dio 3.x 用户,想了解4.0的变更,请参考 4.x更新列表!

一个极简的示例

1
2
3
4
5
6
7
8
9
import 'package:dio/dio.dart';
void getHttp() async {
try {
var response = await Dio().get('http://www.google.com');
print(response);
} catch (e) {
print(e);
}
}

*示例

发起一个 GET 请求 :

1
2
3
4
5
6
7
Response response;
var dio = Dio();
response = await dio.get('/test?id=12&name=wendu');
print(response.data.toString());
// Optionally the request above could also be done as
response = await dio.get('/test', queryParameters: {'id': 12, 'name': 'wendu'});
print(response.data.toString());

发起一个 POST 请求:

1
response = await dio.post('/test', data: {'id': 12, 'name': 'wendu'});

发起多个并发请求:

1
response = await Future.wait([dio.post('/info'), dio.get('/token')]);

下载文件:

1
response = await dio.download('https://www.google.com/', './xx.html');

以流的方式接收响应数据:

1
2
3
4
5
Response<ResponseBody> rs;
rs = await Dio().get<ResponseBody>(url,
options: Options(responseType: ResponseType.stream), //设置接收类型为stream
);
print(rs.data.stream); //响应流

以二进制数组的方式接收响应数据:

1
2
3
4
5
Response<List<int>> rs 
rs = await Dio().get<List<int>>(url,
options: Options(responseType: ResponseType.bytes), //设置接收类型为二进制数组
);
print(rs.data); // 二进制数组

发送 FormData:

1
2
3
4
5
var formData = FormData.fromMap({
'name': 'wendux',
'age': 25,
});
var response = await dio.post('/info', data: formData);

通过FormData上传多个文件:

1
2
3
4
5
6
7
8
9
10
var formData = FormData.fromMap({
'name': 'wendux',
'age': 25,
'file': await MultipartFile.fromFile('./text.txt', filename: 'upload.txt'),
'files': [
await MultipartFile.fromFile('./text1.txt', filename: 'text1.txt'),
await MultipartFile.fromFile('./text2.txt', filename: 'text2.txt'),
]
});
var response = await dio.post('/info', data: formData);

监听发送(上传)数据进度:

1
2
3
4
5
6
7
response = await dio.post(
'http://www.dtworkroom.com/doris/1/2.0.0/test',
data: {'aa': 'bb' * 22},
onSendProgress: (int sent, int total) {
print('$sent $total');
},
);

以流的形式提交二进制数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
List<int> postData = <int>[...];
await dio.post(
url,
data: Stream.fromIterable(postData.map((e) => [e])), //创建一个Stream<List<int>>
options: Options(
headers: {
Headers.contentLengthHeader: postData.length, // 设置content-length
},
),
);

// 二进制数据
List<int> postData = <int>[...];

await dio.post(
url,
data: Stream.fromIterable(postData.map((e) => [e])), //创建一个Stream<List<int>>
options: Options(
headers: {
Headers.contentLengthHeader: postData.length, // 设置content-length
},
),
);

注意:如果要监听提交进度,则必须设置content-length,否则是可选的。

示例目录

你可以在这里查看dio的全部示例.

Dio APIs

创建一个Dio实例,并配置它

建议在项目中使用Dio单例,这样便可对同一个dio实例发起的所有请求进行一些统一的配置,比如设置公共header、请求基地址、超时时间等;这里有一个在Flutter工程中使用Dio单例(定义为top level变量)的示例供开发者参考。

你可以使用默认配置或传递一个可选 BaseOptions参数来创建一个Dio实例 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var dio = Dio(); // with default Options

// Set default configs
dio.options.baseUrl = 'https://www.xx.com/api';
dio.options.connectTimeout = 5000; //5s
dio.options.receiveTimeout = 3000;

// or new Dio with a BaseOptions instance.
var options = BaseOptions(
baseUrl: 'https://www.xx.com/api',
connectTimeout: 5000,
receiveTimeout: 3000,
);
Dio dio = Dio(options);

Dio实例的核心API是 :

Future request(String path, {data,Map queryParameters, Options options,CancelToken cancelToken, ProgressCallback onSendProgress, ProgressCallback onReceiveProgress)

1
2
3
4
5
response = await dio.request(
'/test',
data: {'id':12,'name':'xx'},
options: Options(method:'GET'),
);

请求方法别名

为了方便使用,Dio提供了一些其它的Restful API, 这些API都是request的别名。

Future get(…)

Future post(…)

Future put(…)

Future delete(…)

Future head(…)

Future put(…)

Future path(…)

Future download(…)

Future fetch(RequestOptions)

*请求配置

BaseOptions描述的是Dio实例发起网络请求的的公共配置,而Options类描述了每一个Http请求的配置信息,每一次请求都可以单独配置,单次请求的Options中的配置信息可以覆盖BaseOptions中的配置,下面是BaseOptions的配置项:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
{
/// Http method.
String method;

/// 请求基地址,可以包含子路径,如: "https://www.google.com/api/".
String baseUrl;

/// Http请求头.
Map<String, dynamic> headers;

/// 连接服务器超时时间,单位是毫秒.
int connectTimeout;
/// 2.x中为接收数据的最长时限.
int receiveTimeout;

/// 请求路径,如果 `path` 以 "http(s)"开始, 则 `baseURL` 会被忽略; 否则,
/// 将会和baseUrl拼接出完整的的url.
String path = "";

/// 请求的Content-Type,默认值是"application/json; charset=utf-8".
/// 如果您想以"application/x-www-form-urlencoded"格式编码请求数据,
/// 可以设置此选项为 `Headers.formUrlEncodedContentType`, 这样[Dio]
/// 就会自动编码请求体.
String contentType;

/// [responseType] 表示期望以那种格式(方式)接受响应数据。
/// 目前 [ResponseType] 接受三种类型 `JSON`, `STREAM`, `PLAIN`.
///
/// 默认值是 `JSON`, 当响应头中content-type为"application/json"时,dio 会自动将响应内容转化为json对象。
/// 如果想以二进制方式接受响应数据,如下载一个二进制文件,那么可以使用 `STREAM`.
///
/// 如果想以文本(字符串)格式接收响应数据,请使用 `PLAIN`.
ResponseType responseType;

/// `validateStatus` 决定http响应状态码是否被dio视为请求成功, 返回`validateStatus`
/// 返回`true` , 请求结果就会按成功处理,否则会按失败处理.
ValidateStatus validateStatus;

/// 用户自定义字段,可以在 [Interceptor]、[Transformer] 和 [Response] 中取到.
Map<String, dynamic> extra;

/// Common query parameters
Map<String, dynamic /*String|Iterable<String>*/ > queryParameters;

/// 请求数据中数组的编码的方式,具体可以参考CollectionFormat的定义
late CollectionFormat collectionFormat;
}

这里有一个完成的示例.

*响应数据

当请求成功时会返回一个Response对象,它包含如下字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
/// 响应数据,可能已经被转换了类型, 详情请参考Options中的[ResponseType].
T data;
/// 响应头
Headers headers;
/// 本次请求信息
Options request;
/// Http status code.
int? statusCode;
String? statusMessage;
/// 是否重定向(Flutter Web不可用)
bool? isRedirect;
/// 重定向信息(Flutter Web不可用)
List<RedirectInfo> redirects ;
/// 真正请求的url(重定向最终的uri)
Uri realUri;
/// 响应对象的自定义字段(可以在拦截器中设置它),调用方可以在`then`中获取.
Map<String, dynamic> extra;
}

示例如下:

1
2
3
4
5
Response response = await dio.get('https://www.google.com');
print(response.data);
print(response.headers);
print(response.requestOptions);
print(response.statusCode);

拦截器

每个 Dio 实例都可以添加任意多个拦截器,他们组成一个队列,拦截器队列的执行顺序是FIFO。通过拦截器你可以在请求之前、响应之后和发生异常时(但还没有被 thencatchError处理)做一些统一的预处理操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
dio.interceptors.add(InterceptorsWrapper(
onRequest:(options, handler){
// Do something before request is sent
return handler.next(options); //continue
// 如果你想完成请求并返回一些自定义数据,你可以resolve一个Response对象 `handler.resolve(response)`。
// 这样请求将会被终止,上层then会被调用,then中返回的数据将是你的自定义response.
//
// 如果你想终止请求并触发一个错误,你可以返回一个`DioError`对象,如`handler.reject(error)`,
// 这样请求将被中止并触发异常,上层catchError会被调用。
},
onResponse:(response,handler) {
// Do something with response data
return handler.next(response); // continue
// 如果你想终止请求并触发一个错误,你可以 reject 一个`DioError`对象,如`handler.reject(error)`,
// 这样请求将被中止并触发异常,上层catchError会被调用。
},
onError: (DioError e, handler) {
// Do something with response error
return handler.next(e);//continue
// 如果你想完成请求并返回一些自定义数据,可以resolve 一个`Response`,如`handler.resolve(response)`。
// 这样请求将会被终止,上层then会被调用,then中返回的数据将是你的自定义response.
}
));

一个简单的自定义拦截器示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import 'package:dio/dio.dart';
class CustomInterceptors extends Interceptor {
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
print('REQUEST[${options.method}] => PATH: ${options.path}');
return super.onRequest(options, handler);
}
@override
Future onResponse(Response response, ResponseInterceptorHandler handler) {
print('RESPONSE[${response.statusCode}] => PATH: ${response.requestOptions?.path}');
return super.onResponse(response, handler);
}
@override
Future onError(DioError err, ErrorInterceptorHandler handler) {
print('ERROR[${err.response?.statusCode}] => PATH: ${err.requestOptions?.path}');
return super.onError(err, handler);
}
}

完成和终止请求/响应

在所有拦截器中,你都可以改变请求执行流, 如果你想完成请求/响应并返回自定义数据,你可以resolve一个 Response 对象或返回 handler.resolve(data)的结果。 如果你想终止(触发一个错误,上层catchError会被调用)一个请求/响应,那么可以reject一个DioError 对象或返回 handler.reject(errMsg) 的结果.

1
2
3
4
5
6
7
dio.interceptors.add(InterceptorsWrapper(
onRequest:(options, handler) {
return handler.resolve(Response(requestOptions:options,data:'fake data'));
},
));
Response response = await dio.get('/test');
print(response.data);//'fake data'

QueuedInterceptor

如果同时发起多个网络请求,则它们是可以同时进入Interceptor 的(并行的),而 QueuedInterceptor 提供了一种串行机制:它可以保证请求进入拦截器时是串行的(前面的执行完后后面的才会进入拦截器)。

例子

假设这么一个场景:出于安全原因,我们需要给所有的请求头中添加一个csrfToken,如果csrfToken不存在,我们先去请求csrfToken,获取到csrfToken后再重试。假设刚开始的时候 csrfToken 为 null ,如果允许请求并发,则这些并发请求并行进入拦截器时 csrfToken 都为null,所以它们都需要去请求 csrfToken,这会导致 csrfToken 被请求多次,为了避免不必要的重复请求,可以使用 QueuedInterceptor,这样只需要第一个请求请求一次即可,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
dio.interceptors.add(QueuedInterceptorsWrapper(
onRequest: (options, handler) async {
print('send request:path:${options.path},baseURL:${options.baseUrl}');
if (csrfToken == null) {
print('no token,request token firstly...');
tokenDio.get('/token').then((d) {
options.headers['csrfToken'] = csrfToken = d.data['data']['token'];
print('request token succeed, value: ' + d.data['data']['token']);
print( 'continue to perform request:path:${options.path},baseURL:${options.path}');
handler.next(options);
}).catchError((error, stackTrace) {
handler.reject(error, true);
});
} else {
options.headers['csrfToken'] = csrfToken;
handler.next(options);
}
}
));

完整的示例代码请点击 这里.

日志

我们可以添加 LogInterceptor 拦截器来自动打印请求、响应日志, 如:

1
dio.interceptors.add(LogInterceptor(responseBody: false)); //开启请求日志

由于拦截器队列的执行顺序是FIFO,如果把log拦截器添加到了最前面,则后面拦截器对options的更改就不会被打印(但依然会生效), 所以建议把log拦截添加到队尾。

Cookie管理

dio_cookie_manager 包是Dio的一个插件,它提供了一个Cookie管理器。详细示例可以移步dio_cookie_manager

自定义拦截器

开发者可以通过继承Interceptor/QueuedInterceptor 类来实现自定义拦截器,这是一个简单的缓存示例拦截器

*错误处理

当请求过程中发生错误时, Dio 会包装 Error/Exception 为一个 DioError:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
try {
//404
await dio.get('https://wendux.github.io/xsddddd');
} on DioError catch (e) {
// The request was made and the server responded with a status code
// that falls out of the range of 2xx and is also not 304.
if (e.response) {
print(e.response.data)
print(e.response.headers)
print(e.response.requestOptions)
} else {
// Something happened in setting up or sending the request that triggered an Error
print(e.requestOptions)
print(e.message)
}
}

DioError 字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 {
/// Request info.
RequestOptions request;

/// Response info, it may be `null` if the request can't reach to
/// the http server, for example, occurring a dns error, network is not available.
Response response;

/// 错误类型,见下文
DioErrorType type;

///原始的error或exception对象,通常type为DEFAULT时存在。
dynamic error;
}

DioErrorType

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
enum DioErrorType {
/// It occurs when url is opened timeout.
connectTimeout,

/// It occurs when url is sent timeout.
sendTimeout,

///It occurs when receiving timeout.
receiveTimeout,

/// When the server response, but with a incorrect status, such as 404, 503...
response,

/// When the request is cancelled, dio will throw a error with this type.
cancel,

/// Default error type, Some other Error. In this case, you can
/// use the DioError.error if it is not null.
other,
}

使用application/x-www-form-urlencoded编码

默认情况下, Dio 会将请求数据(除过String类型)序列化为 JSON. 如果想要以 application/x-www-form-urlencoded格式编码, 你可以显式设置contentType :

1
2
3
4
5
6
7
8
//Instance level
dio.options.contentType= Headers.formUrlEncodedContentType;
//or works once
dio.post(
'/info',
data: {'id': 5},
options: Options(contentType: Headers.formUrlEncodedContentType),
);

这里有一个示例.

FormData

Dio支持发送 FormData, 请求数据将会以 multipart/form-data方式编码, FormData中可以一个或多个包含文件 .

1
2
3
4
5
6
var formData = FormData.fromMap({
'name': 'wendux',
'age': 25,
'file': await MultipartFile.fromFile('./text.txt',filename: 'upload.txt')
});
response = await dio.post('/info', data: formData);

注意: 只有 post 方法支持发送 FormData.

这里有一个完整的示例.

多文件上传

多文件上传时,通过给key加中括号“[]”方式作为文件数组的标记,大多数后台也会通过key[]这种方式来读取。不过RFC中并没有规定多文件上传就必须得加“[]”,所以有时不带“[]”也是可以的,关键在于后台和客户端得一致。v3.0.0 以后通过Formdata.fromMap()创建的Formdata,如果有文件数组,是默认会给key加上“[]”的,比如:

1
2
3
4
5
6
FormData.fromMap({
'files': [
MultipartFile.fromFileSync('./example/upload.txt', filename: 'upload.txt'),
MultipartFile.fromFileSync('./example/upload.txt', filename: 'upload.txt'),
]
});

最终编码时会key会为 “files[]”,如果不想添加“[]”,可以通过Formdata的API来构建:

1
2
3
4
5
6
7
8
9
var formData = FormData();
formData.files.addAll([
MapEntry('files',
MultipartFile.fromFileSync('./example/upload.txt',filename: 'upload.txt'),
),
MapEntry('files',
MultipartFile.fromFileSync('./example/upload.txt',filename: 'upload.txt'),
),
]);

这样构建的FormData的key是不会有“[]”。

转换器

转换器Transformer 用于对请求数据和响应数据进行编解码处理。Dio实现了一个默认转换器DefaultTransformer作为默认的 Transformer. 如果你想对请求/响应数据进行自定义编解码处理,可以提供自定义转换器,通过 dio.transformer设置。

请求转换器 Transformer.transformRequest(...) 只会被用于 ‘PUT’、 ‘POST’、 'PATCH’方法,因为只有这些方法才可以携带请求体(request body)。但是响应转换器 Transformer.transformResponse() 会被用于所有请求方法的返回数据。

Flutter中设置

如果你在开发Flutter应用,强烈建议json的解码通过compute方法在后台进行,这样可以避免在解析复杂json时导致的UI卡顿。

注意,根据笔者实际测试,发现通过compute在后台解码json耗时比直接解码慢很多,建议开发者仔细评估。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 必须是顶层函数
_parseAndDecode(String response) {
return jsonDecode(response);
}

parseJson(String text) {
return compute(_parseAndDecode, text);
}

void main() {
...
// 自定义 jsonDecodeCallback
(dio.transformer as DefaultTransformer).jsonDecodeCallback = parseJson;
runApp(MyApp());
}

其它示例

这里有一个 自定义Transformer的示例.

执行流

虽然在拦截器中也可以对数据进行预处理,但是转换器主要职责是对请求/响应数据进行编解码,之所以将转化器单独分离,一是为了和拦截器解耦,二是为了不修改原始请求数据(如果你在拦截器中修改请求数据(options.data),会覆盖原始请求数据,而在某些时候您可能需要原始请求数据). Dio的请求流是:

请求拦截器 >> 请求转换器 >> 发起请求 >> 响应转换器 >> 响应拦截器 >> 最终结果

这是一个自定义转换器的示例.

HttpClientAdapter

HttpClientAdapter是 Dio 和 HttpClient之间的桥梁。2.0抽象出adapter主要是方便切换、定制底层网络库。Dio实现了一套标准的、强大API,而HttpClient则是真正发起Http请求的对象。我们通过HttpClientAdapter将Dio和HttpClient解耦,这样一来便可以自由定制Http请求的底层实现,比如,在Flutter中我们可以通过自定义HttpClientAdapter将Http请求转发到Native中,然后再由Native统一发起请求。再比如,假如有一天OKHttp提供了dart版,你想使用OKHttp发起http请求,那么你便可以通过适配器来无缝切换到OKHttp,而不用改之前的代码。

Dio 使用DefaultHttpClientAdapter作为其默认HttpClientAdapter,DefaultHttpClientAdapter使用dart:io:HttpClient 来发起网络请求。

设置Http代理

DefaultHttpClientAdapter 提供了一个onHttpClientCreate 回调来设置底层 HttpClient的代理,我们想使用代理,可以参考下面代码:

1
2
3
4
5
6
7
8
9
10
11
12
import 'package:dio/dio.dart';
import 'package:dio/adapter.dart';
...
(dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = (client) {
// config the http client
client.findProxy = (uri) {
//proxy all request to localhost:8888
return 'PROXY localhost:8888';
};
// you can also create a HttpClient to dio
// return HttpClient();
};

完整的示例请查看这里.

Https证书校验

有两种方法可以校验https证书,假设我们的后台服务使用的是自签名证书,证书格式是PEM格式,我们将证书的内容保存在本地字符串中,那么我们的校验逻辑如下:

1
2
3
4
5
6
7
8
9
String PEM='XXXXX'; // certificate content
(dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = (client) {
client.badCertificateCallback=(X509Certificate cert, String host, int port){
if(cert.pem==PEM){ // Verify the certificate
return true;
}
return false;
};
};

X509Certificate是证书的标准格式,包含了证书除私钥外所有信息,读者可以自行查阅文档。另外,上面的示例没有校验host,是因为只要服务器返回的证书内容和本地的保存一致就已经能证明是我们的服务器了(而不是中间人),host验证通常是为了防止证书和域名不匹配。

对于自签名的证书,我们也可以将其添加到本地证书信任链中,这样证书验证时就会自动通过,而不会再走到badCertificateCallback回调中:

1
2
3
4
5
6
7
(dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate  = (client) {
SecurityContext sc = SecurityContext();
//file is the path of certificate
sc.setTrustedCertificates(file);
HttpClient httpClient = HttpClient(context: sc);
return httpClient;
};

注意,通过setTrustedCertificates()设置的证书格式必须为PEM或PKCS12,如果证书格式为PKCS12,则需将证书密码传入,这样则会在代码中暴露证书密码,所以客户端证书校验不建议使用PKCS12格式的证书。

Http2支持

dio_http2_adapter 包提供了一个支持Http/2.0的Adapter,详情可以移步 dio_http2_adapter

请求取消

你可以通过 cancel token 来取消发起的请求:

1
2
3
4
5
6
7
8
9
10
11
CancelToken token = CancelToken();
dio.get(url, cancelToken: token)
.catchError((DioError err){
if (CancelToken.isCancel(err)) {
print('Request canceled! '+ err.message)
}else{
// handle error.
}
});
// cancel the requests with "cancelled" message.
token.cancel("cancelled");

注意: 同一个cancel token 可以用于多个请求,当一个cancel token取消时,所有使用该cancel token的请求都会被取消。

完整的示例请参考取消示例.

继承 Dio class

Dio 是一个拥有factory 构造函数的接口类,因此不能直接继承 Dio ,但是可以通过 DioForNativeDioForBrowser 来间接实现:

1
2
3
4
5
6
7
8
import 'package:dio/dio.dart';
import 'package:dio/native_imp.dart'; //在浏览器中, import 'package:dio/browser_imp.dart'

class Http extends DioForNative {
Http([BaseOptions options]):super(options){
// 构造函数做一些事
}
}

我们也可以直接实现 Dio接口类 :

1
2
3
class MyDio with DioMixin implements Dio{
// ...
}

一些其他库

awesome-dio

🎉 A curated list of awesome things related to dio.

PluginsStatusDescription
dio_cookie_managerPubA cookie manager for Dio
dio_http2_adapterPubA Dio HttpClientAdapter which support Http/2.0
dio_smart_retryPubFlexible retry library for Dio
http_certificate_pinningPubHttps Certificate pinning for Flutter
curl_logger_dio_interceptorPubA Flutter curl-command generator for Dio.
dio_http_cachePubA simple cache library for Dio like Rxcache in Android
pretty_dio_loggerPubPretty Dio logger is a Dio interceptor that logs network calls in a pretty, easy to read format.

异步与同步

原理

异步

也就是说,代码执行中,某段代码的执行并不会影响后面代码的执行。

说得好像有点抽象,让我们看看异步的实现方式:

  • 多线程

    • 开启另一条线程执行一段耗时代码,这样两条线程可以并列执行,自然不会阻塞线程而影响后面代码的执行了。
    • 多线程在适量并合理地使用下,可以说真香。但是其缺点也是显而易见的:
      • 开启线程会带来额外的资源和性能消耗,在遇到大量并发时,会给服务器带来极大的压力。
      • 多个线程操作共享内存时需要加锁控制,锁竞争会降低性能和效率,复杂情况下还容易造成死锁。
  • 单线程

    • 一条执行线上,同时且只能执行一个任务(事件),其他任务都必须在后面排队等待被执行。也就是说,在一条执行线上,为了不阻碍代码的执行,每遇到的耗时任务都会被挂起放入任务队列,待执行结束后再按放入顺序依次执行队列上的任务,从而达到异步效果。

​ 上述执行线为什么不直接说是线程?因为Dart没有线程概念,只有Isolate。所以,Dart线程这个说法就是错的。当然,在此方面理解为线程,甚至理解为一个函数体也没毛病…

​ 单线程模型的优势就是避免了上述多线程的缺点,然而这种方式比较适合于往往把时间浪费在等待对方传送数据或者返回结果的耗时操作,如网络请求,IO流操作等。对于尽可能利用处理器的多核实现并行计算的计算密集型操作相对来说多线程更为合适。

Dart事件循环机制

上文说了Dart是基于事件循环机制单线程模型,那么问题来了…

为什么要采用单线程模型?

App使用过程中,多数时间处于空闲状态,并不需要进行密集或高并发的处理,计算以及UI渲染,多线程方式显然有些多余。

为什么要基于事件循环机制?

对于用户点击,滑动,硬盘IO访问等等事件,你不知道何时发生或以什么顺序发生,所以得有一个永不停歇且不能阻塞的循环来等待并处理这些“突发”事件。

Dart事件循环机制是怎样的?

Dart事件循环机制是由一个 消息循环(Event looper) 和两个消息队列:事件队列(Event queue)微任务队列(MicroTask queue) 构成。

Event Looper

Dart代码的运行是从main函数开始的,main函数执行完后,Event looper开始工作,Event looper优先全部执行完Microtask queue中的event
直到Microtask queue为空时,才会执行Event queue中的event,后者为空时才可以退出循环,这里强调“可以”而不是“一定”要退出,视场景而定。

更官方的:

执行完main()函数后将会创建一个Main Isolate

  • Event Loop会处理两个队列MicroTask queueEvent queue中的任务;
  • Event queue主要处理外部的事件任务:I/O,手势事件,定时器,isolate间的通信等;
  • MicroTask queue主要处理内部的任务:譬如处理I/O事件的中间过程中可能涉及的一些特殊处理等;
  • 两个队列都是先进先出的处理逻辑,优先处理MicroTask queue的任务,当MicroTask queue队列为空后再执行Event queue中的任务;
  • 当两个队列都为空的时候就进行GC操作,或者仅仅是在等待下个任务的到来。

为了比较好的理解 Event Loop 的异步逻辑,我们来打个比喻:就像我去某网红奶茶品牌店买杯“幽兰拿铁”(由于是现做的茶,比较耗时)的过程。

  1. 我来到前台给服务员说我要买一杯你们店的“幽兰拿铁”,然后服务员递给了我一个有编号的飞盘(获取凭证);
  2. 奶茶店的备餐员工就将我的订单放在订单列表的最后面,他们按照顺序准备订单上的商品,准备好一个就让顾客去领取(Event queue 先进先出进行处理),而我就走开了,该干啥干啥去了(异步过程,不等待处理结果);
  3. 突然他们来了个超级VIP会员的订单,备餐员工就把这个超级VIP订单放在了其他订单的最前面,优先安排了这个订单的商品(MicroTask优先处理)—此场景为虚构;
  4. 当我的订单完成后,飞盘开始震动(进行结果回调),我又再次回到了前台,如果前台妹子递给我一杯奶茶(获得结果),如果前台妹子说对不起先生,到您的订单的时候没水了,订单没法完成了给我退钱(获得异常错误错误)。

Event Queue

该队列事件来源于外部事件Future

  1. 外部事件

例如:输入/输出,手势,绘制,计时器,Stream等等;
对于外部事件,一旦没有任何microtask要执行,Event looper就会考虑将列为队列中的第一项并执行它。

  1. Future

用于自定义Event queue事件。

通过创建Future类实例来向Event queue添加事件:

1
2
3
new Future(() {
// 事件任务
});

延时5秒后添加一个事件:

1
2
3
new Future.delayed(const Duration(seconds:5), () {
// 事件任务
});

如果该任务前面有其它任务需要先执行,该任务被执行的时间会大于5秒(单线程模型的缺陷之一,不能基于时钟调度)。

这里拓展下Future的一些用法(了解下就可以了,不是本文重点):

1
2
3
4
new Future(() => doTask) // 执行异步任务
.then((result1) => doChildTask1(result1)) // doTask执行完后的子任务,result为上个任务doTask的返回值
.then((result2) => doChildTask2(result2)) // doChildTask1执行完后的子任务,result为上个任务doChildTask1的返回值
.whenComplete(() => doComplete); // 当所有任务完成后的回调函数

事件任务执行完后会立即依次执行then子任务,最后执行whenComplete函数。

Microtask Queue

  1. 上文已述,Microtask queue的优先级要高于Event queue
  2. 使用场景:想要在稍后完成一些任务(microtask)但又希望在执行下一个事件(event)之前执行。

Microtask一般用于非常短的内部异步动作,并且任务量非常少,如果微任务非常多,就会造成Event queue排不上队,会阻塞Event queue的执行(如,用户点击没有反应)。所以,大多数情况下优先考虑使用Event queue,整个Flutter源代码仅引用scheduleMicroTask()方法7次。

通过创建scheduleMicrotask函数来向Microtask queue添加任务:

1
2
3
scheduleMicrotask(() {
// 事件任务
});

Isolate

所有的Dart代码都是在isolate中运行,它就像是机器上的一个小空间,具有自己的私有内存块和一个运行着Event looper的单个线程。正如上文强调的:Dart中没有线程的概念只有isolate

isolate具有自己的内存和运行事件循环的单个执行线程

每个isolate都是相互隔离(独立)的,并不像线程那样可以共享内存,isolate本身就是隔离的意思…

许多Dart应用都在单个isolate中运行所有代码,但是如果特殊需要,您可以拥有多个。

两个isolate,每个isolate都有自己的内存和执行线程

Isolate间可以一起工作的唯一方法是通过来回传递消息。一个isolate将消息发送到另一个isolate,接收者使用其Event looper处理该消息。

如果你想了解更多,参考Flutter之isolate的使用及通信原理

关键字

异步操作常用关键字:Future,async,await,都是基于Event Loop

  • future 是 Future 类的对象,其表示一个 T 类型的异步操作结果。如果异步操作不需要结果,则 future 的类型可为 Future<void>。当一个返回 future 对象的函数被调用时,会发生两件事:

    • 将函数操作列入队列等待执行并返回一个未完成的 Future 对象。
    • 不久后当函数操作执行完成,Future 对象变为完成并携带一个值或一个错误。
  • 异步函数即在函数头中包含关键字 async 的函数。关键字 await 只能用在异步函数中

请注意异步函数是立即开始执行的(同步地),其将会在下述情况之一首次出现时暂停执行并返回一个未完成的 future 对象:

  • 函数中第一个 await 表达式出现时(在该函数从 await 表达式获取到未完成的 future 之后)。
  • 函数中任何 return 语句的出现时。
  • 函数体的结束。

使用

你可以想上面dio那样直接使用await,这是完全可以的。

1
2
3
Response response;
var dio = Dio();
response = await dio.get('/test?id=12&name=wendu');

.then().whenComplete ()

.then()

上面的请求也可以这么写:

1
2
3
4
5
Response response;
var dio = Dio();
dio.get('/test?id=12&name=wendu').then((value){
response = value;
});

then 方法的第一个参数 FutureOr<R> onValue(T value) 就是 Future 的 onValue 代表的值 , 类型是 Future 泛型类型 R ;

then 方法的第二个参数 {Function? onError} 是可选的 , 用于捕获异常的方法 ;

当然你可以在dio.get()方法前面加await,但加与不加是有区别的,对这个区别的理解程度其实就是对await关键字的理解程度。

.whenComplete()

在 Future 执行快要结束时 , 如果想要执行一些任务 , 可以在链式调用时 , 调用 Future 的 whenComplete 方法 ;

该方法类似于 try … catch … finally 中的 finally 代码块 , 是必定执行的代码 , 即使出险错误 , 也会执行该代码 ;

1
2
3
4
5
6
7
8
dio.get('/test?id=12&name=wendu').then((value) {
print(value);
}).catchError((e) {
print('catchError:');
print(e);
}).whenComplete(() {
print('whenComplete');
});

其他

Future也有其他方法,比如:

  • Future.delay
1
2
3
final future4 = Future.delayed(Duration(seconds: 1), () {
return 4;
});

延迟一定时间再执行

用的不多,需要的话可以自学。

注意事项

Async 方法

当你使用 async 关键字作为方法声明的后缀时,Dart 会将其理解为:

  • 该方法的返回值是一个 Future
  • 同步执行该方法的代码直到第一个 await 关键字,然后它暂停该方法其他部分的执行;
  • 一旦由 await 关键字引用的 Future 执行完成,下一行代码将立即执行。

为了更好地进行说明,让我们通过以下示例并尝试指出其运行的结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
void main() async {
methodA();
await methodB();
await methodC('main');
methodD();
}

methodA(){
print('A');
}

methodB() async {
print('B start');
await methodC('B');
print('B end');
}

methodC(String from) async {
print('C start from $from');
Future((){ // <== 该代码将在未来的某个时间段执行
print('C running Future from $from');
}).then((_){
print('C end of Future from $from');
});
print('C end from $from');
}

methodD(){
print('D');
}

正确的顺序是:

  1. A
  2. B start
  3. C start from B
  4. C end from B
  5. B end
  6. C start from main
  7. C end from main
  8. D
  9. C running Future from B
  10. C end of Future from B
  11. C running Future from main
  12. C end of Future from main

现在,让我们认为上述代码中的 methodC() 为对服务端的调用,这可能需要不均匀的时间来进行响应。我相信可以很明确地说,预测确切的执行流程可能变得非常困难。

如果你最初希望示例代码中仅在所有代码末尾执行 methodD() ,那么你应该按照以下方式编写代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
void main() async {
methodA();
await methodB();
await methodC('main');
methodD();
}

methodA(){
print('A');
}

methodB() async {
print('B start');
await methodC('B');
print('B end');
}

methodC(String from) async {
print('C start from $from');
await Future((){ // <== 在此处进行修改
print('C running Future from $from');
}).then((_){
print('C end of Future from $from');
});
print('C end from $from');
}

methodD(){
print('D');
}

输出序列为:

  1. A
  2. B start
  3. C start from B
  4. C running Future from B
  5. C end of Future from B
  6. C end from B
  7. B end
  8. C start from main
  9. C running Future from main
  10. C end of Future from main
  11. C end from main
  12. D

事实是通过在 methodC() 中定义 Future 的地方简单地添加 await 会改变整个行为。

其他小技巧

快速json解析为对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class BookCollection {
//索书号
String callNo;
//条码号
String barcode;
//出版日期
String publishTime;
//馆藏地
String location;
//是否可借
String status;

BookCollection.fromJson(jsonMap) {
callNo = jsonMap['callNo'] ?? '';
barcode = jsonMap['barcode'] ?? '';
publishTime = jsonMap['publishTime'] ?? '';
location = jsonMap['location'] ?? '';
status = jsonMap['status'] ?? 'false';
}
}
1
2
3
List list = jsonMap['collectionBooks'] ?? [];
List<BookCollection> collections =
list.map((element) => BookCollection.fromJson(element)).toList() ?? [];

参考:

https://github.com/flutterchina/dio/blob/master/README-ZH.md

https://github.com/flutterchina/dio/blob/develop/README-ZH.md

https://blog.csdn.net/androidbye/article/details/118059569?utm_medium=distribute.pc_relevant.none-task-blog-2~default~baidujs_baidulandingword~default-0.pc_relevant_default&spm=1001.2101.3001.4242.1&utm_relevant_index=3