本文基于1.12.13+hotfix.8版本源码分析。

一、RenderBox的用法

1、RenderBox的使用基本流程

在flutter中,我们最常接触的,莫过于各种各样的widget了,但是,实际负责渲染的RenderObject是很少接触的(它们之间的关联可以看看闲鱼的这篇文章:https://www.yuque.com/xytech/flutter/tge705)。而作为一名天天向上的程序员,我们自然要去学习一下它的原理,做到知其然且知其所以然。本文会先来看看RenderBox的用法,以此抛砖引玉,便于后面继续深入flutter的绘制原理。

使用RenderBox进行绘制,我们需要做三件事:

(1)测量

第一步,我们需要确定视图大小,并赋值给父类的size属性。测量有两种情况,第一种是size由自身决定,第二种是由parent决定。

首先,由自身决定size的情况,需要在performLayout方法中完成测量,通过父类的constraints可得到满足约束的值:

  @override
void performLayout() {
size = Size(
constraints.constrainWidth(200),
constraints.constrainHeight(200),
);
}

第二种情况,size由parent决定,这种情况下视图大小应该完全通过parent提供的constraints测量,不存在其它因素。这种情况下,只要parent的约束不发生变化,就不会重新测量。

这种情况需要重写sizedByParent并返回true,然后在performResize中完成测量。

  @override
void performResize() {
size = Size(
constraints.constrainWidth(200),
constraints.constrainHeight(200),
);
} @override
bool get sizedByParent => true;

看到这里,你可能会疑惑了,这两个方法什么时候会被调用?顺序是怎样的?答案在RenderObject的layout方法中:

  void layout(Constraints constraints, { bool parentUsesSize = false }) {
//计算relayoutBoundary
......
//layout
_constraints = constraints;
if (sizedByParent) {
performResize();
}
performLayout();
......
}
}

(2)绘制

RenderBox的绘制与android原生的view绘制非常相似,同样是Paint+Canvas的组合,而且api也非常接近,会非常容易上手。

  @override
void paint(PaintingContext context, Offset offset) {
Paint paint = Paint()
..color = _color
..style = PaintingStyle.fill;
context.canvas.drawRect(
Rect.fromLTRB(
0,
0,
size.width,
size.height,
),
paint);
}

这样是不是就万事大吉了呢?如果通过上面的代码进行绘制,你会发现,不管在外层怎么设置位置,绘制出来的矩形都是固定在屏幕左上角的!怎么回事?

这里就是flutter中绘制与android的最大不同:在这里绘制的坐标系是全局坐标系,即原点在屏幕左上角,而非视图左上角。

细心的同学可能已经发现,paint方法中还有一个offset参数,这就是经过parent的约束后,当前视图的偏移量,绘制时应该将它考虑进去:

  @override
void paint(PaintingContext context, Offset offset) {
Paint paint = Paint()
..color = _color
..style = PaintingStyle.fill;
context.canvas.drawRect(
Rect.fromLTRB(
offset.dx,
offset.dy,
offset.dx + size.width,
offset.dy + size.height,
),
paint);
}

(3)更新

在flutter中,是由Widget的配置发生变更而引起的rebuild,而这就是我们要实现的第三步:当视图属性发生变更时,标记重新布局或重新绘制,当屏幕刷新时就会做相应的刷新。

这里涉及到两个方法:markNeedsLayout、markNeedsPaint。顾名思义,前者标记重布局,后者标记重绘。

我们需要做的,就是根据属性的影响范围,在更新属性时,调用合适的标记方法,例如color变化时调用markNeedsPaint,width变化时调用markNeedsLayout。另外,两者都需要更新的情况下,只调用markNeedsLayout即可,不需要两个方法都调。

  set width(double width) {
if (width != _width) {
_width = width;
markNeedsLayout();
}
} set color(Color color) {
if (color != _color) {
_color = color;
markNeedsPaint();
}
}

2、RenderObjectWidget

(1)简介

