上一篇,我们介绍了当我们添加一个Entity时,通过Graphics封装其对应参数,通过EntityCollection.Add方法,将EntityCollection的Entity传递到DataSourceDisplay.Visualizer中。本篇则从Visualizer开始,介绍数据的处理,并最终实现渲染的过程。

CesiumWidget.prototype.render = function() {
if (this._canRender) {
this._scene.initializeFrame();
var currentTime = this._clock.tick();
this._scene.render(currentTime);
} else {
this._clock.tick();
}
};

如上,在渲染阶段,分别调用了clock.tick()和scene.render()。在这两个阶段中都有很多跟Entity相关的方面,我们分别阐述其大概过程

Viewer.prototype._onTick

我们先温习一下上篇的两个知识点:DataSourceDisplay初始化的时候会调用defaultVisualizersCallback,会针对所有Geometry的Type创建对应的Visualizer;EntityCollection.Add每次添加一个Entity,会通过一系列事件传递,将该Entity传递到每一个Visualizer,保存到Visualizer中_addedObjects队列中。

function Viewer(container, options) {
eventHelper.add(clock.onTick, Viewer.prototype._onTick, this);
} Viewer.prototype._onTick = function(clock) {
var time = clock.currentTime;
var isUpdated = this._dataSourceDisplay.update(time);
} DataSourceDisplay.prototype.update = function(time) {
visualizers = this._defaultDataSource._visualizers;
vLength = visualizers.length;
for (x = 0; x < vLength; x++) {
result = visualizers[x].update(time) && result;
}
}

如上,Viewer初始化时会绑定clock.onTick事件,确保每一帧都会调用。而其内部则调用DataSourceDisplay.update,进而遍历所有的Visualizer,调用其update方法。下面,我们重点看一下Visualizer.update到底干了哪些事情。为了方便,我们还是以Rectangle为例来展开。

GeometryVisualizer.prototype.update = function(time) {
// 获取添加Entity队列
var addedObjects = this._addedObjects;
var added = addedObjects.values; for (i = added.length - 1; i > -1; i--) {
entity = added[i];
id = entity.id;
// 每一个GeometryVisualizer都绑定一个具体的Updater,用来解析Entity,以Rectangle为例
// Rectangle则由对应的RectangleGeometryUpdater来解析
// 通过new Updater,将Entity对应的RectangleGraphics解析为RectangleGeometryUpdater的GeometryOptions
updater = new this._type(entity, this._scene);
this._updaters.set(id, updater);
// 根据该RectangleGeometryUpdater的材质风格创建对应的GeometryInstance,分到对应的批次队列中
// 每一个批次队列中的Geometry风格相同,因此可以通过一次DrawCommand渲染该队列中所有Geometry
// 目的是减少渲染次数,提高渲染效率
insertUpdaterIntoBatch(this, time, updater);
this._subscriptions.set(id, updater.geometryChanged.addEventListener(GeometryVisualizer._onGeometryChanged, this));
} // 清空添加Entity队列,下次update中就不用重复处理
addedObjects.removeAll(); var isUpdated = true;
var batches = this._batches;
var length = batches.length;
for (i = 0; i < length; i++) {
// 对所有批次队列进行更新
// 根据batch的GeometryInstance创建Primitive,并添加到Scene的PrimitiveCollection中
isUpdated = batches[i].update(time) && isUpdated;
} return isUpdated;
};

如上结合代码和注释,分为三步,下面我们详细介绍一下:

  • new Updater

  • insertUpdaterIntoBatch
  • batch.update

1.new Updater

function RectangleGeometryUpdater(entity, scene) {
this._options = new GeometryOptions(entity);
this._onEntityPropertyChanged(entity, 'rectangle', entity.rectangle, undefined);
}

如上,简单说,Updater的构造函数主要就做了一件事情,构建对应的GeometryOptions,并对其赋值。GeometryOptions是Cesium的一个简单的封装,不同的Updater对应不同的Graphics,GeometryOptions的属性也是根据不同的Graphics量身定做,比如RectangleGeometryUpdater中对应的GeometryOptions属性如下:

function GeometryOptions(entity) {
this.id = entity;
this.vertexFormat = undefined;
this.rectangle = undefined;
this.closeBottom = undefined;
this.closeTop = undefined;
this.height = undefined;
this.extrudedHeight = undefined;
this.granularity = undefined;
this.stRotation = undefined;
this.rotation = undefined;
}

大家可以看看其他的Updater,比如PolygonGeometryUpdater,EllipseGeometryUpdater等等,对应的GeometryOptions也不相同。这样,从设计的角度,将不同Graphics中对应的不同属性,封装成一个标准的GeometryOptions,对外表象一致(都是Updater中的_options属性),内部各自提供解析方法(_onEntityPropertyChanged方法),我们再看一下RectangleGeometryUpdater的_onEntityPropertyChanged实现:

RectangleGeometryUpdater.prototype._onEntityPropertyChanged = function(entity, propertyName, newValue, oldValue) {
var rectangle = this._entity.rectangle; // ……
var height = rectangle.height;
// …… var options = this._options;
options.vertexFormat = isColorMaterial ? PerInstanceColorAppearance.VERTEX_FORMAT : MaterialAppearance.MaterialSupport.TEXTURED.vertexFormat;
options.rectangle = coordinates.getValue(Iso8601.MINIMUM_VALUE, options.rectangle);
options.height = defined(height) ? height.getValue(Iso8601.MINIMUM_VALUE) : undefined;
options.extrudedHeight = defined(extrudedHeight) ? extrudedHeight.getValue(Iso8601.MINIMUM_VALUE) : undefined;
options.granularity = defined(granularity) ? granularity.getValue(Iso8601.MINIMUM_VALUE) : undefined;
options.stRotation = defined(stRotation) ? stRotation.getValue(Iso8601.MINIMUM_VALUE) : undefined;
options.rotation = defined(rotation) ? rotation.getValue(Iso8601.MINIMUM_VALUE) : undefined;
options.closeBottom = defined(closeBottom) ? closeBottom.getValue(Iso8601.MINIMUM_VALUE) : undefined;
options.closeTop = defined(closeTop) ? closeTop.getValue(Iso8601.MINIMUM_VALUE) : undefined;
this._isClosed = defined(extrudedHeight) && defined(options.closeTop) && defined(options.closeBottom) && options.closeTop && options.closeBottom;
this._outlineWidth = defined(outlineWidth) ? outlineWidth.getValue(Iso8601.MINIMUM_VALUE) : 1.0;
this._dynamic = false;
this._geometryChanged.raiseEvent(this);
}

如上是代码片段,this._entity.rectangle为RectangleGraphics类,将其属性赋给_options(GeometryOptions类),属性赋值的过程也是一个自检测的过程,如果存在必要属性缺失的情况则指定一个默认值,最终完成了RectangleGraphics到GeometryOptions的转移。

2.insertUpdaterIntoBatch

function insertUpdaterIntoBatch(that, time, updater) {
if (updater.outlineEnabled) {
that._outlineBatches[shadows].add(time, updater);
} if (updater.fillEnabled) {
if (updater.onTerrain) {
that._groundColorBatch.add(time, updater);
} else {
if (updater.isClosed) {
if (updater.fillMaterialProperty instanceof ColorMaterialProperty) {
that._closedColorBatches[shadows].add(time, updater);
} else {
that._closedMaterialBatches[shadows].add(time, updater);
}
} else {
if (updater.fillMaterialProperty instanceof ColorMaterialProperty) {
that._openColorBatches[shadows].add(time, updater);
} else {
that._openMaterialBatches[shadows].add(time, updater);
}
}
}
}
}

不准确的说(但有助于理解),GeometryOptions主要对应RectangleGraphics的几何数值,而在insertUpdaterIntoBatch中则根据RectangleGraphics的材质风格进行分组,只有材质一致的RectangleGeometryUpdater才能分到一起,进行后面的批次。比如学校分班,优等生,中等生分到不同的班级,老师根据不同班级的能力进行适当的区分,就是一种通过分组的方式来优化的思路。打组批次也是同样一个道理。

