显示标签为“WebKit”的博文。显示所有博文
显示标签为“WebKit”的博文。显示所有博文

2008年12月26日星期五

WebKit网页布局实现之布局篇

在我们对WebKit网页布局实现所涉及的主要概念及数据结构有了一定的理解之后,让我们再来看看其布局过程的具体实现。

一、FrameView::layout方法
FrameView作为与View相关的类,其主要涉及与显示相关的内容,而其中对页面元素的布局至关重要,这也是浏览器的核心处理部分。

我们都知道浏览器从Web服务器获得数据后,经解析会构建DOM树、Render树,然后进行布局处理,进而为渲染页面作好准备,其中的布局处理往往由FrameView::layout方法发起,让我们来具体看看其实现,一窥其主要实现过程。
void FrameView::layout(bool allowSubtree)
{
if (d->m_midLayout)
return;
// Always ensure our style info is up-to-date. This can happen in situations where
// the layout beats any sort of style recalc update that needs to occur.
//进行CSSStyleSelector的更新处理,因为一旦CSS发生变化,布局的结果也可能发生相关变化,所以在开始布局之前,需要检查CSS是否发生变化,如果有则需要作相应调整,进而可能影响Render树等。
bool subtree = d->m_layoutRoot;
.......................................................................
RenderObject* root = subtree ? d->m_layoutRoot : document->renderer();
if (!root) {
// FIXME: Do we need to set m_size here?
d->m_layoutSchedulingEnabled = true;
return;
}
//布局的处理可能相互嵌套,这与发起布局处理的时机相关。
d->m_nestedLayoutCount++;
ScrollbarMode hMode = d->m_hmode;
ScrollbarMode vMode = d->m_vmode;

d->m_doFullRepaint = !subtree && (d->m_firstLayout || static_cast(root)->printing());

if (!subtree) {
// Now set our scrollbar state for the layout.
ScrollbarMode currentHMode = hScrollbarMode();
ScrollbarMode currentVMode = vScrollbarMode();
//对于初次布局,则需要设置FrameView的滚动条信息等
if (d->m_firstLayout || (hMode != currentHMode || vMode != currentVMode)) {
suppressScrollbars(true);
if (d->m_firstLayout) {
d->m_firstLayout = false;
d->m_firstLayoutCallbackPending = true;
d->m_lastLayoutSize = IntSize(width(), height());
d->m_lastZoomFactor = root->style()->zoom();

// Set the initial vMode to AlwaysOn if we're auto.
if (vMode == ScrollbarAuto)
ScrollView::setVScrollbarMode(ScrollbarAlwaysOn); // This causes a vertical scrollbar to appear.
// Set the initial hMode to AlwaysOff if we're auto.
if (hMode == ScrollbarAuto)
ScrollView::setHScrollbarMode(ScrollbarAlwaysOff); // This causes a horizontal scrollbar to disappear.
}

if (hMode == vMode)
ScrollView::setScrollbarsMode(hMode);
else {
ScrollView::setHScrollbarMode(hMode);
ScrollView::setVScrollbarMode(vMode);
}

suppressScrollbars(false, true);
}

IntSize oldSize = m_size;

m_size = IntSize(visibleWidth(), visibleHeight());

if (oldSize != m_size)
d->m_doFullRepaint = true;
}
//root往往为RenderView对象
RenderLayer* layer = root->enclosingLayer();
......................................
d->m_midLayout = true;
beginDeferredRepaints();
root->layout();
endDeferredRepaints();
d->m_midLayout = false;
.......................................
d->m_layoutSchedulingEnabled = true;

if (!subtree && !static_cast(root)->printing())
adjustViewSize();

// Now update the positions of all layers.
//对当前Render树布局完后,设置RenderLayer树的布局信息,其中d->m_doFullRepaint描述是否需要发起渲染处理。
beginDeferredRepaints();
layer->updateLayerPositions(d->m_doFullRepaint);///it's very important for RenderLayer to set m_x/m_y/m_width/m_height
endDeferredRepaints();
//因为在布局的过程中,可能进一步获得网页数据,则需要继续布局处理。
if (needsLayout()) {
// Post-layout widget updates or an event handler made us need layout again.
// Lay out again, but this time defer widget updates and event dispatch until after
// we return.
........................
layout();
}
}

FrameView::layout方法,简单的说来就是发起对Render树中的每一个节点按照从父节点到子节点的方式进行x、y、width、height计算,当每一个树节点的位置及大小确定之后就可以进行后面的渲染。

FrameView::layout往往会调用Render树根的layout方法即RenderView::layout。

二、RenderView::layout方法
void RenderView::layout()
{
if (printing())
m_minPrefWidth = m_maxPrefWidth = m_width;

// Use calcWidth/Height to get the new width/height, since this will take the full page zoom factor into account.
bool relayoutChildren = !printing() && (!m_frameView || m_width != viewWidth() || m_height != viewHeight());
if (relayoutChildren)
setChildNeedsLayout(true, false);

ASSERT(!m_layoutState);
LayoutState state;
// FIXME: May be better to push a clip and avoid issuing offscreen repaints.
state.m_clipped = false;
m_layoutState = &state;

if (needsLayout())
RenderBlock::layout();//类继承的好处,直接调用父类的layout

// Ensure that docWidth() >= width() and docHeight() >= height().
setOverflowWidth(m_width);
setOverflowHeight(m_height);

setOverflowWidth(docWidth());
setOverflowHeight(docHeight());

ASSERT(m_layoutStateDisableCount == 0);
ASSERT(m_layoutState == &state);
m_layoutState = 0;
setNeedsLayout(false);
}

void RenderBlock::layout()
{
// Update our first letter info now.
updateFirstLetter();
// Table cells call layoutBlock directly, so don't add any logic here. Put code into
// layoutBlock().
layoutBlock(false);

// It's safe to check for control clip here, since controls can never be table cells.
if (hasControlClip()) {
// Because of the lightweight clip, there can never be any overflow from children.
m_overflowWidth = m_width;
m_overflowHeight = m_height;
m_overflowLeft = 0;
m_overflowTop = 0;
}
}

三、RenderBlock::layoutBlock方法
void RenderBlock::layoutBlock(bool relayoutChildren)
{
......................................................
calcWidth();//先计算宽度
calcColumnWidth();

m_overflowWidth = m_width;
m_overflowLeft = 0;
if (oldWidth != m_width || oldColumnWidth != desiredColumnWidth())
relayoutChildren = true;
clearFloats();

int previousHeight = m_height;
m_height = 0;
m_overflowHeight = 0;
..................................................
//这就是在布局基本概念中提到的Block-level元素的子节点要么是Block-level元素要么为Inline-level元素。
if (childrenInline())
layoutInlineChildren(relayoutChildren, repaintTop, repaintBottom);
else
layoutBlockChildren(relayoutChildren, maxFloatBottom);

// Expand our intrinsic height to encompass floats.
int toAdd = borderBottom() + paddingBottom() + horizontalScrollbarHeight();
if (floatBottom() > (m_height - toAdd) && (isInlineBlockOrInlineTable() || isFloatingOrPositioned() || hasOverflowClip() ||
(parent() && parent()->isFlexibleBox() || m_hasColumns)))
m_height = floatBottom() + toAdd;

// Now lay out our columns within this intrinsic height, since they can slightly affect the intrinsic height as
// we adjust for clean column breaks.
int singleColumnBottom = layoutColumns();

// Calculate our new height.//布局完子节点后确定父节点高度
int oldHeight = m_height;
calcHeight();
....................................................
if (previousHeight != m_height)
relayoutChildren = true;
...................................................
if ((isCell || isInline() || isFloatingOrPositioned() || isRoot()) && !hasOverflowClip() && !hasControlClip())
addVisualOverflow(floatRect());
//另外布局属性为Fixed和absolute的元素
layoutPositionedObjects(relayoutChildren || isRoot());

// Always ensure our overflow width/height are at least as large as our width/height.
m_overflowWidth = max(m_overflowWidth, m_width);
m_overflowHeight = max(m_overflowHeight, m_height);

......................................................

// Update our scroll information if we're overflow:auto/scroll/hidden now that we know if
// we overflow or not.
if (hasOverflowClip())
m_layer->updateScrollInfoAfterLayout();//also is important..

// Repaint with our new bounds if they are different from our old bounds.
bool didFullRepaint = false;
//布局后根据条件确定是否发起渲染处理
if (checkForRepaint)
didFullRepaint = repaintAfterLayoutIfNeeded(oldBounds, oldOutlineBox);
..........................................................
// Make sure the rect is still non-empty after intersecting for overflow above
if (!repaintRect.isEmpty()) {
repaintRectangle(repaintRect); // We need to do a partial repaint of our content.
if (hasReflection())
layer()->reflection()->repaintRectangle(repaintRect);
}
}
setNeedsLayout(false);
}

四、RenderBlock::layoutBlockChildren方法
void RenderBlock::layoutBlockChildren(bool relayoutChildren, int& maxFloatBottom)
{
int top = borderTop() + paddingTop();
int bottom = borderBottom() + paddingBottom() + horizontalScrollbarHeight();

m_height = m_overflowHeight = top;
//遍历子节点
RenderObject* child = firstChild();
while (child) {
if (legend == child) {
child = child->nextSibling();
continue; // Skip the legend, since it has already been positioned up in the fieldset's border.
}
.........................................
// Handle the four types of special elements first. These include positioned content, floating content, compacts and
// run-ins. When we encounter these four types of objects, we don't actually lay them out as normal flow blocks.
bool handled = false;
RenderObject* next = handleSpecialChild(child, marginInfo, compactInfo, handled);
if (handled) { child = next; continue; }

// The child is a normal flow object. Compute its vertical margins now.
child->calcVerticalMargins();
// Do not allow a collapse if the margin top collapse style is set to SEPARATE.

// Try to guess our correct y position. In most cases this guess will
// be correct. Only if we're wrong (when we compute the real y position)
// will we have to potentially relayout.
int yPosEstimate = estimateVerticalPosition(child, marginInfo);

// Cache our old rect so that we can dirty the proper repaint rects if the child moves.
IntRect oldRect(child->xPos(), child->yPos() , child->width(), child->height());

// Go ahead and position the child as though it didn't collapse with the top.
view()->addLayoutDelta(IntSize(0, child->yPos() - yPosEstimate));
//先确定x、y。
child->setPos(child->xPos(), yPosEstimate);
..........................................
bool childNeededLayout = child->needsLayout();
if (childNeededLayout)
child->layout();//子节点进行布局处理
...................................................
// Now place the child in the correct horizontal position
determineHorizontalPosition(child);
// Update our height now that the child has been placed in the correct position.
m_height += child->height();
if (child->style()->marginBottomCollapse() == MSEPARATE) {
m_height += child->marginBottom();
marginInfo.clearMargin();
}
.........................................................
// Update our overflow in case the child spills out the block.
..........................................................
child = child->nextSibling();
}
// Now do the handling of the bottom of the block, adding in our bottom border/padding and
// determining the correct collapsed bottom margin information.
handleBottomOfBlock(top, bottom, marginInfo);
}

五、RenderBlock::layoutInlineChildren方法
这个方法相当复杂,其作用就是布局文字、图像等,对文字行高确定、断行等处理,同时还包括 文字从左到右或从右到左的布局处理。具体可以参考bidi.cpp中的源码实现。

六、调用FrameView::layout方法的时机
由于从Web服务器获取的网页数据不可能一次性完成,往往需要边获取数据,边布局,然后渲染,这样才可能获得良好的用户感受。

