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月18日星期六

Javascript调试工具

Javascript作为一门脚本语言,最近受到人们越来越多的关注,例如Chrome推出V8、Mozilla推出TracMonky、WebKit推出SquirrelFish等等Javascript引擎,使得Javascript实现的性能不断的得到提升,甚至更有Microsoft高级开发人员认为Silverlight的对手竟然是Javascript,而不是Flash,还有诸如Gears、prototype、dojo、jquery等JS库的大力发展,可见Javascript的应用前景相当光明。

但是其复杂性也会越来越高,Web开发人员遇到大量的Javascript往往会觉得头痛,甚至觉得Javascript比较诡异而又神奇,从而难以彻底掌握应用好。不过要掌握好Javascript的应用,其调试是个不得不提的话题,很早以前Microsoft就推出了Script Debugger工具,但Web开发人员使用起来还不是特别的方便,作为一个Web开发应用平台的推动者,Mozilla社区却为我们提供了更优秀的Javascript Debugger工具Venkman、Firebug,它们的出现极大的方便了Javascript的调试,让开发人员爱不释手,将它们比着Web开发的瑞士军刀一点都不为过。

今天我们从Venkman、Firebug的使用入手,进而分析其实现原理,同时了解了解WebKit内核的Javascript调试工具及其实现原理。

一、安装使用Venkman、Firebug
如果你还没使用过它们,现在就该动手了。赶快去Mozilla Addon中心下载JavaScript Debugger VenkmanFirebug吧。作为Firefox的extension,Venkman、Firebug不管是下载、安装、更新及使用,都已经与Firefox进行有机结合,使用起来非常的方便。

作为调试工具,其基本的功能不外乎设置断点、Step-in、Step-out、Step-over、Continue、查看参数及调用堆栈等。操作上与用VS调试C/C++代码几乎一致,但考虑各自特点的不同,下面总结一下个人对这两个工具的使用感受:

1、Firebug非常适合普通的Web开发人员,它可以非常方便、直观的查看html、js、dom等详细内容,查看js执行效率以及网络获取状态等,更值得称赞的是调试时动态更改html、js、dom的内容后,浏览效果会及时显现,从而大大的提供修改、调试效率。

网上一篇《初识Firebug 全文 — firebug的使用》很值得刚接触者参考参考。
有关Firebug更加详细的内容请参考Firebug网站

2、Venkman非常适合与Firefox开发相关的开发者如extension开发者等,虽然它的使用没有Firebug那么的直观方便动态更改内容,但其提供了完整的命令行调试模式,这一点与python的pdb很类似;更为突出的是它可以调试Firefox本身以及各种extensions(即chrome源文件,这一点目前Firebug还难以方便的处理),同时提供方便的过滤功能。有了Venkman,开发研究Firefox(当然包括各种有特色的extension及js库等)也就得心应手。

其中Learning the JavaScript debugger Venkman对Venkman有非常详细的介绍,很值得好好阅读阅读。

有关Venkman更加详细的使用请参考其HomePage

3、使用Venkman、Firebug调试Javascript有时会出现一些异常,没有象用Vs、gdb调试C/C++那样的稳定,也许与其本身的实现有些相关,毕竟是调试脚本语言。下面了解一下其实现也许能解除你的一些困惑。

二、Venkman实现
Venkman作为一个extension,其实现完全基于js+xul等相关技术实现。

1、UI
考虑所调试内容的多样性,Venkman UI方面的实现充分的利用了Gecko中的Overlay、XBL等技术要素,同时其显示的项目及其菜单等都是动态生成,界面风格类似于VS,各个显示区域可动态调整、组装。

可以具体关注一下Venkman-bindings.xml、Venkman.xul、Venkman-overlay.xul等实现;

2、调试逻辑控制
整个Gecko内核调试逻辑控制由一个实现了接口Components.interfaces.jsdIDebuggerService,CTRID为 "@mozilla.org/js/jsd/debugger-service;1"的XPCOM组件,其提供的主要接口如下:
interface jsdIDebuggerService : nsISupports
{
...........................................
/**
* Called when an error or warning occurs.
*/
attribute jsdIErrorHook errorHook;
/**
* Called when a jsdIScript is created or destroyed.
*/
attribute jsdIScriptHook scriptHook;
/**
* Called when the engine encounters a breakpoint.
*/
attribute jsdIExecutionHook breakpointHook;
/**
* Called when the engine encounters the debugger keyword.
*/
attribute jsdIExecutionHook debuggerHook;
/**
* Called when the errorHook returns false.
*/
attribute jsdIExecutionHook debugHook;
/**
* Called before the next PC is executed.
*/
attribute jsdIExecutionHook interruptHook;
/**
* Called when an exception is thrown (even if it will be caught.)
*/
attribute jsdIExecutionHook throwHook;
/**
* Called before and after a toplevel script is evaluated.
*/
attribute jsdICallHook topLevelHook;
/**
* Called before and after a function is called.
*/
attribute jsdICallHook functionHook;
attribute unsigned long flags;
attribute boolean initAtStartup;
readonly attribute boolean isOn;
/**
* Turn on the debugger.
*/
void on ();
[noscript] void onForRuntime (in JSRuntime rt);
void off ();
readonly attribute unsigned long pauseDepth;
unsigned long pause();
unsigned long unPause();
/**
* Force the engine to perform garbage collection.
*/
void GC();
void DumpHeap(in string fileName);
void clearProfileData();
/**
* Adds an execution hook filter. */
void insertFilter (in jsdIFilter filter, in jsdIFilter after);
/**
* Same as insertFilter, except always add to the end of the list.
*/
void appendFilter (in jsdIFilter filter);
....................................................
void enumerateContexts (in jsdIContextEnumerator enumerator);
void enumerateScripts (in jsdIScriptEnumerator enumerator);
void clearAllBreakpoints ();
jsdIValue wrapValue (/*in jsvalue value*/);

/**
* Push a new network queue, and enter a new UI event loop.
*/
unsigned long enterNestedEventLoop (in jsdINestCallback callback);
/**
* Exit the current nested event loop after the current iteration completes,
* and pop the network event queue.
*/
unsigned long exitNestedEventLoop ();
};

调试过程主要控制流程如下:
1、由Debugger(如Venkman)发起调试开始,调用DebuggerService.on方法,DebuggerService创建JSDContext,与当前JSRuntimer建立联系,并将DebuggerService中方法设置为当前JSRuntimer的globalDebugHooks对应元素的Hook(即设置回调函数指针);

2、Debugger将DebuggerService中的errorHook、scriptHook、breakpointHook、debuggerHook、debugHook、interruptHook、throwHook等Hook设置成Debugger提供的方法;

3、当JS引擎执行解析、调用JS脚本时,发现其globalDebugHooks中有对应回调Hook,如每解析完一个JS Function时就会调用newScriptHook,进而由DebuggerService调用Debugger提供的scriptHook,Debugger的scriptHook会根据JS引擎提供的文件名、函数名、行号等,读出对应文件显示出来;

4、当在指定文件对应行设置一个断点时,Debugger由DebuggerService调用JS引擎向指定的函数调用Frame中设置Trap操作码;

5、一旦JS引擎执行到Trap操作码,则会调用其globalDebugHooks中对应的回调Hook,进而调用DebuggerService breakpointHook,从而调用Debugger的debugTrap,其中会根据当时的调用上下文调用DebuggerService的DebuggerService的EnterNestedEventLoop方法,它往往会维护一个嵌套计数,然后作事件处理循环,其主要代码如下:
jsdService::EnterNestedEventLoop (jsdINestCallback *callback, PRUint32 *_rval)
{
// Nesting event queues is a thing of the past. Now, we just spin the
// current event loop.
nsresult rv;
nsCOMPtr
stack(do_GetService("@mozilla.org/js/xpc/ContextStack;1", &rv));
if (NS_FAILED(rv)) return rv;
PRUint32 nestLevel = ++mNestedLoopLevel;
nsCOMPtr thread = do_GetCurrentThread();
if (NS_SUCCEEDED(stack->Push(nsnull))) {
if (callback) {
Pause(nsnull);
rv = callback->OnNest();
UnPause(nsnull);
}
while (NS_SUCCEEDED(rv) && mNestedLoopLevel >= nestLevel) {
if (!NS_ProcessNextEvent(thread))
rv = NS_ERROR_UNEXPECTED;
}
JSContext* cx;
stack->Pop(&cx);
NS_ASSERTION(cx == nsnull, "JSContextStack mismatch");
}
else rv = NS_ERROR_FAILURE;
NS_ASSERTION (mNestedLoopLevel <= nestLevel,
"nested event didn't unwind properly");
if (mNestedLoopLevel == nestLevel)
--mNestedLoopLevel;
*_rval = mNestedLoopLevel;
return rv;
}

其中的事件处理循环很关键,在处理事件时被调试的JS脚本无法继续进行执行(即Block),而Debugger仍然可以处理UI事件,如显示当前CallFrame、查看变量值等;

6、当遇到断点时,用户通过Step-in或Step-out等触发JS引擎调用CallHook等,进而根据嵌套计数跳出上面的处理循环,而进入另一个循环或调试结束。

说明:整个过程与传统的windbg、gdb与OS、硬件协同完成Debug过程的实现非常类似,只不过这里的JS引擎相当与OS,它们都非常清楚源文件与具体执行点之间的对应和当时的上下文信息;一样由debugger来提供一些Hook给JS引擎或OS,当JS引擎或OS遇到Trap指令或异常等,它们会及时调用debugger提供的Hook,以由debugger给出下一步执行的决定。

但它们之间有个最大的不同在于JS debugger与运行的被debug的JS脚本都处于同一浏览器进程中;而windbg、gdb调试程序时debugger与被debug的程序往往处于不同的进程。

这里就导致JS debugger中需要过滤的问题,因为不可能在同一进程中调试自己(如用Venkman调试Venkman或用Firebug调试Firebug),这个过滤的动作往往由debugger来决定什么样的源文件可以设置断点、可对其函数进行Step-in、Step-out等,Venkman与Firebug的过滤机制不一样,导致Firebug不能调试chrome类的源文件。

Firebug有关调试方面的实现与Venkman很类似,一样需要利用DebuggerService来实现交互,有时间可具体参考参考。

三、WebKit Javascript调试实现
其实现逻辑与Gecko差不多,首先由Javascript实现提供一个Debugger接口类(相关于Gecko中的DebugHook),然后由一个实现了该Debugger接口的JavaScriptDebugServer来控制调试窗口与被调试页面之间的关系。

一旦JS引擎遇到解析完JS函数、设置的断点、开始函数调用、结束函数调用等则通知(或回调)调试窗口,以控制下一步的操作。

Drosera-Webkit-Trac对其中的设计有一定的说明。

四、总结
对一个开发人员来讲,不管是Web开发、还是Java、C/C++开发,乃至内核、驱动开发等,调试是一门必须掌握的技术,因为它不仅能让你提高工作效率,同时能让你更加深入的了解掌握计算机技术;如果能更一步的了解调试工作原理,则会让你如虎添翼。有空可以看看《软件调试》、《How debuggers work》等。。。

五、参考资源
Venkman HomePage
Learning the JavaScript debugger Venkman
Firebug HomePage
Drosera-Webkit-Trac

Wiki Debugger
Wiki GNU_Debugger(GDB)
Wiki WinDbg
Advanced Debugging(高端调试)

2008年10月15日星期三

认识了解Gears

前一段时间对Google Chrome有过简单的认识与了解,在体验新鲜出炉的Google Chrome浏览器中提到Google的Google Chrome Comic,其中提出通过Gears来扩展Web应用,但究竟什么是Gears以及其如何来扩展浏览器等,一直令人好奇。通过了解Gears应该可以了解Google将来究竟主推怎样的方式来扩展Web应用,当然包括Chrome浏览器及Android等,因为个人一直认为Gecko内核对扩展的支持非常方便、灵活、实用(具体可参考与Gecko相关的文档),但对基于WebKit内核的扩展相对来说不是那么的方便与直接,为了满足探究感,让我们开始了解Gears之旅吧。

一、什么是Gears?
Gears is an open source project that enables more powerful web applications, by adding new features to web browsers.

目前主要提供一组JS API以供Web应用使用,对应Web开发者来说其类似于prototype、dojo、jquery等JS库,但其实现是通过扩展浏览器来实现(不仅仅包含js代码还包含二进制代码等)。

主要的API模块有:
  • A Database module (powered by SQLite) that can store data locally.
  • A WorkerPool module that provides parallel execution of JavaScript code.
  • A LocalServer module that caches and serves application resources (HTML, JavaScript, images, etc).
  • A Desktop module that lets web applications interact more naturally with the desktop.
  • A Geolocation module that lets web applications detect the geographical location of their users.

通过Google Gears可以了解Gears的发展及目前开发状态等,其中有些已实现或打算实现的API还是非常有意思的如Geolocation、Audio、Camera等,一旦这些API能完善好的话确实非常有利于Web应用的扩展,比如说录音、摄像等。

如果对扩展Web应用如开发RIA等有兴趣的话,可以好好考虑哪些应用或业务,可以或需要扩展到Web上来,通过了解Gears,说不定Gears能给您很大的启发。。。。

二、如何利用Gears?
利用Gears,首先需要用户安装Gears如对Firefox来讲就是一个extension,对Chrome、Safari来讲是一个符合NPAPI的插件,对IE来讲就是一个ActiveX;其次利用Gears API开发相关应用页面,以供最终用户使用。

也许对最终用户来讲稍嫌麻烦,需要额外安装程序,但如果Gears能象Flash一样实用、好用,那时就不再会觉得很麻烦啦。

对开发者来讲利用Gears相对就简单啦。在页面包含
<script src="gears_init.js"></script>
<script>
if (!window.google || !google.gears) {
location.href = "http://gears.google.com/?action=install&message=<your welcome message>" + "&return=<your website url>";
}
</script>
然后就可以使用其中的API啦。

具体可参考Gears Resources and Tools中的gears_init.js、samples及其他API等文档。


三、初步探究Gears基本实现原理
通过初步了解Get gears_init.js可以基本了解Gears提供对IE、Firefox、Chrome、Safari等支持,但其实现方式各有不同。

1、对各浏览器的支持
对Firefox来讲,Gears扩展了一个GearsFactory JS对象,可供JS脚本直接调用,从JS的角度看相对于给window对象新增了一个对象,就像原先的document、navigator对象一样使用,只不过其功能不同而已。

对IE来讲,Gears扩展了一个Gears.Factory ActiveX对象,可通过JS脚本new ActiveXObject('Gears.Factory');来直接调用,从JS的角度看可以象使用XMLHttpRequest对象一样使用Gears.Factory。

对Chrome、Safari来讲,就有点特殊啦,也有点诡异的感觉。需要在当前文档动态添加一个MIMEType为application/x-googlegears的Object插件对象,而这个Object对象就是对应的google.gears.factory对象。

从这里就可以看出WebKit对扩展JS的支持,从架构设计不是那么的直接,需要由NPAPI插件机制来中转实现;而Firefox、IE都从架构上提供了相应扩展机制来扩展,相当的直接明了及与浏览器本身的统一。

2、google.gears.factory对Firefox支持的实现
浅谈Gecko关键部分之六认识javascript实现及应用中我们可以了解到Gecko中XPCOM组件通过xpconnect及对DOMCLASSINFO的定义,就可轻松的将XPCOM组件Binding给JS使用,就像Gecko中对Components、Components.Interfaces、xmlhttprequest等的实现。

在Gears中就是使用了类似的方式来Binding一个GearsFactory组件给JS环境使用。其中实现了一个实现了GearsFactoryInterface接口的名称为GearsFactory,ContractId 为"@google.com/gears/factory;1"的组件,具体代码详见gears\factory\Factory_ff.cc,然后在gears\base\firefox\Module.cc中注册该组件时使用
catMgr->AddCategoryEntry(JAVASCRIPT_DOM_CLASS,
kGearsFactoryClassName,
kDomciExtensionContractId,
PR_TRUE, PR_TRUE, NULL);
将该组件声明为需要由xpconnect Binding给JS使用。一旦启用xpconnect,xpconnect就会作相应处理,具体可参考nsDOMClassInfo::RegisterExternalClasses()等方法。这样就完成JS GearsFactory对象的实现。

3、google.gears.factory对Safari支持的实现
浅谈WebKit之JavaScriptCore/V8篇中我们可以知道WebKit中将DOM实现Binding给Javascript实现往往通过generate-bindings.pl生成的代码来静态Binding,这种方式无法满足动态Binding的需求。但是从浅谈Gecko关键部分之九Plugin中我们了解到NPAPI插件接口提供了一组接口可以将插件的接口动态Binding给JS环境,同时可以让插件与JS之间相互调用。

而WebKit中实现了对NPAPI插件接口的支持,这样Gears中实现了一个mimetype为application/x-googlegears的插件,将该插件对象当作google.gears.factory JS对象来提供给页面使用。

具体可参考gears\base\npapi\Module.cc、Npp_bindings.cc,其中的NPP_New()方法应该是主要入口,同时设置该插件为windowless。至于如何将插件的方法动态Binding给JS使用,可具体参考Webkit中对NPN_CreateObject、NPClass的实现支持等。

4、google.gears.factory对IE支持的实现
按照创建ActiveX及BHO等接口方法来实现Gears.Factory ActiveX,进而将该对象赋值给google.gears.factory。具体关于ActiveX及BHO相关的知识参考Microsoft文档。

5、对其他API接口的实现
虽然google.gears.factory实现了create、getBuildInfo、version等接口,但对通过create创建出来的提供给开发人员用的localserver、geolocation、httprequest、audio等对象是如何实现的呢?

体现Gears强大及方便的地方就在这里。Gears提供了一组统一的类及接口实现可以方便将自定义的类及接口Binding给对应浏览器的JS实现。针对Firefox,它通过扩展SpiderMonkey的方式直接扩展JS实现;针对Safari,它还是使用NPAPI、NPObject的方式来扩展。

具体可以参考一下gears\base\common\Js_runner_np.cc、Js_runner_ff.cc、Js_runner_ie.cc等;

这一组统一扩展JS实现的类及接口有:ModuleImplBaseClass、ModuleWrapperBaseClass、DispatcherInterface、Dispatcher、MarshaledModule、ModuleEnvironment等,其作用就是按照统一的方式方便的将自定义的类及接口Binding给不同的浏览器实现。

如果对这方面有兴趣,可以对这些类及接口之间的关系及实现进一步研究,看Gears究竟是如何将C/C++类实现Binding给JS。其实这一点对JS来讲非常有意义,因为一旦可轻松方便的扩展JS,那么JS的应用就会进一步的扩大,就象python一样,python脚本可以方便扩充其模块,C/C++类库也可方便的扩充成python模块,从而让python几乎无所不能。


四、如何扩展Gears API?
在初步了解Gears实现原理之后,正与Gears CreatingNewModules所描述,我们也可以利用Gears来扩展自己的API,在了解其他API的实现基础上,其实扩展一个自己的Gears API难度似乎并不太大。。。

期望大家有时间可以加入到Google Gears中来一起拓展Web应用。。。

五、总结
通过对Gears的初步了解,觉得Gears对浏览器的扩展主要体现在对JS方面的扩展,相当于JS功能库的扩充,并且主要体现在非UI方面,虽然其中有部分UI方面的内容,但相当少;而Adobe推出的flash、Microsoft推出的Silverlight主要是按照插件的方式基于html标准的embed/object进行扩充,往往涉及到UI方面,并都有自身的特色如内嵌脚本语言、播放video/audio、网络处理等等;而Gecko所拥有的extension确可以是全方位的(包括UI、xpcom、插件等等)。

目前随着Ajax及RIA的推广,各式各样的JS库如prototype、dojo、jquery等也层出不穷,但是针对JS功能库方面的扩展还没有一个相对通用的国际标准,也许Gears正好抓住这样一个发展的趋势才采取这种通过扩展JS接口,为开发人员提供API的方式来扩展Web应用。

Gears API一方面迎合了RIA等方面的需求,另一方面也推动浏览器的扩展。但是它似乎抛弃了UI方面的扩展如使用Gecko的XUL、Microsoft XAML等的可能,也许flash对UI方面有太多的优势,同时html5中的video/audio逐步提上日程,做一个类似于Silverlight的插件意义不大,还不如在JS方面下足功夫,同时全面拥抱html(如gmail、gmap、gdoc等),也许这就是Google目前对扩展Web应用的一点看法吧。。。


六、参考资源
Gears Project
Wiki Gears(software)