2008年7月5日星期六

浅谈Gecko关键部分之四The life of an html http Request

从用户的角度来看,一个浏览器其实是相当简单的,用户输入一个Web地址,然后按回车,等不了多久,一个具有丰富内容的画面就会出现在用户面前,其中包含五彩缤纷的排列整齐的文字、图片、动画甚至视频,用户通过点击自己感兴趣的地方,新的内容画面就会呈现在用户面前,这样用户就可以只需简单的点击一下鼠标就可以在互联网上自由遨游啦。现在回想起来,也许正是浏览器当初在设计的时候充分考虑到用户的易用性才导致互联网的蓬勃发展,正像谷歌提供搜索一样简单易用并且非常实用,这样说来,互联网的发展离不开浏览器,是她带领人们走向了互联网,回顾历史让我们感慨万千,那么浏览器主要做了些啥?她为什么能支持如此丰富的内容?她的发展方向在哪里?为了解答心中的疑问,首先让我们初步了解Gecko内核是如何完成从用户输入Web地址,直到内容丰富的画面显示出来的整个过程,这个过程涉及的面比较广,目前我们主要关心关键过程、关键点及相关的关键类的实现,争取能有个整体的而又完整的印象。

一、创建nsWebShellWindow实例,准备处理输入的WebURI地址;
因为接收用户打开一个新URI的情况比较多,如通过脚本中的window.open或直接在地址栏中输入或者在新开的一个窗口打开或者在新建的一个页签打开等等,通过对这一系列的打开一个页面的外围场景进行处理后,内核都会新建一个或利用原有的nsWebShellWindow实例来处理整个打开URI的过程。其包含的主要成员有nsComPtr mWindow,代表一个原生窗口,将要打开的页面可在其中显示内容如文字、图片等;nsComPtr mDocShell,代表一个协调者,它管理整个打开URI的过程,其实现类nsDocShell也就成为整个Gecko内核的核心部分,它不仅接收处理打开URI请求,同时提供接口以反馈相关信息给外部如提示正在打开某某地址等。在nsWebShellWindow的Initialize()方法中通过mWindow = do_CreateInstance(kWindowCID, &rv);来创建mWindow,通过mDocShell = do_CreateInstance("@mozilla.org/webshell;1");来创建设置mDocShell为打开URI做好前期准备。

二、准备装载指定URI的内容;
首先由协调者nsDocShell的LoadURI方法来发起装载的请求,经过一番安全或请求Policy等方面的设定后创建nsCOMPtr uriLoader=do_GetService(NS_URI_LOADER_CONTRACTID, &rv);
nsCOMPtr channel;
rv = NS_NewChannel(getter_AddRefs(channel),
aURI, nsnull, nsnull, static_cast(this), loadFlags);
由uriLoader->OpenURI(aChannel, (mLoadType == LOAD_LINK), this);来统一继续处理所有的URI请求,其中会创建一个临时的nsCOMPtr loader =new nsDocumentOpenInfo(aWindowContext, aFlags, this);同时将loader作为一个nsIStreamListener来接收请求而来的数据,以分析请求而来的文档类型如text/html、xml等以决定后续不同的处理,然后由rv = channel->AsyncOpen(loader, nsnull);来真正向Web服务发出请求,并通过异步的方式来处理接收数据,这里往往会涉及到以前提到的线程处理等,总之至此基本的准备动作应该都已准备好,就等待Web服务端返回数据啦。

三、根据接收的数据类型创建Document及相关ContentViewer;
根据异步数据的获取,获取数据的线程向MainThread发出nsInputStreamReadyEvent事件,由主线程来处理相关事件,其往往会调用到nsInputStreamPump中去,其关键调用如下:
nsInputStreamPump::OnInputStreamReady(nsIAsyncInputStream *stream)
{
LOG(("nsInputStreamPump::OnInputStreamReady [this=%x]\n", this));

// this function has been called from a PLEvent, so we can safely call
// any listener or progress sink methods directly from here.

for (;;) {
if (mSuspendCount || mState == STATE_IDLE) {
mWaiting = PR_FALSE;
break;
}

PRUint32 nextState;
switch (mState) {
case STATE_START:
nextState = OnStateStart();
break;
case STATE_TRANSFER:
nextState = OnStateTransfer();
break;
case STATE_STOP:
nextState = OnStateStop();
break;
}
...........

mState = nextState;
}
return NS_OK;
}