上面讲了一大堆RenderBox的用法,但是,这玩意儿怎么用到我们熟知的Widget里面去?

按照正常流程,我们得实现一个Element和一个Widget,然后在Widget中创建Element,在Element中创建和更新RenderObject,另外还得管理一大堆状态,处理非常繁琐。所幸flutter为我们封装了这一套逻辑,即RenderObjectWidget。

相信看到这里的同学都对StatelessWidget和StatefulWidget不会陌生,但其实,StatelessWidget和StatefulWidget仅负责属性、生命周期等的管理,在它们的build方法实现中都会创建RenderObjectWidget,通过它来实现与RenderObject的关联。

举个栗子,我们经常使用的Image是个StatefulWidget,对应的state的build方法中实际返回了一个RawImage对象,而这个RawImage是继承自LeafRenderObjectWidget的,这正是RenderObjectWidget的一个子类;再比如Text,它build方法中创建的RichText是继承自MultiChildRenderObjectWidget,这同样是RenderObjectWidget的一个子类。

我们再看看RenderObjectWidget顶部的注释即可明白:

RenderObjectWidgets provide the configuration for [RenderObjectElement]s,
which wrap [RenderObject]s, which provide the actual rendering of the
application.

大概意思就是RenderObject才是实际负责渲染应用的,而RenderObjectWidget提供包装了RenderObject的配置,方便我们使用。

另外,flutter还分别实现了几个子类,进一步封装了RenderObjectWidget,它们分别是LeafRenderObjectWidget、SingleChildRenderObjectWidget、MultiChildRenderObjectWidget。其中,LeafRenderObjectWidget是叶节点,不含子Widget;SingleChildRenderObjectWidget仅有一个child;而MultiChildRenderObjectWidget则是含有children列表。这几个子类根据child的情况分别创建了对应的Element,所以通过这几个子类,我们只需要关注RenderObject的创建和更新。

(2)用法

以最简单的LeafRenderObjectWidget为例,我们需要实现createRenderObject、updateRenderObject两个方法:

  class CustomRenderWidget extends LeafRenderObjectWidget {
CustomRenderWidget({
this.width = 0,
this.height = 0,
this.color,
}); final double width;
final double height;
final Color color; @override
RenderObject createRenderObject(BuildContext context) {
return CustomRenderBox(width, height, color);
} @override
void updateRenderObject(BuildContext context, RenderObject renderObject) {
CustomRenderBox renderBox = renderObject as CustomRenderBox;
renderBox
..width = width
..height = height
..color = color;
}
}

3、非容器控件的hitTest

通过上面的内容,我们已经可以实现自定义控件并用到界面开发中,但是距离一个完整的控件还差最后一步:命中测试。当用户使用手势,flutter会将手势信息交由控件进行检查是否命中。

RenderBox中命中测试的方法有仨:hitTest、hitTestSelf、hitTestChildren,其中hitTest默认实现是调用另外两个方法的:

  bool hitTest(BoxHitTestResult result, { @required Offset position }) {
if (_size.contains(position)) {
// 从这里也能看到,当命中children时,不会再进行自身的命中测试
if (hitTestChildren(result, position: position) || hitTestSelf(position)) {
result.add(BoxHitTestEntry(this, position));
return true;
}
}
return false;
}

所以重写命中测试方法有两个方案,一是重写hitTest,这种方法需要将命中测试的信息加到BoxHitTestResult中;二是重写hitTestSelf和hitTestChildren,这种方法就简单地返回是否命中即可。

非容器类型的控件,只需要重写hitTestSelf,返回true即命中,例如RawImage中:

  @override
bool hitTestSelf(Offset position) => true;

二、容器类型的RenderBox

1、介绍

在绘制篇中,我们已经了解到如何使用RenderObjectWidget和RenderBox进行基础的绘制,在本篇中,我们将继续学习RenderBox如何管理子对象。首先,我们来看看RenderBox顶部的一段注释:

