flutter 卡顿_Flutter 图片卡顿 https 卡顿 ui卡顿

本文指出Flutter是单线程,在iOS上https请求检测OCSP会耗费大量时间,导致卡顿,尤其是证书检查地址在国外时。提出换证书和使用isolate将http请求放入单独线程两种解决办法,并给出普通接口请求和图片加载的解决方案代码。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

背景

由于flutter是单线程,所以即便是网络请求也是占有ui线程的,平时正常网络是没有问题的。

但是在ios上,https请求会检测OCSP,也就是证书是否吊销,这会耗费大量时间。

当然这个检测跟证书是有关系的,如果证书的检查地址在国内就没问题,在国外就卡死了。

所以不仅仅是图片会这样,所有的http请求都会这样。

如何解决

一种办法就是换证书,另外一种就是使用isolate将http请求放入单独的线程中执行。

首先就是要导入包:

isolate: ^2.0.3

然后对http请求和图片重写发起请求的地方

普通接口请求的解决方案代码

dart文件名:api.dart

final Future 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 executeResponse(Response response) {

if (response.statusCode >= 200 && response.statusCode < 300) {

Map 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 executeException(e) {

print(e);

Map errorResponse = Map();

errorResponse.putIfAbsent("code", () => SERVER_ERROR);

errorResponse.putIfAbsent("msg", () => "网络异常");

return errorResponse;

}

//版本检测

static Future> version(String type) {

Map param = new Map();

param.putIfAbsent("type", () => type);

return getRequest("/app/version", param);

}

//get请求,这里跟post请求可以一起改一下,写demo比较懒了

static Future> getRequest(

String url, Map 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(dataLoader, receivePort.sendPort);

final SendPort sendPort = await receivePort.first;

final ReceivePort resultPort = ReceivePort();

sendPort.send([url, resultPort.sendPort, "get", params, externalApi]);

Map 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> postRequest(

String url, Map 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(dataLoader, receivePort.sendPort);

final SendPort sendPort = await receivePort.first;

final ReceivePort resultPort = ReceivePort();

sendPort.send([url, resultPort.sendPort, "post", params, externalApi]);

Map 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 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 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 with TickerProviderStateMixin {

_ImageProviderResolver _imageResolver;

_ImageProviderResolver _placeholderResolver;

AnimationController _controller;

Animation _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('phase', _phase));

description.add(DiagnosticsProperty('pixels', _imageInfo));

description.add(DiagnosticsProperty('image stream', _imageResolver._imageStream));

description.add(DiagnosticsProperty('placeholder stream', _placeholderResolver._imageStream));

}

}

新建image_async_provider.dart

class NetworkImageWithoutAuth extends ImageProvider {

/// 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 headers;

@override

Future obtainKey(ImageConfiguration configuration) {

return SynchronousFuture(this);

}

@override

ImageStreamCompleter load(

NetworkImageWithoutAuth key, DecoderCallback decode) {

return MultiFrameImageStreamCompleter(

codec: _loadAsync(key),

scale: key.scale,

informationCollector: () sync* {

yield DiagnosticsProperty('Image provider', this);

yield DiagnosticsProperty('Image key', key);

},

);

}

static final HttpClient _httpClient = HttpClient();

Future _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.create(2, IsolateRunner.spawn);

class ImageApi {

static final HttpClient _httpClient = HttpClient();

static Future image(String url) async {

return getRequest(url);

}

static Future getRequest(String url) async {

final ReceivePort receivePort = ReceivePort();

final LoadBalancer lb = await loadBalancer;

// 开启一个线程

await lb.run(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)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值