当STATE_START时,表示已获取部分
初始数据,其中OnStateStart()会调用作为Channel的streamListener的OnStartRequest方法,此时streamListener为uriLoader临时创建的nsDocumentOpenInfo实例loader,由它来处理,其方法OnStartRequest进而由nsDocumentOpenInfo的DispatchContent方法来尝试决定能否处理该文档类型,一旦决定可以处理该类型,进而由nsDSURIContentListener的DoContent来调用nsDocShell的方法CreateContentViewer来创建Document及Viewer,其中主要进行代码包括:
{
..........................................
// Instantiate the content viewer object
nsCOMPtr viewer;
nsresult rv = NewContentViewerObj(aContentType, request, mLoadGroup,
aContentHandler, getter_AddRefs(viewer));

NS_ENSURE_SUCCESS(Embed(viewer, "", (nsISupports *) nsnull),
NS_ERROR_FAILURE);
............................................
}
nsresult
nsDocShell::NewContentViewerObj(const char *aContentType,
nsIRequest * request, nsILoadGroup * aLoadGroup,
nsIStreamListener ** aContentHandler,
nsIContentViewer ** aViewer)
{
nsCOMPtr aOpenedChannel = do_QueryInterface(request);

nsresult rv;
nsCOMPtr catMan(do_GetService(NS_CATEGORYMANAGER_CONTRACTID, &rv));
if (NS_FAILED(rv))
return rv;

nsXPIDLCString contractId;
rv = catMan->GetCategoryEntry("Gecko-Content-Viewers", aContentType, getter_Copies(contractId));

// Create an instance of the document-loader-factory
nsCOMPtr docLoaderFactory;
if (NS_SUCCEEDED(rv))
docLoaderFactory = do_GetService(contractId.get());

if (!docLoaderFactory) {
return NS_ERROR_FAILURE;
}

// Now create an instance of the content viewer
// nsLayoutDLF makes the determination if it should be a "view-source" instead of "view"
NS_ENSURE_SUCCESS(docLoaderFactory->CreateInstance("view",
aOpenedChannel,
aLoadGroup, aContentType,
static_cast(this),
nsnull,
aContentHandler,
aViewer),
NS_ERROR_FAILURE);

(*aViewer)->SetContainer(static_cast(this));
return NS_OK;
}

这里主要会根据contentType的不同创建不同的Document对象,它由nsContentDLF::CreateInstance来实现。其主要代码如下

nsCOMPtr doc;
nsCOMPtr docv;
do {
// Create the document
doc = do_CreateInstance(aDocumentCID, &rv);//针对htmldocumnt、xuldocument、imagedocument的不同会对应不同的DocumnetCID
if (NS_FAILED(rv))
break;

// Create the document viewer XXX: could reuse document viewer here!
rv = NS_NewDocumentViewer(getter_AddRefs(docv));
if (NS_FAILED(rv))
break;
docv->SetUAStyleSheet(gUAStyleSheet);

doc->SetContainer(aContainer);

// Initialize the document to begin loading the data. An
// nsIStreamListener connected to the parser is returned in
// aDocListener.
rv = doc->StartDocumentLoad(aCommand, aChannel, aLoadGroup, aContainer, aDocListener, PR_TRUE);
if (NS_FAILED(rv))
break;

// Bind the document to the Content Viewer
rv = docv->LoadStart(doc);
*aDocViewer = docv;
NS_IF_ADDREF(*aDocViewer);
} while (PR_FALSE);