For render objects with children, there are four possible scenarios:
* A single [RenderBox] child. In this scenario, consider inheriting from
[RenderProxyBox] (if the render object sizes itself to match the child) or
[RenderShiftedBox] (if the child will be smaller than the box and the box
will align the child inside itself).
* A single child, but it isn't a [RenderBox]. Use the
[RenderObjectWithChildMixin] mixin.
* A single list of children. Use the [ContainerRenderObjectMixin] mixin.
* A more complicated child model.

从上面我们可以了解到,带有子对象的情况有四种:

(1)子对象只有一个,并且是RenderBox的子类。如果当前视图需要根据子对象调整大小,则继承RenderProxyBox;如果子对象小于当前视图,且在当前视图内部对齐,则继承RenderShiftedBox(想一下Align会好理解一点);

(2)子对象只有一个,且非RenderBox子类,这种情况使用RenderObjectWithChildMixin;

(3)有多个子对象则使用ContainerRenderObjectMixin;

(4)更复杂的情况。

第四种情况是要用非链表的children结构时需要考虑的,比如children要用map或list等结构,这种情况需要继承RenderObject去实现一套绘制协议,我们这里暂且先不讨论。

而前三种情况其实注释里的描述不够明确,其实情况只有两种,第一是带有单一的child,第二是带有一个children列表,上面的第一第二两种情况其实可以合并为一种,为什么这么说呢?看下去吧~

2、单个子对象

(1)RenderProxyBox

这种情况其实就是当前容器没有跟大小相关的属性,size由子类决定,具体逻辑flutter已经在RenderProxyBoxMixin实现了,我们来看看:

  void performLayout() {
if (child != null) {
child.layout(constraints, parentUsesSize: true);
size = child.size;
} else {
performResize();
}
}

逻辑非常简单,如果有child,则直接使用child的size;如果没有,就走performResize,而这里并没有实现performResize,即走RenderBox的默认实现,取约束的最小值:

  void performResize() {
size = constraints.smallest;
assert(size.isFinite);
}

而绘制方法中,通过PaintingContext的paintChild方法,即可绘制child:

  @override
void paint(PaintingContext context, Offset offset) {
if (child != null)
context.paintChild(child, offset);
}

(2)RenderShiftedBox

这种情况则与RenderProxyBox相反,即当前容器有跟大小相关的属性,比如padding。接下来就以非常常见的Padding为例,看看RenderPadding的布局方法:

  @override
void performLayout() {
// 将padding的值按照语言方向解析
_resolve();
assert(_resolvedPadding != null);
if (child == null) {
// 如果没有child,就按照垂直、水平方向的padding值计算得出size
size = constraints.constrain(Size(
_resolvedPadding.left + _resolvedPadding.right,
_resolvedPadding.top + _resolvedPadding.bottom,
));
return;
}
// 如果有child,则将当前约束减去padding值以后,再传给child进行测量
final BoxConstraints innerConstraints = constraints.deflate(_resolvedPadding);
child.layout(innerConstraints, parentUsesSize: true); // 测量完毕以后,计算出坐标偏移量,提供给child绘制时使用
// parentData是RenderObject的属性,提供给父布局使用,用来存取child在父布局中的一些信息,包括位置等
final BoxParentData childParentData = child.parentData;
childParentData.offset = Offset(_resolvedPadding.left, _resolvedPadding.top); // 最后得出大小是padding加上child的大小
size = constraints.constrain(Size(
_resolvedPadding.left + child.size.width + _resolvedPadding.right,
_resolvedPadding.top + child.size.height + _resolvedPadding.bottom,
));
}

可以看到,这里有三个关键步骤:第一,根据属性将约束减去需要额外占用的宽高,然后传给child进行测量;第二,测量完毕后计算出child需要用到的绘制偏移量;第三,根据属性和child的size得出总宽高。

另外,RenderShiftedBox的paint方法逻辑与RenderProxyBox稍微有点不同,会对offset进行处理:

  @override
void paint(PaintingContext context, Offset offset) {
if (child != null) {
final BoxParentData childParentData = child.parentData;
context.paintChild(child, childParentData.offset + offset);
}
}

