背景
由于flutter是单线程,所以即便是网络请求也是占有ui线程的,平时正常网络是没有问题的。
但是在ios上,https请求会检测OCSP,也就是证书是否吊销,这会耗费大量时间。
当然这个检测跟证书是有关系的,如果证书的检查地址在国内就没问题,在国外就卡死了。
所以不仅仅是图片会这样,所有的http请求都会这样。
如何解决
一种办法就是换证书,另外一种就是使用isolate
将http请求放入单独的线程中执行。
首先就是要导入包:
isolate: ^2.0.3
然后对http请求和图片重写发起请求的地方
普通接口请求的解决方案代码
dart文件名:api.dart
final Future<LoadBalancer> loadBalancer =
LoadBalancer.create(2, IsolateRunner.spawn);
class Api {
static int SERVER_ERROR = -1;
static Dio http = init();
static Dio init() {
Dio http = Dio(BaseOptions(
connectTimeout: 15000, receiveTimeout: 15000, sendTimeout: 15000));
http.interceptors.add(LogInterceptor(
responseBody: false,
requestBody: false,
requestHeader: false,
responseHeader: false));
return http;
}
static Map<String, dynamic> executeResponse(Response response) {
if (response.statusCode >= 200 && response.statusCode < 300) {
Map<String, dynamic> decodeMap = json.decode(response.data);
if (decodeMap["code"] == 1) {
return decodeMap;
}
if (decodeMap["code"] == 99) {
decodeMap["msg"] = "登录失效,请重新登录";
navigateTo(Login());
showToast("登录失效,请重新登录");
return decodeMap;
}
if (ObjectUtil.isEmptyString(decodeMap["msg"])) {
decodeMap["msg"] = "服务异常";
}
return decodeMap;
}
}
static Map<String, dynamic> executeException(e) {
print(e);
Map<String, dynamic> errorResponse = Map();
errorResponse.putIfAbsent("code", () => SERVER_ERROR);
errorResponse.putIfAbsent("msg", () => "网络异常");
return errorResponse;
}
//版本检测
static Future<Map<String, dynamic>> version(String type) {
Map<String, dynamic> param = new Map();
param.putIfAbsent("type", () => type);
return getRequest("/app/version", param);
}
//get请求,这里跟post请求可以一起改一下,写demo比较懒了
static Future<Map<String, dynamic>> getRequest(
String url, Map<String, dynamic> params) async {
bool externalApi = true;
if (!url.startsWith("http")) {
url = BuildParam.api + "/api.php" + url;
externalApi = false;
}
final ReceivePort receivePort = ReceivePort();
final LoadBalancer lb = await loadBalancer;
// 开启一个线程
await lb.run<dynamic, SendPort>(dataLoader, receivePort.sendPort);
final SendPort sendPort = await receivePort.first;
final ReceivePort resultPort = ReceivePort();
sendPort.send([url, resultPort.sendPort, "get", params, externalApi]);
Map<String, dynamic> responseMap = await resultPort.first;
BotToast.closeAllLoading();
if (externalApi) {
return Future.value(responseMap);
}
if (responseMap["code"] != 1) {
return Future.error(responseMap);
}
return Future.value(responseMap);
}
//post请求,这里跟get请求可以一起改一下,写demo比较懒了
static Future<Map<String, dynamic>> postRequest(
String url, Map<String, dynamic> params) async {
bool externalApi = true;
if (!url.startsWith("http")) {
url = BuildParam.api + "/api.php" + url;
externalApi = false;
}
final ReceivePort receivePort = ReceivePort();
final LoadBalancer lb = await loadBalancer;
// 开启一个线程
await lb.run<dynamic, SendPort>(dataLoader, receivePort.sendPort);
final SendPort sendPort = await receivePort.first;
final ReceivePort resultPort = ReceivePort();
sendPort.send([url, resultPort.sendPort, "post", params, externalApi]);
Map<String, dynamic> responseMap = await resultPort.first;
BotToast.closeAllLoading();
if (externalApi) {
return Future.value(responseMap);
}
if (responseMap["code"] != 1) {
return Future.error(responseMap);
}
return Future.value(responseMap);
}
// isolate的绑定方法
static dataLoader(SendPort sendPort) async {
final ReceivePort receivePort = ReceivePort();
sendPort.send(receivePort.sendPort);
receivePort.listen((msg) async {
String requestURL = msg[0];
SendPort callbackPort = msg[1];
String requestMethod = msg[2];
Map<String, dynamic> requestParam = msg[3];
bool externalApi = msg[4];
try {
Response response = null;
if (requestMethod == "get") {
response = await http.get(requestURL, queryParameters: requestParam);
}
if (requestMethod == "post") {
response = await http.post(requestURL, queryParameters: requestParam);
}
// 回调返回值给调用者,这里直接返回response是不行的,必须解码出来,不然我也想这样做。
if (externalApi) {
Map map = json.decode(response.data);
callbackPort.send(map);
} else {
Map map = executeResponse(response);
callbackPort.send(map);
}
} catch (e) {
Map map = executeException(e);
callbackPort.send(map);
}
});
}
}
调用:
Api.version("android").then() or await Api.version("android");
图片的解决方案
主要原理就是重写一下图片加载的逻辑,将其中发起http请求读取字节码的地方修改一下。
新建image_async.dart
class FadeInImageWithoutAuth extends StatefulWidget {
const FadeInImageWithoutAuth({
Key key,
@required this.placeholder,
@required this.image,
this.fadeOutDuration = const Duration(milliseconds: 300),
this.fadeOutCurve = Curves.easeOut,
this.fadeInDuration = const Duration(milliseconds: 700),
this.fadeInCurve = Curves.easeIn,
this.width,
this.height,
this.fit,
this.alignment = Alignment.center,
this.repeat = ImageRepeat.noRepeat,
this.matchTextDirection = false,
}) : assert(placeholder != null),
assert(image != null),
assert(fadeOutDuration != null),
assert(fadeOutCurve != null),
assert(fadeInDuration != null),
assert(fadeInCurve != null),
assert(alignment != null),
assert(repeat != null),
assert(matchTextDirection != null),
super(key: key);
/// Creates a widget that uses a placeholder image stored in memory while
/// loading the final image from the network.
///
/// [placeholder] contains the bytes of the in-memory image.
///
/// [image] is the URL of the final image.
///
/// [placeholderScale] and [imageScale] are passed to their respective
/// [ImageProvider]s (see also [ImageInfo.scale]).
///
/// The [placeholder], [image], [placeholderScale], [imageScale],
/// [fadeOutDuration], [fadeOutCurve], [fadeInDuration], [fadeInCurve],
/// [alignment], [repeat], and [matchTextDirection] arguments must not be
/// null.
///
/// See also:
///
/// * [new Image.memory], which has more details about loading images from
/// memory.
/// * [new Image.network], which has more details about loading images from
/// the network.
FadeInImageWithoutAuth.memoryNetwork({
Key key,
@required Uint8List placeholder,
@required String image,
double placeholderScale = 1.0,
double imageScale = 1.0,
this.fadeOutDuration = const Duration(milliseconds: 300),
this.fadeOutCurve = Curves.easeOut,
this.fadeInDuration = const Duration(milliseconds: 700),
this.fadeInCurve = Curves.easeIn,
this.width,
this.height,
this.fit,
this.alignment = Alignment.center,
this.repeat = ImageRepeat.noRepeat,
this.matchTextDirection = false,
}) : assert(placeholder != null),
assert(image != null),
assert(placeholderScale != null),
assert(imageScale != null),
assert(fadeOutDuration != null),
assert(fadeOutCurve != null),
assert(fadeInDuration != null),
assert(fadeInCurve != null),
assert(alignment != null),
assert(repeat != null),
assert(matchTextDirection != null),
placeholder = MemoryImage(placeholder, scale: placeholderScale),
image = NetworkImage(image, scale: imageScale),
super(key: key);
/// Creates a widget that uses a placeholder image stored in an asset bundle
/// while loading the final image from the network.
///
/// [placeholder] is the key of the image in the asset bundle.
///
/// [image] is the URL of the final image.
///
/// [placeholderScale] and [imageScale] are passed to their respective
/// [ImageProvider]s (see also [ImageInfo.scale]).
///
/// If [placeholderScale] is omitted or is null, the pixel-density-aware asset
/// resolution will be attempted for the [placeholder] image. Otherwise, the
/// exact asset specified will be used.
///
/// The [placeholder], [image], [imageScale], [fadeOutDuration],
/// [fadeOutCurve], [fadeInDuration], [fadeInCurve], [alignment], [repeat],
/// and [matchTextDirection] arguments must not be null.
///
/// See also:
///
/// * [new Image.asset], which has more details about loading images from
/// asset bundles.
/// * [new Image.network], which has more details about loading images from
/// the network.
FadeInImageWithoutAuth.network(String image,{
Key key,
String placeholder = "",
AssetBundle bundle,
double placeholderScale,
double imageScale = 1.0,
this.fadeOutDuration = const Duration(milliseconds: 200),
this.fadeOutCurve = Curves.easeOut,
this.fadeInDuration = const Duration(milliseconds: 200),
this.fadeInCurve = Curves.easeIn,
this.width,
this.height,
this.fit,
this.alignment = Alignment.center,
this.repeat = ImageRepeat.noRepeat,
this.matchTextDirection = false,
}) : assert(placeholder != null),
assert(image != null),
placeholder = placeholderScale != null
? ExactAssetImage(placeholder, bundle: bundle, scale: placeholderScale)
: NetworkImageWithoutAuth(placeholder, bundle: bundle),
assert(imageScale != null),
assert(fadeOutDuration != null),
assert(fadeOutCurve != null),
assert(fadeInDuration != null),
assert(fadeInCurve != null),
assert(alignment != null),
assert(repeat != null),
assert(matchTextDirection != null),
image = NetworkImageWithoutAuth(image, scale: imageScale),
super(key: key);
/// Image displayed while the target [image] is loading.
final ImageProvider placeholder;
/// The target image that is displayed.
final ImageProvider image;
/// The duration of the fade-out animation for the [placeholder].
final Duration fadeOutDuration;
/// The curve of the fade-out animation for the [placeholder].
final Curve fadeOutCurve;
/// The duration of the fade-in animation for the [image].
final Duration fadeInDuration;
/// The curve of the fade-in animation for the [image].
final Curve fadeInCurve;
/// If non-null, require the image to have this width.
///
/// If null, the image will pick a size that best preserves its intrinsic
/// aspect ratio. This may result in a sudden change if the size of the
/// placeholder image does not match that of the target image. The size is
/// also affected by the scale factor.
final double width;
/// If non-null, require the image to have this height.
///
/// If null, the image will pick a size that best preserves its intrinsic
/// aspect ratio. This may result in a sudden change if the size of the
/// placeholder image does not match that of the target image. The size is
/// also affected by the scale factor.
final double height;
/// How to inscribe the image into the space allocated during layout.
///
/// The default varies based on the other fields. See the discussion at
/// [paintImage].
final BoxFit fit;
/// How to align the image within its bounds.
///
/// The alignment aligns the given position in the image to the given position
/// in the layout bounds. For example, an [Alignment] alignment of (-1.0,
/// -1.0) aligns the image to the top-left corner of its layout bounds, while an
/// [Alignment] alignment of (1.0, 1.0) aligns the bottom right of the
/// image with the bottom right corner of its layout bounds. Similarly, an
/// alignment of (0.0, 1.0) aligns the bottom middle of the image with the
/// middle of the bottom edge of its layout bounds.
///
/// If the [alignment] is [TextDirection]-dependent (i.e. if it is a
/// [AlignmentDirectional]), then an ambient [Directionality] widget
/// must be in scope.
///
/// Defaults to [Alignment.center].
///
/// See also:
///
/// * [Alignment], a class with convenient constants typically used to
/// specify an [AlignmentGeometry].
/// * [AlignmentDirectional], like [Alignment] for specifying alignments
/// relative to text direction.
final AlignmentGeometry alignment;
/// How to paint any portions of the layout bounds not covered by the image.
final ImageRepeat repeat;
/// Whether to paint the image in the direction of the [TextDirection].
///
/// If this is true, then in [TextDirection.ltr] contexts, the image will be
/// drawn with its origin in the top left (the "normal" painting direction for
/// images); and in [TextDirection.rtl] contexts, the image will be drawn with
/// a scaling factor of -1 in the horizontal direction so that the origin is
/// in the top right.
///
/// This is occasionally used with images in right-to-left environments, for
/// images that were designed for left-to-right locales. Be careful, when
/// using this, to not flip images with integral shadows, text, or other
/// effects that will look incorrect when flipped.
///
/// If this is true, there must be an ambient [Directionality] widget in
/// scope.
final bool matchTextDirection;
@override
State<StatefulWidget> createState() => _FadeInImageState();
}
/// The phases a [FadeInImage] goes through.
@visibleForTesting
enum FadeInImagePhase {
/// The initial state.
///
/// We do not yet know whether the target image is ready and therefore no
/// animation is necessary, or whether we need to use the placeholder and
/// wait for the image to load.
start,
/// Waiting for the target image to load.
waiting,
/// Fading out previous image.
fadeOut,
/// Fading in new image.
fadeIn,
/// Fade-in complete.
completed,
}
typedef _ImageProviderResolverListener = void Function();
class _ImageProviderResolver {
_ImageProviderResolver({
@required this.state,
@required this.listener,
this.imageStreamListener
});
final _FadeInImageState state;
final _ImageProviderResolverListener listener;
final ImageStreamListener imageStreamListener;
FadeInImageWithoutAuth get widget => state.widget;
ImageStream _imageStream;
ImageInfo _imageInfo;
void resolve(ImageProvider provider) {
final ImageStream oldImageStream = _imageStream;
final ImageStreamListener listener = ImageStreamListener(_handleImageChanged);
_imageStream = provider.resolve(createLocalImageConfiguration(
state.context,
size: widget.width != null && widget.height != null ? Size(widget.width, widget.height) : null
));
assert(_imageStream != null);
if (_imageStream.key != oldImageStream?.key) {
oldImageStream?.removeListener(listener);
_imageStream.addListener(listener);
}
}
void _handleImageChanged(ImageInfo imageInfo, bool synchronousCall) {
_imageInfo = imageInfo;
listener();
}
void stopListening() {
_imageStream?.removeListener(imageStreamListener);
}
}
class _FadeInImageState extends State<FadeInImageWithoutAuth> with TickerProviderStateMixin {
_ImageProviderResolver _imageResolver;
_ImageProviderResolver _placeholderResolver;
AnimationController _controller;
Animation<double> _animation;
FadeInImagePhase _phase = FadeInImagePhase.start;
FadeInImagePhase get phase => _phase;
@override
void initState() {
_imageResolver = _ImageProviderResolver(state: this, listener: _updatePhase);
_placeholderResolver = _ImageProviderResolver(state: this, listener: () {
setState(() {
// Trigger rebuild to display the placeholder image
});
});
_controller = AnimationController(
value: 1.0,
vsync: this,
);
_controller.addListener(() {
setState(() {
// Trigger rebuild to update opacity value.
});
});
_controller.addStatusListener((AnimationStatus status) {
_updatePhase();
});
super.initState();
}
@override
void didChangeDependencies() {
_resolveImage();
super.didChangeDependencies();
}
@override
void didUpdateWidget(FadeInImageWithoutAuth oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.image != oldWidget.image || widget.placeholder != oldWidget.placeholder)
_resolveImage();
}
@override
void reassemble() {
_resolveImage(); // in case the image cache was flushed
super.reassemble();
}
void _resolveImage() {
_imageResolver.resolve(widget.image);
// No need to resolve the placeholder if we are past the placeholder stage.
if (_isShowingPlaceholder)
_placeholderResolver.resolve(widget.placeholder);
if (_phase == FadeInImagePhase.start)
_updatePhase();
}
void _updatePhase() {
if(!mounted){
return;
}
setState(() {
switch (_phase) {
case FadeInImagePhase.start:
if (_imageResolver._imageInfo != null)
_phase = FadeInImagePhase.completed;
else
_phase = FadeInImagePhase.waiting;
break;
case FadeInImagePhase.waiting:
if (_imageResolver._imageInfo != null) {
// Received image data. Begin placeholder fade-out.
_controller.duration = widget.fadeOutDuration;
_animation = CurvedAnimation(
parent: _controller,
curve: widget.fadeOutCurve,
);
_phase = FadeInImagePhase.fadeOut;
_controller.reverse(from: 1.0);
}
break;
case FadeInImagePhase.fadeOut:
if (_controller.status == AnimationStatus.dismissed) {
// Done fading out placeholder. Begin target image fade-in.
_controller.duration = widget.fadeInDuration;
_animation = CurvedAnimation(
parent: _controller,
curve: widget.fadeInCurve,
);
_phase = FadeInImagePhase.fadeIn;
_placeholderResolver.stopListening();
_controller.forward(from: 0.0);
}
break;
case FadeInImagePhase.fadeIn:
if (_controller.status == AnimationStatus.completed) {
// Done finding in new image.
_phase = FadeInImagePhase.completed;
}
break;
case FadeInImagePhase.completed:
// Nothing to do.
break;
}
});
}
@override
void dispose() {
_imageResolver.stopListening();
_placeholderResolver.stopListening();
_controller.dispose();
super.dispose();
}
bool get _isShowingPlaceholder {
assert(_phase != null);
switch (_phase) {
case FadeInImagePhase.start:
case FadeInImagePhase.waiting:
case FadeInImagePhase.fadeOut:
return true;
case FadeInImagePhase.fadeIn:
case FadeInImagePhase.completed:
return false;
}
return null;
}
ImageInfo get _imageInfo {
return _isShowingPlaceholder
? _placeholderResolver._imageInfo
: _imageResolver._imageInfo;
}
@override
Widget build(BuildContext context) {
assert(_phase != FadeInImagePhase.start);
final ImageInfo imageInfo = _imageInfo;
return RawImage(
image: imageInfo?.image,
width: widget.width,
height: widget.height,
scale: imageInfo?.scale ?? 1.0,
color: Color.fromRGBO(255, 255, 255, _animation?.value ?? 1.0),
colorBlendMode: BlendMode.modulate,
fit: widget.fit,
alignment: widget.alignment,
repeat: widget.repeat,
matchTextDirection: widget.matchTextDirection,
);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder description) {
super.debugFillProperties(description);
description.add(EnumProperty<FadeInImagePhase>('phase', _phase));
description.add(DiagnosticsProperty<ImageInfo>('pixels', _imageInfo));
description.add(DiagnosticsProperty<ImageStream>('image stream', _imageResolver._imageStream));
description.add(DiagnosticsProperty<ImageStream>('placeholder stream', _placeholderResolver._imageStream));
}
}
新建image_async_provider.dart
class NetworkImageWithoutAuth extends ImageProvider<NetworkImageWithoutAuth> {
/// Creates an object that fetches the image at the given URL.
///
/// The arguments must not be null.
const NetworkImageWithoutAuth(this.url, {this.scale = 1.0, this.headers})
: assert(url != null),
assert(scale != null);
/// The URL from which the image will be fetched.
final String url;
/// The scale to place in the [ImageInfo] object of the image.
final double scale;
/// The HTTP headers that will be used with [HttpClient.get] to fetch image from network.
final Map<String, String> headers;
@override
Future<NetworkImageWithoutAuth> obtainKey(ImageConfiguration configuration) {
return SynchronousFuture<NetworkImageWithoutAuth>(this);
}
@override
ImageStreamCompleter load(
NetworkImageWithoutAuth key, DecoderCallback decode) {
return MultiFrameImageStreamCompleter(
codec: _loadAsync(key),
scale: key.scale,
informationCollector: () sync* {
yield DiagnosticsProperty<ImageProvider>('Image provider', this);
yield DiagnosticsProperty<NetworkImageWithoutAuth>('Image key', key);
},
);
}
static final HttpClient _httpClient = HttpClient();
Future<ui.Codec> _loadAsync(NetworkImageWithoutAuth key) async {
assert(key == this);
//解决不安全证书校验通不过的问题
_httpClient.badCertificateCallback =
(X509Certificate cert, String host, int port) {
return true;
};
final Uint8List bytes = await ImageApi.image(key.url);
if (bytes.lengthInBytes == 0)
throw Exception('NetworkImage is an empty file');
return PaintingBinding.instance.instantiateImageCodec(bytes);
}
@override
bool operator ==(dynamic other) {
if (other.runtimeType != runtimeType) return false;
final NetworkImageWithoutAuth typedOther = other;
return url == typedOther.url && scale == typedOther.scale;
}
@override
int get hashCode => hashValues(url, scale);
@override
String toString() => '$runtimeType("$url", scale: $scale)';
}
新建image_api.dart
import 'dart:io';
import 'dart:isolate';
import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'package:isolate/isolate_runner.dart';
import 'package:isolate/load_balancer.dart';
final Future<LoadBalancer> loadBalancer =
LoadBalancer.create(2, IsolateRunner.spawn);
class ImageApi {
static final HttpClient _httpClient = HttpClient();
static Future<Uint8List> image(String url) async {
return getRequest(url);
}
static Future<Uint8List> getRequest(String url) async {
final ReceivePort receivePort = ReceivePort();
final LoadBalancer lb = await loadBalancer;
// 开启一个线程
await lb.run<dynamic, SendPort>(dataLoader, receivePort.sendPort);
final SendPort sendPort = await receivePort.first;
final ReceivePort resultPort = ReceivePort();
sendPort.send([url, resultPort.sendPort]);
Uint8List response = await resultPort.first;
return Future.value(response);
}
// isolate的绑定方法
static dataLoader(SendPort sendPort) async {
final ReceivePort receivePort = ReceivePort();
sendPort.send(receivePort.sendPort);
receivePort.listen((msg) async {
String requestURL = msg[0];
SendPort callbackPort = msg[1];
final Uri resolved = Uri.base.resolve(requestURL);
final HttpClientRequest request = await _httpClient.getUrl(resolved);
final HttpClientResponse response = await request.close();
if (response.statusCode != HttpStatus.ok)
throw Exception(
'HTTP request failed, statusCode: ${response?.statusCode}, $resolved');
final Uint8List bytes =
await consolidateHttpClientResponseBytes(response);
// 回调返回值给调用者
callbackPort.send(bytes);
});
}
}
使用:
FadeInImageWithoutAuth.network("图片地址", fit: BoxFit.cover)
网友评论