从mixin机制理解Flutter App启动

文章目录

  • 1-mixin机制
    • 1.1-前言
    • 1.2-mixin特性
  • 2-runApp启动
    • 2.1-binding初始化
    • 2.2-绑定根节点
    • 2.3-绘制热身帧
  • 总结

1-mixin机制

1.1-前言

  转载注明出处:https://juejin.im/post/5efbf499e51d4534b979050c
  mixin机制并非dart独创,在其他前端语言中也有很广泛应用。但对于一个刚开始看Flutter源码的客户端开发来说,各种mixin直接劝退,不得不先恶补下mixin。
  mixin首要特性就是实现函数复用,所以在开始mixin机制解析前,先从第一个问题出发:

  • 怎么实现函数的复用?

  对应面向对象语言来说,通常的做法就是继承,即在基类中实现某个函数,子类继承该基类就可使用函数了。举个例子:狗和鹰都可以移动,通过在基类Animal中实现moveTo方法,Dog和Eagle继承后都能使用moveTo方法了

class Animal {
    void moveTo(){...//Do some thing}
}
class Dog extends Animal{}
class Eagle extends Animal{}

  但通过继承的方式实现函数复用会有另外一个问题。即函数是与基类耦合的,子类继承了基类后就继承了基类的所有方法和属性。如果汽车要复用moveTo方法的话显然继承Animal是不合适的,由此引申出问题2:

  • 怎么才能只复用Animal的moveTo方法而不继承其他方法和属性?

  首先肯定要把moveTo方法从Animal中解耦出来,定义一个接口CommonBehavior来实现。在java8及Kotlin的接口支持函数的默认实现,java8需要default关键字。kotlin接口定义的方法同样支持默认实现,不过为了兼容java之前的版本,采用的是编译时生成一个静态类,通过调用静态类的静态方法moveTo方法来实现。

interface CommonBehavior {
    defalut void moveTo(){...//Do some Thing}
}
class Dog implements CommonBehavior{}

  此外Koltin还可以通过类委托来实现方法复用。除了接口外,还需声明一个moveTo具体实现的委托类BehaviorDelegate。

interface CommonBehavior {
    fun moveTo()
}
class BehaviorDelegate : CommonBehavior {
    override fun moveTo(){...//Do some thing}
}
class Dog : CommonBehavior by BehaviorDelegate()

  Kotlin类委托机制就不再详述了,原理是通过代理实现。Java当然也是可以通过代理实现的,不过没有by这种语法糖用起来爽。转到正题:

  • 在Dart中怎么去实现代码复用呢?

