详解flutter刷新流程,让你的应用更流畅

news2024/11/27 22:27:04

本文已授权公众号【缦图技术团队】发布

详解flutter刷新流程,让你的应用更流畅

一、概述

Flutter 是谷歌推出的高性能、跨端UI框架,可以通过一套代码,支持 iOSAndroidWindows/MAC/Linux 等多个平台,且能达到原生性能。Flutter 也可以与平台原生代码进行混合开发,其更新迭代速度很快,技术发展也日趋成熟,如今已经有很多公司在使用这种新跨端技术。我们知道在 flutter 中可以使用 setState() 来刷新 StatefulWidget 的 UI,这会遍历调用子 Widget 的 build() 重构视图。当一个页面内容比较复杂时,会包含多个 widget,如果直接调用根组件的 setState(),会遍历所有子 Widget 的 build(),刷新整个页面,这样会造成很多不必要的开销,刷新的成本相对较大。如果数据很多接口响应又慢的话,还会有界面闪烁的现象。那么 flutter 到底是如何实现界面刷新的,调用 setState({})后 flutter framework 到底做了哪些操作?接下来我们一起来揭开 flutter 刷新界面的神秘面纱。

二、Flutter 渲染中的三棵树

在了解flutter的刷新机制之前,先来看看flutter渲染过程中的三棵树。在Flutter的渲染过程中由WidgetElementRenderObject这个三个元素组成三棵树。Widget控件树,Element 元素树,RenderObject 渲染树。Widget内部调用 createElement()会创建对应的 ElementElement内部调用 createRenderObject()会创建对应的 RenderObject,所以我们只需要关心 Widget 就可以快速的构建视图界面了。为什么使用三棵树而不是 Widget RenderObject 两棵树呢?这里是为了复用 Element 提升渲染性能,因为 Widget 面向业务它的改变会很频繁,如果根据 Widget 直接生成 RenderObject 会导致渲染性能下降。

flutter 三棵树依赖关系

RenderObject 渲染树在上屏前会生成一棵 Layer 树去进行屏幕渲染。

三、刷新流程分析

在开始流程分析之前,先上个图来梳理下整个刷新流程,脑海里对整体先有个初步认识,这样再跟着下面的源码一步步往里深入分析,思路会更加清晰一些:

setState() 刷新流程

对整体的刷新流程有了大概的认识之后,我们对照着上面这个图的流程来看看调用setState({})之后,系统具体都做了哪些操作:

注:

setState() 源码位于 flutetr_sdk/packages/flutter/lib/src/widgets/framework.dart 文件中

本文源码基于 Flutter 3.3.8 Dart 2.18.4 • DevTools 2.15.0

  @protected
  void setState(VoidCallback fn) {
    assert(fn != null);
    assert(() {
      if (_debugLifecycleState == _StateLifecycle.defunct) {
        throw FlutterError.fromParts(<DiagnosticsNode>[
    			// 省略不重要代码
        ]);
      }
      if (_debugLifecycleState == _StateLifecycle.created && !mounted) {
        throw FlutterError.fromParts(<DiagnosticsNode>[
    			// 省略不重要代码
        ]);
      }
      return true;
    }());
    final Object? result = fn() as dynamic;
    ...
    // 省略不重要代码
    _element!.markNeedsBuild();
  }

setState() 中传入的回调函数是立刻同步执行的,不能是异步的。该方法前面主要是 assert 部分的一些校验逻辑,不允许传入的回调函数为null且不能为异步函数,这里有一个点需要注意:在 widget 构造函数中以及 dispose 调用之后,不允许再调用 setState() 方法去刷新界面,可以在调用前考虑使用 mounted 标志来检测该 widget 是否还挂载在 widget 树上

最关键的代码在最后一行:_element!.markNeedsBuild();这里的_element就是statefullWidget 创建的 StatefulElement 对象,我们看下 Element 类的markNeedsBuild() 方法做了什么,代码依然在 framework.dart中:

  void markNeedsBuild() {
    assert(_lifecycleState != _ElementLifecycle.defunct);
    if (_lifecycleState != _ElementLifecycle.active) {
      return;
    }
    assert(owner != null);
    assert(_lifecycleState == _ElementLifecycle.active);
    // ...
    if (dirty) {
      return;
    }
    _dirty = true;
    owner!.scheduleBuildFor(this);
  }

