我们在编译webkit代码时总希望webview在显示一些界面时如果界面超过当前显示的窗口,并且只是大一点的情况下,比较期待窗口不要因为这几个像素的差距而在使用时出现晃动感。本篇文章就是来探究这其中的原理,当然其中有很多部分的内容是引用其他前辈的文章,或者借鉴对比得来的,如果有不当之前还请谅解。
随着GPU硬件能力的增强,浏览器可以借助于其处理图形方面的性能来对渲染实现加速。此时不再将所有层绘制到一起,而是进行分层渲染,合成之后再显示到屏幕上。
整个渲染过程基本上可以分为相对独立的三个步骤:
– Layers Sync
– Layers Draw to BackingStore
– Compositing Backing Store to Window byWebView
一,LayerSync(Layer 更新)
该步骤即WebCore更新自己的树结构的过程。
在这里稍微解释一下Layer的概念,我们都知道WebCore中的三棵树:DOM树,Render树及RenderLayer树。事实上远不止这三棵树,在开启硬件加速的情况下,WebView会构成一棵与RenderLayer树结构并行的Layer树,通常RenderLayer树中的一个或多个节点对于Layer树中的一个节点。
Layer类是一个基类,BaseLayerAndroid和LayerAndroid类继承于它。Layer树中BaseLayerAndroid为根,其代表最大的surface,通常是一个普通的web页面,比如上图中的Layer1;Layer树中其他节点为LayerAndroid,它代表一些特殊的surface,如video,插件等,比如上图中的其他Layer。
二,Layers Draw to Backing Store
该步骤是将WebCore渲染的内容绘制到后端存储的过程
这个过程也是我们控制底层如何只把我们需要显示的内容画出来的关键地方。也就是说在这一步做一个操作就会影响整个后面的显示。
后端存储有两种。一种是BaseLayerAndroid类的PictureSet,一种是LayerAndroid类的SkPicture。SkPicture记录了一系列的绘制命令,而PictureSet是SkPicture的集合。它们的实现步骤也不相同。写到BaseLayerAndroid的PictureSet的步骤称为Page Backing Store,而写到LayerAndroid则称为Layer Backing Store。我们只介绍PageBacking Store的实现过程。
PageBacking Store该过程可以从android::WebViewCore::webkitDraw开始看起(这个在java层)。下面给出一个精简的调用堆栈(省略了一些中间步骤):(webkitDrawjava层源码)
private void webkitDraw() {//java层函数
mDrawIsScheduled = false;
DrawData draw = new DrawData();
if (DebugFlags.WEB_VIEW_CORE) Log.v(LOGTAG, "webkitDraw start");
draw.mBaseLayer = nativeRecordContent(draw.mInvalRegion, draw.mContentSize);/*nativeRecordContent在c层注册为recordContent*/
if (draw.mBaseLayer == 0) {
if (mWebView != null && !mWebView.isPaused()) {
if (DebugFlags.WEB_VIEW_CORE) Log.v(LOGTAG, "webkitDraw abort, resending draw message");
mEventHub.sendMessage(Message.obtain(null, EventHub.WEBKIT_DRAW));
} else {
if (DebugFlags.WEB_VIEW_CORE) Log.v(LOGTAG, "webkitDraw abort, webview paused");
}
return;
}
mLastDrawData = draw;
webkitDraw(draw);/*在获取到详细参数后开始调用底层函数画出需要显示的内容*/
}
在android::WebViewCore中recordContent函数会先调用recordPictureSet,再调用createBaseLayer函数。
recordContent函数C层源码:
BaseLayerAndroid* WebViewCore::recordContent(SkRegion* region, SkIPoint* point)
{
DBG_SET_LOG("start recordContent");
// If there is a pending style recalculation, just return.
if (m_mainFrame->document()->isPendingStyleRecalc()) {
DBG_SET_LOG("recordContent: pending style recalc, ignoring.");
return 0;
}
float progress = (float) m_mainFrame->page()->progress()->estimatedProgress();
m_progressDone = progress <= 0.0f || progress >= 1.0f;
recordPictureSet(&m_content);//从这个函数调用函数recordPictureSet,从而遍历整个树frame->tree
if (!m_progressDone && m_content.isEmpty()) {
DBG_SET_LOGD("empty (progress=%g)", progress);
return 0;
}
region->set(m_addInval);
m_addInval.setEmpty();
#if USE(ACCELERATED_COMPOSITING)
#else
region->op(m_rebuildInval, SkRegion::kUnion_Op);
#endif
m_rebuildInval.setEmpty();
point->fX = m_content.width();
point->fY = m_content.height();
DBG_SET_LOGD("region={%d,%d,r=%d,b=%d}", region->getBounds().fLeft,
region->getBounds().fTop, region->getBounds().fRight,
region->getBounds().fBottom);
DBG_SET_LOG("end");
return createBaseLayer(region);
}
其中recordPictureSet有一个输出参数,它获取PictureSet。而createBaseLayer则创建一个BaseLayerAndroid对象,并将前者获取的PictureSet设置给该对象(setContent)。
recordPictureSet函数C层源码:
void WebViewCore::recordPictureSet(PictureSet* content)
{
// if there is no document yet, just return
if (!m_mainFrame->document()) {
DBG_SET_LOG("!m_mainFrame->document()");
return;
}
if (m_addInval.isEmpty()) {
DBG_SET_LOG("m_addInval.isEmpty()");
return;
}
// Call layout to ensure that the contentWidth and contentHeight are correct
// it's fine for layout to gather invalidates, but defeat sending a message
// back to java to call webkitDraw, since we're already in the middle of
// doing that
/*调用布局,以确保contentWidth和contentHeight是正确适宜的布局,收集无效内容,
如果没有失败就发送回一个消息到java层调用webkitDraw,此时我们已经在java层做相应的操作。*/
m_skipContentDraw = true;
bool success = layoutIfNeededRecursive(m_mainFrame);
m_skipContentDraw = false;
// We may be mid-layout and thus cannot draw.如果此时正在布局就不能draw
if (!success)
return;
{ // collect WebViewCoreRecordTimeCounter after layoutIfNeededRecursive
#ifdef ANDROID_INSTRUMENT
TimeCounterAuto counter(TimeCounter::WebViewCoreRecordTimeCounter);
#endif
// if the webkit page dimensions changed, discard the pictureset and redraw.
/*如果webkit页面的体积改变,就丢掉原来的pictureset并且重画*/
WebCore::FrameView* view = m_mainFrame->view();
int width = view->contentsWidth();
int height = view->contentsHeight();
// Use the contents width and height as a starting point.用页面内容的宽和高作为起始点
SkIRect contentRect;
contentRect.set(0, 0, width, height);
SkIRect total(contentRect);
// Traverse all the frames and add their sizes if they are in the visible
// rectangle.
/*遍历所有的frames,如果他们的尺寸大小在可视矩形之内就添加他们 */
for (WebCore::Frame* frame = m_mainFrame->tree()->traverseNext(); frame;
frame = frame->tree()->traverseNext()) {
// If the frame doesn't have an owner then it is the top frame and the
// view size is the frame size.
/*如果一个frame没有owner那么他就是top frame而且它的可视大小就是frame的大小*/
WebCore::RenderPart* owner = frame->ownerRenderer();
if (owner && owner->style()->visibility() == VISIBLE) {
int x = owner->x();
int y = owner->y();
// Traverse the tree up to the parent to find the absolute position
// of this frame.
/*遍历树直到根节点找到其绝对位置的frame*/
WebCore::Frame* parent = frame->tree()->parent();
while (parent) {
WebCore::RenderPart* parentOwner = parent->ownerRenderer();
if (parentOwner) {
x += parentOwner->x();
y += parentOwner->y();
}
parent = parent->tree()->parent();
}
// Use the owner dimensions so that padding and border are
// included.
/*使用owner的大小以便填充的边界被包含进去*/
int right = x + owner->width();
int bottom = y + owner->height();
SkIRect frameRect = {x, y, right, bottom};
// Ignore a width or height that is smaller than 1. Some iframes
// have small dimensions in order to be hidden. The iframe
// expansion code does not expand in that case so we should ignore
// them here.
/*忽略掉宽或者高小于1的。一些iframes拥有较小的尺寸以便于隐藏。
这些iframe扩展时不需再加入代码,所以此处我们忽略他们*/
if (frameRect.width() > 1 && frameRect.height() > 1
&& SkIRect::Intersects(total, frameRect))
/*Returns true if total and frameRect are not empty, and they intersect */
total.join(x, y, right, bottom);
}
}
// If the new total is larger than the content, resize the view to include
// all the content.
/*如果新的total比content较大,调整view的大小以便包含所有的内容*/
if (!contentRect.contains(total)) {
// Resize the view to change the overflow clip.
/*调整view的大小裁剪并改变溢出*/
view->resize(total.fRight, total.fBottom);
// We have to force a layout in order for the clip to change.
/*我们需要推出一个布局,以适应裁剪修改后的尺寸*/
m_mainFrame->contentRenderer()->setNeedsLayoutAndPrefWidthsRecalc();
view->forceLayout();
// Relayout similar to above 重布局,与上面相似
m_skipContentDraw = true;
bool success = layoutIfNeededRecursive(m_mainFrame);
m_skipContentDraw = false;
if (!success)
return;
// Set the computed content width 设定计算出的内容宽高
width = view->contentsWidth();
height = view->contentsHeight();
}
/***we need add start***/
{
if((height<800)&&(height>720))//yake add
{
height=720;
}
if((width<=1300)&&(width>=1280))
{
width=1280;
}
}
/***we need add end***/
if (cacheBuilder().pictureSetDisabled())
content->clear();
#if USE(ACCELERATED_COMPOSITING)//使用硬件加速
// Detects if the content size has changed 监听内容的大小是否发生变化
bool contentSizeChanged = false;
if (content->width() != width || content->height() != height)
contentSizeChanged = true;
#endif
content->setDimensions(width, height, &m_addInval);
// Add the current inval rects to the PictureSet, and rebuild it.
/*把当前获取的矩形加入PictureSet,并且重建它*/
content->add(m_addInval, 0, 0, false);
// If we have too many invalidations, just get the area bounds
/*如果有太多的失效区域,只获取区域边界*/
SkRegion::Iterator iterator(m_addInval);
int nbInvals = 0;
while (!iterator.done()) {
iterator.next();
nbInvals++;
if (nbInvals > MAX_INVALIDATIONS)
break;
}
if (nbInvals > MAX_INVALIDATIONS) {
SkIRect r = m_addInval.getBounds();
m_addInval.setRect(r);
}
// Rebuild the pictureset (webkit repaint)
/*重建图层集,webkit重画。此函数会调用到rebuildPicture*/
rebuildPictureSet(content);
#if USE(ACCELERATED_COMPOSITING)
// We repainted the pictureset, but the invals are not always correct when
// the content size did change. For now, let's just reset the
// inval we will pass to the UI so that it invalidates the entire
// content -- tiles will be marked dirty and will have to be repainted.
// FIXME: the webkit invals ought to have been enough...
/*重画图层集,由于获取区域并不一直都刚好包含这些尺寸改变。现在,我们将通过UI重置inval,
以便于将整个内容瓷砖将标志为脏,并且被重画。FIXME:webkit invals应该已经足够……*/
if (contentSizeChanged) {
SkIRect r;
r.fLeft = 0;
r.fTop = 0;
r.fRight = width;
r.fBottom = height;
m_addInval.setRect(r);
}
#endif
} // WebViewCoreRecordTimeCounter
//............后面的就省略了,我们暂时不做讨论
}
至此我们可以看出,在recordPictureSet函数中过程如下:首先遍历m_mainFrame树查看此时是否需要needsLayout,如果此时正在布局中就不能draw。之后用content记录m_mainFrame->view的尺寸。在遍历m_mainFrame树后用total对比其大小,遍历所有的frames,如果他们的尺寸大小在可视矩形之内就添加合并他们。最后比较content与total,如果新的total比content较大,调整view的大小以便包含所有的内容。调整后就会设定计算出的最终内容宽高,所以我们可以在此处判定最终的页面内容的高度和大小,并改变他们,就实现了我们控制画出的页面大小。
如上我们在we need add start至we need add end之间添加的代码,上面只是一个实例他的意思是,把高度为720至800之间的高度均容忍为高度720,把宽度在1280至1300之间均容忍为1280。这只是一个特例,我们可以根据自己的实际情况把相应的内容修改即可。
这就完成了整个PageBacking Store的过程。
而在rebuildPicture中会通过SkPicture来构造GraphicsContext类。然后作为参数传给RenderLayer,在遍历RenderLayer树的过程中,由GraphicsContext来进行最终的绘制工作。
三,CompositingBacking Store to Window
此过程主要是合成后台存储中的影像并显示在窗口上。
最终WebView被重绘时,进行合成,并显示出来。
参考文章:
1,http://blog.csdn.net/yangkai6121/article/details/9404819
2,http://blog.csdn.net/yangkai6121/article/details/9404859
3,http://blog.csdn.net/yangkai6121/article/details/9404903
4,http://blog.csdn.net/yangkai6121/article/details/9404875