  Dart中没有interface关键字,而是用mixin进行混合,将moveTo抽离到一个mixin修饰的CommonBehavior。这样就能通过混入CommonBehavior直接使用moveTo方法了。

class Animal{}
mixin CommonBehavior{
  moveTo(){...//Do some thing};
}
class Dog extends Animal with CommonBehavior {}

1.2-mixin特性

  实现代码复用只是mixin的基本功能,mixin还有其他强大的特性。
混入多个mixin时会向前覆盖,即后混入的mixin类中的方法会覆盖前面继承或混入的相同方法。我们先来看一个简单的例1

//例1
class SuperClass{
	fun()=>print('SuperClass');
}
mixin MixA{
  fun()=>print('MixA');
}
mixin MixB{
	fun()=>print('MixB'); 
}
class Child extends SuperClass with MixA,MixB {}
main(){
  Child child = Child();
  child.fun();
}

运行后的结果:
MixB
  先混入的MixA含有fun(),覆盖了SuperClass的fun()。而后混入的MixB也有fun(),覆盖了MixA的方法,最终调用的是MixB的fun()方法。由此也可以知道后混入的mixin类的方法是最先调用的。为了验证这一调用顺序我们对例1进行如下改动:

//例2
class SuperClass{
  fun(){
    print('-->SuperClass.fun()');
    print('-->SuperClass');
  }

}
mixin MixA on SuperClass{
  fun(){
    print('-->MixA.fun()');
    super.fun();
    print('-->MixA');
  }
}
mixin MixB on SuperClass{
  fun(){
    print('-->MixB.fun()');
    super.fun();
    print('-->MixB');
  }
}
class Child extends SuperClass with MixA,MixB {}
main(){
  Child child = Child();
  child.fun();
}

输出结果:

-->MixB.fun()
-->MixA.fun()
-->SuperClass.fun()
-->SuperClass
-->MixA
-->MixB

  由输出结果可以看出通过mixin机制的调用关系,在形式上实现了类似"多继承"一样的继承链。
  这里使用了mixin on。mixin MixA on SuperClass 这样支持在MixA中像继承一样通过super来调用SuperClass的方法。同时也限定了要混入MixA的类必须继承自SuperClass。
  在继承关系方面,输出结果给人一种child–>MixB–>MixA–>SuperClass继承关系的错觉,其实不然。混合机制相当于在SuperClass的顶层混入mixin类并生成一个新类,类似于Android中的帧布局SuperClass属于最下层父布局,mixin类属于其中的子元素,mixin类之间并无父子关系相互解耦。后加入的mixin类在“帧布局”中层级越靠上,会覆盖下层的相同位置方法。用伪代码来描述上面例子中的继承关系:

class SuperMixA = SuperClass with MixA;
class SuperMixAMixB = SuperMixA with MixB;
class Child extends SuperMixAMixB {}

  这种"继承链"如下图所示,Child最终继承的是在Super、MixA、MixB的一个混合,Child 的实例child 类型 属于Super、MixA、MixB的混合,用类型判读is得到的结果都是ture。但MixA与MixB直接却并没有直接关系,这也就符合了开闭原则,在不修改Child的基础上通过mixin对其进行扩展。

  我们对例2稍加修改,就更接近Flutter App启动过程的调用关系了:

//例3
class SuperClass{
  SuperClass() {
    print('-->SuperClass init');
    fun();
  }
  fun(){
    print('-->SuperClass.fun() start');
    print('-->SuperClass.fun() end');
  }

}
mixin MixA on SuperClass{
  fun(){
    print('-->MixA.fun() start');
    super.fun();
    print('-->MixA.fun() end');
  }
}
mixin MixB on SuperClass{
  fun(){
    print('-->MixB.fun() start');
    super.fun();
    print('-->MixB.fun() end');
  }
}
class Child extends SuperClass with MixA,MixB {
  Child() {
    print('-->Child init');
  }
}
main(){
  Child child = Child();
}

输出结果:

-->SuperClass init
-->MixB.fun() start
-->MixA.fun() start
-->SuperClass.fun() start
-->SuperClass.fun() end
-->MixA.fun() end
-->MixB.fun() end
-->Child init

  至此,mixin机制的讲解就先告一段落,这些都是便于我们理解第2章讲到的Flutter App启动初始化过程。至于mixin其实还有其他相关特性,没有构造函数、with还可以混入非mixin类等等,这里就不再展开了。

2-runApp启动

  FlutterApp启动过程在Android中主要是从

  • FlutterApplication.onCreate完成加载引擎libflutter.so、注册JNI方法等
  • FlutterActivity.onCreate中通过FlutterJNI的attachJNI来初始化引擎Engine、Dart VM、UI/GPU/IO线程初始化等
  • main.dart 中runApp

  本文主要是结合mixin机制从main.dart中的main()开始,讲解dart层面的初始化启动过程

void main() => runApp(MyApp());

  接着是binding.dart中的runApp(),这里是核心。这里也是runApp启动的三个主流程,我们从这三行代码来一一解析。

void runApp(Widget app) {
  WidgetsFlutterBinding.ensureInitialized()
    ..scheduleAttachRootWidget(app)
    ..scheduleWarmUpFrame();
}

2.1-binding初始化

  WidgetsFlutterBinding.ensureInitialized()其实就是一个获取WidgetsFlutterBinding单例的过程,真正的初始化实现代码在其7个mixin中。7个mixin分别完成不同部分的初始化工作,且根据mixin机制具有严格的先后调用链关系。至于这7个mixin的具体分工我们后面再细说。

class WidgetsFlutterBinding extends BindingBase with GestureBinding, ServicesBinding, SchedulerBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding {
	static WidgetsBinding ensureInitialized() {
    if (WidgetsBinding.instance == null)
      WidgetsFlutterBinding();
    return WidgetsBinding.instance;
  }
}

  WidgetsFlutterBinding继承了BindingBase,而mixin是没有构造函数的。所以先执行了父类BindingBase构造函数。

BindingBase() {
  developer.Timeline.startSync('Framework initialization');
  assert(!_debugInitialized);
  initInstances();
  assert(_debugInitialized);
  assert(!_debugServiceExtensionsRegistered);
  initServiceExtensions();
  assert(_debugServiceExtensionsRegistered);
  developer.postEvent('Flutter.FrameworkInitialization', <String, String>{});
  developer.Timeline.finishSync();
}

  7个mixin都重写了initInstances()方法,BindingBase.initInstances()会从最后混入的WidgetsBinding进行调用,而WidgetsBinding的initInstances函数中先通过super向上调用,属于后续遍历,所以调用顺序和函数逻辑执行顺序是相反的。回过头看看第1章最后的例3,是不是很像。调用链如图:
mixin2.png
  由于是通过super实现了后序遍历的调用,所以函数的逻辑执行顺序是相反的,BindingBase的initInstances先执行,然后是GestureBinding…最后到WidgetsBinding,依次完成了各mixin的相关初始化工作。
  (1)GestureBinding.initInstances 手势事件绑定。进行一些变量初始化。GestureBinding中主要处理触屏幕指针事件的分发以及事件最终回调处理。

void initInstances() {
  super.initInstances();
  _instance = this;
  //将事件处理回调赋值给window,供window收到屏幕指针事件后调用
  window.onPointerDataPacket = _handlePointerDataPacket;
}

  这里将事件处理回调_handlePointerDataPacket赋值给window,供window收到屏幕指针事件后调用。window类似Android中的WindowManager,是framework层与engine层处理屏幕相关事件的桥梁。
  发生屏幕指针事件后会回调window.onPointerDataPacket即这里的_handlePointerDataPacket。_handlePointerDataPacket中会先调用hitTest进行命中测试。GestureBinding及RenderBinding都实现了hitTest方法,按照mixin顺序会优先调用RenderBinding.hitTest。RenderBinding.hitTest会从renderTree的根节点递归调用命中测试,返回命中的深度最大的节点到根节点路径上的所有节点。然后再执行dispatchEvent根据返回的hitTest命中节点列表遍历分发事件,事件分发的顺序是先子节点后父节点最终到根节点,类似前端的事件冒泡机制。
  (2)ServicesBinding.initInstances Flutter与Platform通信服务绑定。

void initInstances() {
  super.initInstances();
  _instance = this;
  //构建一个_DefaultBinaryMessenger实例用于platform与flutter层通信,消息信使
  _defaultBinaryMessenger = createBinaryMessenger();
  //window设置监听回调,处理platform发送的消息
  window.onPlatformMessage = defaultBinaryMessenger.handlePlatformMessage;
  initLicenses();
  //设置处理platform发送的系统消息
  SystemChannels.system.setMessageHandler(handleSystemMessage);
}

  ServicesBinding主要就是platform与flutter层通信相关服务的初始化,BinaryMessenger作为二者之间通信的信使,在这里被初始化,且同样是交给window来处理消息。最后设置处理system消息handleSystemMessage,而ServicesBinding的handleSystemMessage是空实现,PaintingBinding及WidgetsBinding都实现了该方法。调用顺序是WidgetsBinding.handleSystemMessage–>PaintingBinding.handleSystemMessage–>ServicesBinding.handleSystemMessage。同样是通过super后续遍历调用,先在PaintingBinding中处理系统字体变动事件,后在WidgetsBinding中处理系统发送的内存紧张信号。
  (3)SchedulerBinding.initInstances 绘制调度绑定

void initInstances() {
  super.initInstances();
  _instance = this;
  //设置AppLifecycleState生命周期回调
  SystemChannels.lifecycle.setMessageHandler(_handleLifecycleMessage);
  //根据生命周期变化设置window处理回调
  //resumed || inactive状态时才允许响应Vsync信号进行绘制
  readInitialLifecycleStateFromNativeWindow();
  //debug编译模式时统计绘制流程时长,开始、运行、构建、光栅化。
  if (!kReleaseMode) {
    int frameNumber = 0;
    addTimingsCallback((List<FrameTiming> timings) {
      for (final FrameTiming frameTiming in timings) {
        frameNumber += 1;
        _profileFramePostEvent(frameNumber, frameTiming);
      }
    });
  }
}

  SchedulerBinding.initInstances 主要就是注册监听了flutter app的生命周期变化事件,根据生命周期状态决定是否允许发起绘制任务。而SchedulerBinding的作用就是在window监听到Vsync信号后,通过SchedulerBinding来发起绘制任务。
  (4)PaintingBinding 绘制绑定。除了前面讲的监听系统字体变化事件,这里主要是在绘制热身帧之前预热Skia渲染引擎。

void initInstances() {
  super.initInstances();
  _instance = this;
  //初始化图片缓存
  _imageCache = createImageCache();
  if (shaderWarmUp != null) {
    //第一帧绘制前的预热工作
    shaderWarmUp.execute();
  }
}

  (5)SemanticsBinding.initInstances 渲染辅助类绑定。SemanticsBinding主要负责关联语义树与Flutter Engine。

void initInstances() {
  super.initInstances();
  _instance = this;
  _accessibilityFeatures = window.accessibilityFeatures;
}

  (6)RendererBinding.initInstances 渲染绑定,RendererBinding是render tree 与 Flutter engine的粘合剂,因为它持有了render tree的根节点renderView。

void initInstances() {
  super.initInstances();
  _instance = this;
  //初始化PipelineOwner管理渲染流程
  _pipelineOwner = PipelineOwner(
    onNeedVisualUpdate: ensureVisualUpdate,
    onSemanticsOwnerCreated: _handleSemanticsOwnerCreated,
    onSemanticsOwnerDisposed: _handleSemanticsOwnerDisposed,
  );
  //设置window回调,处理屏幕参数、文本缩放因子、亮度等变化时回调。
  window
    ..onMetricsChanged = handleMetricsChanged
    ..onTextScaleFactorChanged = handleTextScaleFactorChanged
    ..onPlatformBrightnessChanged = handlePlatformBrightnessChanged
    ..onSemanticsEnabledChanged = _handleSemanticsEnabledChanged
    ..onSemanticsAction = _handleSemanticsAction;
  //初始化一个RenderView作为render tree的根节点,作为渲染流水线执行入口
  initRenderView();
  //设置是否根据render tree生成语义树
  _handleSemanticsEnabledChanged();
  assert(renderView != null);
  //绘制流水线回调
  addPersistentFrameCallback(_handlePersistentFrameCallback);
  initMouseTracker();//鼠标监听
}

  回过头看看(1)GestureBinding.initInstances方法中的事件处理,调用的就是这里的renderView.hitTest从根节点开始命中测试的。正因为RenderBinding创建并持有了RenderView实例,所以GestureBinding中通过mixin机制将RenderBinding的hitTest方法混入,从而可以实现命中测试,相当于需要用到命中测试的地方都通过mixin委托给RenderBinding来实现了。
  addPersistentFrameCallback将绘制处理回调_handlePersistentFrameCallback加入到Persistent类型回调列表,_handlePersistentFrameCallback中的drawFrame方法是实现绘制流水线的地方,包括布局和绘制流程,后面绘制热身帧会用到。
  (7)WidgetsBinding.initInstances 组件绑定

void initInstances() {
  super.initInstances();
  _instance = this;
  assert(() {
    _debugAddStackFilters();
    return true;
  }());
	//初始化BuildOwnder,处理需要绘制的Element的构建工作
  _buildOwner = BuildOwner();
  //通过SchedulerBinding初始化window的onBeginFrame、onDrawFrame回调
  //如果app可见,通过window.scheduleFrame向engine发起绘制请求
  buildOwner.onBuildScheduled = _handleBuildScheduled;
  //语言环境变化处理
  window.onLocaleChanged = handleLocaleChanged;
  //platform访问权限变化处理
  window.onAccessibilityFeaturesChanged = handleAccessibilityFeaturesChanged;
  //处理系统发送的push/pop页面请求
  SystemChannels.navigation.setMethodCallHandler(_handleNavigationInvocation);
  FlutterErrorDetails.propertiesTransformers.add(transformDebugCreator);
}

  WidgetsBinding属于最外层的mixin,作为处理Widget相关事件的入口。在初始化过程中主要是生成了BuildOwner实例,以及window的onBeginFrame、onDrawFrame回调,后面渲染流程会用到。
  BindingBase先通过按顺序执行7个mixin的initInstances方法,完成了相关初始化工作,以及两个重要类的实例化PipelineOwner、BuildOwner。然后就是执行了initServiceExtensions方法,实现了该方法的mixin按调用顺序为WidgetsBinding–>RendererBinding–>SchedulerBinding–>ServicesBinding主要就是在debug模式下注册相关拓展服务。

2.2-绑定根节点

  ensureInitialized完成后,就开始执行scheduleAttachRootWidget(app)将用户传入的Widget绑定到一个跟节点并构建三棵树。

void scheduleAttachRootWidget(Widget rootWidget) {
  Timer.run(() {
    attachRootWidget(rootWidget);
  });
}

  由于是组件相关,attachRootWidget具体的实现在WidgetsBinding里

void attachRootWidget(Widget rootWidget) {
  _readyToProduceFrames = true;
  _renderViewElement = RenderObjectToWidgetAdapter<RenderBox>(
    container: renderView,
    debugShortDescription: '[root]',
    child: rootWidget,
  ).attachToRenderTree(buildOwner, renderViewElement as RenderObjectToWidgetElement<RenderBox>);
}

  类似Android中将DecorView与ViewRootImpl绑定,通过ViewRootImpl来作为视图操作根节点入口。Flutter中也是将app的主widget(即用户定义的MyApp)和根节点绑定。其中render tree的根节点就是前面初始化流程中RendererBinding.initInstances过程创建的RenderView,RenderView是继承自RenderObject的,所以还需要创建Element和Widget与之关联,而创建的Element和Widget分别对应另外两棵树的根节点。
  (1)先是通过传入的MyApp及RenderView实例化了一个RenderObjectToWidgetAdapter对象,而RenderObjectToWidgetAdapter是继承自RenderObjectWidget,即创建了Widget树的根节点。
  (2)createElement创建根element,并通过BuildOwner构建需要构建的element

RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [ RenderObjectToWidgetElement<T> element ]) {
  if (element == null) {
    owner.lockState(() {
      //创建了一个RenderObjectToWidgetElement实例作为element tree的根节点
      element = createElement();
      assert(element != null);
      //绑定BuildOwner
      element.assignOwner(owner);
    });
    //标记需要构建的element,并rebuild
    owner.buildScope(element, () {
      element.mount(null, null);
    });
    SchedulerBinding.instance.ensureVisualUpdate();
  } else {
    element._newWidget = this;
    element.markNeedsBuild();
  }
  return element;
}

2.3-绘制热身帧