这个方法我们在自定义 flutter 组件中经常见到,前面的 assert 部分是一些生命周期的校验,不在 active 状态,则不进行后续操作。这里要注意,该方法不能在build期间调用。关键代码还是在最后几行,将当前的 element 标记为 dirty,然后把它加入到全局的 widget 列表中,然后在下一帧中去进行重绘。这里的owner指的是 BuildOwner,它是 widget 的管理类,该类跟踪哪些 widget 需要重建,并处理整体上应用于 widget 树的其他任务,例如管理树的非活动元素列表,以及在调试时的热重载期间在必要时触发“重新组装”命令。BuildOwner 是我们一开始通过 runApp()初始化时创建的,它内部维护了一个_dirtyElements列表,用以保存被标记为“脏”的 element

再来看下 BuildOwner 类的 scheduleBuildFor() 方法:

  void scheduleBuildFor(Element element) {
    assert(element != null);
    assert(element.owner == this);
    // ...
    if (element._inDirtyList) {
      // ...
      _dirtyElementsNeedsResorting = true;
      return;
    }
    if (!_scheduledFlushDirtyElements && onBuildScheduled != null) {
      _scheduledFlushDirtyElements = true;
      onBuildScheduled!();
    }
    _dirtyElements.add(element);
    element._inDirtyList = true;
  }

该方法 assert 部分依旧是一些必要的参数校验,关键部分是如果 element 已经在_inDirtyList 列表中,设置 _dirtyElementsNeedsResorting = true 后直接返回。不在的话,如果是新的 dirty element,且当前这个 dirty element 列表还未注册过 VSync 信号监听,则还需要执行 onBuildScheduled(),该方法会调用PlatformDispatcher.scheduleFrame()向平台注册 VSync 监听。最后添加 element 到标记为 dirty element 的列表中,以便当 WidgetsBinding.drawFrame 调用 buildScope方法时执行重建。

这里的_dirtyElementsNeedsResorting 表示是否需要对 dirt elements 重新排序,因为之前被标“脏”的 element,它在树中的位置(深度)可能已经变了,需要对 dirty element 重新排序。

来看下 onBuildScheduled()方法,它在 WidgetBinding 初始化的时候就已经创建了:

mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, 
  GestureBinding, RendererBinding, SemanticsBinding {
  @override
  void initInstances() {
    super.initInstances();
    _instance = this;
    _buildOwner = BuildOwner();
    buildOwner!.onBuildScheduled = _handleBuildScheduled;
    platformDispatcher.onLocaleChanged = handleLocaleChanged;
    platformDispatcher.onAccessibilityFeaturesChanged = 
      handleAccessibilityFeaturesChanged;
    SystemChannels.navigation.setMethodCallHandler(_handleNavigationInvocation);

    platformMenuDelegate = DefaultPlatformMenuDelegate();
  }
...
  
}

可以看到 onBuildScheduled()回调最终等于回调 _handleBuildScheduled()

  void _handleBuildScheduled() {
    ...
    // 省略不重要代码
    ensureVisualUpdate();
  }


  void ensureVisualUpdate() {
    switch (schedulerPhase) {
      // 没有正在处理的帧,可能正在执行的是 WidgetsBinding.scheduleTask,
      // scheduleMicrotask,Timer,事件 handlers,或者其他回调等
      case SchedulerPhase.idle:
      // 主要是清理和计划执行下一帧的工作
      case SchedulerPhase.postFrameCallbacks:
        scheduleFrame();
        return;
      // SchedulerBinding.handleBeginFrame 过程, 处理动画状态更新
      case SchedulerPhase.transientCallbacks:
      // 处理 transientCallbacks 阶段触发的微任务(Microtasks)
      case SchedulerPhase.midFrameMicrotasks:
      // WidgetsBinding.drawFrame 和 SchedulerBinding.handleDrawFrame 过程,
      // build/layout/paint 流水线工作
      case SchedulerPhase.persistentCallbacks:
        return;
    }
  }