其中需要注意的是在StartDocumentLoad的时候会创建一个mParser = do_CreateInstance(kCParserCID, &rv);作为DocListener,以供后面解析文档内容用;同时会创建nsCOMPtr sink,以供后面组织文档内容用;创建ContentSink的主要代码逻辑如下:
// create the content sink
nsCOMPtr sink;

if (aSink)
sink = aSink;
else {
if (IsXHTML()) {
nsCOMPtr xmlsink;
rv = NS_NewXMLContentSink(getter_AddRefs(xmlsink), this, uri,
docShell, aChannel);

sink = xmlsink;
} else {
nsCOMPtr htmlsink;

rv = NS_NewHTMLContentSink(getter_AddRefs(htmlsink), this, uri,
docShell, aChannel);

sink = htmlsink;
}
NS_ENSURE_SUCCESS(rv, rv);

NS_ASSERTION(sink,
"null sink with successful result from factory method");
}

mParser->SetContentSink(sink);
// parser the content of the URI
mParser->Parse(uri, nsnull, (void *)this);
}

一旦初步组织好nsDocShell、Document、Viewer、Parser、ContentSink之间的关系后,在nsDocumentOpenInfo调用DispatchContent时将返回的DocListener也即nsParser实例赋值给nsDocumentOpenInfo的m_targetStreamListener,以供后续使用,同时告诉Parser、ContentSink作好解析、组织文档准备;

另外通过nsDocShell的Embed方法,初始化DocumentViewer,其主要实现如下:
nsDocShell::SetupNewViewer(nsIContentViewer * aNewViewer)
{
..........................
mContentViewer = aNewViewer;
nsCOMPtr widget;
NS_ENSURE_SUCCESS(GetMainWidget(getter_AddRefs(widget)), NS_ERROR_FAILURE);

nsCOMPtr deviceContext;
if (widget) {
deviceContext = do_CreateInstance(kDeviceContextCID);
NS_ENSURE_TRUE(deviceContext, NS_ERROR_FAILURE);
deviceContext->Init(widget->GetNativeData(NS_NATIVE_WIDGET));
}

nsRect bounds(x, y, cx, cy);

mContentViewer->Init(widget, deviceContext, bounds);
..........................
}

