我的编程空间,编程开发者的网络收藏夹
学习永远不晚

Flutter系统网络图片加载流程解析

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

Flutter系统网络图片加载流程解析

Flutter原生支持在Image组件上显示网络图片,最简单的使用方式如下,调用Image的命名构造方法Image.network即可实现网络图片的下载显示。

Widget image = Image.network(imageUrl);

那么,它内部是如何实现的呢?是否有做缓存处理或其他优化操作呢?带着疑问,我们一起来看下它的底层究竟是如何实现的。

一、从构造函数开始

我们以最简单的调用方式举例,当我们使用Image.network(imageUrl)这种方式来显示图片时,Image组件内部image属性就会被赋值NetworkImage

// 此为简化过的Image组件类结构
class Image extends StatefulWidget {
	Image.network(
    String class="lazy" data-src,
	) : image = NetworkImage(class="lazy" data-src);
	
// 图片数据处理的基类
	final ImageProvider image;
}

这里引出了一个类叫NetworkImage,它是ImageProvider的子类,专门实现网络图片的下载和解析逻辑。当然你直接点进去看到的其实是个抽象类,并不是真正实现下载逻辑的地方,真正实现网络图片下载解析的在'_network_image_io.dart’这个文件下。构造函数知道这些就够了。接下来就看Image是在何时触发网络图片的下载的。

二、图片下载入口

Image是一个StatefulWidget,它又一个对应的State_ImageState。在这个_ImageState的生命周期中,控制着图片的下载过程。

State的生命周期可以简单的分为:构造函数 → initState → didChangeDependencies → build

因此,我们顺着这个顺序找,很快看到一个可疑的地方,didChangeDependencies中的_resolveImage方法。而TickerMode则是用于控制动画的,在这里被用于判断是否禁用了动画。关于TickerMode的相关介绍,可以看下这篇文章

// 完整源码
@override
  void didChangeDependencies() {
    _updateInvertColors();
// 处理图片的入口
    _resolveImage();

// 当动画被禁用时,图片也是无法显示的,这个
    if (TickerMode.of(context))
// 添加图片流处理的监听
      _listenToStream();
    else
      _stopListeningToStream(keepStreamAlive: true);

    super.didChangeDependencies();
  }

我们进入到_resolveImage方法中去。

void _resolveImage() {
// ScrollAwareImageProvider包装了我们的NetworkImage
    final ScrollAwareImageProvider provider = ScrollAwareImageProvider<Object>(
      context: _scrollAwareContext,
      imageProvider: widget.image,
    );
// 新建图片流
    final ImageStream newStream =
      provider.resolve(createLocalImageConfiguration(
        context,
        size: widget.width != null && widget.height != null ? Size(widget.width!, widget.height!) : null,
      ));
    assert(newStream != null);
// 更新图片流
    _updateSourceStream(newStream);
  }

_resolveImage方法就做了三件事。

1、用ScrollAwareImageProvider包装了NetworkImage

2、创建图片流对象ImageStream

3、更新图片流

2.1、ScrollAwareImageProvider

ScrollAwareImageProvider也是ImageProvider的子类,它的作用很简单,就是防止在快速滑动的时候加载图片,当存在快速滑动时,会将解析图片的工作放到下一帧处理。至于具体如何实现,我们放在后面再提。

2.2、ImageConfiguration

ImageConfiguration由方法createLocalImageConfiguration创建,保存了图片的基本配置信息,如Bundle,屏幕项目比devicePixelRatio,本地化local,尺寸size,平台platform等。

2.3、ImageStream

表示一个图片流,可以添加观察者ImageStreamCompleter来监听图片是否处理完成。一个图片流可以添加多个观察者。

ImageStreamproviderresolve方法调用后创建。通过源码可知,此处的provider就是ScrollAwareImageProvider对象。但是它内部并没有实现resolve方法,因此此处调用的是父类ImageProviderresolve方法。

三、图片流和Key

以下代码截取自ImageProvider,并且删减了无关代码。