上面的 ensureVisualUpdate()分别处理了 SchedulerPhase的 5 个枚举值,这里主要看下处理 postFrameCallbacks 分支,它执行了 SchedulerBinding.scheduleFrame()方法:

  void scheduleFrame() {
    if (_hasScheduledFrame || !framesEnabled) {
      return;
    }
    ensureFrameCallbacksRegistered();
    platformDispatcher.scheduleFrame();
    _hasScheduledFrame = true;
  }

看下SchedulerBinding.ensureFrameCallbacksRegistered()方法:

  @protected
  void ensureFrameCallbacksRegistered() {
    platformDispatcher.onBeginFrame ??= _handleBeginFrame;
    platformDispatcher.onDrawFrame ??= _handleDrawFrame;
  }

这里可以关注下 _handleBeginFrame()_handleDrawFrame()这两个方法,它确保PlatformDispatcher.onBeginFramePlatformDispatcher.onDrawFrame 回调已注册,然后合适的时机进行相关回调,细节就不展开了。

最后执行 platformDispatcher.scheduleFrame(),再来看看这个方法:

  void scheduleFrame() native 'PlatformConfiguration_scheduleFrame';

  @pragma('vm:entry-point')
  void _drawFrame() {
    PlatformDispatcher.instance._drawFrame();
  }

  // Called from the engine, via hooks.dart
  void _drawFrame() {
    _invoke(onDrawFrame, _onDrawFrameZone);
  }

它是一个native方法,会在需要布局绘制下一个帧的适当时机调用SchedulerBinding.handleDrawFrame()方法去绘制新的一帧:

  void handleDrawFrame() {
    assert(_schedulerPhase == SchedulerPhase.midFrameMicrotasks);
    _frameTimelineTask?.finish();
    try {
      // PERSISTENT FRAME CALLBACKS
      _schedulerPhase = SchedulerPhase.persistentCallbacks;
      for (final FrameCallback callback in _persistentCallbacks) {
        _invokeFrameCallback(callback, _currentFrameTimeStamp!);
      }

      // POST-FRAME CALLBACKS
      _schedulerPhase = SchedulerPhase.postFrameCallbacks;
      final List<FrameCallback> localPostFrameCallbacks =
          List<FrameCallback>.of(_postFrameCallbacks);
      _postFrameCallbacks.clear();
      
      for (final FrameCallback callback in localPostFrameCallbacks) {
        _invokeFrameCallback(callback, _currentFrameTimeStamp!);
      }
    } finally {
      _schedulerPhase = SchedulerPhase.idle;
      _frameTimelineTask?.finish();
      _currentFrameTimeStamp = null;
    }
  }

这里去执行了之前注册的回调,最终回调到 WidgetsBinding.drawFrame方法:

 @override
  void drawFrame() {
    assert(!debugBuildingDirtyElements);
    assert(() {
      debugBuildingDirtyElements = true;
      return true;
    }());

    TimingsCallback? firstFrameCallback;
    if (_needToReportFirstFrame) {
      assert(!_firstFrameCompleter.isCompleted);

      firstFrameCallback = (List<FrameTiming> timings) {
        SchedulerBinding.instance.removeTimingsCallback(firstFrameCallback!);
        firstFrameCallback = null;
        _firstFrameCompleter.complete();
      };
      SchedulerBinding.instance.addTimingsCallback(firstFrameCallback!);
    }

    try {
      if (renderViewElement != null) {
        buildOwner!.buildScope(renderViewElement!);
      }
      super.drawFrame();
      buildOwner!.finalizeTree();
    } finally {
     // ...
    }
    _needToReportFirstFrame = false;
    if (firstFrameCallback != null && !sendFramesToEngine) {
      _needToReportFirstFrame = true;
      SchedulerBinding.instance.removeTimingsCallback(firstFrameCallback!);
    }
  }