DocumentViewerImpl::InitInternal(nsIWidget* aParentWidget,
nsISupports *aState,
nsIDeviceContext* aDeviceContext,
const nsRect& aBounds,
PRBool aDoCreation,
PRBool aInPrintPreview,
PRBool aNeedMakeCX /*= PR_TRUE*/)
{
.......................................
if (aParentWidget && !mPresContext) {
// Create presentation context
if (mIsPageMode) {
//Presentation context already created in SetPageMode which is calling this method
}
else
mPresContext =
new nsPresContext(mDocument, nsPresContext::eContext_Galley);
NS_ENSURE_TRUE(mPresContext, NS_ERROR_OUT_OF_MEMORY);

nsresult rv = mPresContext->Init(aDeviceContext);
if (NS_FAILED(rv)) {
mPresContext = nsnull;
return rv;
}

if (aDoCreation && mPresContext) {
// The ViewManager and Root View was created above (in
// MakeWindow())...

rv = InitPresentationStuff(!makeCX, !makeCX);
}
................................................
}

DocumentViewerImpl::InitPresentationStuff(PRBool aDoInitialReflow, PRBool aReenableRefresh)
{
..............................................

// Create the style set...
nsStyleSet *styleSet;
nsresult rv = CreateStyleSet(mDocument, &styleSet);
NS_ENSURE_SUCCESS(rv, rv);

// Now make the shell for the document
rv = mDocument->CreateShell(mPresContext, mViewManager, styleSet,
getter_AddRefs(mPresShell));

mPresShell->BeginObservingDocument();//mDocument->AddObserver(mPresShell);
..............................................

}

nsDocument::doCreateShell(nsPresContext* aContext,
nsIViewManager* aViewManager, nsStyleSet* aStyleSet,
nsCompatibility aCompatMode,
nsIPresShell** aInstancePtrResult)
{
*aInstancePtrResult = nsnull;

NS_ENSURE_FALSE(mShellsAreHidden, NS_ERROR_FAILURE);

FillStyleSet(aStyleSet);

nsCOMPtr shell;
nsresult rv = NS_NewPresShell(getter_AddRefs(shell));
if (NS_FAILED(rv)) {
return rv;
}

rv = shell->Init(this, aContext, aViewManager, aStyleSet, aCompatMode);
NS_ENSURE_SUCCESS(rv, rv);

// Note: we don't hold a ref to the shell (it holds a ref to us)
NS_ENSURE_TRUE(mPresShells.AppendElementUnlessExists(shell),
NS_ERROR_OUT_OF_MEMORY);
shell.swap(*aInstancePtrResult);

return NS_OK;
}
这样同时准备好与显示相关的nsDeviceContext、nsPresContext、nsPresShell,其中需要留意的是nsPresShell对象是作为nsDocument
实例对象的一个Observer,同时也是nsViewManager实例对象的一个Observer;至此初始化完nsPresShell,并加入到nsViewManager的管理机制中去,为后面创建新的nsView对象及进行布局安排、显示内容作好准备。

四、解析、组织文档内容,同时作好布局
准备
继续查看nsInputStreamPump::OnInputStreamReady(nsIAsyncInputStream *stream)方法中的STATE_TRANSFER时,表示已获取相关文档数据,此时会继续利用uriLoader临时创建的nsDocumentOpenInfo,由它的方法OnDataAvailable来继续处理获得的数据,其实现如下:

NS_IMETHODIMP nsDocumentOpenInfo::OnDataAvailable(nsIRequest *request, nsISupports * aCtxt, nsIInputStream * inStr, PRUint32 sourceOffset, PRUint32 count)
{
// if we have retarged to the end stream listener, then forward the call....
// otherwise, don't do anything
nsresult rv = NS_OK;
if (m_targetStreamListener)
rv = m_targetStreamListener->OnDataAvailable(request, aCtxt, inStr, sourceOffset, count);
return rv;
}
此时其
m_targetStreamListener值也即上面创建出来的nsParser实例,
这样正好一环套一环,任务转交给了上面准备好的nsParser;
Gecko内核中的Parser有HtmlParser以解析html文档;xmlParser以解析符合xml格式的文档如xul、svg;CssParser以解析Css文档等。解析器通过Tokenizer来认识文档中的标识,然后结合不同文档类型预定义的Atom及ContentSink来组织想要的文档结构。整个HtmlParser的解析过程还是蛮复杂的,不过无论解析的过程怎样,其结果就是会适时的调用ContentSink中的主要方法,如OpenHead、OpenBody、CloseBody、OpenForm、CloseForm、OpenContainer、CloseContainer、StartLayout、ProcessLINKTag、ProcessSCRIPTEndTag、ProcessSTYLEEndTag等;

其中需要留意的是在构建文档结构的同时往往需要对不同的Content Node 创建对应Frame对象,同时针对不同的Frame,还时可能还需要构建nsView,如nsBoxFrame,在一些条件下需要调用方法CreateViewForFrame来创建一个nsView实例,其主要代码如下:
{
// Create a view
nsIView *view = viewManager->CreateView(aFrame->GetRect(), parentView, visibility);
if (view) {
// Insert the view into the view hierarchy. If the parent view is a
// scrolling view we need to do this differently
nsIScrollableView* scrollingView = parentView->ToScrollableView();
if (scrollingView) {
scrollingView->SetScrolledView(view);
} else {
viewManager->SetViewZIndex(view, autoZIndex, zIndex);
// XXX put view last in document order until we can do better
viewManager->InsertChild(parentView, view, nsnull, PR_TRUE);
}
}

// Remember our view
aFrame->SetView(view);
}

NS_IMETHODIMP_(nsIView *)
nsViewManager::CreateView(const nsRect& aBounds,
const nsIView* aParent,
nsViewVisibility aVisibilityFlag)
{
nsView *v = new nsView(this, aVisibilityFlag);
if (v) {
v->SetPosition(aBounds.x, aBounds.y);
nsRect dim(0, 0, aBounds.width, aBounds.height);
v->SetDimensions(dim, PR_FALSE);
v->SetParent(static_cast(const_cast(aParent)));
}
return v;
}

同时对于一些nsView还需要通过其nsIView::CreateWidget方法来创建原生的窗口,主要代码如下

nsresult nsIView::CreateWidget(const nsIID &aWindowIID,
nsWidgetInitData *aWidgetInitData,
nsNativeWidget aNative,
PRBool aEnableDragDrop,
PRBool aResetVisibility,
nsContentType aContentType,
nsIWidget* aParentWidget)
{
nsView* v = static_cast(this);

v->LoadWidget(aWindowIID))
..................................
mWindow->Create(aNative, trect, ::HandleEvent, dx, nsnull, nsnull, aWidgetInitData);
..............................
}
在mWindow->Create中会接着调用如nsWindow::StandardWindowCreate、BaseCreate等;
其中
参数::HandleEvent会在创建原生窗口时保存在nsWindow的属性成员mEventCallback中;这个函数句柄在原生窗口的处理函数中经过一定处理后往往根据一定上下文而去调用它,它会处理如WM_SIZE、WM_PAINT等主要消息,后面的处理会引用到。