ImageStream resolve(ImageConfiguration configuration) {
// 创建流,这里直接调用了ImageStream的构造函数,并没有用到configuration
    final ImageStream stream = createStream(configuration);
// 关键在这里,这里会根据configuration创建一个唯一key
    _createErrorHandlerAndKey(
      configuration,
// 成功的回调
      (T key, ImageErrorListener errorHandler) {
        resolveStreamForKey(configuration, stream, key, errorHandler);
      },
// 下面是错误回调,可以不关注
      (T? key, Object exception, StackTrace? stack) async {
        await null; // wait an event turn in case a listener has been added to the image stream.
        InformationCollector? collector;
        if (stream.completer == null) {
          stream.setCompleter(_ErrorImageCompleter());
        }
        stream.completer!.reportError(
          exception: exception,
          stack: stack,
          context: ErrorDescription('while resolving an image'),
          silent: true, // could be a network error or whatnot
          informationCollector: collector,
        );
      },
    );
    return stream;
  }

resolve方法的作用是创建图片流对象ImageStream,并根据传入的图片配置信息configuration,创建对应的Key,这个Key用于图片缓存。

那么这个key到底是怎么创建的呢,我们进入到_createErrorHandlerAndKey方法中查看。关键代码如下,已删除无关代码。

Future<T> key;
      try {
        key = obtainKey(configuration);
      } catch (error, stackTrace) {
        handleError(error, stackTrace);
        return;
      }
      key.then<void>((T key) {
        obtainedKey = key;
        try {
          successCallback(key, handleError);
        } catch (error, stackTrace) {
          handleError(error, stackTrace);
        }
      }).catchError(handleError);

可以看到方法实现中调用了ImageProviderobtainKey方法,而这个方法在ImageProvider并没有具体实现,需要子类完成对应的实现。

Future<T> obtainKey(ImageConfiguration configuration);

还记得上文的分析不,我们说传入的imageProvider实例是ScrollAwareImageProvider对象,因此对应的实现也要到这个类中去查找。很快,我们找到obtainKey方法的实现,可以看到它做了个透传,具体是由它包装的类也就是NetworkImage来实现的。

@override
  Future<T> obtainKey(ImageConfiguration configuration) => imageProvider.obtainKey(configuration);

那么,我们就去NetworkImageobtainKey

注意下真正的NetworkImage实现是在_network_image_io.dart文件下的。

Future<NetworkImage> obtainKey(image_provider.ImageConfiguration configuration) {
    return SynchronousFuture<NetworkImage>(this);
  }

到这,我们就知道了NetworkImagekeySynchronousFuture

获取到key后的下一步就是调用_createErrorHandlerAndKey方法的successCallback回调。从而触发了下一个流程resolveStreamForKey

_createErrorHandlerAndKey(
      configuration,
      (T key, ImageErrorListener errorHandler) {
// 拿到Key之后的回调
        resolveStreamForKey(configuration, stream, key, errorHandler);
      }
)

四、根据key来处理图片流

还是回到子类ScrollAwareImageProvider中,它重写了父类的resolveStreamForKey方法,前文提到,ScrollAwareImageProvider是用来防止列表在快速滑动的时候来加载图片的,那么它是如何实现的?我们就从resolveStreamForKey这个方法中来一探究竟。

// 以下代码已去掉无关逻辑
@override
  void resolveStreamForKey(
    ImageConfiguration configuration,
    ImageStream stream,
    T key,
    ImageErrorListener handleError,
  ) {
// 滑动过快
    if (Scrollable.recommendDeferredLoadingForContext(context.context!)) {
      SchedulerBinding.instance!.scheduleFrameCallback((_) {
// 放入下一帧再尝试处理,如果下一帧还是过快,那么将一直被推迟
        scheduleMicrotask(() => resolveStreamForKey(configuration, stream, key, handleError));
      });
      return;
    }
// 当前可以加载,那么透传给包装的imageProvider来处理,这里是NetworkImage
    imageProvider.resolveStreamForKey(configuration, stream, key, handleError);
  }

