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