(3)RenderObjectWithChildMixin

回到上面的问题,为什么说RenderBox和非RenderBox的单一子对象是一样的呢?其实,RenderProxyBox和RenderShiftedBox是专门为RenderBox的子类再封装了一层便于使用,它们本身还是with了RenderObjectWithChildMixin:

class RenderProxyBox extends RenderBox with RenderObjectWithChildMixin<RenderBox>, RenderProxyBoxMixin<RenderBox> {
/// 略
} abstract class RenderShiftedBox extends RenderBox with RenderObjectWithChildMixin<RenderBox> {
/// 略
}

经过前面的分析,我们知道RenderProxyBox和RenderShiftedBox只负责测量和绘制,那么RenderObjectWithChildMixin是做什么的呢?借助Android Studio的Structure窗口,我们可以看到:

除去debug的方法以外,这个类方法并不多。以attach为例:

  @override
void attach(PipelineOwner owner) {
super.attach(owner);
if (_child != null)
_child.attach(owner);
}

代码很少,就是在上层attach过来时,再attach自己的child,这里就涉及到渲染树的知识点,这又是另一个话题了,现在我也还没看到这里,后续我们再来分析这玩意儿~

一言蔽之,RenderObjectWithChildMixin实现了与渲染树相关的child的管理。

(4)SingleChildRenderObjectWidget

同样,定义完RenderBox以后,需要在一个Widget中进行创建,单个child的情况我们可以使用SingleChildRenderObjectWidget,与LeafRenderObjectWidget不同的地方在于需要在构造函数将child传入:

class CustomRenderWidget extends SingleChildRenderObjectWidget {
CustomRenderWidget(Widget child) : super(child: child);
}

3、多个子对象

(1)ContainerRenderObjectMixin

相对于上面只有单个child的情况,多个子对象的情况稍微复杂一点,但也只是一点,其实区别不太大。同样,关于与渲染树相关的子对象管理,flutter也是提供了一个ContainerRenderObjectMixin,这里我们就不再分析它的原理了,只需要注意一个地方,当RenderBox被创建时,需要调一下addAll方法将children加入:

  RenderListBody({
List<RenderBox> children,
AxisDirection axisDirection = AxisDirection.down,
}) : assert(axisDirection != null),
_axisDirection = axisDirection {
// 把children交给ContainerRenderObjectMixin管理
addAll(children);
}

(2)ContainerParentDataMixin

另外,ContainerDefaultsMixin指定了使用的ParentData必须是ContainerParentDataMixin的子类。ContainerParentDataMixin并不复杂,它的作用仅仅是实现了双向链表结构的ParentData:

mixin ContainerParentDataMixin<ChildType extends RenderObject> on ParentData {
ChildType previousSibling;
ChildType nextSibling;
}

指定了ParentData的类型后,还需要在RenderBox的setupParentData检查child使用的data类型是否符合,不符合则重新创建并替换:

  @override
void setupParentData(RenderObject child) {
super.setupParentData(child);
if (child.parentData is! MultiChildLayoutParentData) {
child.parentData = MultiChildLayoutParentData();
}
}

(3)案例源码分析

下面我们再以RenderStack为例,看看它的测量(函数写得有点长,但分段看挺容易理解的):

  @override