针对GeometryVisualizer,一共提供了四种Batch类型:

  • StaticGeometryColorBatch

  • StaticGeometryPerMaterialBatch
  • StaticGroundGeometryColorBatch
  • StaticOutlineGeometryBatch

不同的Batch,根据材质属性的不同,会选择Updater的对应方法,创建GeometryInstance。比如RectangleGeometryUpdater提供了createFillGeometryInstance和createOutlineGeometryInstance两个方法来创建其面和边线对应的GeometryInstance。如下是RectangleGeometryUpdater对应的一种逻辑情况:

StaticOutlineGeometryBatch.prototype.add = function(time, updater) {
// Key 1
var instance = updater.createOutlineGeometryInstance(time);
var width = this._scene.clampLineWidth(updater.outlineWidth);
var batches;
var batch;
if (instance.attributes.color.value[3] === 255) {
batches = this._solidBatches;
batch = batches.get(width);
if (!defined(batch)) {
batch = new Batch(this._primitives, false, width, this._shadows);
batches.set(width, batch);
}
// Key 2
batch.add(updater, instance);
}
};
// Key 1
RectangleGeometryUpdater.prototype.createFillGeometryInstance = function(time) {
return new GeometryInstance({
id : entity,
geometry : new RectangleGeometry(this._options),
attributes : attributes
});
};
// Key 2
Batch.prototype.add = function(updater, instance) {
var id = updater.entity.id;
this.createPrimitive = true;
this.geometry.set(id, instance);
this.updaters.set(id, updater);
};

第一是构建GeometryInstance,这里有一个新的对象RectangleGeometry,之前我们对于Rectangle的理解,都是在Graphics这样的一个概念,可以认为这是一个参数化的对象,对用户而言容易理解。比如一个圆对应的参数化信息就是圆心+半径,我们很好理解,但对计算机,或者WebGL则不能理解,WebGL能理解的是三角形,所以我们就需要把这个圆分解成三角形的拼接,比如切成一块块的西瓜状。将参数化的图形分解成非参数的简单三角形。这个过程是在Primitive.update中完成的,但最终是由RectangleGeometry提供的算法来实现。其他Geometry也是同样的一个逻辑。第二,把创建的GeometryInstance放到Batch队列中。

3.batch.update

之前的步骤1和步骤2,我们对当前这一帧中新增的Entity进行解析,构造成对应的GeometryInstance,放到对应的Batch队列中。比如有两个Rectangle类型的Entity,假设他们的风格一样,都是纯色的,当然颜色可能不相同,但最终都是在一个批次队列(StaticOutlineGeometryBatch)。接下来,1每一个批次队列会构建一个Primitive,包括该队列中所有的GeometryInstances,因为显卡强大的并行能力,绘制一个三角面和绘制N个三角面的所需的时间是一样的(N取决于顶点数),2所以尽可能的将多个Geometry封装成一个VBO是提高渲染性能的一个关键思路(批次&实例化)。而这个batch.update完成的前半部分,而Primitive.update则完成了最后,也是最关键的一步。

function Batch(primitives, translucent, appearanceType, closed, shadows) {
// 每一个Batch中的GeometryInstance队列
this.geometry = new AssociativeArray();
} Batch.prototype.add = function(updater, instance) {
var id = updater.entity.id;
// 添加新的GeometryInstance,并标识,此时需要创建Primitvie
this.createPrimitive = true;
this.geometry.set(id, instance);
}; Batch.prototype.update = function(time) {
var isUpdated = true;
var removedCount = 0;
var primitive = this.primitive;
var primitives = this.primitives;
var attributes;
var i; // 检测需要创建Primitive
if (this.createPrimitive) {
var geometries = this.geometry.values;
var geometriesLength = geometries.length;
if (geometriesLength > 0) {
// 将队列中所有的GeometryInstances封装成一个primitive对象
primitive = new Primitive({
asynchronous : true,
geometryInstances : geometries,
appearance : new this.appearanceType({
translucent : this.translucent,
closed : this.closed
}),
shadows : this.shadows
});
// 将primitive添加到Scene.primitives中
// 既然已经绑定到Scene,接下来就要准备渲染
primitives.add(primitive);
isUpdated = false;
} this.attributes.removeAll();
this.primitive = primitive;
this.createPrimitive = false;
this.waitingOnCreate = true;
}
return isUpdated;
};

