Flutter 入門與實戰(三十一):從原始碼深入瞭解Dio 的 CancelToken

島上碼農發表於2021-07-18

上一篇Flutter 入門與實戰(三十):Dio之戛然而止講了 Dio 的 CancelToken 的使用,本篇來從原始碼解析 CancelToken 是如何實現取消網路請求的。相關的內容如下:

  • CanelToken 類的實現
  • CancelToken 如何取消網路請求

CancelToken 類

CalcelToken類的程式碼並不多,我們直接複製下來一個個過一遍。

import 'dart:async';
import 'dio_error.dart';
import 'options.dart';

/// You can cancel a request by using a cancel token.
/// One token can be shared with different requests.
/// when a token's [cancel] method invoked, all requests
/// with this token will be cancelled.
class CancelToken {
  CancelToken() {
    _completer = Completer<DioError>();
  }

  /// Whether is throw by [cancel]
  static bool isCancel(DioError e) {
    return e.type == DioErrorType.cancel;
  }

  /// If request have been canceled, save the cancel Error.
  DioError? _cancelError;

  /// If request have been canceled, save the cancel Error.
  DioError? get cancelError => _cancelError;

  late Completer<DioError> _completer;

  RequestOptions? requestOptions;

  /// whether cancelled
  bool get isCancelled => _cancelError != null;

  /// When cancelled, this future will be resolved.
  Future<DioError> get whenCancel => _completer.future;

  /// Cancel the request
  void cancel([dynamic reason]) {
    _cancelError = DioError(
      type: DioErrorType.cancel,
      error: reason,
      requestOptions: requestOptions ?? RequestOptions(path: ''),
    );
    _cancelError!.stackTrace = StackTrace.current;
    _completer.complete(_cancelError);
  }
}

複製程式碼

首先看註釋,我們可以瞭解到 CancelToken 的一個非常有用的地方,一個 CancelToken 可以和多個請求關聯,取消時可以同時取消多個關聯的請求。這對於我們一個頁面有多個請求時非常有用。大部分的是一些屬性:

  • _cancelError:被取消後儲存的取消錯誤資訊,對外可以通過 get 方式可以獲取。
  • _completer:一個Completer<DioError>物件,Completer 本是一個抽象類,用於管理非同步操作事件。構建時返回的是一個 Future 物件,可以呼叫對應的 complete(對應正常完成) 或completeError(對應錯誤處理)。該屬性為私有屬性,外部不可訪問。
  • requestOptionsRequestOptions物件,是請求的一些可選屬性(比如 headers,請求引數,請求方式等等),可以為空。該屬性是公共屬性,說明可以在外部修改。
  • isCancelled:布林值,用於標識是否取消,實際是通過 _cancelError是否為空判斷的,如果不為空說明是被取消了。
  • whenCancel:實際就是_completerfuture 物件,可以用來處理操作的響應,這樣也相當於對_completer做了一個封裝,只暴露了其 future 物件。
  • cancel:取消方法,也就是核心方法了,這個方法構建了一個 DioError 物件(用於儲存取消的錯誤),這裡如果呼叫時傳了 reason 物件,也會將 reason 傳遞到 error 引數中,然後就是 requestOptions 引數,如果 requestOptions 為空則構建一個空的RequestOptions物件。同時還會將當前的堆疊資訊存入到_cancelError 的 stackTrace 中,方便跟蹤堆疊資訊。最後是呼叫_completer.complete非同步方法。這個是關鍵方法,我們看一下這個方法做了什麼事情。

Completer類

我們進入 Completer 類來看一下 complete方法做了什麼事情:

/// All listeners on the future are informed about the value.
void complete([FutureOr<T>? value]);
複製程式碼

可以看到這個方法是一個抽象方法,意味著應該是由 Completer 的具體實現類來實現的。同時從註釋可以看到,這個方法是會呼叫監聽器來告知 complete 方法的 value 泛型物件。可以理解為是通知觀察者處理該物件。那我們就可以猜測是在請求的時候,如果有 cancelToken 引數時,應該是給 cancelToken 增加了一個監聽器。繼續來看 Dio 的請求程式碼的實現。

Dio 的請求程式碼

到 Dio 的原始碼 dio.dart看的時候,發現全部請求其實是fetch<T>(RequestOptionsrequestOptions)的別名,也就是實際全部的請求都是通過該方法完成的。我們看一下這個方法的原始碼。程式碼很長,如果有興趣的可以仔細閱讀一下,我們這裡只找出與 cancelToken 相關的程式碼。