所以一旦获得主要数据如css数据及body等标签后,就可以开始布局,布局完后会根据当前条件决定是否将布局的数据渲染出来,或者继续布局处理后来获取的数据,这样也增加了布局处理过程的复杂度。

而调用layout方法的时机也至关重要,因为layout本身就可能需要花费大量的时间如layoutBlockChildren、layoutInlineChildren等处理,其往往与网页的内容有关,而网页的内容却由网页开发者来确定,对浏览器来讲是千变万化的,这就对layout方法的实现及调用时机提出更高的要求,同时确定了其复杂性。

调用layout的时机主要有获得一定DOM文档数据后调用Document::updateLayout()、需要重新使用CSS数据时调用Document::recalcStyle()、改变窗口大小后调用Frame::forceLayout()等来实现。。。

七、总结
其实WebKit涉及网页布局方面的layout方法蛮复杂的,如其他RenderObject子类也会根据自身情况重载实现layout,还有对float、fixed、absolute、inline元素等的处理,但其主要逻辑就象上面所提,这里只是汇总一下主要流程及概念,针对每一个具体标签或RenderObject的布局实现则需要更深一步的了解,希望大家能对了解WebKit的网页布局过程有一个清晰而准确的认识。。

八、参考资源
The WebKit Open Source Project

2008年12月18日星期四

WebKit网页布局实现之主要数据结构篇之二

上一篇WebKit网页布局实现之主要数据结构篇中,我们对WebKit网页布局所涉及的主要数据结构有了初步的了解,认识到Render树的构成及CSS属性的描述RenderStyle等,对主要 RenderObject基类及子类有了一定的了解,但在构成Render树时所涉及的一些关键方法还未具体描述,同时还有其他一些为布局及渲染所准备的数据结构也需要有更深入的理解,下面将进一步的了解相关数据结构及相关方法。

一、继续Render树的构成
1、子类RenderButton
RenderButton代表html中input标签type为button时对应的Render树节点,它直接继承自RenderFlexibleBox;
RenderFlexibleBox代表能按居中、左对齐、右对齐等水平或垂直方向布局子节点的树节点;
RenderButton主要数据成员

图一

其中m_buttonText为button上的文字对应的树节点,而m_inner为添加m_buttonText时创建的匿名对象,以便于居中等处理等。这些成员的创建来自于方法updateFromElement;
void RenderButton::updateFromElement()
{
// If we're an input element, we may need to change our button text.
if (element()->hasTagName(inputTag)) {
HTMLInputElement* input = static_cast(element());
String value = input->valueWithDefault();
setText(value);
}
}
void RenderButton::setText(const String& str)
{
..........................
m_buttonText = new (renderArena()) RenderTextFragment(document(), str.impl());
m_buttonText->setStyle(style());
addChild(m_buttonText);
......................
}
void RenderButton::addChild(RenderObject* newChild, RenderObject* beforeChild)
{
if (!m_inner) {
// Create an anonymous block.
m_inner = createAnonymousBlock();
m_inner->style()->setBoxFlex(1.0f);
RenderFlexibleBox::addChild(m_inner);
}
m_inner->addChild(newChild, beforeChild);
}
在缺省的html.css中对应button的css属性如下:
input[type="button"] {
-webkit-appearance: push-button;
white-space: pre
}
input[type="button"]{
-webkit-box-align: center;
text-align: center;
cursor: default;
color: ButtonText;
padding: 2px 6px 3px 6px;
border: 2px outset ButtonFace;
background-color: ButtonFace;
-webkit-box-sizing: border-box
}
这 些css属性通过CSSStyleSelector::applyProperty方法来设定其成员m_RenderStyle对应的值,其中包含 m_style->setAppearance(PushButtonAppearance);尤其值得关注,其初步决定了button是如何画出 来的。。

2、子类RenderTextControl

RenderTextControl代表html中input标签type为text或textarea标签对应的Render树节点,它直接继承自RenderBlock;
RenderTextControl主要数据成员

图二
其中成员m_multiLine以描述是textarea或text input;m_innerText为其中包括的文字对应的树节点;当作搜索按钮时
m_cancelButton/m_resultsButton为对应的树节点;这些成员的创建来自于方法updateFromElement;
void RenderTextControl::updateFromElement()
{
HTMLFormControlElement* element = static_cast(node());

createSubtreeIfNeeded();

...................................

m_innerText->renderer()->style()->setUserModify(element->isReadOnlyControl() || element->disabled() ? READ_ONLY : READ_WRITE_PLAINTEXT_ONLY);

if ((!element->valueMatchesRenderer() || m_multiLine) && !m_placeholderVisible) {
String value;
if (m_multiLine)
value = static_cast(element)->value();
else
value = static_cast(element)->value();
if (value.isNull())
value = "";
else
value = value.replace('\\', backslashAsCurrencySymbol());
if (value != text() || !m_innerText->hasChildNodes()) {
if (value != text()) {
if (Frame* frame = document()->frame())
frame->editor()->clearUndoRedoOperations();
}
ExceptionCode ec = 0;
m_innerText->setInnerText(value, ec);
if (value.endsWith("\n") || value.endsWith("\r"))
m_innerText->appendChild(new HTMLBRElement(document()), ec);
m_dirty = false;
m_userEdited = false;
}
....................................
}
....................................
}
void RenderTextControl::createSubtreeIfNeeded()
{
............................................
if (!m_innerText) {
m_innerText = new HTMLTextFieldInnerTextElement(document(), m_innerBlock ? 0 : node());
RenderTextControlInnerBlock* textBlockRenderer = new (renderArena()) RenderTextControlInnerBlock(m_innerText.get());
m_innerText->setRenderer(textBlockRenderer);
m_innerText->setAttached();
m_innerText->setInDocument(true);

RenderStyle* parentStyle = style();
if (m_innerBlock)
parentStyle = m_innerBlock->renderer()->style();
RenderStyle* textBlockStyle = createInnerTextStyle(parentStyle);
textBlockRenderer->setStyle(textBlockStyle);

// Add text block renderer to Render tree
if (m_innerBlock) {
m_innerBlock->renderer()->addChild(textBlockRenderer);
ExceptionCode ec = 0;
// Add text block to the DOM
m_innerBlock->appendChild(m_innerText, ec);
} else
RenderBlock::addChild(textBlockRenderer);
}
.................................
}
在缺省的html.css中对应标签的css属性如下:
textarea {
-webkit-appearance: textarea;
background-color: white;
border: 1px solid;
-webkit-rtl-ordering: logical;
-webkit-user-select: text;
-webkit-box-orient: vertical;
resize: auto;
cursor: auto;
}
input, input[type="password"], input[type="search"], isindex {
-webkit-appearance: textfield;
padding: 1px;
background-color: white;
border: 2px inset;
-webkit-rtl-ordering: logical;
-webkit-user-select: text;
cursor: auto;
}
其中-webkit-appearance属性分别为textarea、textfield;

3、子类RenderListBox
RenderListBox代表html中select标签对应的Render树节点,它直接继承自RenderBlock;
RenderListBox主要数据成员

图三
其相关成员同样通过方法updateFromElement来设置初值;
在缺省的html.css中对应标签的css属性如下:
select {
-webkit-appearance: menulist;
-webkit-box-sizing: border-box;
-webkit-box-align: center;
border: 1px solid;
..............................................................
}

4、子类RenderTheme
RenderTheme在html标签中没有对应的页面元素,其作用主要用于如何渲染按钮、输入框、列表框等,其实现往往有一定平台相关性。
RenderTheme主要数据成员及方法

图四
RenderTheme 往往提供一个接口,不同的图形库对其中不同的方法如paintbutton、paintcheckbox、painttextfield等进行了实现;其中 paint方法则根据appearance属性的不同以分别调用不同的paintxxx方法,其示例代码如下:
bool RenderTheme::paint(RenderObject* o, const RenderObject::PaintInfo& paintInfo, const IntRect& r)
{
........................................
if (paintInfo.context->paintingDisabled())
return false;
// Call the appropriate paint method based off the appearance value.
switch (o->style()->appearance()) {
case CheckboxAppearance:
return paintCheckbox(o, paintInfo, r);
case RadioAppearance:
return paintRadio(o, paintInfo, r);
case PushButtonAppearance:
case SquareButtonAppearance:
case DefaultButtonAppearance:
case ButtonAppearance:
return paintButton(o, paintInfo, r);
case MenulistAppearance:
return paintMenuList(o, paintInfo, r);
break;
....................................................
default:
break;
}
return true; // We don't support the appearance, so let the normal background/border paint.
}
其中的appearance就是上面RenderButton、RenderTextControl、RenderListBox中提到的属性,至于 html中涉及到的类似标签或属性如radio、checkbox等等,其相关代码基本类似,至于不同的平台如Qt、Gtk、Win、Mac等究竟是如何画按钮、下拉框、列表框、多选框、单选框等等,则需详细参考RenderThemeQt/RenderThemeGtk/RenderThemeWin /RenderThemeMac等中的实现。

通过上述的了解我们应该对html中form标签内的输入框、按钮、下拉框等实现有了一定的认识。

5、子类RenderTable、RenderTableRow、RenderTableCol、RenderTableCell
这一组子类主要对应与html中table标签相关的树节点;
Table标签相关类主要数据成员

图五
RenderTable主要通过addChild方法来维护对RenderTableCell、RenderTableCol、RenderTableRow等对象的管理及维护;
void RenderTableCell::updateFromElement()
{
Node* node = element();
if (node && (node->hasTagName(tdTag) || node->hasTagName(thTag))) {
HTMLTableCellElement* tc = static_cast(node);
int oldRSpan = m_rowSpan;
int oldCSpan = m_columnSpan;

m_columnSpan = tc->colSpan();
m_rowSpan = tc->rowSpan();
if ((oldRSpan != m_rowSpan || oldCSpan != m_columnSpan) && style() && parent()) {
setNeedsLayoutAndPrefWidthsRecalc();
if (section())
section()->setNeedsCellRecalc();
}
}
}

void RenderTableCol::updateFromElement()
{
int oldSpan = m_span;
Node* node = element();
if (node && (node->hasTagName(colTag) || node->hasTagName(colgroupTag))) {
HTMLTableColElement* tc = static_cast(node);
m_span = tc->span();
} else
m_span = !(style() && style()->display() == TABLE_COLUMN_GROUP);
if (m_span != oldSpan && style() && parent())
setNeedsLayoutAndPrefWidthsRecalc();
}
RenderTableRow通过方法layout和paint方法来布局管理RenderTableCell对象;
这一组子类主要实现人们熟知的表格布局,具体的实现可具体参考相关类实现;

6、子类RenderFrame
RenderFrame代表html中标签frame对应的Render树节点,其继承关系如下:
RenderFrame类继承关系


图六

其中属性m_widget、m_view代表frame对应的widget及frameview,通过其中setwidget方法来设置m_widget属性,m_view属性则在对象创建的时候设置为当前document对应的frameview。

其中html中的embed/object插件标签对应的Render树节点为RenderPartObject对象。

7、构建Render树
从上一篇中我们了解到构建Render树的基本实现流程如下:void Element::attach()=>createRendererIfNeeded()=>createRenderer;以前我们着重了解过createRenderer,现在我们回头再来看看createRendererIfNeeded(),以更深入的了解是如何构建Render树。
void Node::createRendererIfNeeded()
{
if (!document()->shouldCreateRenderers())
return;
Node *parent = parentNode();
RenderObject *parentRenderer = parent->renderer();
if (parentRenderer && parentRenderer->canHaveChildren()
#if ENABLE(SVG)
&& parent->childShouldCreateRenderer(this)
#endif
) {
RenderStyle* style = styleForRenderer(parentRenderer);
if (rendererIsNeeded(style)) {
if (RenderObject* r = createRenderer(document()->renderArena(), style)) {
if (!parentRenderer->isChildAllowed(r, style))
r->destroy();
else {
setRenderer(r);
renderer()->setAnimatableStyle(style);
parentRenderer->addChild(renderer(), nextRenderer());
}
}
}
style->deref(document()->renderArena());
}
}

