本文基于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. NGINX反向代理,后端服务器获取真实IP

    一般使用中间件做一个反向代理后,后端的web服务器是无法获取到真实的IP地址. 但是生产上,这又是不允许的,那么怎么解决? 1.在NGINX反向代理服务器上进行修改 2.修改后端web服务器配置文件 ...

  2. c语言实现数字的倒序输出

    c语言实现数字的倒序输出, 例如: 输入数值:1234倒序输出: 4321 #include <stdio.h> int main(int argc, char *argv[]) { in ...

  3. java基于OpenCV的人脸识别

    基于Java简单的人脸和人眼识别程序 使用这个程序之前必须先安装配置OpenCV详细教程见:https://www.cnblogs.com/prodigal-son/p/12768948.html 注 ...

  4. Django中修改DATABASES后,执行python manage.py ****报错!UnicodeEncodeError

    Django中修改DATABASES后,执行python manage.py ****报错!UnicodeEncodeError: 'latin-1' codec can't encode chara ...

  5. 重识TCP/IP协议族与HTTP基础

    不忘初心 砥砺前行, Tomorrow Is Another Day ! 本文概要: TCP/IP协议族的网络分层 TCP三次握手四次挥手 Http简介 报文结构 Http的请求方法及状态码 常用的H ...

  6. 虚拟化VMware之虚拟机备份(1)

    之虚拟机备份() 模版:是一种开放,公用.安全的虚拟机压缩格式,通常使用的是扩展名为.ova可以在多个主流虚拟化平台下进行操作 是和 通过技术协作推出的基于磁盘的备份和恢复的新一代解决方案,可靠且易部 ...

  7. centos下的redis一键安装shell脚本

    #!/bin/bash yum install cpp binutils glibc-kernheaders glibc-common glibc-devel gcc make wget #安装依赖库 ...

  8. TestNG测试用例重跑详解及实践优化

    测试用例运行稳定性是自动化质量的一个重要指标,在运行中需要尽可能的剔除非bug造成的测试用例执行失败,对于失败用例进行重跑是常用策略之一.一种重跑策略是所有用例运行结束后对失败用例重跑,另一种重跑策略 ...

  9. C. Game with Chips(陷阱暴力题)

    \(为什么说这是个陷阱呢??\) \(因为不管你脑洞多大,数学多好,都发现会束手无策\) \(每移动一次不知道往哪个方向,不知道先访问哪个点,同时要记录所有点的坐标,记录每个点是否访问过目标点.... ...

  10. 网上流行护眼色的RGB值和颜色代码

    网上流行护眼色的RGB值和颜色代码   绿豆沙色能有效的减轻长时间用电脑的用眼疲劳!色调:85,饱和度:123,亮度:205: RGB颜色红:199,绿:237,蓝:204:十六进制颜色:#C7EDC ...