void performLayout() {
// 根据textDirection解析alignment
_resolve();
assert(_resolvedAlignment != null);
_hasVisualOverflow = false;
bool hasNonPositionedChildren = false; // 如果没有子对象,stack会充满父布局
if (childCount == 0) {
size = constraints.biggest;
assert(size.isFinite);
return;
} double width = constraints.minWidth;
double height = constraints.minHeight; // 根据fit属性调整约束
BoxConstraints nonPositionedConstraints;
assert(fit != null);
switch (fit) {
case StackFit.loose:
nonPositionedConstraints = constraints.loosen();
break;
case StackFit.expand:
nonPositionedConstraints = BoxConstraints.tight(constraints.biggest);
break;
case StackFit.passthrough:
nonPositionedConstraints = constraints;
break;
}
assert(nonPositionedConstraints != null); // 遍历所有没有通过Positioned指定位置或大小的子对象,进行布局
RenderBox child = firstChild;
while (child != null) {
final StackParentData childParentData = child.parentData; if (!childParentData.isPositioned) {
hasNonPositionedChildren = true; // 这种情况通过根据fit转换后的约束测量子对象
child.layout(nonPositionedConstraints, parentUsesSize: true); // 测量完以后对比大小取最大值
final Size childSize = child.size;
width = math.max(width, childSize.width);
height = math.max(height, childSize.height);
} child = childParentData.nextSibling;
} if (hasNonPositionedChildren) {
// 如果存在没用Positioned指定位置或大小的子对象,则取这些子对象的最大size(上面测量后得到的)
size = Size(width, height);
assert(size.width == constraints.constrainWidth(width));
assert(size.height == constraints.constrainHeight(height));
} else {
// 否则充满父布局
size = constraints.biggest;
} assert(size.isFinite); // 遍历计算约束、offset
child = firstChild;
while (child != null) {
final StackParentData childParentData = child.parentData; if (!childParentData.isPositioned) { // 没指定位置或大小,则根据alignment来计算offset
childParentData.offset = _resolvedAlignment.alongOffset(size - child.size); } else {
BoxConstraints childConstraints = const BoxConstraints(); if (childParentData.left != null && childParentData.right != null)
// 指定了left和right,根据stack的宽度算出child的宽度
childConstraints = childConstraints.tighten(width: size.width - childParentData.right - childParentData.left);
else if (childParentData.width != null)
// 这里直接指定了宽度
childConstraints = childConstraints.tighten(width: childParentData.width); // 跟上面逻辑一样
if (childParentData.top != null && childParentData.bottom != null)
childConstraints = childConstraints.tighten(height: size.height - childParentData.bottom - childParentData.top);
else if (childParentData.height != null)
childConstraints = childConstraints.tighten(height: childParentData.height); // 测量child
child.layout(childConstraints, parentUsesSize: true); // 计算offset
double x;
if (childParentData.left != null) {
x = childParentData.left;
} else if (childParentData.right != null) {
x = size.width - childParentData.right - child.size.width;
} else {
x = _resolvedAlignment.alongOffset(size - child.size).dx;
} if (x < 0.0 || x + child.size.width > size.width)
// 标记溢出,在paint的时候会用
_hasVisualOverflow = true; double y;
if (childParentData.top != null) {
y = childParentData.top;
} else if (childParentData.bottom != null) {
y = size.height - childParentData.bottom - child.size.height;
} else {
y = _resolvedAlignment.alongOffset(size - child.size).dy;
} if (y < 0.0 || y + child.size.height > size.height)
_hasVisualOverflow = true; childParentData.offset = Offset(x, y);
} assert(child.parentData == childParentData);
child = childParentData.nextSibling;
}
}

抽丝剥茧以后,不难理解,其实多个子对象和单个子对象本质上是一样的,提供子对象约束让它进行测量,然后根据测量结果决定自己的size,最后再计算子对象绘制的offset。就这样~

最后再看看绘制方法:

  @protected
void paintStack(PaintingContext context, Offset offset) {
// 其它情况则直接使用RenderBoxContainerDefaultsMixin提供的默认绘制方法
defaultPaint(context, offset);
} @override
void paint(PaintingContext context, Offset offset) {
// 处理方式为clip时,溢出部分裁减掉,_hasVisualOverflow在上面计算offset时进行了标记
if (_overflow == Overflow.clip && _hasVisualOverflow) {
context.pushClipRect(needsCompositing, offset, Offset.zero & size, paintStack);
} else {
paintStack(context, offset);
}
}

4、getXxxIntrinsicXxx和computeXxxIntrinsicXxx的作用、用法