  绑定完根节点后,就开始立即执行scheduleWarmUpFrame()绘制首帧的工作了。前面window.scheduleFrame发起绘制请求是在收到Vsync信号后才开始的,app初始化时为了节省时间并未等待Vsync信号直接开始绘制,所以叫热身Frame。和普通绘制一样,热身帧也是通过handleBeginFrame、handleDrawFrame这两个回调来进行绘制流程,在前面WidgetBinding初始化时将这两个回调交给了window,具体代码逻辑是在SchedulerBinding。

void scheduleWarmUpFrame() {
  if (_warmUpFrame || schedulerPhase != SchedulerPhase.idle)
    return;
  _warmUpFrame = true;
  Timeline.startSync('Warm-up frame');
  final bool hadScheduledFrame = _hasScheduledFrame;
  // Timer任务会加入到event queue
  // 所以在执行绘制前先处理完microtask queue中的任务
  Timer.run(() {
    assert(_warmUpFrame);
		// 绘制Frame前工作,主要是处理Animate动画
    handleBeginFrame(null);
  });
  // 绘制前有机会执行完microtask queue
  Timer.run(() {
    assert(_warmUpFrame);
    // 开始Frame绘制
    handleDrawFrame();
    resetEpoch();
    _warmUpFrame = false;
    if (hadScheduledFrame)
      //后续Frame绘制请求
      scheduleFrame();
  });
  lockEvents(() async {
    await endOfFrame;
    Timeline.finishSync();
  });
}