Scrollable用于滑动组件,它有个方法叫recommendDeferredLoadingForContext,表示是否建议延迟加载。内部最终是根据滑动速度和当前设备的最大物理尺寸的边去比较,如果大于,表示速度过快,那么就建议延迟。具体逻辑在scroll_physics.dart文件下。这里不多做介绍。

一旦当前应用处于滑动状态,并且速度过快,那么,图片的加载将会被推迟到下一帧再进行尝试。因此我们说,当处于快速滑动时,图片是无法加载的。

当判断可以加载图片时,操作流将会被移交给被包装类imageProvider,这里是NetworkImage来处理。但是,NetworkImage没有实现resolveStreamForKey方法,因此最终还是跑到了ImageProvider类中的resolveStreamForKey方法下。

@protected
  void resolveStreamForKey(ImageConfiguration configuration, ImageStream stream, T key, ImageErrorListener handleError) {
// 第一次进来还没有设置completer,因此不会进入这个分支中
    if (stream.completer != null) {
      final ImageStreamCompleter? completer = PaintingBinding.instance!.imageCache!.putIfAbsent(
        key,
        () => stream.completer!,
        onError: handleError,
      );
      assert(identical(completer, stream.completer));
      return;
    }
    final ImageStreamCompleter? completer = PaintingBinding.instance!.imageCache!.putIfAbsent(
      key,
      () => load(key, PaintingBinding.instance!.instantiateImageCodec),
      onError: handleError,
    );
    if (completer != null) {
      stream.setCompleter(completer);
    }
  }

当第一次加载网络图的时候,会直接走到下面这个逻辑中。这里涉及到一个很重要的类,ImageCache。它是做图片缓存用的。

final ImageStreamCompleter? completer = PaintingBinding.instance!.imageCache!.putIfAbsent(
      key,
      () => load(key, PaintingBinding.instance!.instantiateImageCodec),
      onError: handleError,
    );

4.1、ImageCache

图片缓存类,只做了内存缓存。它由PaintingBinding持有,是一个单利。它的内部通过三个Map来缓存图片。

// 加载中的图片
final Map<Object, _PendingImage> _pendingImages = <Object, _PendingImage>{};
// 缓存中的图片
  final Map<Object, _CachedImage> _cache = <Object, _CachedImage>{};
// 正在使用的图片
  final Map<Object, _LiveImage> _liveImages = <Object, _LiveImage>{}

从图片缓存器中获取图片的逻辑集中在putIfAbsent方法中。以下代码已经去掉无关代码。