RenderStyle* Element::styleForRenderer(RenderObject* parentRenderer)
{
return document()->styleSelector()->styleForElement(this);
}

void RenderObject::setAnimatableStyle(RenderStyle* style)
{
if (!isText() && m_style && style)
style = animation()->updateImplicitAnimations(this, style);

setStyle(style);
}

从createRendererIfNeeded中我们可以了解到创建完RenderObject子类对象后,会为其设置RenderStyle属性,然后在该DOM Node的父节点对应的RenderObject中添加刚新建的RenderObject,这样以构建Render树。

但是在setStyle的过程中可能会调用createAnonymousFlow或createAnonymousBlock来创建匿名对象,其中RenderBox的setStyle方法如下:
void RenderBox::setStyle(RenderStyle* newStyle)
{
bool wasFloating = isFloating();
bool hadOverflowClip = hasOverflowClip();
RenderStyle* oldStyle = style();
if (oldStyle)
oldStyle->ref();

RenderObject::setStyle(newStyle);
....................................................................
setInline(newStyle->isDisplayInlineType());

switch (newStyle->position()) {
case AbsolutePosition:
case FixedPosition:
setPositioned(true);
break;
default:
setPositioned(false);

if (newStyle->isFloating())
setFloating(true);

if (newStyle->position() == RelativePosition)
setRelPositioned(true);
}

// We also handle and , whose overflow applies to the viewport.
if (!isRoot() && (!isBody() || !document()->isHTMLDocument()) && (isRenderBlock() || isTableRow() || isTableSection())) {
// Check for overflow clip.
if (newStyle->overflowX() != OVISIBLE) {
if (!hadOverflowClip)
// Erase the overflow
repaint();
setHasOverflowClip();
}
}
..............................................
if (requiresLayer()) {
if (!m_layer) {
if (wasFloating && isFloating())
setChildNeedsLayout(true);
m_layer = new (renderArena()) RenderLayer(this);
setHasLayer(true);
m_layer->insertOnlyThisLayer();
if (parent() && !needsLayout() && containingBlock())
m_layer->updateLayerPositions();
}
} else if (m_layer && !isRoot() && !isRenderView()) {
.......................................................
}
..................................................................
}

bool RenderObject::requiresLayer()
{
return isRoot() || isPositioned() || isRelPositioned() || isTransparent() || hasOverflowClip() || hasTransform() || hasMask() || hasReflection();
}

bool
RenderObject::isRoot() const { return document()->documentElement() == node(); }
bool
RenderObject::isPositioned() const { return m_positioned; } // absolute or fixed positioning
bool
RenderObject::isRelPositioned() const { return m_relPositioned; } // relative positioning
bool
RenderObject::isTransparent() const { return style()->opacity() <>RenderObject::hasOverflowClip() const { return m_hasOverflowClip; }
bool
RenderObject::hasTransform() const { return m_hasTransform; }
bool
RenderObject::hasMask() const { return style() && style()->hasMask(); }
bool
RenderObject::hasReflection() const { return m_hasReflection; }

通过上面的了解我们知道在setStyle时符合一定条件的RenderObject会创建RenderLayer对象,那么究竟什么是RenderLayer类,其有什么作用,下面作初步的介绍。

二、RenderLayer树
1、类RenderLayer
RenderLayer类其实是一个非常复杂并且很重要的类,其主要数据成员如下:
RenderLayer类主要数据成员


图六
RenderLayer类主要与处理分层布局、渲染页面元素等相关如处理z-index、opacity等,只有符合一个条件的RenderObject才会创建RenderLayer对象,并且将这些RenderLayer对象组织成一颗树。

2、构建RenderLayer树
通过方法
insertOnlyThisLayer来组织这颗RenderLayer树。
void RenderLayer::insertOnlyThisLayer()
{
if (!m_parent && renderer()->parent()) {
// We need to connect ourselves when our renderer() has a parent.
// Find our enclosingLayer and add ourselves.
RenderLayer* parentLayer = renderer()->parent()->enclosingLayer();
RenderLayer* beforeChild = parentLayer->reflectionLayer() != this ? renderer()->parent()->findNextLayer(parentLayer, renderer()) : 0;
if (parentLayer)
parentLayer->addChild(this, beforeChild);
}

// Remove all descendant layers from the hierarchy and add them to the new position.
for (RenderObject* curr = renderer()->firstChild(); curr; curr = curr->nextSibling())
curr->moveLayers(m_parent, this);

// Clear out all the clip rects.
clearClipRects();
}

void RenderLayer::addChild(RenderLayer* child, RenderLayer* beforeChild)
{
RenderLayer* prevSibling = beforeChild ? beforeChild->previousSibling() : lastChild();
if (prevSibling) {
child->setPreviousSibling(prevSibling); prevSibling->setNextSibling(child);
} else
setFirstChild(child);

if (beforeChild) { beforeChild->setPreviousSibling(child); child->setNextSibling(beforeChild); } else setLastChild(child); child->setParent(this);

if (child->isOverflowOnly())
dirtyOverflowList();

if (!child->isOverflowOnly() || child->firstChild()) {
// Dirty the z-order list in which we are contained. The stackingContext() can be null in the
// case where we're building up generated content layers. This is ok, since the lists will start
// off dirty in that case anyway.
RenderLayer* stackingContext = child->stackingContext();
if (stackingContext)
stackingContext->dirtyZOrderLists();
}

child->updateVisibilityStatus();
if (child->m_hasVisibleContent || child->m_hasVisibleDescendant)
childVisibilityChanged(true);
}

static void addLayers(RenderObject* obj, RenderLayer* parentLayer, RenderObject*& newObject,
RenderLayer*& beforeChild)
{
if (obj->hasLayer()) {
if (!beforeChild && newObject) {
// We need to figure out the layer that follows newObject. We only do
// this the first time we find a child layer, and then we update the
// pointer values for newObject and beforeChild used by everyone else.
beforeChild = newObject->parent()->findNextLayer(parentLayer, newObject);
newObject = 0;
}
parentLayer->addChild(obj->layer(), beforeChild);
return;
}

for (RenderObject* curr = obj->firstChild(); curr; curr = curr->nextSibling())
addLayers(curr, parentLayer, newObject, beforeChild);
}

void RenderObject::addLayers(RenderLayer* parentLayer, RenderObject* newObject)
{
if (!parentLayer)
return;

RenderObject* object = newObject;
RenderLayer* beforeChild = 0;
WebCore::addLayers(this, parentLayer, object, beforeChild);
}

void RenderObject::removeLayers(RenderLayer* parentLayer)
{
if (!parentLayer)
return;

if (hasLayer()) {
parentLayer->removeChild(layer());
return;
}

for (RenderObject* curr = firstChild(); curr; curr = curr->nextSibling())
curr->removeLayers(parentLayer);
}

void RenderObject::moveLayers(RenderLayer* oldParent, RenderLayer* newParent)
{
if (!newParent)
return;

if (hasLayer()) {
if (oldParent)
oldParent->removeChild(layer());
newParent->addChild(layer());
return;
}

for (RenderObject* curr = firstChild(); curr; curr = curr->nextSibling())
curr->moveLayers(oldParent, newParent);
}

通过上面一组方法我们可以了解到拥有RenderLayer对象的RenderObject对象,按照Render树中最近的原则将含有的RenderLayer对象依Render树对应的父子关系组织RenderLayer树,RenderLayer对象的存在是依附于RenderObject对象而存在。

RenderView对象拥有对应的RenderLayer对象,同时其作为RenderLayer树根。

3、RenderLayer树与Render树的关系
通过RenderContainer::addChild方法回过头再来具体看看Render树自身的构成。
void RenderContainer::addChild(RenderObject* newChild, RenderObject* beforeChild)
{
bool needsTable = false;
//检查是否为Table的情况
if (needsTable) {
......................................................
} else {
// just add it...
insertChildNode(newChild, beforeChild);
}
.........................................................
}

void RenderContainer::insertChildNode(RenderObject* child, RenderObject* beforeChild, bool fullInsert)
{
if (!beforeChild) {
appendChildNode(child);
return;
}

while (beforeChild->parent() != this && beforeChild->parent()->isAnonymousBlock())
beforeChild = beforeChild->parent();

if (beforeChild == m_firstChild)
m_firstChild = child;

RenderObject* prev = beforeChild->previousSibling();
child->setNextSibling(beforeChild);
beforeChild->setPreviousSibling(child);
if(prev) prev->setNextSibling(child);
child->setPreviousSibling(prev);

child->setParent(this);

if (fullInsert) {
// Keep our layer hierarchy updated. Optimize for the common case where we don't have any children
// and don't have a layer attached to ourselves.
RenderLayer* layer = 0;
if (child->firstChild() || child->hasLayer()) {
layer = enclosingLayer(); child->addLayers(layer, child);
}

// if the new child is visible but this object was not, tell the layer it has some visible content
// that needs to be drawn and layer visibility optimization can't be used
if (style()->visibility() != VISIBLE && child->style()->visibility() == VISIBLE && !child->hasLayer()) {
if (!layer)
layer = enclosingLayer();
if (layer)
layer->setHasVisibleContent(true);
}
...........................................................
}

child->setNeedsLayoutAndPrefWidthsRecalc();
if (!normalChildNeedsLayout())
setChildNeedsLayout(true); // We may supply the static position for an absolute positioned child.
.................................................................
}
通过上面代码的了解我们知道通过addChild不仅维护Render树的构成,同时会将拥有的RenderLayer树构建起来。

4、RenderLayer树的作用
RenderLayer树的构建为渲染阶段处理z-index、opacity、overflow、scrollbar等打下一定的基础,在我们了解渲染的处理过程时我们再来深入的了解。

在这里我们初步的了解到在构建Render树的同时会维护一颗RenderLayer树,为分层布局、渲染作准备。


三、总结
其 实WebKit涉及网页布局方面的数据结构还有关于SVG方面的,但通过上面的理解,如果对SVG感兴趣的话,应该对理解SVG有一定的参考作用。 当然数据结构方面还有相当多的内容未提及,这里只是列出一些关键类或结构,以便有个整体的抽象认识,希望能对了解WebKit的网页布局渲染有一定的基础性作用。

四、参考资源
The WebKit Open Source Project

2008年11月29日星期六

WebKit网页布局实现之主要数据结构篇

在有了对CSS网页布局标准及相关概念的认识之后,我们可以更加深入的理解WebKit究竟是如何实现其网页布局,同时实现对CSS布局标准的支持。
毕竟标准归标准,要高效的实现这些标准,不同的实现肯定有其不同的实现方式,就像不同的Web服务器对HTTP协议标准的实现有所不同一样,当然不同的实现也会增加一些自身特有的属性。
下面我们从数据结构的角度来了解WebKit中为实现网页布局所设计的主要类结构及其主要方法。

一、Render树的构成
在我们编写网页及使用JS的时候,大概都知道DOM树及其主要构成,了解到DOM树的构建其实质是对一个html或xml文件的内容采取树结构的方式来组织及描述,不同的标签及其在文档中的位置决定了其在整颗DOM树的地位及属性,针对具体DOM树的构成及不同树节点的描述,可以参考有关DOM的相关标准等,以后有机会我们也会单独来了解。

也许对于Render树大家就不那么了解了,简单的说来,它是对DOM树更进一步的描述,其描述的内容主要与布局渲染等CSS相关属性如left、top、width、height、color、font等有关,因为不同的DOM树结点可能会有不同的布局渲染属性,甚至布局时会按照标准动态生成一些匿名节点,所以为了更加方便的描述布局及渲染,WebKit内核又生成一颗Render树来描述DOM树的布局渲染等特性,当然DOM树与Render树不是一一对应,但可以相互关联,下面分别描述其主要节点:

1、基类RenderObject
RenderObject作为所有Render树节点的基类,完全类似与DOM树中的Node基类,它是构成Render树的基础,作用非比寻常,其中包含了构成Render树所可能涉及到的一些基本属性及方法,内容相当多,其主要数据成员及方法分别如下:
RenderObject主要数据成员
图一
其中成员m_parent、m_previous、m_next为构建Render树设置好关联基础;
m_Node则为DOM树中对应的节点;
m_style成员则描述该节点对应的各种CSS基本属性数据,下面会单独介绍;
至于其他的诸如m_positioned、m_isText、m_inline、m_floating、m_replaced等则描述其特性,就像CSS标准对不同元素的属性分类定义一样,从字面上我们就可以上一节WebKit网页布局实现之基本概念及标准篇中可以找到它们这么定义的踪影。

成员m_needsPositionedMovementLayout、m_normalChildNeedsLayout、m_posChildNeedsLayout、m_needsLayout等主要用来描述该RenderObject是否确实需要重新布局;
当一个新的RenderObject对象插入到Render树的时候,它会设置其m_needsLayout属性为true,同时会根据该RenderObject对象在祖先RenderObject看来是一个positioned(拥有positiong:absolute或fixed属性)状态的孩子,如是则将相应祖先RenderObject对象的属性m_posChildNeedsLayout设置为true;

如果是一个in-flow(positon:static或relative)状态的孩子,则将相应祖先RenderObject对象的属性m_normalChildNeedsLayout设置为true;

主要方法:
//与是否需要layout相关
bool needsLayout() const { return m_needsLayout || m_normalChildNeedsLayout ||m_posChildNeedsLayout; }
bool selfNeedsLayout() const { return m_needsLayout; }
bool posChildNeedsLayout() const { return m_posChildNeedsLayout; }
bool normalChildNeedsLayout() const { return m_normalChildNeedsLayout; }

//与基本属性相关
bool isFloating() const { return m_floating; }
bool isPositioned() const { return m_positioned; } // absolute or fixed positioning
bool isRelPositioned() const { return m_relPositioned; } // relative positioning
bool isText() const { return m_isText; }
bool isInline() const { return m_inline; } // inline object
bool isCompact() const { return style()->display() == COMPACT; } // compact object
bool isRunIn() const { return style()->display() == RUN_IN; } // run-in object
bool isDragging() const { return m_isDragging; }
bool isReplaced() const { return m_replaced; } // a "replaced" element (see CSS)

//与外部DOM关联相关
RenderView* view() const;
// don't even think about making this method virtual!
Node* element() const { return m_isAnonymous ? 0 : m_node; }
Document* document() const { return m_node->document(); }
void setNode(Node* node) { m_node = node; }
Node* node() const { return m_node; }

// RenderObject tree manipulation
//////////////////////////////////////////
virtual bool canHaveChildren() const;
virtual bool isChildAllowed(RenderObject*, RenderStyle*) const { return true; }
virtual void addChild(RenderObject* newChild, RenderObject* beforeChild = 0);
virtual void removeChild(RenderObject*);
virtual bool createsAnonymousWrapper() const { return false; }

// raw tree manipulation
virtual RenderObject* removeChildNode(RenderObject*, bool fullRemove = true);
virtual void appendChildNode(RenderObject*, bool fullAppend = true);
virtual void insertChildNode(RenderObject* child, RenderObject* before, bool fullInsert = true);
// Designed for speed. Don't waste time doing a bunch of work like layer updating and repainting when we know that our
// change in parentage is not going to affect anything.
virtual void moveChildNode(RenderObject*);

virtual void paint(PaintInfo&, int tx, int ty);

/*
* This function should cause the Element to calculate its
* width and height and the layout of its content
*
* when the Element calls setNeedsLayout(false), layout() is no
* longer called during relayouts, as long as there is no
* style sheet change. When that occurs, m_needsLayout will be
* set to true and the Element receives layout() calls
* again.
*/
virtual void layout() = 0;

其中很多方法如paint()、layout()等是虚拟的,不同的子类可以重载它;

其中方法container() 、containingBlock()、paint()、layout()很值得大家深入研究;

总的说来RenderObject基类定义一些通用属性、方法,以便维护、布局、渲染Render树。

2、子类RenderBox
RenderBox代表描述CSS标准中的Box Model,它继承自RenderObject;
RenderBox主要数据成员
图二
其主要重载了部分继承而来的方法。

3、子类RenderContainer
RenderContainer类用来描述可以拥有子RenderObject成员的容器类,它继承自RenderBox;
RenderContainer主要数据成员
图三
其主要重载了RenderObject提供的维护Render树新增、删除树节点等方面的方法。

4、子类RenderFlow
RenderFlow主要用来描述CSS标准中提到的能进行inline-flow、block-flow相关处理的Render树结点,它继承自RenderContainer;
RenderFlow主要数据成员
图四
其主要方法包括在flow的过程中创建、关联匿名对象等;

5、子类RenderBlock
RenderBlock代表CSS标准中的block-level元素,它继承自RenderFlow;
RenderBlock主要数据成员
图五
它维护了一组由它定位的positioned树节点,以及有关overflow方面的设置;
其主要重载了RenderObject继承下来的layout、paint等方法;

因为html中的body、div、p等标签对应RenderBlock类对象,其在Render树具有非常重要的地位,其layout、paint等方法的实现,往往是WebKit整个布局、渲染处理的发起中心,内容比较多并且复杂,以后有机会详解。

6、子类RenderInline
RenderInline代表inline-level元素,其继承自RenderFlow,主要重载了RenderObject关于inline-flow方面处理的方法,提供了splitFlow、splitInlines等处理自动换行的方法。

7、子类RenderText
RenderText代表对html中Text node对应的Render树节点,它直接继承自RenderObject;
RenderText主要数据成员
图六
它提供关于处理文字方面如显示文字、行高计算、整个Text node对应的宽度等;它没有重载layout方法,因为它自身的定位往往由RenderBlock、RenderInline父对象来处理;

8、子类RenderImage
RenderImage代表html中img标签对应的树节点,它继承自RenderBox;
RenderImage继承关系及主要数据成员
图七
其主要提供关于图片显示、大小设置等方面的处理,其中paintReplaced方法将其图片显示出来;

9、子类RenderView
RenderView对应整个html文档对象的树节点,可看成是Render树的根,它继承自RenderBlock;
RenderView主要数据成员
图八
其中m_frameview成员对应整个文档对应的FrameView,而m_widgets则包括了该文档可能包含的plugin插件等对应的Render树节点;

RenderView对象作为Render树的根,它往往随着Document对象的创建而创建,它的layout、paint方法的发起往往是整颗Render树布局、渲染处理的开始;其中也包含了对选择处理。

10、其他
整个Render树中涉及的树节点类型,还有很多如RenderButton、RenderTable、RenderMedia等;并且各个类的方法及数据成员非常多,这里只是初步列出主要的类及其主要方法,特别是可能涉及到布局、渲染方方面的方法,以便我们能从中大致WebKit布局、渲染所涉及的基本内容及方法。

二、CSS属性的描述
1、RenderStyle类
RenderObject对象的m_style成员为RenderStyle类对象,它往往用来描述一个RenderObject所可能涉及的CSS属性数据(如left、top、align、color、font等等),其数据成员往往对应于CSS中定义的所有属性项,内容非常的庞杂,简单的说来就是将CSS标准中的所有属性按照一定分类定义到一个数据结构中。

2、RenderStyle类主要方法
为了获取、设置CSS属性所对应的值,RenderStyle类提供了所有的获取、设置CSS属性的方法如:
void setDisplay(EDisplay v) { noninherited_flags._effectiveDisplay = v; }
void setOriginalDisplay(EDisplay v) { noninherited_flags._originalDisplay = v; }
void setPosition(EPosition v) { noninherited_flags._position = v; }
void setFloating(EFloat v) { noninherited_flags._floating = v; }

void setLeft(Length v) { SET_VAR(surround,offset.left,v) }
void setRight(Length v) { SET_VAR(surround,offset.right,v) }
void setTop(Length v) { SET_VAR(surround,offset.top,v) }
void setBottom(Length v){ SET_VAR(surround,offset.bottom,v) }

void setWidth(Length v) { SET_VAR(box,width,v) }
void setHeight(Length v) { SET_VAR(box,height,v) }
等等。。。。

三、RenderObject及子类对象的生成
1、CSSParser
CSSParser类顾名思义,主要用来解析文本中各种CSS属性,并且有效的组织在一个RenderStyle对象中。
其主要方法parseValue、applyProperty的部分代码示例如下:
bool CSSParser::parseValue(int propId, bool important)
{
.....................................................
case CSSPropertyFloat:
// left | right | none | inherit + center for buggy CSS
if (id == CSSValueLeft || id == CSSValueRight ||
id == CSSValueNone || id == CSSValueCenter)
valid_primitive = true;
break;

case CSSPropertyClear: // none | left | right | both | inherit
if (id == CSSValueNone || id == CSSValueLeft ||
id == CSSValueRight|| id == CSSValueBoth)
valid_primitive = true;
break;

case CSSPropertyWebkitBoxAlign:
if (id == CSSValueStretch || id == CSSValueStart || id == CSSValueEnd ||
id == CSSValueCenter || id == CSSValueBaseline)
valid_primitive = true;
break;
.....................................................
case CSSPropertyWebkitBoxPack:
if (id == CSSValueStart || id == CSSValueEnd ||
id == CSSValueCenter || id == CSSValueJustify)
valid_primitive = true;
break;
....................................................
}

void CSSStyleSelector::applyProperty(int id, CSSValue *value)
{
case CSSPropertyOpacity:
HANDLE_INHERIT_AND_INITIAL(opacity, Opacity)
if (!primitiveValue || primitiveValue->primitiveType() != CSSPrimitiveValue::CSS_NUMBER)
return; // Error case.
// Clamp opacity to the range 0-1
m_style->setOpacity(min(1.0f, max(0.0f, primitiveValue->getFloatValue())));
return;
case CSSPropertyWebkitBoxAlign:
{
HANDLE_INHERIT_AND_INITIAL(boxAlign, BoxAlign)
if (!primitiveValue)
return;
EBoxAlignment boxAlignment = *primitiveValue;
if (boxAlignment != BJUSTIFY)
m_style->setBoxAlign(boxAlignment);
return;
}
...................................................
}

2、CSSStyleSelector类
CSSStyleSelector类其作用是基于所有用户的stylesheets集合为一个给定的DOM Element创建出其对应的RenderStyle对象。其主要功能由方法RenderStyle* styleForElement(Element*, RenderStyle* parentStyle = 0, bool allowSharing = true, bool resolveForRootDefault = false);来实现。

3、构建Render树
在构建DOM树的过程中,Dom Element对象创建完后,往往通过attach方法来创建RenderObject对象,进而构建Render树。
其基本实现流程如下:void Element::attach()=>createRendererIfNeeded()=>createRenderer;

RenderObject* Element::createRenderer(RenderArena* arena, RenderStyle* style)
{
if (document()->documentElement() == this && style->display() == NONE) {
// Ignore display: none on root elements. Force a display of block in that case.
RenderBlock* result = new (arena) RenderBlock(this);
if (result)
result->setAnimatableStyle(style);
return result;
}
return RenderObject::createObject(this, style);
}

RenderObject* RenderObject::createObject(Node* node, RenderStyle* style)
{
Document* doc = node->document();
RenderArena* arena = doc->renderArena();

const ContentData* contentData = style->contentData();
if (contentData && !contentData->m_next && contentData->m_type == CONTENT_OBJECT && doc != node) {
RenderImageGeneratedContent* image = new (arena) RenderImageGeneratedContent(node);
image->setStyle(style);
if (StyleImage* styleImage = contentData->m_content.m_image)
image->setStyleImage(styleImage);
return image;
}

RenderObject* o = 0;

switch (style->display()) {//往往在CSSStyleSelector::styleForElement或CSSStyleSelector::adjustRenderStyle时//调用setDisplay()以确定其display属性。
case NONE:
break;
case INLINE:
o = new (arena) RenderInline(node);
break;
case BLOCK:
o = new (arena) RenderBlock(node);
break;
case INLINE_BLOCK:
o = new (arena) RenderBlock(node);
break;
case LIST_ITEM:
o = new (arena) RenderListItem(node);
break;
case RUN_IN:
case COMPACT:
o = new (arena) RenderBlock(node);
break;
case TABLE:
case INLINE_TABLE:
o = new (arena) RenderTable(node);
break;
case TABLE_ROW_GROUP:
case TABLE_HEADER_GROUP:
case TABLE_FOOTER_GROUP:
o = new (arena) RenderTableSection(node);
break;
case TABLE_ROW:
o = new (arena) RenderTableRow(node);
break;
case TABLE_COLUMN_GROUP:
case TABLE_COLUMN:
o = new (arena) RenderTableCol(node);
break;
case TABLE_CELL:
o = new (arena) RenderTableCell(node);
break;
case TABLE_CAPTION:
o = new (arena) RenderBlock(node);
break;
case BOX:
case INLINE_BOX:
o = new (arena) RenderFlexibleBox(node);
break;
}
return o;
}
这样就不同的DOM树节点结合不同的显示属性,创建出不同的RenderObject子类对象,进而形成一个Render树。

四、总结
其实WebKit涉及网页布局方面的数据结构远不止这些,其中有的也比较复杂,这里只是列出自己认为较为重要的一小部分,希望能对了解WebKit的网页布局渲染有一定的基础性作用。。

五、参考资源
The WebKit Open Source Project

2008年11月22日星期六

WebKit网页布局实现之基本概念及标准篇

作为一个广受好评的浏览器引擎,其网页布局的质量(包括速度、效率、符合标准度等)往往是其关键,那么WebKit究竟是如何布局网页上的所有元素(包括滚动条、文字、图片、按钮、下拉框等)呢?其主要数据结构及流程都包括哪些呢?其布局的基本概念及标准都有哪些呢?下面分别介绍WebKit对其实现及运用。我们首先从关于布局的基本概念及标准的认识开始。

一、CSS布局相关标准介绍
其实我们对要素的布局都有不同程度的了解如我们使用Office时经常使用对一段文字的居中、靠左等操作,复杂一点有设置编号及文字与图片的环绕对应关系等,其实布局的关键在于确定页面元素的显示位置及大小,而页面中主要包括有文字、图片、按钮等页面元素,为了有效的组织布局这些页面元素,一些专家学者经过多年的摸索,总结并设计了布局这些元素所涉及的一些规则及标准,这就是CSS标准。

其中Visual formatting model details对其主要规则进行过具体描述,通过下面相关总结和汇总希望能对其主要要点有一定的认识与理解。

二、布局页面的基本概念
要在一块指定的画布(或窗口)上布局一些要素,往往需要按从上到下或从左到右(或从右到左)的规则来布局这些元素,而有些元素则可以包含其他元素,当作布局容器来使用。其中浏览网页的原生窗口就可看作一个布局容器的根。

由于页面内容的大小可能超过原生窗口提供的显示区域的大小,CSS中称页面上当前显示出来的区域为ViewPort,这个ViewPort相对页面的原始位置可通过滚动条来调整;

CSS标准中定义了html中的一些标签所对应的元素可当成容器使用的,以建立坐标定位所包含的元素如p、div,CSS中称这样的元素为block-level元素,相邻的block-level元素往往从上到下垂直排列;

而其他象i、a、b、span等标签及text node对应的元素则缺省为inline-level元素,inline-level元素不能用来定位其他元素,但可以包含其他同为inline-level元素,相邻的inline-level元素,往往按照从左到右或从右到左的水平方向排列;

block-level元素所包含的元素往往要么全为block-level元素要么全为inline-level元素,在一定条件下布局时可能会产生匿名block-level元素;

而页面上的每一个元素必须对应一个布局容器称之为Containing Block,只有block-level元素可以成为Containing Block;

一个Containing Block元素究竟包含哪些子元素或者某一元素的Containing Block元素究竟是谁,由其自身position属性及其在文档层次结构中所处的位置所确定,下一节会描述相关内容;

而每一个元素至少包含一个Box模型即由margin、border、padding、content width/height等属性所能描述的矩形区域;而这块区域相对于布局容器的坐标top、left,往往由布局容器按照block-flow、inline-flow等规则布局该元素时确定;

CSS中将布局block-level元素的过程称为block-flow;将布局inline-level相关元素的过程称为line-flow;

而CSS对html中诸如标签frame、image、object、embed、form等对应的元素称为replaced元素,它表示这些元素的内部布局不由Css来定义,而由浏览器来实现,而这些元素从外部来看相当于block-level元素,但可通过设置display:inline将其从外部看设为inline-level元素;

不同的html标签元素可以通过display:inline、display:block、display:inline-block等方式来调整其缺省block-level或inline-level属性;

三、如何确定页面元素显示位置
一个html标签元素的position属性可以设置为static、relative、fixed、absolute、inherit等,所有元素缺省为static,其Containing Block布局容器元素为最近的祖先block-level元素,其属性left、top、right、bottom不起作用;

position属性为relative的元素同static属性元素一样,但其left、top等属性可以有效,其坐标相对于布局容器而言;

position属性为absolute的元素的布局容器元素是最近的除了其属性不为static的祖先block-level元素;

position属性为fiexed的元素的布局容器元素是往往是根布局容器,但其定位坐标需要根据ViewPort的位置作相应调整;

一旦确定了其Containing Block布局容器,同时结合其自身的block-level或inline-level特性,布局时根据block flow和inline flow规则就可确定其起始位置,其中inline-level元素可在其布局容器提供的区域内自动换行;而block-level元素可在其布局容器提供的区域内自动换一个段落。

另外float属性为left或right元素较为特殊,则不遵守上面的规则,该元素让在其高度范围内的其他元素始终在其左边或右边。

四、如何确定页面元素大小
对于有定义其宽高的页面元素,则按照其定义的宽高来确定其大小,而对于象text node这样的inline-level则需要结合其字体大小及文字的多少等来确定其对应的宽高;如果页面元素所确定的宽高超过了布局容器Containing Block所能提供的宽高,同时其overflow属性为visible或auto,则会提供滚动条来保证可以显示其所有内容。

除非定义了页面元素的宽高,一般说来页面元素的宽高是在布局的时候通过相关计算得出来的。

五、如何理解z-index的使用
页面元素z-index属性的出现,引入了页面元素三维布局的思路,提出分层的概念,具有同一z-index属性的所有元素按照上面提到的二维布局方式(确定其位置及大小)来布局,而不同z-index所代表的层的元素有可能被其他层的元素所覆盖。每一个页面元素只能处在一个z-index所对应的层中,所有元素缺省z-index为0。

六、总结
CSS布局标准的内容相当多,有的还相当复杂,这里只是初步的了解其基本原则及要素,也未必在各种条件下都成立,希望能为我们能从WebKit代码去了解WebKit究竟是如何布局页面元素作一定准备而已,如果要想对CSS标准有更深入的具体理解,只有不断的练习及阅读理解CSS布局标准文档。

七、参考资源
CSS Wiki
CSS Specification
CSS basic box model
Visual formatting model details

2008年10月29日星期三

也谈WebKit、Gecko使用图形库

阅读了Graphics in Google Chrome之后,觉得作为浏览器内核WebKit、Gecko,为了能高效美观的显示页面的内容,选择适当的图形库非常重要。如果图形库选择不当,往往会导致页面上显示的文字、图片不美观,看起来总让人觉得别扭,更为糟糕的是排列布局出现紊乱,简直无法阅览。

从浏览器发展的历史来看,IE系列浏览器的网页布局、文字图片显示的美观程度还是相当高的,也许这与Microsoft图形显示方面的功力相关,到目前为止linux桌面显示还是与传统的windows桌面显示有相当的差距。

相比较Firefox1.5,Firefox3.0图形显示方面也有相当大的进步,这应该归功于完全采取Cario图形库来显示页面,目前应当完全达到了IE6的显示效果。可见图形显示的好与坏,直接决定了一款浏览器的质量以及用户接受程度。那究竟什么是图形库?其主要功能是什么?目前WebKit、Gecko可使用哪些图形库?它们在浏览器中是如何发挥其应有的作用呢?

一、图形库概述及其主要功能
A graphics library is a program designed to aid in rendering computer graphics to a monitor. This typically involves providing optimized versions of functions that handle common rendering tasks.

This can be done purely in software and running on the CPU, common in embedded systems, or being hardware accelerated by a GPU, more common in PCs. By employing these functions, a program can assemble an image to be output to a monitor. This relieves the programmer of the task of creating and optimizing these functions, and allows them to focus on building the graphics program.

目前主要的图形库有:
windows提供的GDI/GDI+、DirectX、OpenGL;
支持X的有Cario、GTK、QT、OpenGL;
其他的还有Skia(google提供)、Quartz 2D(apple提供)、wxWidget等;

一般说来图形库只提供绘画图形,渲染文字、图片等,不管是2D还是3D,其往往不提供消息处理,简单的说来就是如何高效的在一块指定的画布上将线条、文字、图片显示出来,其中往往涉及字体、颜色等;典型的图形库如GDI/GDI+、Cario、DirectX、Quartz 2D等;

而按钮、菜单、窗口等图形组件往往是基于图形库的基础上绘画出来的并有相对固定形状,同时一般具有消息处理功能;相关实现有GTK、QT、wxWidget、windows组件等;

其中GTK、QT、wxWidget、Skia等不仅提供图形组件,同时提供图形库的功能;而Cario则是一个纯粹的图形库,类似与Quartz 2D,目前GTK2则完全基于Cario来实现;

由于浏览器页面元素的数量存在不确定性,将页面上的一些元素对应成图形组件可能导致一个页面使用组件过多的问题(早期的IE就曾出现使用GDI对象过多的现象)。因此尽可能的将一个页面的所有元素显示在一个图形组件上,至于其显示交给图形库来处理,其消息响应交互交给DOM及原生窗口消息循环来完成。

从这里我们可以进一步的确认图形库在浏览器中的重要性,以及随着用户需求的增加及硬件的提升,浏览器中使用3D效果应该是一个大的方向。

二、Gecko中使用图形库Cario

1、Cario概述
Cairo is a 2D graphics library with support for multiple output devices. Currently supported output targets include the X Window System, Quartz, Win32, image buffers, PostScript, PDF, and SVG file output. Experimental backends include OpenGL (through glitz), XCB, BeOS, OS/2, and DirectFB.

Cairo is designed to produce consistent output on all output media while taking advantage of display hardware acceleration when available (eg. through the X Render Extension).

其主要优点在于其在X、Win32、Quartz的基础上统一了图形库的操作方式,同时支持PS、PDF、SVG、PNG/JPEG等图像格式的输出,极大的方便页面的再次利用,在glitz的支持下支持部分3D效果。

2、Cario在Gecko中的使用
首先提供一个gfxASurface抽象类,代表一块可以作画的画布;提供一个gfxContext类,它用来管理究竟如何作画,如画圆形、旋转,维护画布的状态、当前颜色、路径等,其往往需要一个gfxASurface子类实例来初始化;

其次不同的图形输出实现不同的gfxASurface子类如gfxWindowsSurface、gfxXlibSurface、gfxQuartzSurface、gfxGlitzSurface、gfxQuartzPDFSurface、gfxPSSurface等;

其次提供一个DeviceContextImpl类实现nsIDeviceContext接口,以描述指定原生Widget相关的字体属性及创建可以在该原生Widget上作画的nsIRenderingContext接口实现;

而nsThebesRenderingContext类实现了nsIRenderingContext接口,以供外部(如不同的DOM Node页面元素对应的不同Frame)在其上显示文字、图像、作图形等;

然后当解析、布局完DOM页面元素后需要画出不同的页面元素时则由DeviceContextImpl类实例来创建nsThebesRenderingContext类实现,并初始化它,其初始化代码如下:
nsThebesRenderingContext::Init(nsIDeviceContext* aContext, nsIWidget *aWidget)
{
nsThebesDeviceContext *thebesDC = static_cast(aContext);

mDeviceContext = aContext;
mWidget = aWidget;

mThebes = new gfxContext(aWidget->GetThebesSurface());

return (CommonInit());
}

nsThebesRenderingContext类中包含:
nsRefPtr mThebes;
nsCOMPtr mWidget;
而mThebes初始时需要的gfxASurface子类实例则由原生的Widget的GetThebesSurface()方法来实现,针对不同的图形输出实现返回不同的gfxASurface子类如gfxWindowsSurface、gfxXlibSurface、gfxQuartzSurface、gfxGlitzSurface的实例;

最后将nsThebesRenderingContext类实现实例返回,由不同的Frame自身决定如何在提供的画布上画出其表示的内容。

至于不同的Frame应该怎么画(显示)由其本身及CSS来决定,而nsIRenderingContext接口提供了所有可能用到的作画方式,具体每一个方法的含义需详细了解。

nsIRenderingContext接口提供了一组方法,向外部提供了图形库能实现的功能,其相当于一个中间层,从图形库的角度看,通过这个接口向外部提供了其能提供的功能;从需要图形功能的外部看,可不关心具体图形库的实现,而直接通过该接口使用图形库能提供的功能。

让我们来看看
nsIRenderingContext接口究竟有哪些主要方法?
// RenderingContext interface
class nsIRenderingContext : public nsISupports
{
public:
.........................................................................
/**
* Initialize the RenderingContext
* @param aContext the device context to use.
* @param aWidget the widget to hook up to
* @result The result of the initialization, NS_Ok if no errors
*/
NS_IMETHOD Init(nsIDeviceContext* aContext, nsIWidget *aWidget) = 0;
/**
* Get the DeviceContext that this RenderingContext was initialized
* with. This function addrefs the device context. Though it might
* be better if it just returned it directly, without addrefing.
* @result the device context
*/
NS_IMETHOD GetDeviceContext(nsIDeviceContext *& aDeviceContext) = 0;

/**
* Sets the forground color for the RenderingContext
* @param aColor The color to set the RenderingContext to
*/
NS_IMETHOD SetColor(nscolor aColor) = 0;

/**
* Get the forground color for the RenderingContext
* @return The current forground color of the RenderingContext
*/
NS_IMETHOD GetColor(nscolor &aColor) const = 0;

/**
* Sets the font for the RenderingContext
* @param aFont The font to use in the RenderingContext
*/
NS_IMETHOD SetFont(const nsFont& aFont, nsIAtom* aLangGroup) = 0;

/**
* Sets the font for the RenderingContext
* @param aFontMetric The font metrics representing the
* font to use in the RenderingContext
*/
NS_IMETHOD SetFont(nsIFontMetrics *aFontMetrics) = 0;

/**
* Get the current fontmetrics for the RenderingContext
* @return The current font of the RenderingContext
*/
NS_IMETHOD GetFontMetrics(nsIFontMetrics *&aFontMetrics) = 0;

/**
* Add in a translate to the RenderingContext's transformation matrix
* @param aX The horizontal translation
* @param aY The vertical translation
*/
NS_IMETHOD Translate(nscoord aX, nscoord aY) = 0;

/**
* Add in a scale to the RenderingContext's transformation matrix
* @param aX The horizontal scale
* @param aY The vertical scale
*/
NS_IMETHOD Scale(float aSx, float aSy) = 0;

/**
* Draw a line
* @param aXO starting horiztonal coord in twips
* @param aY0 starting vertical coord in twips
* @param aX1 end horiztonal coord in twips
* @param aY1 end vertical coord in twips
*/
NS_IMETHOD DrawLine(nscoord aX0, nscoord aY0, nscoord aX1, nscoord aY1) = 0;

/**
* Draw a rectangle
* @param aRect The rectangle to draw
*/
NS_IMETHOD DrawRect(const nsRect& aRect) = 0;

/**
* Draw a string in the RenderingContext
* @param aString The string to draw
* @param aLength The length of the aString
* @param aX Horizontal starting point of baseline
* @param aY Vertical starting point of baseline.
* @param aSpacing inter-character spacing to apply
*/
NS_IMETHOD DrawString(const char *aString, PRUint32 aLength,
nscoord aX, nscoord aY,
const nscoord* aSpacing = nsnull) = 0;

/*
* Tiles an image over an area
* @param aImage Image to tile
* @param aXImageStart x location where the origin (0,0) of the image starts
* @param aYImageStart y location where the origin (0,0) of the image starts
* @param aTargetRect area to draw to
* @param aSubimageRect the subimage (in tile space) which we expect to
* sample from; may be null to indicate that the whole image is
* OK to sample from
*/
NS_IMETHOD DrawTile(imgIContainer *aImage,
nscoord aXImageStart, nscoord aYImageStart,
const nsRect * aTargetRect,
const nsIntRect * aSubimageRect) = 0;
...............................................................................
};

其中DrawString、DrawTile方法最常用,其分别对应如何显示文字及图像。
针对图形库显示文字的基本原理可以参考Font technology and FreetypeFreeType Glyph Conventions

至于图形库如何显示不同格式的图像可参考如gifjpegpng等。

Gecko对Cario的使用还体现在对canvas标签的实现,具体可参考nsCanvasRenderingContext2D.cpp、nsHTMLCanvasElement.cpp等。

三、WebKit中使用图形库
1、WebKit支持的图形库
目前WebKit支持的图形库包括Cairo、Gtk、Qt、Wx、Cg、Mac、Skia等,虽然不同的图形库能支持不同的平台,但其在不同平台上的显示效果也不尽相同。至于在一个指定的平台上究竟使用何种库,则显示出很大的灵活性。就目前来看,在windows平台上可选的图形库有Cairo、Qt、Wx、Cg、Skia等,其中Graphics in Google Chrome阐述了Chrome关于图形库的选择。

其实从WebKit的角度来看,它通过提供一组与Gecko中nsIRenderingContext类似的公共图形接口,而不同的图形库则根据自身的不同实现了这些公共图形接口,以提供给WebCore元素使用,从而可以让WebKit支持不同的图形库。

2、WebKit支持不同图形库的实现
在WebKit中提供了一个GraphicsContext类,其中包括所有的图形接口,完全类似nsIRenderingContext,针对不同平台的特性,其定义中包含一些不同平台特有的
宏及元素定义。

在目录webcore\platform\graphics\下的子目录Cairo、Cg、Gtk、Mac、Qt、Win、Wx分别提供了GraphicsContext类部分方法的实现,而公共的实现则在webcore\platform\graphics\GraphicsContext.cpp中提供。

其中我们非常值得关注的方法有drawText与drawImage,其实现如下:
void GraphicsContext::drawText(const TextRun& run, const IntPoint& point, int from, int to)
{
if (paintingDisabled())
return;

font().drawText(this, run, point, from, to);
}

void GraphicsContext::drawImage(Image* image, const FloatRect& dest, const FloatRect& src, CompositeOperator op, bool useLowQualityScale)
{
if (paintingDisabled() || !image)
return;

float tsw = src.width();
float tsh = src.height();
float tw = dest.width();
float th = dest.height();

if (tsw == -1)
tsw = image->width();
if (tsh == -1)
tsh = image->height();

if (tw == -1)
tw = image->width();
if (th == -1)
th = image->height();

if (useLowQualityScale) {
save();
setUseLowQualityImageInterpolation(true);
}
image->draw(this, FloatRect(dest.location(), FloatSize(tw, th)), FloatRect(src.location(), FloatSize(tsw, tsh)), op);
if (useLowQualityScale)
restore();
}

最终的实现转交给类Font、Image的方法drawText、draw来实现,而不同实现如Cairo、Cg、Gtk、Mac、Qt、Win、Wx则会针对类Font、Image分别提供部分对应的实现,而公共的实现则在webcore\platform\graphics\Font.cpp及Image.cpp中提供。

3、不同平台GraphicsContext实例创建及使用
GraphicsContext创建的时机往往在对应平台的WebView获得Paint消息事件时,进而将该GraphicsContext类实例传递给FrameView及其不同的RenderObject实例,由不同的RenderObject实例来决定究竟如何来显示自身的内容,而GraphicsContext类实例提供了各种的显示文字、图形、图像的方法以供RenderObject实例调用。其调用关系基本上与Gecko中的不同Frame对象使用nsIRenderingContext接口方法类似。

创建GraphicsContext实例的示例如下:
//Gtk
static gboolean webkit_web_view_expose_event(GtkWidget* widget, GdkEventExpose* event)
{
WebKitWebView* webView = WEBKIT_WEB_VIEW(widget);
WebKitWebViewPrivate* priv = webView->priv;

Frame* frame = core(webView)->mainFrame();
GdkRectangle clip;
gdk_region_get_clipbox(event->region, &clip);
cairo_t* cr = gdk_cairo_create(event->window);
GraphicsContext ctx(cr);
ctx.setGdkExposeEvent(event);
if (frame->contentRenderer() && frame->view()) {
frame->view()->layoutIfNeededRecursive();

if (priv->transparent) {
cairo_save(cr);
cairo_set_operator(cr, CAIRO_OPERATOR_CLEAR);
cairo_paint(cr);
cairo_restore(cr);
}

frame->view()->paint(&ctx, clip);
}
cairo_destroy(cr);

return FALSE;
}

//win
void WebView::paintIntoBackingStore(FrameView* frameView, HDC bitmapDC, const IntRect& dirtyRect)
{
LOCAL_GDI_COUNTER(0, __FUNCTION__);

RECT rect = dirtyRect;

#if FLASH_BACKING_STORE_REDRAW
HDC dc = ::GetDC(m_viewWindow);
OwnPtr yellowBrush = CreateSolidBrush(RGB(255, 255, 0));
FillRect(dc, &rect, yellowBrush.get());
GdiFlush();
Sleep(50);
paintIntoWindow(bitmapDC, dc, dirtyRect);
::ReleaseDC(m_viewWindow, dc);
#endif

FillRect(bitmapDC, &rect, (HBRUSH)GetStockObject(WHITE_BRUSH));
if (frameView && frameView->frame() && frameView->frame()->contentRenderer()) {
GraphicsContext gc(bitmapDC);
gc.save();
gc.clip(dirtyRect);
frameView->paint(&gc, dirtyRect);
gc.restore();
}
}

//wx
void wxWebView::OnPaint(wxPaintEvent& event)
{
if (m_beingDestroyed || !m_impl->frame->view() || !m_impl->frame)
return;

wxAutoBufferedPaintDC dc(this);

if (IsShown() && m_impl->frame && m_impl->frame->document()) {
#if USE(WXGC)
wxGCDC gcdc(dc);
#endif

if (dc.IsOk()) {
wxRect paintRect = GetUpdateRegion().GetBox();

WebCore::IntSize offset = m_impl->frame->view()->scrollOffset();
#if USE(WXGC)
gcdc.SetDeviceOrigin(-offset.width(), -offset.height());
#endif
dc.SetDeviceOrigin(-offset.width(), -offset.height());
paintRect.Offset(offset.width(), offset.height());

#if USE(WXGC)
WebCore::GraphicsContext* gc = new WebCore::GraphicsContext(&gcdc);
#else
WebCore::GraphicsContext* gc = new WebCore::GraphicsContext((wxWindowDC*)&dc);
#endif
if (gc && m_impl->frame->contentRenderer()) {
if (m_impl->frame->view()->needsLayout())
m_impl->frame->view()->layout();

m_impl->frame->paint(gc, paintRect);
}
}
}
}

//Qt
void QWebFrame::render(QPainter *painter, const QRegion &clip)
{
if (!d->frame->view() || !d->frame->contentRenderer())
return;

d->frame->view()->layoutIfNeededRecursive();

GraphicsContext ctx(painter);
QVector vector = clip.rects();
WebCore::FrameView* view = d->frame->view();
for (int i = 0; i <>paint(&ctx, vector.at(i));
}

/*!
Render the frame into \a painter.
*/
void QWebFrame::render(QPainter *painter)
{
if (!d->frame->view() || !d->frame->contentRenderer())
return;

d->frame->view()->layoutIfNeededRecursive();

GraphicsContext ctx(painter);
WebCore::FrameView* view = d->frame->view();
view->paint(&ctx, view->frameGeometry());
}

4、WebKit 3D Port实现
Clutter WebKit port中提供了WebKit 对3D Port的支持与实现,其实现类似于Gtk+/Cairo图形库的实现,但其3D效果仅实现在Port层,没有对页面上的元素如文字、图像实现3D效果支持。

这里只是简单的了解WebKit中如何整合不同的图形库及其与WebCore的交互。要想更加深入的了解页面上的文字、图形、图像究竟是如何显示出来的,则需要更进一步的针对不同平台库进行学习与了解。

WebKit中也支持canvas标签,该标签提供的接口与Gecko能提供的几乎一致,其具体实现可参考webcore\html \CanvasRenderingContext2D.cpp,结合GraphicsContext类的实现,应该能对canvas标签的实现有充分的理 解。

四、总结
其实关于图形库及其使用的内容非常的多,而对浏览器内核来讲能对图形库进行高效使用也是非常重要的一部分,所以在这里所谈到的内容也许只是一些皮毛,但希望能在此开阔一下了解浏览器内核特别是图形库使用方面的思路。

五、参考资源
Wiki Rendering (computer graphics)
Wiki Cairo
Cairo HomePage
Wiki Qt
Wiki GTK+
Wiki wxWidgets
Wiki GDI
Wiki DirectX
Wiki Quartz 2D
Wiki OpenGL
Wiki OpenGL ES

Wiki gif
Wiki jpeg
Wiki png

Clutter Toolkit

Font technology and Freetype

2008年10月8日星期三

浅谈WebKit之Port篇

WebKit作为一个浏览器引擎,其相对于Gecko而言一个较大的特点就是便于移植,嵌入到其他程序中,目前大家已了解使用WebKit引擎的应用包括Safari、iPhone、Chrome、Android、Nokia S60 Browser及KDE QT4.4等,同时还有其他方面的移植如Gtk、wxWidget、3D等,可以说WebKit从架构上讲其Port移植方面的设计及应用,是非常优秀的。这一点相对于Gecko有相当大的优势,有时间可以参考一下浅谈Gecko关键部分之十二Embedding。为了更深入的了解WebKit,我们现在就从WebKit有关Ports方面入手,了解其有关Port方面的设计,从而了解究竟如何能移植WebKit到自己的应用中。

一、有关Port方面的概述
在通过了解浅谈WebKit之WebCore篇之后,应该说WebKitPort方面的内容是可以很广的,例如可将不同的图形库、网络库与WebCore集成,提供不同的Port接口供外部程序使用等,例如同样在windows平台上可以运行的Google Chrome和Safari就是针对WebKit的不同移植。

我们想了解有关Port方面的主要内容在于提供不同的Port接口供外部程序使用以及如何与外部程序交互,因为WebKit中的其它两部分WebCore、Javascript实现,从逻辑上讲是不直接提供接口给外部程序使用的。同时为了完成浏览器的核心功能,WebKit也需要从外部程序中通过Port接口的方式获取一些支持。

从这个角度讲WebKit作为一个相对独立的整体,它与外部程序之间的交互也就有一组相对固定的接口来定义及维护它们之间的关系,它们之间的关系与插件跟浏览器引擎之间的关系完全类似,接口相当一组协议,有的是由WebKit来实现,而供外部程序调用;有的的正好相反。

通过前面的了解我们知道WebKit的主要功能集中在分析Html、渲染布局Web内容以及Javascript实现方面等,而这些Web内容显示在哪个窗口及消息处理的启动循环等都需要由外部程序来提供。

二、初步分析已有WebKit Port移植实现
1、与WebCore交互接口的实现
在WebKit源代码目录结构中WebKit目录下分别包含gtk、mac、qt、win、wx目录,其分别对应不同的Port移植方式,在每一个目录下面都包括WebCoreSupport目录,而在不同的WebCoreSupport目录下分别包含有对类接口WebCore::ChromeClient、WebCore::ContextMenuClient、WebCore::DragClient、WebCore::EditorClient、WebCore::FrameLoaderClient、WebCore::InspectorClient等的实现,它们代表外部程序提供给WebKit内部使用的接口实现,其中WebCore::ChromeClient、WebCore::FrameLoaderClient非常重要。

初步了解其接口定义能基本了解其对应的含义,这些接口往往需要由Port移植部分来提供实现,往往由WebKit内部根据一定的条件来调用。下面初步来了解几个主要接口:

WebCore::ChromeClient接口:
//往往在运行window.open脚本时调用,以便由外部程序决定如何打开一个新页面如新建一个窗口、新建一个Tab页签等;
virtual WebCore::Page* createWindow(WebCore::Frame*, const WebCore::FrameLoadRequest&, const WebCore::WindowFeatures&);

//通知外部程序显示页面;
virtual void show();

virtual bool canRunModal();

//通知外部程序以Modal的方式显示页面;
virtual void runModal();

//通知外部程序显示JS警告提示窗口;
virtual void runJavaScriptAlert(WebCore::Frame*, const WebCore::String&);

//通知外部程序显示JS警告确认窗口;
virtual bool runJavaScriptConfirm(WebCore::Frame*, const WebCore::String&);

WebCore::FrameLoaderClient接口:
//检查是否拥有主页面窗口;
virtual bool hasWebView() const;
//检查是否拥有页面窗口;
virtual bool hasFrameView() const;

//通知外部程序有关http请求开始、结束、获取数据等,如通常浏览器状态栏显示的信息;
virtual void dispatchDidReceiveResponse(WebCore::DocumentLoader*, unsigned long identifier, const WebCore::ResourceResponse&);
virtual void dispatchDidReceiveContentLength(WebCore::DocumentLoader*, unsigned long identifier, int lengthReceived);
virtual void dispatchDidFinishLoading(WebCore::DocumentLoader*, unsigned long identifier);
virtual void dispatchDidFailLoading(WebCore::DocumentLoader*, unsigned long identifier, const WebCore::ResourceError&);

//通知外部程序WebKit内部主要事件处理,以便外部程序及时响应或创建维护数据等
virtual void dispatchDidHandleOnloadEvents();
virtual void dispatchDidReceiveServerRedirectForProvisionalLoad();
virtual void dispatchDidCancelClientRedirect();
virtual void dispatchWillPerformClientRedirect(const WebCore::KURL&, double interval, double fireDate);
virtual void dispatchDidChangeLocationWithinPage();
virtual void dispatchWillClose();
virtual void dispatchDidReceiveIcon();
virtual void dispatchDidStartProvisionalLoad();
virtual void dispatchDidReceiveTitle(const WebCore::String&);
virtual void dispatchDidCommitLoad();
virtual void dispatchDidFinishDocumentLoad();
virtual void dispatchDidFinishLoad();
virtual void dispatchDidFirstLayout();

//告诉外部程序需要提供切换到一个新页面状态。此时外部程序往往会新建FrameView,并将FrameView与Frame关联,设置原生窗口句柄及其消息处理机制等等;
virtual void transitionToCommittedForNewPage();

//告诉外部程序创建一个新的Frame,如遇到html中iframe标签时,需要外部程序创建一个新的Frame及原生窗口句柄等;
virtual PassRefPtr createFrame(const WebCore::KURL& url, const WebCore::String& name, WebCore::HTMLFrameOwnerElement* ownerElement,
const WebCore::String& referrer, bool allowsScrolling, int marginWidth, int marginHeight);

//告诉外部程序需要创建一个Plugin实例,从而创建其原生窗口等等;
virtual WebCore::Widget* createPlugin(const WebCore::IntSize&, WebCore::Element*, const WebCore::KURL&, const Vector&, const Vector&, const WebCore::String&, bool loadManually);

2、对WebCore中的page/loader等方面的类提供对应Port的实现支持
如EventHandlerWin.cpp、FrameLoaderWin.cpp、DocumentLoaderWin.cpp、DocumentLoaderWin.cpp、WidgetWin.cpp、KeyEventWin.cpp等

3、实现WebView及WebFrame等以便外部程序嵌入WebKit
不同的Port移植对WebView及WebFrame的定义及实现有所不同,但其与WebCore中的Page、Frame之间的关系大致与浅谈WebKit之WebCore篇图一描述相一致。

具体关于WebView、WebFrame的定义与实现,特别是初始化时的动作可根据不同的Port移植而有所不同,同时初始化时会将上面提到的WebCore Port接口实现告诉WebKit内部。主要示例代码如下:
static void webkit_web_view_init(WebKitWebView* webView)
{
WebKitWebViewPrivate* priv = WEBKIT_WEB_VIEW_GET_PRIVATE(webView);
webView->priv = priv;
priv->corePage = new Page(new WebKit::ChromeClient(webView), new WebKit::ContextMenuClient(webView), new WebKit::EditorClient(webView), new WebKit::DragClient, new WebKit::InspectorClient);
priv->mainFrame = WEBKIT_WEB_FRAME(webkit_web_frame_new(webView));
priv->lastPopupXPosition = priv->lastPopupYPosition = -1;
priv->editable = false;
................................

priv->webSettings = webkit_web_settings_new();
webkit_web_view_update_settings(webView);
..................................
}

WebKitWebFrame* webkit_web_frame_new(WebKitWebView* webView)
{
g_return_val_if_fail(WEBKIT_IS_WEB_VIEW(webView), NULL);

WebKitWebFrame* frame = WEBKIT_WEB_FRAME(g_object_new(WEBKIT_TYPE_WEB_FRAME, NULL));
WebKitWebFramePrivate* priv = frame->priv;
WebKitWebViewPrivate* viewPriv = WEBKIT_WEB_VIEW_GET_PRIVATE(webView);

priv->webView = webView;
priv->client = new WebKit::FrameLoaderClient(frame);
priv->coreFrame = Frame::create(viewPriv->corePage, 0, priv->client).get();
priv->coreFrame->init();

return frame;
}

4、Chrome中对Port移植方面的实现
其基本上与其他Port移植类似,其主要代码在webkit\glue目录中,可重点关注带client_impl.cc后缀的文件、webview_impl.cc、webwidget_impl.cc等;但是其究竟如何创建原生windows窗口、如何创建Render进程、Render进程与创建的原生windows窗口的关系如何等需要更进一步深入研究Chrome,如果能从上面提到的Port部分入手也许很快就可得到答案,这一点以后有机会单独研究。

5、Android中对Port移植方面的实现
其实现有点特殊,
由于Andriod将WebKit以一个Java类接口的方式提供给Java环境使用(不像上面提到的Chrome、Safari等都是将WebKit以 一个C++动态或静态库的方式供C/C++外部程序调用),这样WebKit内部与外部即JavaVM的交互(如上面提到的ChromeClient、 FrameLoaderClient接口实现)需要一个Bridge类来协调处理,同时WebView、WebFrame接口绑定给JavaVM的jni接口实现也需要通过这个Bridge来支持协调处理。具体可详细参考android源码代码中WebCore\platform\android目录下的源文件。

6、通过进一步了解WebCore Port接口及其实现,可以加深这样一个认识:
如果
从MVC的角度来看整个基于WebKit的浏览器(当然不尽合理),WebKit的Port部分相当于V部分,它提供显示页面内容及其辅助信息(如提示状态)的场所(即原生窗口)以及控制该显示场所的状态变化及消息响应(如改变大小、鼠标移动等);而M部分往往由WebCore来实现,至于WebCore如何组织DOM则往往由htmlparser部分根据DOM定义来组织,如何在提供的显示场所显示Web内容则往往由WebCore中的layout部分来实现,其中充分利用了Css定义来布局显示该显示的内容;一旦涉及控制或动态处理往往由Port部分发起而由Javascript脚本来实现处理,其任务由JavascriptCore或V8来完成。

一般说来新打开一个页面,Port部分需要提供一个主显示场所(即原生窗口),如果页面中含有iframe标签,则需要在主显示场所内创建一个子显示场所,以显示iframe标签对应src的内容;如果页面中含有embed/object等插件标签同样往往也需要
在主显示场所内创建一个子显示场所(除非windowless),以交由插件实现在提供的显示场所中显示内容。

特别需要说明的是我们通常看到的页面表单元素input text field、textArea、button、radiobutton等往往不像window图形库中的按钮、菜单、输入框等会对应一个原生窗口,页面中的表单元素在一个显示场所(即原生窗口)中完全是利用Css等通过layout方式来达到我们所看到的类似原生按钮、输入框、列表框、滚动条等效果,其中特别是能准确定位元素大小、设置focus、光标显示、响应事件等,这充分的说明了浏览器引擎内部布局部分的威力所在。

从另外一个角度来看一个页面一般说来(除非遇到iframe或插件需要另外提供一块子画布)相当于一块画布,浏览器引擎能在其精确的位置绘制不同颜色的文字、图片、图标等,同时根据当前的鼠标及一个模拟的输入提示光标位置,接收键盘输入操作。页面中的绝大多数元素与原生的窗口元素几乎没有关联,完全通过组合、布局、准确定位来处理一切。。。

三、如何利用WebKit?
了解WebKit Port部分,对我们如何利用WebKit有非常现实的意义,目前已经将WebKit移植到多种平台如windows、qt、gtk、mac、wx、java、framebuffer等,甚至移植到python、ruby及3D等环境中去。通过借鉴或利用这些已有的WebKit Port实现,完全可以将WebKit发扬广大。

前一阶段正好得到一个网友抓取网页的需求,试想目前移植利用WebKit基本都用来显示页面,往往涉及图形显示方面,但随着ajax及动态页面的广泛使用,未来动态生成的页面越来越多,传统的搜索引擎仅仅抓取静态的页面内容显然是不够的,现代化的搜索引擎应该能抓取动态的页面内容,这样它从某种意义讲相当于一个能获取对应的动态页面但不真正显示出其内容的浏览器,这样一个搜索引擎不仅能分析DOM树,同时能运行Javascript脚本(如运行ajax),以真正完整获取页面内容,其实这样一个搜索引擎如果利用WebKit来实现的话,应该是个不错的选择,在我们了解WebKit Port部分之后,我们是否可以来模拟一个不真正具备图形显示方面的Port,进而充分利用WebKit中的WebCore及Javascript实现方面的功能呢?一点想法,今后有机会可以试试,或许Google、Yahoo的搜索引擎已经有了相关的实现,不知是否使用的就是WebKit?应该不会,有谁清楚的话,烦请通知一声。

但愿我们也能利用利用WebKit整出一个象模象样的东东如机顶盒浏览器、手机浏览器等等。。

四、参考资源
The WebKit Open Source Project
Google Chrome Home
Android - An Open Handset Alliance Project

2008年9月25日星期四

浅谈WebKit之JavaScriptCore/V8篇

WebKit作为一个浏览器引擎,其中Javascript实现包括JavaScriptCore和V8,为了能更全面的了解WebKit,我们需要深入的了解Javascript实现的基本原理、其在WebKit中的作用以及与其他部分之间的交互,同时与Gecko中的Javacript实现作初步的对比。让我们开始了解WebKit之Javascript实现JavaScriptCore、V8之旅吧。

一、Javascript实现的作用
正与浅谈Gecko关键部分之六认识javascript实现及应用部分对什么是javascript的描述那样,在WebKit中其Javascript实现,同样相当于一个符合ECMAScript标准的动态库,其往往依附于浏览器引擎,由浏览器引擎来提供运行环境,并控制或发起javascript实现进行编译、解析执行脚本、垃圾回收等,同样需提供对浏览器引擎扩展的支持如Dom Binding等;

由于Web2.0的提出,动态网页的交互如运行ajax更加的频繁,Javascript脚本运行的总体效率以及安全往往成为浏览器内核的关键,而其Javascript实现就担负着如此重任。

二、JavaScriptCore实现特点
相对于其他的Javascript实现,JavaScriptCore提出了虚拟机的概念,在编译脚本时生成高效的bytecode,bytecode统一在一个虚拟机的环境中执行。而其高效的虚拟机实现常称为SquirrelFish,通过Announcing SquirrelFishIntroducing SquirrelFish Extreme可更进一步了解关于SquirrelFish的相关内容。

三、V8实现特点
Fast Property Access
To reduce the time required to access JavaScript properties, V8 does not use dynamic lookup to access properties. Instead, V8 dynamically creates hidden classes behind the scenes. This basic idea is not new - the prototype-based programming language Self used maps to do something similar. (See for example, An Efficient Implementation of Self, a Dynamically-Typed Object-Oriented Language Based on Prototypes). In V8, an object changes its hidden class when a new property is added.

Dynamic Machine Code Generation
V8 compiles JavaScript source code directly into machine code when it is first executed. There are no intermediate byte codes, no interpreter. Property access is handled by inline cache code that may be patched with other machine instructions as V8 executes.

During initial execution of the code for accessing a property of a given object, V8 determines the object's current hidden class. V8 optimizes property access by predicting that this class will also be used for all future objects accessed in the same section of code and uses the information in the class to patch the inline cache code to use the hidden class. If V8 has predicted correctly the property's value is assigned (or fetched) in a single operation. If the prediction is incorrect, V8 patches the code to remove the optimisation.

Efficient Garbage Collection
V8 reclaims memory used by objects that are no longer required in a process known as garbage collection. To ensure fast object allocation, short garbage collection pauses, and no memory fragmentation V8 employs a stop-the-world, generational, accurate, garbage collector. This means that V8:

  • stops program execution when performing a garbage collection cycle.
  • processes only part of the object heap in most garbage collection cycles. This minimizes the impact of stopping the application.
  • always knows exactly where all objects and pointers are in memory. This avoids falsely identifying objects as pointers which can result in memory leaks.

In V8, the object heap is segmented into two parts: new space where objects are created, and old space to which objects surviving a garbage collection cycle are promoted. If an object is moved in a garbage collection cycle, V8 updates all pointers to the object.


四、JavaScriptCore、V8如何与WebCore交互
在WebCore::Frame的数据结构中包含数据成员KJSProxy* m_jscript;而在Chrome的代码中调整为JSBridge* m_jscript;而针对不同实现JavaScriptCore、V8,分别有:
class KJSBridge : public JSBridge {
public:
KJSBridge(Frame* frame) : m_proxy(new KJSProxy(frame)) { }
virtual ~KJSBridge() { delete m_proxy; }
........................
private:
KJSProxy* m_proxy;
};

class V8Bridge : public JSBridge {
public:
explicit V8Bridge(Frame* frame);
virtual ~V8Bridge();
.......................
private:
V8Proxy* m_proxy;
};
V8Bridge::V8Bridge(Frame* frame) {
m_proxy = new V8Proxy(frame);
}
V8Bridge::~V8Bridge() {
delete m_proxy;
}

而不同的KJSProxy与V8Proxy分别对应不同的Javascript实现,它们分别实现了与WebCore之间的共同接口,其主要数据结构分别如下:
class KJSProxy {
Frame* m_frame;
KJS::ProtectedPtr< KJS::JSDOMWindow> m_globalObject;
int m_handlerLineno;
.........................................
};

class V8Proxy {
Frame* m_frame;
v8::Persistent<v8::context> m_context;
v8::Persistent<v8::object> m_global;
// Special handling of document wrapper;
v8::Persistent m_document;
int m_handlerLineno;
...........................
};
具体不同Javascript实现如何实现与WebCore的接口,需了解不同Javascript实现逻辑;

如对Javascript实现逻辑及基本原理感兴趣,可具体参考其提供的api及sample等等;

至于Dom Binding的实现,JavaScriptCore与V8通过通过同样的方式来实现,可参考浅谈WebKit之WebCore篇 中所描述的Javascript实现如何与WebCore集成等;

具体Dom Binding的实现可参考generate-bindings.pl生成的代码,其中的内容一定会让你受益非浅,同时为将Javascript实现嵌入到其他应用中去提供非常有益的参考。如对window的实现,特别是open方法的实现,很值得研究研究。。。

五、初步对比JavaScriptCore、V8、SpiderMonkey等
具体JavaScriptCore、V8、SpiderMonkey、TracMonkey执行效率对比如何,不同的测试方法会有不同的测试结果,在这里不再阐述。

就个人了解而言,觉得JavaScriptCore关于对象的方法、属性的安全访问控制方面略有欠缺;

SpiderMonkey作为最早一批实现Javascript的引擎,其代码使用C语言来实现,稍现复杂,没有象后来的实现如JavaScriptCore、V8等借鉴了最新的虚拟机技术如JVM等;

V8作为新近推出的Javascript实现,正与其特点所描述,拥有很多优势,同时基于C++实现,充分利用了C++ template,代码相对简洁,便于学习使用;

关于TracMonkey请参考Firefox to get massive JavaScript performance boost

六、参考资源
Wiki Javascript
V8
Announcing SquirrelFish
Introducing SquirrelFish Extreme
SpiderMonkey Internals
Tamarin