其中 renderViewElement 不为null的时候则会执行buildOwner!.buildScope(renderViewElement!)方法,再看下 buildScope()方法:

 @pragma('vm:notify-debugger-on-exception')
  void buildScope(Element context, [ VoidCallback? callback ]) {
    if (callback == null && _dirtyElements.isEmpty) {
      return;
    }
    // ... 
    // 省略不重要代码
    try {
      _scheduledFlushDirtyElements = true;
      if (callback != null) {
        assert(_debugStateLocked);
        Element? debugPreviousBuildTarget;
        _dirtyElementsNeedsResorting = false;
        try {
          callback();
        } finally {
          // ...
        }
      }
      _dirtyElements.sort(Element._sort);
      _dirtyElementsNeedsResorting = false;
      int dirtyCount = _dirtyElements.length;
      int index = 0;
      while (index < dirtyCount) {
        final Element element = _dirtyElements[index];
        try {
          element.rebuild();
        } catch (e, stack) {
          // ...
        }
        if (isTimelineTracked) {
          Timeline.finishSync();
        }
        index += 1;
        if (dirtyCount < _dirtyElements.length || 
            _dirtyElementsNeedsResorting!) {
          _dirtyElements.sort(Element._sort);
          _dirtyElementsNeedsResorting = false;
          dirtyCount = _dirtyElements.length;
          while (index > 0 && _dirtyElements[index - 1].dirty) {
            index -= 1;
          }
        }
      }
        return true;
      }());
    } finally {
      for (final Element element in _dirtyElements) {
        assert(element._inDirtyList);
        element._inDirtyList = false;
      }
      _dirtyElements.clear();
      _scheduledFlushDirtyElements = false;
      _dirtyElementsNeedsResorting = null;
  }

在该方法中对 _dirtyElements 进行了判空,如果非空,则会进行排序_dirtyElements.sort(Element._sort),然后循环遍历取出所有“脏” element,最后通过执行 element.rebuild() 重建标记为 dirty 的 element

  @pragma('vm:prefer-inline')
  void rebuild() {
    assert(_lifecycleState != _ElementLifecycle.initial);
    if (_lifecycleState != _ElementLifecycle.active || !_dirty) {
      return;
    }
    assert(_lifecycleState == _ElementLifecycle.active);
    // ...
    performRebuild();
    assert(!_dirty);
  }
在rebuild()中执行了performRebuild(),它最终调用Element也就是StatefulWidget中的build():

  @override
  @pragma('vm:notify-debugger-on-exception')
  void performRebuild() {
    assert(_debugSetAllowIgnoredCallsToMarkNeedsBuild(true));
    Widget? built;
    try {
      built = build();
      debugWidgetBuilderValue(widget, built);
    } catch (e, stack) {
      _debugDoingBuild = false;
    } finally {
      _dirty = false;
      assert(_debugSetAllowIgnoredCallsToMarkNeedsBuild(false));
    }
    try {
      _child = updateChild(_child, built, slot);
      assert(_child != null);
    } catch (e, stack) {
      // ...
      _child = updateChild(null, built, slot);
    }
  }

至此,setState()的工作就完成了,当下一个 VSync 信号到来时,flutter 就会自动帮我们更新那些标记为“脏”的 element 了,重建完成后清理掉“脏”列表中的 element

至于注册 VSync 信号监听的时机是在 engine 层,具体注册 VSync 监听的代码体现在PlatformDispatcher这个类里面,这个类封装于 dart:ui 库中的platform_dispatcher.dart文件中,它主要负责派发从平台过来的各种从平台配置到屏幕和窗口的创建或销毁的事件,里面定义了许多的 native 方法,负责 flutter 与平台底层的交互。flutter 启动 App 时,从入口方法 runApp() 开始执行,然后会调用ensureInitialized()方法去初始化 WidgetsBindingBuildOwner 等对象,当下一个VSync 信号来临时,通过回调方法最终会调用 PlatformDispatcher.scheduleFrame()方法,注册VSync信号监听,然后会调用 PlatformDispatcher.onBeginFrame() PlatformDispatcher.onDrawFrame(),在 onDrawFrame 中会一步步执行 buildlayoutpaintcomposite 等过程。具体就不再细说了,感兴趣的可以去翻一翻这一块源码的详细实现。

四、其他的刷新方式