ImageStreamCompleter? putIfAbsent(Object key, ImageStreamCompleter Function() loader, { ImageErrorListener? onError }) {
    TimelineTask? timelineTask;
    TimelineTask? listenerTask;
    ImageStreamCompleter? result = _pendingImages[key]?.completer;
// 正在加载,直接返回
    if (result != null) {
      return result;
    }
    // 这边有个小知识,dart中的Map是有顺序的,因此利用这点可以实现LRU算法。
// 最近用到了这图片,因此删除对应键值对,并更新,就能让它的位置处于前面
    final _CachedImage? image = _cache.remove(key);
    if (image != null) {
// 更新 _liveImages
      _trackLiveImage(
        key,
        image.completer,
        image.sizeBytes,
      );
      _cache[key] = image;
      return image.completer;
    }
    final _LiveImage? liveImage = _liveImages[key];
    if (liveImage != null) {
// 更新 _cache,这里还会根据最大缓存数量和大小来最限制
      _touch(
        key,
        _CachedImage(
          liveImage.completer,
          sizeBytes: liveImage.sizeBytes,
        ),
        timelineTask,
      );
      return liveImage.completer;
    }
// 加载流程,这是个回调,由各ImageProvider子类来实现
    try {
      result = loader();
// 下载完更新 _liveImages
      _trackLiveImage(key, result, null);
    } catch (error, stackTrace) {
      if (onError != null) {
        onError(error, stackTrace);
        return null;
      } else {
        rethrow;
      }
    }
    bool listenedOnce = false;
    _PendingImage? untrackedPendingImage;
// 设置图片加载监听,一旦加载完毕,那么会删除_pendingImages下对应的图片,并移除监听。同时更新_cache和_liveImages
    void listener(ImageInfo? info, bool syncCall) {
      int? sizeBytes;
      if (info != null) {
        sizeBytes = info.sizeBytes;
        info.dispose();
      }
      final _CachedImage image = _CachedImage(
        result!,
        sizeBytes: sizeBytes,
      );
      _trackLiveImage(key, result, sizeBytes);
      if (untrackedPendingImage == null) {
        _touch(key, image, listenerTask);
      } else {
        image.dispose();
      }
      final _PendingImage? pendingImage = untrackedPendingImage ?? _pendingImages.remove(key);
      if (pendingImage != null) {
        pendingImage.removeListener();
      }
      listenedOnce = true;
    }
// 设置加载监听,主要用来管理_pendingImages
    final ImageStreamListener streamListener = ImageStreamListener(listener);
    if (maximumSize > 0 && maximumSizeBytes > 0) {
      _pendingImages[key] = _PendingImage(result, streamListener);
    } else {
      untrackedPendingImage = _PendingImage(result, streamListener);
    }
    result.addListener(streamListener);
    return result;
  }

4.2、 load

一旦在ImageCache中找不到缓存的图片,就会通过loader回调出来,走真正的下载流程。

final ImageStreamCompleter? completer = PaintingBinding.instance!.imageCache!.putIfAbsent(
      key,
// 本地图片找不到,需要去对应的ImageProvider子类里实现加载逻辑
      () => load(key, PaintingBinding.instance!.instantiateImageCodec),
      onError: handleError,
    );

还是先看ScrollAwareImageProvider类,里面实现了load方法,并透传给了NetworkImage来实现。

@override
  ImageStreamCompleter load(T key, DecoderCallback decode) => imageProvider.load(key, decode);

NetworkImage下,可以找到对应的load方法实现。里面有个_loadAsync方法,它就是我们要找的图片下载核心代码。

@override
  ImageStreamCompleter load(image_provider.NetworkImage key, image_provider.DecoderCallback decode) {
    final StreamController<ImageChunkEvent> chunkEvents = StreamController<ImageChunkEvent>();
// 多帧图片流管理器
    return MultiFrameImageStreamCompleter(
// 核心异步加载逻辑
      codec: _loadAsync(key as NetworkImage, chunkEvents, decode),
      chunkEvents: chunkEvents.stream,
      scale: key.scale,
      debugLabel: key.url,
      informationCollector: () => <DiagnosticsNode>[
        DiagnosticsProperty<image_provider.ImageProvider>('Image provider', this),
        DiagnosticsProperty<image_provider.NetworkImage>('Image key', key),
      ],
    );
  }

五、图片下载

饶了一大圈,终于来到了下载图片的地方了。可以看到下载图片的逻辑很简单,创建一个下载的http请求,设置header,下载图片。一旦下载成功,就会通过decode这个回调将图片的二进制数据返回出去decode(bytes)