细心的同学可能会发现,实现了performLayout的类中都重写了一系列compute开头的方法,另外也会有些地方调用了getMaxIntrinsicWidth等几个"get系列"的方法。从名字上看,这几个方法分别是用来计算和获取最大最小宽高的,但按照我们前面的说法,直接在performLayout或performResize中通过constrains计算宽高也可以,那么这几个方法有什么作用?跟我们前面的做法又有什么区别呢?别着急,接下来我们就来解开这些疑惑。

根据getMinIntrinsicWidth方法的注释,可以得出几个要点:

(1)getMinIntrinsicWidth用来获取能够完整绘制所有内容的最小宽度;

(2)这个方法是给父布局使用的,如果父布局调用了某个child的这个方法,当child调用markNeedsLayout时,父布局也会被通知刷新;

(3)这个方法的算法复杂的是O(N^2),所以非必要的情况不要用它;

(4)不要重写这个方法,有需要的话重写computeMinIntrinsicWidth。

结合这些说明,情况基本明确了。compute系列的方法是需要重写,并计算返回相应的大小;而get系列的方法则是提供给父布局使用,让父布局能够在child测量前就知道child的size。这么实现的原因是规避android原生那种measure两次的问题,详情可以看看闲鱼这篇文章:https://zhuanlan.zhihu.com/p/90195812

5、容器类控件的hitTest

相对于非容器类的控件,容器控件的命中测试需要额外考虑child的命中情况,结合上述内容,我们只需要实现hitTestChildren即可,不过需要注意一点,这个方法接收的postion需要是相对于当前控件的(即原点在当前控件左上角),在对child进行命中测试前,我们需要把position转成原点在child左上角的相对坐标位置。 HitTestResult类提供的一些方法会帮助我们完成这个转换。我们来看看RenderBoxContainerDefaultsMixin中的默认实现:

  bool defaultHitTestChildren(BoxHitTestResult result, { Offset position }) {
ChildType child = lastChild;
while (child != null) {
final ParentDataType childParentData = child.parentData; // addWithPaintOffset会将根据offset将position转换成child的相对位置
final bool isHit = result.addWithPaintOffset(
offset: childParentData.offset,
position: position,
hitTest: (BoxHitTestResult result, Offset transformed) {
// 这里的offset已经经过转换
return child.hitTest(result, position: transformed);
},
);
if (isHit)
return true;
child = childParentData.previousSibling;
}
return false;
}

举个栗子,一个宽高为200的正方形容器中,装有一个宽高为100的小正方形,小正方形位于容器右下角:

这个时候childParentData中offset是(100,100),假设点击到正方形容器的左上角,那么容器的hitTestChildren方法拿到的position为(0,0),经过转换后,小正方形的hitTest方法中拿到的postion就应该是(-100, -100)。

RenderBox使用说明书&原理浅析的更多相关文章

  1. HTTP长连接和短连接原理浅析

    原文出自:HTTP长连接和短连接原理浅析

  2. Javascript自执行匿名函数(function() { })()的原理浅析

    匿名函数就是没有函数名的函数.这篇文章主要介绍了Javascript自执行匿名函数(function() { })()的原理浅析的相关资料,需要的朋友可以参考下 函数是JavaScript中最灵活的一 ...

  3. [转帖]Git数据存储的原理浅析

    Git数据存储的原理浅析 https://segmentfault.com/a/1190000016320008   写作背景 进来在闲暇的时间里在看一些关系P2P网络的拓扑发现的内容,重点关注了Ma ...

  4. Android-Binder原理浅析

    Android-Binder原理浅析 学习自 <Android开发艺术探索> 写在前头 在上一章,我们简单的了解了一下Binder并且通过 AIDL完成了一个IPC的DEMO.你可能会好奇 ...

  5. Dubbo学习(一) Dubbo原理浅析

    一.初入Dubbo Dubbo学习文档: http://dubbo.incubator.apache.org/books/dubbo-user-book/ http://dubbo.incubator ...

  6. 沉淀,再出发:docker的原理浅析

    沉淀,再出发:docker的原理浅析 一.前言 在我们使用docker的时候,很多情况下我们对于一些概念的理解是停留在名称和用法的地步,如果更进一步理解了docker的本质,我们的技术一定会有质的进步 ...

  7. 阻塞和唤醒线程——LockSupport功能简介及原理浅析

    目录 1.LockSupport功能简介 1.1 使用wait,notify阻塞唤醒线程 1.2 使用LockSupport阻塞唤醒线程 2. LockSupport的其他特色 2.1 可以先唤醒线程 ...

  8. 【Spark Core】TaskScheduler源代码与任务提交原理浅析2

    引言 上一节<TaskScheduler源代码与任务提交原理浅析1>介绍了TaskScheduler的创建过程,在这一节中,我将承接<Stage生成和Stage源代码浅析>中的 ...

  9. vue的双向绑定原理浅析与简单实现

    很久之前看过vue的一些原理,对其中的双向绑定原理也有一定程度上的了解,只是最近才在项目上使用vue,这才决定好好了解下vue的实现原理,因此这里对vue的双向绑定原理进行浅析,并做一个简单的实现. ...