  handleBeginFrame处理动画相关逻辑,动画回调后并不立即执行动画,而是改变了animation.value,并调用setSate()来发起绘制请求。动画的过程就是在Vsync信号到来时根据动画进度计算出对应的value,而对应的Widget也会随着animation.value的变化而重建,从而形成动画,是不是和Android的属性动画原理差不多。

void handleBeginFrame(Duration rawTimeStamp) {
    ...
    _hasScheduledFrame = false;
    try {
      // 处理回调前设置为瞬态
      _schedulerPhase = SchedulerPhase.transientCallbacks;
      final Map<int, _FrameCallbackEntry> callbacks = _transientCallbacks;
      _transientCallbacks = <int, _FrameCallbackEntry>{};
      //处理Animation回调
      callbacks.forEach((int id, _FrameCallbackEntry callbackEntry) {
        if (!_removedIds.contains(id))
          _invokeFrameCallback(callbackEntry.callback, _currentFrameTimeStamp, callbackEntry.debugStack);
      });
      _removedIds.clear();
    } finally {
        //回调处理完,设置为中间态,即先处理microTask任务队列
      _schedulerPhase = SchedulerPhase.midFrameMicrotasks;
    }
}

  handleBeginFrame处理完后,会优先处理microTask任务队列。然后才是event Task,window.onDrawFrame(),对应SchedulerBinding.handleDrawFrame()。(Timer任务会加入到event queue,flutter的事件处理机制是优先处理micro queue中任务)

void handleDrawFrame() {
    try {
      // 处理Persistent类型回调,主要包括build\layout\draw流程
      _schedulerPhase = SchedulerPhase.persistentCallbacks;
      for (FrameCallback callback in _persistentCallbacks)
        _invokeFrameCallback(callback, _currentFrameTimeStamp);
    
      // 处理Post-Frame回调,主要是状态清理,准备调度下一帧绘制请求
      _schedulerPhase = SchedulerPhase.postFrameCallbacks;
      final List<FrameCallback> localPostFrameCallbacks =
          List<FrameCallback>.from(_postFrameCallbacks);
      _postFrameCallbacks.clear();
      for (FrameCallback callback in localPostFrameCallbacks)
        _invokeFrameCallback(callback, _currentFrameTimeStamp);
    } finally {
        //处理完成,状态idle
      _schedulerPhase = SchedulerPhase.idle;
      _currentFrameTimeStamp = null;
    }
}

  WidgetsBinding.drawFrame()为Persistent类型的一个回调,在前面讲到的RendererBinding初始化时通过addPersistentFrameCallback中加入了RendererBinding.drawFrame,所以这里也是用到了mixin机制,在WidgetsBinding.drawFrame()中完成组件的构建任务,在RendererBinding.drawFrame完成组件的布局、绘制任务。是不是分工明确。

//WidgetsBinding.drawFrame()
void drawFrame() {
   try {
    if (renderViewElement != null)
      //调用BuildOwner.buildScope开始构建
      buildOwner.buildScope(renderViewElement);
      //调用RendererBinding.drawFrame,开始布局、绘制阶段。
    super.drawFrame();
    //从element tree中移除不需要的element,unmount
    buildOwner.finalizeTree();
  } finally {
     ...
  }
}

  绘制流程结束后会产生这一帧的数据Scene,由window.render交给Engine,最终显示到屏幕。整个热身帧绘制流程如图:
绘制.png

总结

(1) mixin机制在FlutterApp启动过程可谓秀的飞起,通过如上分析也得到了mixin机制带来的优势有哪些:

  • 高内聚低耦合。适合应用于需要多个功能模块配合完成的场景,将功能模块通过mixin解耦,各模块职责单一,相互之间不直接引用。
  • 代码复用。通过混入模块,就可以像调用自身方法一样调用混入模块的方法。
  • 保证调用顺序。mixin配合super调用,可以实现同名方法的“继承链”式调用,保证串行执行顺序。


(2)Flutter App的启动过程总结:

  • ensureInitialized 通过7个mixin 按顺序完成相关初始化工作
  • scheduleAttachRootWidget 绑定app 应用启动Widget到根节点,主要是render tree的根节点RenderView,RenderView又关联了widget tree 的根节点和 element tree的根节点
  • scheduleWarmUpFrame 立即完成首帧绘制


参考文章:
[1] Dart: What are mixins?
[2] Dart 2 Mixin Declarations
[3] 彻底理解 Dart mixin 机制

热门文章

暂无图片
编程学习 ·

JAVA中的自定义异常

JAVA中的自定义异常SUN提供的内置的异常肯定是不够用的,在实际开发中我们需要自己自定义一些与业务相关的异常java中如何自定义异常: 第一步:编写一个类继承 Exception 或者 RuntimeException 设定为编译时异常就继承Exception 设定为运行时异常就继承RuntimeException 第二…
暂无图片
编程学习 ·

ubuntu软件安装

ubuntu软件安装apt和apt-getdpkg源码安装 apt和apt-get 在ubuntu安装软件时常用命令apt-get install xxx命令来安装。从字面上理解install即是安装的意思,get即获取的意思,apt此处理解为工具名称,全称 Advanced Packaging Tool(APT)字义是先进的包装工具,但在linux系统中…
暂无图片
编程学习 ·

spring注解式开发

一、@Configuration 声明一个类相当于配置类似于xml的配置文件,声明一个或者多个@Bean方法,并由spring容器管理,以便于在运行中为这些bean生成BeanDefinition和服务请求。 @Configuration //包扫描 @ComponentScan(value = "com.alibaba") }) public class MyConf…
暂无图片
编程学习 ·

ClassName(类名)命名

ClassName命名 ClassName的命名应该尽量精短、明确,必须以字母开头命名,且全部字母为小写,单词之间统一使用下划线 “_” 连接。 eg:.nav_top 注意事项 ad、banner、gg、guanggao 等有机会和广告挂勾的字眠不建议直接用来做ClassName,因为有些浏览器插件(Chrome的广告拦截…
暂无图片
编程学习 ·

Java-eclipse 设置字体字号大小,如何设置居中

文字一般都放在 标签内: 这里以 JLable 与 按钮 为实例: JLabel label; JButton exitBtn; //表题**标签** label = new JLabel("个人停车详情",JLabel.CENTER);//设置字体居中 label.setFont(new Font("微软雅黑", Font.BOLD, 20));//设置字体字号大小 l…
暂无图片
编程学习 ·

718. 最长重复子数组

给两个整数数组 A 和 B ,返回两个数组中公共的、长度最长的子数组的长度。目录1、题目分析2、解题分析3、代码示例 1:输入: A: [1,2,3,2,1] B: [3,2,1,4,7] 输出: 3 解释: 长度最长的公共子数组是 [3, 2, 1]。1、题目分析求两个数组公共的子数组的长度,那么可以用较短的那个…
暂无图片
编程学习 ·

05 Pandas(2)

# Author:Nimo_Dingimport pandas as pd import numpy as np from pandas import Series,DataFrame# 数据表的合并 df1=DataFrame({name:[ZhangFei,GuanYu,a,b,c],data1:range(5) }) df2=DataFrame({name:[ZhangFei,GuanYu,A,B,c],data2:range(5) }) print(\n1、基于指定列进行…
暂无图片
编程学习 ·

使用命令删除node_modules文件夹

先安装删除工具npm install rimraf -g然后使用删除命令rimraf node_modulesnpm install -g 全局安装,以后直接使用删除命令即可。vscode工具,前端项目文件夹下记住这个命令,不要再手动删除目录文件,很慢。
暂无图片
编程学习 ·

3、【STM32F0系列学习】之—中断和事件

【STM32F0系列学习】之—中断和事件1、什么是“中断”2、中断优先级3、中断嵌套4、嵌套向量中断控制器 (NVIC)5、中断与事件的区别和主要特性6、外部中断(EXTI)配置6.1【标准库】的配置方式6.2【HAL库】的配置方式 1、什么是“中断”CPU执行程序时,由于发生了某种随机的事件…
暂无图片
编程学习 ·

基于Token的身份验证的原理

目录1 发展史2 Cookie3 Session3.1 cookie和session的区别4 Token4.1 传统方式——基于服务器的验证4.2 基于服务器验证方式暴露的一些问题4.3 基于Token的验证原理4.5 Tokens的优势1 发展史1、很久很久以前,Web 基本上就是文档的浏览而已, 既然是浏览,作为服务器, 不需要记…
暂无图片
编程学习 ·

Java使用poi将office文件转为html

一、前言 功能需求:上传office文档,并提供文件在线预览。 解决方案:使用Aspose.cells.jar包,将文档转换为pdf格式; 使用libreOffice,将文档转换为pdf格式; 使用poi将文档转换为html格式。方案一:通过Aspose的方式,该功能是付费版,需要破解,所以是能抛弃。 方案二,使…
暂无图片
编程学习 ·

二值化方法

一、全局阈值法1.固定阈值方法该方法是对于输入图像中的所有像素点统一使用同一个固定阈值。其基本思想如下:其中,T为全局阈值。缺点:很难为不同的输入图像确定最佳阈值。2.Otsu算法Otsu算法又称最大类间方差法先明确两个概念:(1)均值(2)方差图像的阈值化处理,就是将图像分为…
暂无图片
编程学习 ·

vue项目实现路由按需加载(路由懒加载)的3种方式

vue异步组件es提案的import()webpack的require,ensure()vue异步组件技术 ==== 异步加载 vue-router配置路由 , 使用vue的异步组件技术 , 可以实现按需加载 . 但是,这种情况下一个组件生成一个js文件/* vue异步组件技术 */ {path: /home,name: home,component: resolve => re…
暂无图片
编程学习 ·

HTML笔记及案例(全)

一、 HTML简介 1.HTML是什么? HTML: HYper Text language超文本 标记(标签)语言有多种标签组成,用来制作网页,告浏览器该如何显示页面 2.HTML语言作用? 制作网页,控制网页、内容的显示 插入图片、音乐、视频、动画等多媒体 通过链接检索信息 使用表单获取用户信息,实现…
暂无图片
编程学习 ·

docker常用命令

1, docker常用命令 #查看本地镜像 docker images #查看镜像历史,httpd为镜像名称 docker history httpd 运行容器:docker start 容器ID|容器名称 停止容器:docker stop 容器ID|容器名称 重启容器:docker restart 容器ID|容器名称 删除容器:docker rm 容器ID|容器名称 删…
暂无图片
编程学习 ·

JVM-面试题

一、什么是Java虚拟机?为什么Java被称作是“平台无关的编程语言”?Java虚拟机是一个可以执行Java字节码的虚拟机进程。Java源文件被编译成能被Java虚拟机执行的字节码文件。 Java被设计成允许应用程序可以运行在任意的平台,而不需要程序员为每一个平台单独重写或者是重新编译…
暂无图片
编程学习 ·

整理得吐血了,二叉树、红黑树、B&B+树超齐全,快速搞定数据结构

前言没有必要过度关注本文中二叉树的增删改导致的结构改变,规则操作什么的了解一下就好,看不下去就跳过,本文过多的XX树操作图片纯粹是为了作为规则记录,该文章主要目的是增强下个人对各种常用XX树的设计及缘由的了解,也从中了解到常用的实现案例使用XX树实现的原因。数据…
暂无图片
编程学习 ·

Metasploit使用msfcli命令行接口编写shell脚本程序

问题描述使用msfcli命令行接口编写一个Shell脚本程序,实现用户只需输入目标Linux靶机IP地址作为参数,就可以使用usermap_script漏洞渗透攻击模块,获得靶机的远程Shell访问。 解决办法由于2005年时便取消了msfcli,因此使用msfconsole -x来代替msfcli。先提供脚本内容如下: …