Future<ui.Codec> _loadAsync(
    NetworkImage key,
    StreamController<ImageChunkEvent> chunkEvents,
    image_provider.DecoderCallback decode,
  ) async {
    try {
      assert(key == this);
      final Uri resolved = Uri.base.resolve(key.url);
      final HttpClientRequest request = await _httpClient.getUrl(resolved);
      headers?.forEach((String name, String value) {
        request.headers.add(name, value);
      });
      final HttpClientResponse response = await request.close();
      if (response.statusCode != HttpStatus.ok) {
        // The network may be only temporarily unavailable, or the file will be
        // added on the server later. Avoid having future calls to resolve
        // fail to check the network again.
        await response.drain<List<int>>(<int>[]);
        throw image_provider.NetworkImageLoadException(statusCode: response.statusCode, uri: resolved);
      }
      final Uint8List bytes = await consolidateHttpClientResponseBytes(
        response,
        onBytesReceived: (int cumulative, int? total) {
          chunkEvents.add(ImageChunkEvent(
            cumulativeBytesLoaded: cumulative,
            expectedTotalBytes: total,
          ));
        },
      );
      if (bytes.lengthInBytes == 0)
        throw Exception('NetworkImage is an empty file: $resolved');
      return decode(bytes);
    } catch (e) {
      // Depending on where the exception was thrown, the image cache may not
      // have had a chance to track the key in the cache at all.
      // Schedule a microtask to give the cache a chance to add the key.
      scheduleMicrotask(() {
        PaintingBinding.instance!.imageCache!.evict(key);
      });
      rethrow;
    } finally {
      chunkEvents.close();
    }
  }

而回调出去的这些二进制数据,是在MultiFrameImageStreamCompleter中被处理的。MultiFrameImageStreamCompleterImageStreamCompleter的子类,用于管理多帧图片的加载。

MultiFrameImageStreamCompleter的构造方法中,我们可以看到它对codec做了回调处理。而这个codec就是前面提到的_loadAsync异步方法。

MultiFrameImageStreamCompleter({
    required Future<ui.Codec> codec,
    required double scale,
    String? debugLabel,
    Stream<ImageChunkEvent>? chunkEvents,
    InformationCollector? informationCollector,
  }) : assert(codec != null),
       _informationCollector = informationCollector,
       _scale = scale {
    this.debugLabel = debugLabel;
// 这里处理了_loadAsync的回调
    codec.then<void>(_handleCodecRead);

_handleCodecRead方法中回判断是否有观察者,有就进入解码流程。

void _handleCodecReady(ui.Codec codec) {
    _codec = codec;
    assert(_codec != null);

    if (hasListeners) {
// 存在观察者,开始解码
      _decodeNextFrameAndSchedule();
    }
  }

_decodeNextFrameAndSchedule方法可以看成是图片的解码方法,当然实际解码的地方位于更底层的Native。图片解码后会将信息保存在FrameInfo中,由_nextFrame持有。这里我们只考虑单帧图片,不考虑gif图。解码后的信息会被封装在ImageInfo中,其中image就是真正的图片数据。并调用_emitFrame方法,更新图片信息。而_emitFrame方法则主要是调用了setImage来通知观察者更新。我们直接看setImage方法即可。

Future<void> _decodeNextFrameAndSchedule() async {
    _nextFrame?.image.dispose();
    _nextFrame = null;
    try {
// 解码得到一帧图片信息,保存在FrameInfo中
      _nextFrame = await _codec!.getNextFrame();
    } catch (exception, stack) {
      return;
    }
// 当帧图片就将数据封装在ImageInfo中回调出去
    if (_codec!.frameCount == 1) {
      if (!hasListeners) {
        return;
      }
      _emitFrame(ImageInfo(
        image: _nextFrame!.image.clone(),
        scale: _scale,
        debugLabel: debugLabel,
      ));
      _nextFrame!.image.dispose();
      _nextFrame = null;
      return;
    }
// 多帧则继续往下走
    _scheduleAppFrame();
  }

通过ImageStreamListener通知更新,刷新界面展示。

void setImage(ImageInfo image) {
    _checkDisposed();
    _currentImage?.dispose();
    _currentImage = image;

    if (_listeners.isEmpty)
      return;
    // Make a copy to allow for concurrent modification.
    final List<ImageStreamListener> localListeners =
        List<ImageStreamListener>.of(_listeners);
    for (final ImageStreamListener listener in localListeners) {
      try {
// 设置新图篇,通知更新界面展示
        listener.onImage(image.clone(), false);
      } catch (exception, stack) {
        reportError(
          context: ErrorDescription('by an image listener'),
          exception: exception,
          stack: stack,
        );
      }
    }
  }

说到这里,我们好像还没提到过什么时候设置的观察者,好,我们再次回到最初的入口,_ImageState组件的didChangeDependencies方法中。

六、添加观察者实现界面更新

这个观察者就是通过_listenToStream方法添加的。

@override
  void didChangeDependencies() {
    _updateInvertColors();
    _resolveImage();

    if (TickerMode.of(context))
// 添加观察者
      _listenToStream();
    else
      _stopListeningToStream(keepStreamAlive: true);

    super.didChangeDependencies();
  }

并且在创建观察者ImageStreamListener的时候,设置了onImage的回调。

// 这里是获取观察者的入口
ImageStreamListener _getListener({bool recreateListener = false}) {
    if(_imageStreamListener == null || recreateListener) {
      _lastException = null;
      _lastStack = null;
      _imageStreamListener = ImageStreamListener(
// 这个就是onImage的回调
        _handleImageFrame,
        onChunk: widget.loadingBuilder == null ? null : _handleImageChunk,
        onError: widget.errorBuilder != null || kDebugMode
            ? (Object error, StackTrace? stackTrace) {
                setState(() {
                  _lastException = error;
                  _lastStack = stackTrace;
                });
              }
            : null,
      );
    }
    return _imageStreamListener!;
  }

onImage的入参被设置了_handleImageFrame,因此当下载完图片后调用的就是_handleImageFrame方法。

void _handleImageFrame(ImageInfo imageInfo, bool synchronousCall) {
    setState(() {
// 更新图片信息,实现图片加载
      _replaceImage(info: imageInfo);
      _loadingProgress = null;
      _lastException = null;
      _lastStack = null;
      _frameNumber = _frameNumber == null ? 0 : _frameNumber! + 1;
      _wasSynchronouslyLoaded = _wasSynchronouslyLoaded | synchronousCall;
    });
  }

void _replaceImage({required ImageInfo? info}) {
    _imageInfo?.dispose();
    _imageInfo = info;
  }

到此,图片下载和更新的流程已经都串起来了。下载完的图片存放在ImageInfo中,在setState后,会被设置进RawImage组件中实现渲染。

总结

网络图片的加载逻辑可以分为以下几个步骤:

1、根据图片类型,生成对应的key

2、根据key去全局的ImageCache下查找图片缓存,命中则直接返回刷新

3、图片缓存没有命中,调用Http去下载图片

4、下载完图片后,将图片的二进制数据回调出去触发界面刷新,同时会做内存缓存

5、在RawImage中显示网络图片

到此这篇关于Flutter系统网络图片加载过程解析的文章就介绍到这了,更多相关Flutter图片加载流程内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

Flutter系统网络图片加载流程解析

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

Flutter加载图片流程MultiFrameImageStreamCompleter解析

这篇文章主要为大家介绍了Flutter加载图片流程MultiFrameImageStreamCompleter示例解析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
2023-05-16

Flutter加载图片流程之ImageCache源码示例解析

这篇文章主要为大家介绍了Flutter加载图片流程之ImageCache源码解析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
2023-05-16

Flutter加载图片流程之ImageProvider源码示例解析

这篇文章主要为大家介绍了Flutter加载图片流程之ImageProvider源码示例解析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
2023-05-16

String项目中 加载网络img图片失败怎么解决

这篇文章将为大家详细讲解有关String项目中 加载网络img图片失败怎么解决,文章内容质量较高,因此小编分享给大家做个参考,希望大家阅读完这篇文章后对相关知识有一定的了解。1. 概述当从网络加载图片失败 希望显示默认图img 标签有个 o
2023-05-31

从源码分析Android的Glide库的图片加载流程及特点

0.基础知识 Glide中有一部分单词,我不知道用什么中文可以确切的表达出含义,用英文单词可能在行文中更加合适,还有一些词在Glide中有特别的含义,我理解的可能也不深入,这里先记录一下。 (1)View: 一般情况下,指Android中的
2022-06-06

Android图片加载框架最新解析:从源码的角度理解Glide的执行流程

文章目录准备源码开始阅读1、with()2、load()3、into()总结 众所周知Glide是Android开发中普遍使用的图片加载框架,功能非常强大,API非常简便,也是Google官方唯一推荐的图片加载框架。 基本用法,本文不再叙述
2022-06-06

员工工资管理系统数据流程图解析

本文将详细解析员工工资管理系统中的数据流程图,包括数据的获取、处理、存储和应用。我们将从系统的不同角度来理解数据的流转,以帮助读者更好地理解这个系统的运行方式。一、数据的获取员工工资管理系统数据流程图的第一步是数据的获取。这通常涉及到员工的出勤记录、绩效考核结果、职务级别、工作年限等因素。这些数据可以从人力资源部
员工工资管理系统数据流程图解析
2023-12-15

windows8系统怎么添加网络打印机详细步骤图解

首先,在浏览器通过打印机IP查看打印机是否连线:为保证能够顺利安装网络打印机,有必要先取消防火墙:然后再在WIN8桌面下按WIN+X组合键,选择“控制面板”:在弹出的窗口中单击“查看设备和打印机&rdqu
2022-06-04

win7添加XP系统里的网络共享打印机图文教程

装了Win7之后,添加网络打印机的时候无法添加,老是提示Windows 无法连接到打印机,点开详细信息显示“操作失败,错误为0x000003e3”后来查了下原来是因为网络打印机是装在WinXP的电脑上的原因,因为XP
2023-05-29

Win8系统下安装网络打印机的详细步骤(全程图解)

网络打印机的使用是办公一族必须掌握的一个技能,很多用户都能够在Win7或WinXP下轻松自如的安装运行网络打印机,可是到了Win8,就没那么顺手,有些用户表示在Win8系统下安装不来网络打印机,求教大神帮忙。Win8系统下安装网络打印机的方
2022-06-04

win7系统安装打印机(光盘安装/网络下载)两种方式图文教程

打印机作为目前企业办公必用设备,很多上班族都会接触点,对于python不少小白朋友来说,可能觉得打印机安装很复杂,其实不然。打印机安装其实和安装其他一些电脑外设一样,只要熟悉都可以轻松入手,菜鸟朋友只要了解步骤,也就轻松学会了。接下来www
2023-05-30

Win10系统网络诊断工具在哪?Win10系统诊断工具解决上网问题的使用方法图文教程

在电脑使用中,我们经常会遇到一些莫名的网络问题,比如“连接不可用”、“找不到可用网络”或者是“网络连接受限”。导致网络不可用,有时候是网络本身的问题,但有时候也可能是电
2023-05-22

win10系统开机需修复网络才能上网的原因及两种解决方法图文教程

不python少使用Windows10正式版系统的用户反馈自己的电脑只要关机,重新打开后就要修复网络才能上网,这种情况怎么办呢?下面我们的小编给大家分享两种解决方法。 原因分析: 其实,该问题是系统中网络服务、IP地址配置不正确导致的 解决
2023-05-21

Win7连不上WiFi怎么办 windows7系统恢复无线网络连接图文教程详解

在网络越来越发达的今天,javascriptWiFi网络已经不可或缺。那么,如果Win7连不上WiFi怎么办?发生这种情况,有许多原因,小编本次就来一一演示恢复无线网络连接图文教程。 具体方法: 第一步、我们点击右下角的无线网络,打开网络和
2023-06-05

Win8系统无线网络适配器显示未连接状态的两种解决方法图文教程

WIFI网络一种无线网络传输技术,无论是笔记本还是手机都可以连接上网。Win8系统开机后发现在网络选项里面看不到WIFI,通过网络共享中心可以看到无线网络适配器显示“未连接状态”,即使重启路由器并重启计算机,问题还是
2022-06-04

Win10系统提示你目前没有连接到任何网络的两种解决方法图文教程

在使用Windows 10系统过程中,发现电脑无法联网,打开【查看网络和共享中心】界面活动网络下遇到提示:你目前没有连接到任何网络。这是怎么回事呢?下编程客栈面就随我们的小编看看Win10提php示“你目前没有连接到js任何网络
2023-05-22

Win8系统开机弹出网页bing的原因分析及2种解决方法图文教程

一位Win8纯净版系统用户开机弹出网页bing,每次都要手动关闭,这是怎么回事呢?有什么把办法能关闭呢?其实有两种方法,第一种是通过组策略、另外一种就是通过注册表。原因分析: 这是因为Win8集成的网络活动检测功能造成的,断网后检测到网络已
2022-06-04

编程热搜

  • Android:VolumeShaper
    VolumeShaper(支持版本改一下,minsdkversion:26,android8.0(api26)进一步学习对声音的编辑,可以让音频的声音有变化的播放 VolumeShaper.Configuration的三个参数 durati
    Android:VolumeShaper
  • Android崩溃异常捕获方法
    开发中最让人头疼的是应用突然爆炸,然后跳回到桌面。而且我们常常不知道这种状况会何时出现,在应用调试阶段还好,还可以通过调试工具的日志查看错误出现在哪里。但平时使用的时候给你闹崩溃,那你就欲哭无泪了。 那么今天主要讲一下如何去捕捉系统出现的U
    Android崩溃异常捕获方法
  • android开发教程之获取power_profile.xml文件的方法(android运行时能耗值)
    系统的设置–>电池–>使用情况中,统计的能耗的使用情况也是以power_profile.xml的value作为基础参数的1、我的手机中power_profile.xml的内容: HTC t328w代码如下:
    android开发教程之获取power_profile.xml文件的方法(android运行时能耗值)
  • Android SQLite数据库基本操作方法
    程序的最主要的功能在于对数据进行操作,通过对数据进行操作来实现某个功能。而数据库就是很重要的一个方面的,Android中内置了小巧轻便,功能却很强的一个数据库–SQLite数据库。那么就来看一下在Android程序中怎么去操作SQLite数
    Android SQLite数据库基本操作方法
  • ubuntu21.04怎么创建桌面快捷图标?ubuntu软件放到桌面的技巧
    工作的时候为了方便直接打开编辑文件,一些常用的软件或者文件我们会放在桌面,但是在ubuntu20.04下直接直接拖拽文件到桌面根本没有效果,在进入桌面后发现软件列表中的软件只能收藏到面板,无法复制到桌面使用,不知道为什么会这样,似乎并不是很
    ubuntu21.04怎么创建桌面快捷图标?ubuntu软件放到桌面的技巧
  • android获取当前手机号示例程序
    代码如下: public String getLocalNumber() { TelephonyManager tManager =
    android获取当前手机号示例程序
  • Android音视频开发(三)TextureView
    简介 TextureView与SurfaceView类似,可用于显示视频或OpenGL场景。 与SurfaceView的区别 SurfaceView不能使用变换和缩放等操作,不能叠加(Overlay)两个SurfaceView。 Textu
    Android音视频开发(三)TextureView
  • android获取屏幕高度和宽度的实现方法
    本文实例讲述了android获取屏幕高度和宽度的实现方法。分享给大家供大家参考。具体分析如下: 我们需要获取Android手机或Pad的屏幕的物理尺寸,以便于界面的设计或是其他功能的实现。下面就介绍讲一讲如何获取屏幕的物理尺寸 下面的代码即
    android获取屏幕高度和宽度的实现方法
  • Android自定义popupwindow实例代码
    先来看看效果图:一、布局
  • Android第一次实验
    一、实验原理 1.1实验目标 编程实现用户名与密码的存储与调用。 1.2实验要求 设计用户登录界面、登录成功界面、用户注册界面,用户注册时,将其用户名、密码保存到SharedPreference中,登录时输入用户名、密码,读取SharedP
    Android第一次实验

目录