五、显示文档内容
在上面解析、组织文档结构时,当调用OpenBody时会适时调用StartLayout,其主要代码如下
{
nsPresShellIterator iter(mDocument);
nsCOMPtr shell;
while ((shell = iter.GetNextShell())) {
// Make sure we don't call InitialReflow() for a shell that has
// already called it. This can happen when the layout frame for
// an iframe is constructed *between* the Embed() call for the
// docshell in the iframe, and the content sink's call to OpenBody().
// (Bug 153815)

PRBool didInitialReflow = PR_FALSE;
shell->GetDidInitialReflow(&didInitialReflow);
if (didInitialReflow) {
// XXX: The assumption here is that if something already
// called InitialReflow() on this shell, it also did some of
// the setup below, so we do nothing and just move on to the
// next shell in the list.
continue;
}

nsRect r = shell->GetPresContext()->GetVisibleArea();
nsCOMPtr shellGrip = shell;
nsresult rv = shell->InitialReflow(r.width, r.height);

}

由nsPresShell的InitialReflow方法来启动布局,主要作用是根据文档元素的类型、属性的不同,而决定是否需要重新布局该文档元素及其子元素,一旦觉得有必要重新布局则调用nsPresShell的PostReflowEvent方法,它通过向主线程MainThread发送一个ReflowEvent,一旦MainThread接收到该ReflowEvent,其会由对应nsPresShell的DoFlushPendingNotifications方法来处理,其最终根据当前的nsView及原生窗口的不同,在windows平台上它会对原生的窗口句柄进行InvalidateRect或UpdateWindow处理,按照windows图形管理的逻辑,系统会根据相关条件,及时向该原生窗口句柄发送原生的WM_PAINT消息,而对处理原生的窗口消息,一般会由窗口创建时提供的窗口回调函数来处理,经过一番处理判断后会触发调用nsWindow::DispatchWindowEvent,进而会调用上面提到的
mEventCallback所对应的公共的函数,其实现如下:
//
// Main events handler
//
nsEventStatus PR_CALLBACK HandleEvent(nsGUIEvent *aEvent)
{
//printf(" %d %d %d (%d,%d) \n", aEvent->widget, aEvent->widgetSupports,
// aEvent->message, aEvent->point.x, aEvent->point.y);
nsEventStatus result = nsEventStatus_eIgnore;
nsView *view = nsView::GetViewFor(aEvent->widget);

if (view)
{
view->GetViewManager()->DispatchEvent(aEvent, &result);
}

return result;
}
进而由nsViewManager::DispatchEvent统一来处理事件,这时经过一些处理,往往需要重新渲染相关View,而渲染View的主要代码如下:

void nsViewManager::RenderViews(nsView *aView, nsIRenderingContext& aRC,
const nsRegion& aRegion)
{
if (mObserver) {
nsView* displayRoot = GetDisplayRootFor(aView);
nsPoint offsetToRoot = aView->GetOffsetTo(displayRoot);
nsRegion damageRegion(aRegion);
damageRegion.MoveBy(offsetToRoot);

aRC.PushState();
aRC.Translate(-offsetToRoot.x, -offsetToRoot.y);
mObserver->Paint(displayRoot, &aRC, damageRegion);
aRC.PopState();
}
}
而上面提到的nsPresShell实例往往作为nsViewManager的一个Observer,这样工作就这样转交给nsPresShell来处理。而nsPresShell的Paint方法的主要内容如下:

PresShell::Paint(nsIView* aView,
nsIRenderingContext* aRenderingContext,
const nsRegion& aDirtyRegion)
{
AUTO_LAYOUT_PHASE_ENTRY_POINT(GetPresContext(), Paint);
nsIFrame* frame;
nsresult rv = NS_OK;

if (mIsDestroying) {
NS_ASSERTION(PR_FALSE, "A paint message was dispatched to a destroyed PresShell");
return NS_OK;
}

NS_ASSERTION(!(nsnull == aView), "null view");

frame = static_cast(aView->GetClientData());
nscolor backgroundColor;
mViewManager->GetDefaultBackgroundColor(&backgroundColor);
for (nsIView *view = aView; view; view = view->GetParent()) {
if (view->HasWidget()) {
PRBool widgetIsTransparent;
view->GetWidget()->GetHasTransparentBackground(widgetIsTransparent);
if (widgetIsTransparent) {
backgroundColor = NS_RGBA(0,0,0,0);
break;
}
}
}

if (!frame) {
if (NS_GET_A(backgroundColor) > 0) {
aRenderingContext->SetColor(backgroundColor);
aRenderingContext->FillRect(aDirtyRegion.GetBounds());
}
return NS_OK;
}

nsLayoutUtils::PaintFrame(aRenderingContext, frame, aDirtyRegion,
backgroundColor);

return rv;
}

相关主要工作又转交给nsLayoutUtils::PaintFrame;其主要工作就是通过 nsDisplayListBuilder builder来建立一个nsDisplayList list,并调用nsDisplayList::Paint方法,其实现如下:

void nsDisplayList::Paint(nsDisplayListBuilder* aBuilder, nsIRenderingContext* aCtx,
const nsRect& aDirtyRect) const {
for (nsDisplayItem* i = GetBottom(); i != nsnull; i = i->GetAbove()) {
i->Paint(aBuilder, aCtx, aDirtyRect);
}
nsCSSRendering::DidPaint();
}

在其中的for循环中会根据DisplayList Item构成的不同,会调用不同frame的Paint方法等,如
nsDisplayText::Paint(nsDisplayListBuilder* aBuilder,
nsIRenderingContext* aCtx, const nsRect& aDirtyRect) {
static_cast(mFrame)->
PaintText(aCtx, aBuilder->ToReferenceFrame(mFrame), aDirtyRect);
}
通过这样一个流程,就会显示出相关内容给用户。


六、总结分析
通过上面初步的整理,对整个过程有了初步的了解,其中关键在于了解到LoadURI的发起、nsIStreamListener数据的接收、nsParser及nsContentSink对文档的组织构成、nsPresShell结合nsFrame、nsView、nsViewManager、nsWindow对内容的显示。总的说来整个过程是相当的复杂,毕竟自从Web服务端获取数据后在内核中需要不断的动态创建很多
对应的类对象,但关键流程的流转在于对nsInputStreamReadyEvent事件和ReflowEvent两个事件的处理,有了这样基本的认识之后,对于css文档、image文档、js文档、plugin的处理,应该有了扎实的基础准备,至于具体每一html标签、xul标签具体对应哪些Node、Frame、Widget还须具体结合自身的不同加以分析。

七、参考网址
The Life Of An HTML HTTP Request

没有评论: