WebKit 在渲染頁面之前,需要確定各個元素的位置、大小,而這個過程就是layout(布局)。下面,我們對layout的主要過程進行一番說明。
一、FrameView::layout方法
FrameView作為與View相關的類,其主要涉及與顯示相關的內容,而其中對頁面元素的布局至關重要,這也是瀏覽器的核心處理部分。
我們都知道瀏覽器從Web服務器獲得數據后,經解析會構建DOM樹、Render樹,然后進行布局處理,進而為渲染頁面作好準備,其中的布局處理往往由FrameView::layout方法發起,讓我們來具體看看其實現,一窺其主要實現過程。
void FrameView::layout(bool allowSubtree)
{
if (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樹等。
if (m_frame->needsReapplyStyles())
m_frame->reapplyStyles();
else if (document->childNeedsStyleRecalc())
document->recalcStyle();
bool subtree = m_layoutRoot;
RenderObject* root = subtree ? m_layoutRoot : document->renderer();
if (!root) {
// FIXME: Do we need to set m_size here?
m_layoutSchedulingEnabled = true;
return;
}
//布局的處理可能相互嵌套,這與發起布局處理的時機相關。
//設置滾動條相關
if (m_canHaveScrollbars) {
hMode = ScrollbarAuto;
vMode = ScrollbarAuto;
} else {
hMode = ScrollbarAlwaysOff;
vMode = ScrollbarAlwaysOff;
}
if (!subtree) {
// Now set our scrollbar state for the layout.
if (body->hasTagName(framesetTag) && !m_frame->settings()->frameFlatteningEnabled()) {
// frameset 而且設置了frameFlatteningEnabled,則關閉滾動條
body->renderer()->setChildNeedsLayout(true);
vMode = ScrollbarAlwaysOff;
hMode = ScrollbarAlwaysOff;
} else if (body->hasTagName(bodyTag)) {
// 設置滾動條
applyOverflowToViewport(o, hMode, vMode);
}
}
//root往往為RenderView對象
RenderLayer* layer = root->enclosingLayer();
m_midLayout = true;
beginDeferredRepaints();
root->layout();
endDeferredRepaints();
m_midLayout = false;
m_layoutSchedulingEnabled = true;
if (!subtree && !static_cast(root)->printing())
adjustViewSize();
// Now update the positions of all layers.
//對當前Render樹布局完后,設置RenderLayer樹的布局信息,其中m_doFullRepaint描述是否需要發起渲染處理。
beginDeferredRepaints();
layer->updateLayerPositions(...);
endDeferredRepaints();
// 設置快速blit
setCanBlitOnScroll(!useSlowRepaints());
//因為在布局的過程中,可能進一步獲得網頁數據,則需要繼續布局處理。
if (!m_postLayoutTasksTimer.isActive()) {
// Calls resumeScheduledEvents()
performPostLayoutTasks();
if (!m_postLayoutTasksTimer.isActive() && 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.
m_postLayoutTasksTimer.startOneShot(0);
pauseScheduledEvents();
layout();
}
} else {
resumeScheduledEvents();
}
}
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);
for (RenderObject* child = firstChild(); child; child = child->nextSibling()) {
if (child->style()->height().isPercent() || child->style()->minHeight().isPercent() || child->style()->maxHeight().isPercent())
child->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
// Reset overflow and then replace it with docWidth and docHeight.
m_overflow.clear();
addLayoutOverflow(IntRect(0, 0, docWidth(), 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 we have a lightweight clip, there can never be any overflow from children.
if (hasControlClip() && m_overflow)
clearLayoutOverflow();
}
三、RenderBlock::layoutBlock方法
layoutBlock 會分成兩個分支:layoutInlineChildren 和 layoutBlockChildren
void RenderBlock::layoutBlock(bool relayoutChildren)
{
...
calcWidth();//先計算寬度
calcColumnWidth();
m_overflow.clear();
if (oldWidth != width() || oldColumnWidth != desiredColumnWidth())
relayoutChildren = true;
clearFloats();
int previousHeight = height();
setHeight(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)))
setHeight(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 = height();
calcHeight();
if (previousHeight != height())
relayoutChildren = true;
//另外布局屬性為Fixed和absolute的元素
layoutPositionedObjects(relayoutChildren || isRoot());
// Update our scroll information if we're overflow:auto/scroll/hidden now that we know if we overflow or not.
updateScrollInfoAfterLayout();
// Repaint with our new bounds if they are different from our old bounds.
bool didFullRepaint = repainter.repaintAfterLayout();
if (!didFullRepaint && repaintTop != repaintBottom && (style()->visibility() == VISIBLE || enclosingLayer()->hasVisibleContent())) {
// 設置repaintRect
// 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())
repaintRectangle(reflectedRect(repaintRect));
}
}
setNeedsLayout(false);
}
四、RenderBlock::layoutBlockChildren方法
void RenderBlock::layoutBlockChildren(bool relayoutChildren, int& maxFloatBottom)
{
int top = borderTop() + paddingTop();
int bottom = borderBottom() + paddingBottom() + horizontalScrollbarHeight();
// Fieldsets need to find their legend and position it inside the border of the object.
// The legend then gets skipped during normal layout.
RenderObject* legend = layoutLegend(relayoutChildren);
//遍歷子節點
RenderBox* next = firstChildBox();
while (next) {
RenderBox* child = next;
next = child->nextSiblingBox();
if (legend == child)
continue; // Skip the legend, since it has already been positioned up in the fieldset's border.
// Make sure we layout children if they need it.
// FIXME: Technically percentage height objects only need a relayout if their percentage isn't going to be turned into
// an auto value. Add a method to determine this, so that we can avoid the relayout.
if (relayoutChildren || ((child->style()->height().isPercent() || child->style()->minHeight().isPercent() || child->style()->maxHeight().isPercent()) && !isRenderView()))
child->setChildNeedsLayout(true, false);
// If relayoutChildren is set and we have percentage padding, we also need to invalidate the child's pref widths.
if (relayoutChildren && (child->style()->paddingLeft().isPercent() || child->style()->paddingRight().isPercent()))
child->setPrefWidthsDirty(true, false);
// 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.
if (handleSpecialChild(child, marginInfo))
continue;
// Lay out the child.
layoutBlockChild(child, marginInfo, previousFloatBottom, maxFloatBottom);
}
// 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);
}
layoutBlockChild 方法就比較復雜了,具體可以自己看代碼。
五、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的網頁布局過程有一個清晰而準確的認識。。
posted on 2012-04-23 17:35
Bluesea 閱讀(2259)
評論(0) 編輯 收藏 引用 所屬分類:
Android-WebKit