@override
Future<Response<T>> fetch<T>(RequestOptions requestOptions) async {
  if (requestOptions.cancelToken != null) {
    requestOptions.cancelToken!.requestOptions = requestOptions;
  }

  if (T != dynamic &&
      !(requestOptions.responseType == ResponseType.bytes ||
          requestOptions.responseType == ResponseType.stream)) {
    if (T == String) {
      requestOptions.responseType = ResponseType.plain;
    } else {
      requestOptions.responseType = ResponseType.json;
    }
  }

  // Convert the request interceptor to a functional callback in which
  // we can handle the return value of interceptor callback.
  FutureOr Function(dynamic) _requestInterceptorWrapper(
    void Function(
      RequestOptions options,
      RequestInterceptorHandler handler,
    )
        interceptor,
  ) {
    return (dynamic _state) async {
      var state = _state as InterceptorState;
      if (state.type == InterceptorResultType.next) {
        return listenCancelForAsyncTask(
          requestOptions.cancelToken,
          Future(() {
            return checkIfNeedEnqueue(interceptors.requestLock, () {
              var requestHandler = RequestInterceptorHandler();
              interceptor(state.data, requestHandler);
              return requestHandler.future;
            });
          }),
        );
      } else {
        return state;
      }
    };
  }

  // Convert the response interceptor to a functional callback in which
  // we can handle the return value of interceptor callback.
  FutureOr<dynamic> Function(dynamic) _responseInterceptorWrapper(
      interceptor) {
    return (_state) async {
      var state = _state as InterceptorState;
      if (state.type == InterceptorResultType.next ||
          state.type == InterceptorResultType.resolveCallFollowing) {
        return listenCancelForAsyncTask(
          requestOptions.cancelToken,
          Future(() {
            return checkIfNeedEnqueue(interceptors.responseLock, () {
              var responseHandler = ResponseInterceptorHandler();
              interceptor(state.data, responseHandler);
              return responseHandler.future;
            });
          }),
        );
      } else {
        return state;
      }
    };
  }

  // Convert the error interceptor to a functional callback in which
  // we can handle the return value of interceptor callback.
  FutureOr<dynamic> Function(dynamic, StackTrace stackTrace)
      _errorInterceptorWrapper(interceptor) {
    return (err, stackTrace) {
      if (err is! InterceptorState) {
        err = InterceptorState(assureDioError(
          err,
          requestOptions,
          stackTrace,
        ));
      }

      if (err.type == InterceptorResultType.next ||
          err.type == InterceptorResultType.rejectCallFollowing) {
        return listenCancelForAsyncTask(
          requestOptions.cancelToken,
          Future(() {
            return checkIfNeedEnqueue(interceptors.errorLock, () {
              var errorHandler = ErrorInterceptorHandler();
              interceptor(err.data, errorHandler);
              return errorHandler.future;
            });
          }),
        );
      } else {
        throw err;
      }
    };
  }

  // Build a request flow in which the processors(interceptors)
  // execute in FIFO order.

  // Start the request flow
  var future = Future<dynamic>(() => InterceptorState(requestOptions));

  // Add request interceptors to request flow
  interceptors.forEach((Interceptor interceptor) {
    future = future.then(_requestInterceptorWrapper(interceptor.onRequest));
  });

  // Add dispatching callback to request flow
  future = future.then(_requestInterceptorWrapper((
    RequestOptions reqOpt,
    RequestInterceptorHandler handler,
  ) {
    requestOptions = reqOpt;
    _dispatchRequest(reqOpt).then(
      (value) => handler.resolve(value, true),
      onError: (e) {
        handler.reject(e, true);
      },
    );
  }));

  // Add response interceptors to request flow
  interceptors.forEach((Interceptor interceptor) {
    future = future.then(_responseInterceptorWrapper(interceptor.onResponse));
  });

  // Add error handlers to request flow
  interceptors.forEach((Interceptor interceptor) {
    future = future.catchError(_errorInterceptorWrapper(interceptor.onError));
  });

  // Normalize errors, we convert error to the DioError
  return future.then<Response<T>>((data) {
    return assureResponse<T>(
      data is InterceptorState ? data.data : data,
      requestOptions,
    );
  }).catchError((err, stackTrace) {
    var isState = err is InterceptorState;

    if (isState) {
      if ((err as InterceptorState).type == InterceptorResultType.resolve) {
        return assureResponse<T>(err.data, requestOptions);
      }
    }

    throw assureDioError(
      isState ? err.data : err,
      requestOptions,
      stackTrace,
    );
  });
}
複製程式碼

首先在一開始就檢查了當前請求 requestOptionscancelToken 是不是為空,如果不為空,就設定 cancelTokenrequestOptions為當前請求的requestOptions,相當於在 cancelToken 快取了所有的請求引數。

接下來是攔截器的處理,包括了請求攔截器,響應攔截器和錯誤攔截器。分別定義了一個內建的攔截器包裝方法,用於將攔截器封裝為函式式回撥,以便進行統一的攔截處理。這個我們跳過,關鍵是每個攔截器的包裝方法都有一個listenCancelForAsyncTask方法,在攔截器狀態是 next(說明還有攔截要處理)的時候,會呼叫該方法並返回其返回值。這個方法第一個引數就是 cancelToken。從方法名看就是監聽非同步任務的取消事件,看看這個方法做了什麼事情。

非同步任務取消事件監聽

listenCancelForAsyncTask方法很簡單,其實就是返回了一個 Future.any 物件,然後在這個 Future裡,如果 cancelToken 不為空的話,在響應 cancelToken 的取消事件時執行後續的處理。Future.any 的特性是將一系列的非同步函式按統一的介面組裝起來,按次序執行(上一個攔截器的處理完後輪到下一個攔截器執行),以執行 onValue (正常情況)和onError(異常情況) 方法。

這裡如果 cancelToken 不為空,就會把cancelToken的取消事件方法放到攔截器中,然後出現異常的時候會將異常丟擲。這其實相當於是前置攔截,就是說如果請求還沒處理(未加入處理佇列)的時候,直接使用攔截器攔截。而如果請求已經加入到了處理佇列,就需要在佇列排程中處理了。

static Future<T> listenCancelForAsyncTask<T>(
      CancelToken? cancelToken, Future<T> future) {
  return Future.any([
    if (cancelToken != null) cancelToken.whenCancel.then((e) => throw e),
    future,
  ]);
}
複製程式碼
/// Returns the result of the first future in [futures] to complete.
///
/// The returned future is completed with the result of the first
/// future in [futures] to report that it is complete,
/// whether it's with a value or an error.
/// The results of all the other futures are discarded.
///
/// If [futures] is empty, or if none of its futures complete,
/// the returned future never completes.
static Future<T> any<T>(Iterable<Future<T>> futures) {
  var completer = new Completer<T>.sync();
  void onValue(T value) {
    if (!completer.isCompleted) completer.complete(value);
  }

  void onError(Object error, StackTrace stack) {
    if (!completer.isCompleted) completer.completeError(error, stack);
  }

  for (var future in futures) {
    future.then(onValue, onError: onError);
  }
  return completer.future;
}
複製程式碼

請求排程

實際的請求排程是在 dio_mixin.dart 中的_dispatchRequest方法完成的,該方法實際在上面的 fetch 方法中呼叫。這個方法有兩個地方用到了 canlToken,一個是使用 httpClientAdapter 的 fetch方法時傳入了 cancelToken 的whenCancel 屬性。在 httpClientAdapter引用是為了在取消請求後,能夠回撥告知監聽器請求被取消。另外就是呼叫了一個 checkCancelled 方法,用於檢查是否要停止請求。

responseBody = await httpClientAdapter.fetch(
  reqOpt,
  stream,
  cancelToken?.whenCancel,
);

// If the request has been cancelled, stop request and throw error.
static void checkCancelled(CancelToken? cancelToken) {
  if (cancelToken != null && cancelToken.cancelError != null) {
    throw cancelToken.cancelError!;
  }
}
複製程式碼

從這裡我們就能夠大致明白基本的機制了。實際上我們呼叫 cancelTokencancel 方法的時候,標記了 cancelToken 的錯誤資訊 cancelError,以讓_dispatchRequest被排程的時候來檢測是否取消。在_dispatchRequest中如果檢測到cancelError不為空,就會丟擲一個 cancelError,中止當前以及接下來的請求。

總結

從原始碼來看,有一堆的 Future,以及各類包裝方法,閱讀起來相當費勁,這也看出來 Dio 的厲害之處,讓一般人想這些網路請求頭都能想炸去。實際我們從原始碼以及除錯跟蹤來看,CancelToken 的機制是:

  • 事前取消:如果請求的 cancelToken 不為空,就會將 cancelToken 的非同步處理加入到攔截器,取消後直接在攔截器環節就把請求攔住,不會到後續的排程環節。
  • 事中取消:如果請求已經加入到了排程佇列,此時取消的話會丟擲異常,中止請求的發出。
  • 事後取消:請求已經發出了,當服務端返回結果的時候,會在響應處理環節(包括出錯)攔截,中止後續的響應處理。即便是服務端返回了資料,也會被攔截,不過這個實際意義不太大,無法降低服務端的負荷,只是避免後續的資料處理過程了。

相關文章