如上是this._clock.tick()的一个大概过程,一步一步摩擦,最终创建了Primitive,并添加到PrimitiveCollection队列中,如果以Rectangle类型的Entity为例,大概的流程如下:

DataSourceDisplay.prototype.update
GeometryVisualizer.prototype.update
updater = new this._type(entity, this._scene);
new GeometryOptions(entity);
_onEntityPropertyChanged() insertUpdaterIntoBatch
StaticGeometryColorBatch.prototype.add
RectangleGeometryUpdater.prototype.createFillGeometryInstance
new GeometryInstance()
Batch.prototype.add batches[i].update(time)
StaticGeometryColorBatch.prototype.update
Batch.prototype.update
new Primitive()
primitives.add(primitive);

Primitive.prototype.update

看完了tick()后,马不停蹄的则开始了this._scene.render(currentTime),大概经过下面的几层,最终开始了Primitive.prototype.update,也就是接下来我们介绍的中重点。

this._scene.render(currentTime);
Scene.prototype.render
function render(scene, time)
function updateAndExecuteCommands()
function executeCommandsInViewport()
function updatePrimitives()
PrimitiveCollection.prototype.update()
for (var i = 0; i < primitives.length; ++i) {
primitives[i].update(frameState);
}

如上的铺垫工作结束,进入正文。Primitive.prototype.update究竟做了哪些重要的事情.

var PrimitiveState = {
READY : 0,
CREATING : 1,
CREATED : 2,
COMBINING : 3,
COMBINED : 4,
COMPLETE : 5,
FAILED : 6
};

似曾相识的感觉有没有。在设计上,Primitive和Globe相似,也是基于状态的管理:每个状态都有专门的模块来负责,而每一帧主要用来维护和更新状态,并根据当前的状态来调用对应的模块。我们看看PrimitiveState,里面主要有三类:CREATE,COMBINE,COMPLETE。心里大概有个一知半解,下面来解惑。

Primitive.prototype.update = function(frameState) {
if (this._batchTable.attributes.length > 0) {
this._batchTable.update(frameState);
} if (this._state !== PrimitiveState.COMPLETE && this._state !== PrimitiveState.COMBINED) {
if (this.asynchronous) {
loadAsynchronous(this, frameState);
} else {
loadSynchronous(this, frameState);
}
} if (this._state === PrimitiveState.COMBINED) {
createVertexArray(this, frameState);
} if (!this.show || this._state !== PrimitiveState.COMPLETE) {
return;
} if (createRS) {
var rsFunc = defaultValue(this._createRenderStatesFunction, createRenderStates);
rsFunc(this, context, appearance, twoPasses);
} if (createSP) {
var spFunc = defaultValue(this._createShaderProgramFunction, createShaderProgram);
spFunc(this, frameState, appearance);
} if (createRS || createSP) {
var commandFunc = defaultValue(this._createCommandsFunction, createCommands);
commandFunc(this, appearance, material, translucent, twoPasses, this._colorCommands, this._pickCommands, frameState);
} updateAndQueueCommandsFunc();
}

如上是Primitive.update的主要过程,我们以状态的变化为序,介绍一下loadAsynchronous,createVertexArray以及create*这几个内容。

loadAsynchronous

Primitive初始化时,默认为READY状态。Update方法中,首先会进入loadAsynchronous方法。这里主要做了两个事情:Create&Combine。

function loadAsynchronous(primitive, frameState) {
var instances;
var geometry;
var i;
var j; var instanceIds = primitive._instanceIds; //开始进入createGeometry
if (primitive._state === PrimitiveState.READY) {
instances = (isArray(primitive.geometryInstances)) ? primitive.geometryInstances : [primitive.geometryInstances];
var length = primitive._numberOfInstances = instances.length; var promises = [];
var subTasks = [];
for (i = 0; i < length; ++i) {
geometry = instances[i].geometry;
instanceIds.push(instances[i].id); // 用于处理数据的线程名称
// 需要进行数据处理的geometry对象
subTasks.push({
moduleName : geometry._workerName,
geometry : geometry
});
} // 根据当前浏览器允许的最大线程数,创建N个createGeometry线程,方便后续通过Workers线程处理
if (!defined(createGeometryTaskProcessors)) {
createGeometryTaskProcessors = new Array(numberOfCreationWorkers);
for (i = 0; i < numberOfCreationWorkers; i++) {
createGeometryTaskProcessors[i] = new TaskProcessor('createGeometry', Number.POSITIVE_INFINITY);
}
} // 分摊任务,当前Primitive中可能需要对多个Geometry进行处理
// 平坦任务,均分
var subTask;
subTasks = subdivideArray(subTasks, numberOfCreationWorkers); for (i = 0; i < subTasks.length; i++) {
var packedLength = 0;
var workerSubTasks = subTasks[i];
var workerSubTasksLength = workerSubTasks.length;
for (j = 0; j < workerSubTasksLength; ++j) {
subTask = workerSubTasks[j];
geometry = subTask.geometry;
if (defined(geometry.constructor.pack)) {
subTask.offset = packedLength;
packedLength += defaultValue(geometry.constructor.packedLength, geometry.packedLength);
}
} var subTaskTransferableObjects; // 将Geometry中的参数化信息保存到arraybuffer中
// 方便后续传入到线程
if (packedLength > 0) {
var array = new Float64Array(packedLength);
subTaskTransferableObjects = [array.buffer]; for (j = 0; j < workerSubTasksLength; ++j) {
subTask = workerSubTasks[j];
geometry = subTask.geometry;
if (defined(geometry.constructor.pack)) {
geometry.constructor.pack(geometry, array, subTask.offset);
subTask.geometry = array;
}
}
} // 调用线程,传入参数subTask,subTaskTransferableObjects中以引用方式,非复制
promises.push(createGeometryTaskProcessors[i].scheduleTask({
subTasks : subTasks[i]
}, subTaskTransferableObjects));
} // creating状态,线程中处理
primitive._state = PrimitiveState.CREATING; when.all(promises, function(results) {
// 成功后更新状态,已经创建成功,返回值results
primitive._createGeometryResults = results;
primitive._state = PrimitiveState.CREATED;
}).otherwise(function(error) {
setReady(primitive, frameState, PrimitiveState.FAILED, error);
});
} else if (primitive._state === PrimitiveState.CREATED) {
// 如下,同上面的思路一致,通过combine线程,将多个geometry的返回值合并成一个vbo
var transferableObjects = [];
instances = (isArray(primitive.geometryInstances)) ? primitive.geometryInstances : [primitive.geometryInstances]; var scene3DOnly = frameState.scene3DOnly;
var projection = frameState.mapProjection; var promise = combineGeometryTaskProcessor.scheduleTask(PrimitivePipeline.packCombineGeometryParameters({
createGeometryResults : primitive._createGeometryResults,
instances : instances,
ellipsoid : projection.ellipsoid,
projection : projection,
elementIndexUintSupported : frameState.context.elementIndexUint,
scene3DOnly : scene3DOnly,
vertexCacheOptimize : primitive.vertexCacheOptimize,
compressVertices : primitive.compressVertices,
modelMatrix : primitive.modelMatrix,
createPickOffsets : primitive._createPickOffsets
}, transferableObjects), transferableObjects); primitive._createGeometryResults = undefined;
primitive._state = PrimitiveState.COMBINING; when(promise, function(packedResult) {
var result = PrimitivePipeline.unpackCombineGeometryResults(packedResult);
primitive._geometries = result.geometries;
primitive._attributeLocations = result.attributeLocations;
primitive.modelMatrix = Matrix4.clone(result.modelMatrix, primitive.modelMatrix);
primitive._pickOffsets = result.pickOffsets;
primitive._instanceBoundingSpheres = result.boundingSpheres;
primitive._instanceBoundingSpheresCV = result.boundingSpheresCV; if (defined(primitive._geometries) && primitive._geometries.length > 0) {
primitive._state = PrimitiveState.COMBINED;
} else {
setReady(primitive, frameState, PrimitiveState.FAILED, undefined);
}
}).otherwise(function(error) {
setReady(primitive, frameState, PrimitiveState.FAILED, error);
});
}
}

如上是一段代码示意,从中可见,Create和Combine这类计算量比较大的操作,都是放在线程中进行的,避免阻塞主线程。这样,通过loadAsynchronous函数,将参数化的geometry转化为三角形,同时对同类的geometry合并成一个渲染批次,进而优化了渲染效率。可以说,这一块是Cesium对Geometry处理的核心。此时,状态已经更新为PrimitiveState.COMBINED。

备注:如果对Workers不太了解,可以参考之前写的《Cesium原理篇:4Web Workers剖析

create*

  • createVertexArray
    上面的Geometry已经将数据处理为indexBuffer和vertexBuffer,下面则需要将该数据结合attributes创建为vbo&vao,这个过程就是通过createVertexArray完成

  • createRS
    创建RenderState
  • createSP
    创建ShaderProgram

很明显,渲染主要是数据+风格,当我们满足了geometry的数据部分已经符合WebGL渲染的格式后,结合appearance封装的材质,设置对应的RenderState以及Shader和所需要的参数。最后,我们构造出最终的DrawCommand,添加到DrawCommandList中,完成最终的渲染。这块就不对细节展开了,涉及到Renderer模块的,在之前的Renderer系列都有详细介绍,这里主要介绍了大概的流程。

总结

Entity牵扯到的内容很多,从方便用户使用,到Geometry类型以及风格的多样性,到最终构造出DrawCommand,以及渲染Pass的优先级,里面牵扯的内容非常多,同时出于渲染性能的优化,还要打组批次,搞多线程,里面随便一个点都有很多值得学习,借鉴的地方。

自问如果要自己来做这一套Geometry渲染,首先多线程是必须要设计的,不然性能上负担不起,打组也是一个技术要点,但优先级不是最高。个人可能不会把Add,Updater以及Primitive分的这么细,材质上压根就想不出来该如何做。Cesium在设计上确实很优雅,但这在性能上多少也是有代价的。

终于将大概的过程写完了,总觉得欠了一些内容,有点力不从心。希望能把这个流程的大概介绍清楚,后面可以针对某一个局部细节可以细细钻研,学习里面的技巧,理解其中的设计原委。

Cesium原理篇:7最长的一帧之Entity(下)的更多相关文章

  1. Cesium原理篇:5最长的一帧之影像

    如果把地球比做一个人,地形就相当于这个人的骨骼,而影像就相当于这个人的外表了.之前的几个系列,我们全面的介绍了Cesium的地形内容,详见: Cesium原理篇:1最长的一帧之渲染调度 Cesium原 ...

  2. Cesium原理篇:3最长的一帧之地形(2:高度图)

           这一篇,接着上一篇,内容集中在高度图方式构建地球网格的细节方面.        此时,Globe对每一个切片(GlobeSurfaceTile)创建对应的TileTerrain类,用来维 ...

  3. Cesium原理篇:7最长的一帧之Entity(上)

    之前的最长的一帧系列,我们主要集中在地形和影像服务方面.简单说,之前我们都集中在地球是怎么造出来的,从这一系列开始,我们的目光从GLOBE上解放出来,看看球面上的地物是如何渲染的.本篇也是先开一个头, ...

  4. Cesium原理篇:Property

    之前主要是Entity的一个大概流程,本文主要介绍Cesium的属性,比如defineProperties,Property(ConstantProperty,CallbackProperty,Con ...

  5. Cesium原理篇:3最长的一帧之地形(3:STK)

    有了之前高度图的基础,再介绍STK的地形相对轻松一些.STK的地形是TIN三角网的,基于特征值,坦白说,相比STK而言,高度图属于淘汰技术,但高度图对数据的要求相对简单,而且支持实时构建网格,STK具 ...

  6. Cesium原理篇:1最长的一帧之渲染调度

    原计划开始着手地形系列,但发现如果想要从逻辑上彻底了解地形相关的细节,那还是需要了解Cesium的数据调度过程,这样才能更好的理解,因此,打算先整体介绍一下Cesium的渲染过程,然后在过渡到其中的两 ...

  7. Cesium原理篇:3最长的一帧之地形(1)

    前面我们从宏观上分析了Cesium的整体调度以及网格方面的内容,通过前两篇,读者应该可以比较清楚的明白一个Tile是怎么来的吧(如果还不明白全是我的错).接下来,在前两篇的基础上,我们着重讨论一下地形 ...

  8. Cesium原理篇:3最长的一帧之地形(4:重采样)

           地形部分的原理介绍的差不多了,但之前还有一个刻意忽略的地方,就是地形的重采样.通俗的讲,如果当前Tile没有地形数据的话,则会从他父类的地形数据中取它所对应的四分之一的地形数据.打个比方 ...

  9. Cesium原理篇:2最长的一帧之网格划分

    上一篇我们从宏观上介绍了Cesium的渲染过程,本章延续上一章的内容,详细介绍一下Cesium网格划分的一些细节,包括如下几个方面: 流程 Tile四叉树的构建 LOD 流程 首先,通过上篇的类关系描 ...

随机推荐

  1. HDU--航海舰队

    海舰队 Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)Total Submiss ...

  2. android ADT 无法查看第三方jar源代码

    Source not foundThe JAR of this class file belongs to container 'Android Private Libraries' which do ...

  3. 方维 o2o app源码出售

    方维 o2o app源码出售 方维o2oapp源码出售 1.本人官方5万购买,现把方维o2o app 源码低价出售: 2.包括网站源码本地搭建包成功提供指导 3.包括网站说明文档,不包含app说明文档 ...

  4. 修改sql数据库文件 物理文件名称

    -- 允许配置高级选项 EXEC sp_configure 'show advanced options', 1 GO -- 重新配置 RECONFIGURE GO -- 启用xp_cmdshell ...

  5. for循环后面跟分号 - for (i = 0; i <= 3; i++);这不是错误语句

    #include<iostream> int main() { using namespace std; ; ; i <= ; i++); t = t + i; cout <& ...

  6. Union-Find 检测无向图有无环路算法

    不相交集合数据结构(Disjoint-set data structure)是一种用于跟踪集合被分割成多个不相交的子集合的数据结构,每个集合通过一个代表来标识,代表即集合中的某个成员. Union-F ...

  7. Javascript本质第二篇:执行上下文

    在上一篇文章<Javascript本质第一篇:核心概念>中,对Javascript执行上下文做了解释,但是这些都是基于Javascript标准中对执行上下文的定义,也就是说理论上的东西,本 ...

  8. ASP.NET MVC 5 - 控制器

    MVC代表: 模型-视图-控制器 .MVC是一个架构良好并且易于测试和易于维护的开发模式.基于MVC模式的应用程序包含: · Models: 表示该应用程序的数据并使用验证逻辑来强制实施业务规则的数据 ...

  9. EF优缺点的理解

    原先用的是三层架构中ADO.NET做底层开发,纯手工sql语句拼装.后来遇到一个MVC+EF项目,体会到了EF的强大性. 它是微软封装好一种ADO.NET数据实体模型,将数据库结构以ORM模式映射到应 ...

  10. WCF 安全性 之 自定义证书验证

    案例下载 http://download.csdn.net/detail/woxpp/4113172 客户端调用代码 通过代理类 代理生成 参见 http://www.cnblogs.com/woxp ...