了解了setState()的刷新原理,我们在自定义一些flutter组件的时候,或者实际开发工作中,就可以很清楚的知道何时怎样刷新我们的组件,既增加了定制组件的灵活性,也减小了性能开销。除了setState()的刷新方式,还有一些其他的轻量一些的局部刷新方式,可以在不同的场景选择合适的刷新方式,来提高刷新渲染效率和用户体验。当然局部刷新最底层的实现都和setState() 是一样的,只是对 setState() 的一层封装,使刷新的开销尽可能的降低。

1、利用GlobalKey进行局部刷新

在一个界面中,通常只需要刷新某个子组件或者某个子组件的部分 UI,这种情况下调用父级State的 setState 方法会造成不必要的资源浪费,此时使用局部刷新就更合适。这种刷新方式是对上面 setState()方法的改进,根本的方法还是 setState(),只不过是通过方法去刷新某个子控件:

GlobalKey<CustomWidgetState> _globalKey = GlobalKey();
int count = 0;

....
CustomWidget(key: _globalKey);
// 更新
_globalKey.currentState.update(count);

2、通过 provider 进行局部刷新

我们也可以使用 provider 来进行局部刷新,provider 是 2019年Google I/O大会上Flutter 官方新推荐的状态管理方式之一。它内部的 DelegateWidget 是一个 StatefulWidget,所以具有生命周期状态感知,其状态共享是使用了 InheritedProvider 这个 InheritedWidget 实现的,配合 ChangeNotifier notifyListeners()可以实现跨组件数据的传递以及状态监听界面自动刷新。官方地址:provider | Flutter Package

provider 刷新流程图

3、通过 StreamBuilder 进行局部刷新

final StreamController _streamController = StreamController<int>();
StreamBuilder<int> (
    stream: _streamController.stream,
    initialData: 0,
    builder: (BuildContext context, AsyncSnapshot<int> snapshot) {}))

// 触发更新
_streamController.sink.add(--count);

这里通过 sink.add 方法向 streamController.sink 中添加一个事件流,StreamBuilder接收到这个 stream 流后,触发 builder 方法,接着去重绘页面。StreamBuilder 其实还有很多的其他用处,不仅仅是简单的局部刷新,例如发起一个网络请求后不断的接收 stream 流事件,可以实现不断地从后端获取最新数据实时刷新页面

类似的组件还有 FutureBuilderAnimatedBuilder,都是官方提供的可以局部刷新的组件。

4、通过 ValueNotifier 进行局部刷新

// 创建
final ValueNotifier<int> _curPageIndex = ValueNotifier(0);
// 更新
_curPageIndex.value = index;
// 使用
ValueListenableBuilder(
  valueListenable: _curPageIndex,
  builder: (BuildContext context, int value, Widget? child) {
    return Text(
      '$value',
      style: TextStyle(color: Colors.white, fontSize: 11.sp),
    );
  },
),

这样当 value 改变后,对应监听的组件会自动更新,而不需要再调用 setState(),其本质也是使用了 ChangeNotifier配合notifyListeners()来实现刷新,有点类似观察者模式。

五、总结

以上介绍了在 flutter 中通过 setState()刷新界面的实现流程,并且介绍了其他几种局部刷新界面的方式。最后我们还需要明确一点,直接使用 setState() 进行更新,只是轻量级的配置信息重新创建,而 ElementRenderObjectState 这些对象一般情况下不会重新创建,只是根据配置信息进行了更新,相比较于首次创建 widget 开销要小,但是毕竟涉及到 UI 界面重绘,还是要合理的使用。在实际开发中,直接使用 setState()时要保持对应的 Widget 尽量小层级尽量低,且回调中的逻辑不能是耗时操作。否则考虑局部刷新的方式,具体可以结合上面几种刷新方式,选择恰当的方式来提升刷新性能和获得更好的用户体验。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/551075.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

pthread_getspecific和pthread_setspecific详解

写在前面 在Linux系统中使用C/C进行多线程编程时&#xff0c;我们遇到最多的就是对同一变量的多线程读写问题&#xff0c;大多情况下遇到这类问题都是通过锁机制来处理&#xff0c;但这对程序的性能带来了很大的影响&#xff0c;当然对于那些系统原生支持原子操作的数据类型来…

【CV】Yolov8:ultralytics目标检测、关键点检测、语义分割

note Yolov8提供了一个全新的 SOTA 模型&#xff0c;包括 P5 640 和 P6 1280 分辨率的目标检测网络和基于 YOLACT 的实例分割模型。和 YOLOv5 一样&#xff0c;基于缩放系数也提供了 N/S/M/L/X 尺度的不同大小模型&#xff0c;用于满足不同场景需求骨干网络和 Neck 部分可能参…

KVM(一)Linux部署KVM及新建虚拟机

目录 一、准备工作 1.1 防火墙、SElinux 二、安装KVM 2.1 yum源 2.2 安装工具包 2.3 安装KVM组件 2.4 查看磁盘/新建目录 2.5 安装Linux GUI可视化界面 三、KVM桌面版新建虚拟机 3.1 挂载目录 3.2 新建raw/qcow2文件 3.3 新建虚拟机 3.4 KVM命令行新建虚拟机 一、…

Linux rootfs

前言 通过《initrd&init进程》我们知道rootfs 是文件系统的根目录&#xff0c;其包含了操作系统所需的所有文件和目录&#xff0c;包括程序、库文件、配置文件、设备文件等&#xff0c;它是系统启动时必须加载的文件系统之一。当系统启动后&#xff0c;内核会首先挂载 roo…

本地Jrebel 许可服务器搭建

一、下载 下载 JrebelLicenseServer.zip 二、解压 解压后进入 bin 目录 三、服务安装|启动|停止|删除|... 安装服务&#xff1a;JrebelLicenseServer.bat install启动服务&#xff1a;JrebelLicenseServer.bat start停止服务&#xff1a;JrebelLicenseServer.bat stop删除服…

【Three.js】第一、二章 入门指南和基础知识

01.介绍 Three.js 非常庞大&#xff0c;你可以用它做无数的事情。 在第一章中&#xff0c;我们将学习所有基础知识&#xff0c;例如创建第一个场景、渲染、添加对象、选择正确的材料、添加纹理、为所有内容制作动画&#xff0c;甚至将其放到网上。有些人可能会觉得这部分有点…

如何轻松掌握接口测试——POST请求和COOKIE使用技巧?

目录 引言 请求方法&#xff1a; POST请求方法&#xff1a;添加资源&#xff08;对服务端已存在的资源也可以做修改和删除操作&#xff09; 实战练习 实战练习2 COOKIE&#xff1a; Status Code&#xff1a;协议状态码 接口文档 结语 引言 对于初学者或者没有接口测试…

[笔记]渗透测试工具Burpsuit《一》Burpsuit介绍

文章目录 前言一、安装配置1.1 环境1.2 安装过程1.3 科技过程 二、常用功能2.1 Manual penetration testing features2.2 Advanced/custom automated attacks2.3 Automated scanning for vulnerabilities2.4 Productivity tools2.5 Extensions 三、拓展功能 前言 Burp Suite(b…

设计原则-单一职责原则

在编程大环境中&#xff0c;评价代码组织方式质量的好坏涉及到各个方面&#xff0c;如代码的可读性、可维护性、可复用性、稳定性等各个方面。而在面向对象语言中也可以通过以下各个方面&#xff1a; 类中方法的设计类中属性的设计类(接口、抽象类、普通类)的设计类与类之间的…

IMU 积分进行航迹推算

IMU 积分进行航迹推算 Reference https://github.com/gaoxiang12/slam_in_autonomous_driving 1.0 递推方程推导 \quad 连续时间内的 IMU 运动学方程&#xff1a; R ˙ R ω ∧ q 1 2 q ω ˙ p ˙ v v ˙ a \dot{\mathbf{R}}\mathbf{R}\omega ^{\wedge} \\ \dot{\mathbf{…

[CTF/网络安全] 攻防世界 weak_auth 解题详析

[CTF/网络安全] 攻防世界 weak_auth 解题详析 弱认证弱认证绕过方法姿势Burp Suite 爆破 总结 题目描述&#xff1a;小宁写了一个登陆验证页面&#xff0c;随手就设了一个密码。 弱认证 weak_auth翻译&#xff1a;弱认证 这个术语通常用来描述一种较弱的安全认证方法或机制&am…

HTML语法、常用标签、表单,CSS选择器。简单登录页面的实现

HTML和CSS粗略介绍 文章目录 HTML和CSS粗略介绍HTML页面第一个HTML页面添加图片和视频 HTML语法规范div标签span标签转义字符 HTML常用标签换行和分割线标题超链接列表元素表格 HTML表单输入框和按钮多行文本 CSS样式CSS选择器input标签选择器id选择器类选择器 组合选择器和优先…

【HackTheBox Bagel】打靶记录

一、namp扫描到5000 8000 22 端口 二、访问8000端口&#xff0c;看到跳转到域名bagel.htb&#xff0c;加入到hosts 看到该url 像文件包含&#xff0c;尝试fuzz一波 尝试找公私钥均未果&#xff0c;找到了cmdline 进一步对其包含 HTTP/1.1 200 OK Server: Werkzeug/2.2.2 …

Java多线程异常处理

文章目录 一. 线程中出现异常的处理1. 线程出现异常的默认行为2. setUncaoughtExceptionHandler()方法处理异常3. setDefaultUncaoughtExceptionHandler()方法进行异常处理 二. 线程组内出现异常 一. 线程中出现异常的处理 1. 线程出现异常的默认行为 当单线程中初出现异常时…

工业缺陷检测数据及代码(附代码)

介绍 目前,基于机器视觉的表面缺陷检测设备已广泛取代人工视觉检测,在包括3C、汽车、家电、机械制造、半导体与电子、化工、制药、航空航天、轻工等多个行业领域得到应用。传统的基于机器视觉的表面缺陷检测方法通常采用常规图像处理算法或人工设计的特征加分类器。一般而言…

【Tomcat下载及使用说明】

&#x1f389;&#x1f389;&#x1f389;点进来你就是我的人了博主主页&#xff1a;&#x1f648;&#x1f648;&#x1f648;戳一戳,欢迎大佬指点! 欢迎志同道合的朋友一起加油喔&#x1f93a;&#x1f93a;&#x1f93a; 目录 1.什么是Tomcat 2.Tomcat下载流程及注意问题 …

Eclipse将代码收缩if/for/try,支持自定义区域收缩

Hi, I’m Shendi Eclipse将代码收缩if/for/try&#xff0c;支持自定义区域收缩 最近忙于给网站增加功能&#xff0c;在使用 Eclipse 编写 Java 代码时发现一个函数内代码过多&#xff0c;并且 if&#xff0c;for&#xff0c;try这种代码块无法收缩&#xff08;在IDEA&#xff0…

【快速入门-简单实现】使用Java实现的单播、组播和广播

说明 TCP是一个面向连接的协议&#xff0c;TCP一定是点对点的,一点是两个主机来建立连接的&#xff0c;基于TCP实现的肯定是单播(但单播还可以使用UDP协议实现)。只有UDP才会使用广播和组播。 Java中的单播、组播和广播可以使用TCP或UDP协议来实现&#xff0c;具体取决于应用程…

【C语言】实现猜数字游戏——随机数

&#x1f6a9;纸上得来终觉浅&#xff0c; 绝知此事要躬行。 &#x1f31f;主页&#xff1a;June-Frost &#x1f680;专栏&#xff1a;C语言 该篇将对 选择与循环语句 进行运用&#xff0c;实现猜数字游戏。 需求&#xff1a;游戏后可以选择再次进行游戏&#xff0c;也可以选择…

【Java-Crawler】HttpClient+Jsoup实现简单爬虫

Java编写网络爬虫 网络爬虫1. 爬虫入门程序 网络爬虫1. 网络爬虫的介绍2. 为什么学习网络爬虫 HttpClient1. Get请求2. 带参数的GET请求3. Post请求4. 带参数的 Post 请求5. 连接池6. 请求参数 Jsoup1. jsoup 介绍2.1 功能1.1-解析url2.2 功能1.2-解析字符串2.3 功能1.3-解析文…