随机推荐

  1. 关于XSS弹窗的小姿势

    最近快比赛了想刷刷题,做合天XSS进阶的时候遇到了过滤了alert然后还要弹窗效果的题目,这让我这个JS只学了一点点的菜鸡倍感无力.     在百度了其他资料后,发现confirm('xss')和pr ...

  2. opencv-5-图像遍历与图像改变

    opencv-5-图像遍历与图像改变 opencvc++qt 目录 目录 开始 图像的像素点访问与遍历 opencv 座标定义 下标访问 指针访问 迭代器法访问 遍历访问时间对比 图像操作 图像叠加 ...

  3. 非阻塞同步机制和CAS

    目录 什么是非阻塞同步 悲观锁和乐观锁 CAS 非阻塞同步机制和CAS 我们知道在java 5之前同步是通过Synchronized关键字来实现的,在java 5之后,java.util.concur ...

  4. 数据挖掘入门系列教程(十二)之使用keras构建CNN网络识别CIFAR10

    简介 在上一篇博客:数据挖掘入门系列教程(十一点五)之CNN网络介绍中,介绍了CNN的工作原理和工作流程,在这一篇博客,将具体的使用代码来说明如何使用keras构建一个CNN网络来对CIFAR-10数 ...

  5. npm小工具、技巧合集:让你的npm“健步如飞”

    1.解决安装速度慢问题-nrm 解决痛点 由于npm的包仓库是在国外,npm包安装速度较慢,部分包甚至无法安装. 对比cnpm的优势 1.cnpm增加了大脑的记忆和思维负担,常常需要考虑使用npm还是 ...

  6. Bubble Cup 11 - Finals [Online Mirror, Div. 1]题解 【待补】

    Bubble Cup 11 - Finals [Online Mirror, Div. 1] 一场很好玩的题啊! I. Palindrome Pairs 枚举哪种字符出现奇数次. G. AI robo ...

  7. Codeforces Round #561 (Div. 2) A. Silent Classroom(贪心)

    A. Silent Classroom time limit per test1 second memory limit per test256 megabytes inputstandard inp ...

  8. 洛谷 2016 战略游戏(树形DP)

    题目描述 Bob喜欢玩电脑游戏,特别是战略游戏.但是他经常无法找到快速玩过游戏的办法.现在他有个问题. 他要建立一个古城堡,城堡中的路形成一棵树.他要在这棵树的结点上放置最少数目的士兵,使得这些士兵能 ...

  9. python(time 模块)

    一.Time 模块 1.时间戳 时间戳是指格林威治时间 1970年01月01日00时00分00秒(北京时间1970年01月01日08时00分00秒) 起至现在的总秒数 最早出现的 UNIX 操作系统考 ...

  10. Centos7.x 装机优化

    Linux 服务器装机后优化 参考 https://blog.csdn.net/u010133338/article/details/81055475 优化初始化脚本 vim init_optimiz ...