webkit之技术详解

来源:互联网 发布:淘宝达人申请理由 编辑:程序博客网 时间:2024/06/08 12:13

Webkit

WebkitLoader模块介绍

前面说过, webkit只是一个排版引擎,在Webkit排版/渲染一个网页之前,它肯定需要从网络上、或者本地文件系统中读到网页的http数据,对吧,对webkit来讲,他要的就是数据,不管你是从网络读的还是本地文件读的。

Loader就是这样一个模块,它承上启下,不仅负责为webkit引擎提供数据,还控制着webkit的绘制。另外,它同时还与提供数据的“来源”打交道。

 

先简单举例说明:

用户输入一个url,这时是Loader接收url请求,它把url传递给curl,设置curl的回调函数,当curl读到数据,loader把数据传递给Parser,开始生成DOM。

 

一.下面重点介绍一下与Loader相关的数据结构和模块。

 

Frame:可以看做是浏览器外壳调用Loader的总入口,它就像我们印象中的一个网页,它关注的是页面的显示 (FrameView) 、页面数据的加载(FrameLoader)、页面内的各种控制器 (Editor, EventHandler,ScriptController, etc.) 等等,它包含以下模块(只列出重点):

Document

Page

FrameView

RenderView

FrameLoader

DOMWindow

 

下面分别介绍(PS: 必须要了解这些概念,不然后面的东东都无法理解):

 

1)Document:这个类的爷爷类是 Node ,它是 DOM 树各元素的基类; Document 有个子类是HTMLDocument ,它是整个文档 DOM 树的根结点,这样就明白了:原来 Document 就是描述具体文档的代码,看一下它的头文件,就更明白了,它的属性与方法就是围绕着各种各样的结点: Text , Comment , CDATASection , Element……

 

2)Page:我的理解是,Page与Frame(严格说是FrameView)是一一对应的,Frame关注UI,Page关注数据。现在的浏览器一般都提供同时打开多个窗口,每一个窗口对应的数据就是这个Page在管理了。

在 page.cpp 文件里,还有个重要的全局指针变量: static HashSet<Page*>* allPages; 这个变量包含了所有的page 实例。

 

3)FrameView:可以理解为为一个网页的ViewPort, 它提供一个显示区域,同时包含的有Render根节点、layout排版相关接口、Scroll相关等。 FrameView是Layout排版的总入口。

 

4)RenderView:与FrameView差不多,只是分工不同,它管理与Render树相关的东东。

 

5) FrameLoader:重点,FrameLoader类将Documents加载到Frames。当点击一个链接时,FrameLoader创建一个新的处于“policy”状态的DocumentLoader对象,一旦webkit指示FrameLoader将本次加载视为一个导航(navigation),FrameLoader就推动DocumentLoader进入“provisional”状态,(在该状态,DocumentLoader发调用CURL发起一个网络请求,并等待是html还是下载文件。)同时,DocumentLoader会创建一个MainResourceLoader对象(该对象在后面单独介绍)。

6)。DOMWindow:实现了Dom的一些接口,如CreateNode等。后面可以详细讲讲。

 

上面介绍的概念比较多,我也不晓得好不好理解,没理解也不怕,多去看看代码,这是必须的。

 

另外看看下面的图,就会清晰很多的。

 


二、Webkit的Loader有两条加载数据的主线: (从上图可以看到)

1. DocumentLoader-àMainResourceLoader:该模块主要加载主网页请求。后面称为MainResource。

2. DocLoader:该模块除了主网页外的所有子请求,如:.js文件,图片资源,.css文件。后面称为SubResource。

 

MainResource部分:

FrameLoader->DocumentLoader->MainResourceLoader-ResourceHandleDocumentLoader经历状态:1)"policy"2) "provisional" 3) "commited"分别是等待、作为navigation发送networkrequest、文件下载完毕

 

Subresource部分:

DocLoader->Cache->[CacheObjects]example: CacheImage->SubresourceLoader->ResourceHandle 当请求一个资源时,首先查看Cache的requestResource中是否存在该对象,如果存在直接返回;如果不存在,创建该Cache对象(如CacheImage),然后创建一个SubresourceLoader,加载资源。

举例说明:

加载图片时, DocLoader首先询问Cache, 在内存中是否也存在(CachedImage对象),如果已存在,则直接加载,即省了时间又省了流量。如果图片不在Cache中, Cache首先创建一个新的CachedImage对象来代表该图片,然后由CachedImage对象调用Loader对象发起一个网络请求,Loader对象创建SubResourceLoader。后面的流程就一样了,SubResourceLoader也是直接把ResourceHandle打交道的。

 

接下来跟踪一下Loader发送请求的代码实现:

1. 用户输入URL后,最先调用的接口是:

FrameLoader::load(constResourceRequest& request)

 

ResourceRequest包含了:

KURL(处理url的一个类)、setHTTPHeaderField、setHTTPContentType等与HTTP头部相关的函数

 

2.Load()通过ResourceRequest数据调用createDocumentLoader(request, substituteData)来创建一个DocumentLoader。

 

3.Load()函数继续给request设置HttpAccept,Cache-Control HTTP头等信息。

 

4. 设置FrameLoader::checkNavigationPolicy函数进入"Policy"状态。

 

5.判断该url是否在Cache中等一系列状态判断后,进入DocumentLoader::startLoadingMainResource函数准备加载MainResource。该函数首先会创建调用MainResourceLoader。

 

6.进入MainResourceLoader::load函数,调用illSendRequest(r, ResourceResponse());做发送请求前的准备。

 

7.调用PolicyCheck检查policy的状态后,进入 FrameLoader::callContinueLoadAfterNavigationPolicy继续往下走。

 

8:在MainResourceLoader::loadNow(ResourceRequest& r)函数里创建ResourceHandle,在创建ResourceHandle函数中,调用start函数,start函数把ResourceHandle自已添加到ResourceHandleManager的m_resourceHandleList队列里。

同时,调用m_downloadTimer.startOneShot激活网页请求下载的定时器。(这是个毫秒级的定时器,采用定时器的原因也是为了实现异步的请法)

 

可以看到m_downloadTimer的定义:Timer<ResourceHandleManager> m_downloadTimer;

  m_downloadTimer是实现的一个定时器模块类,在它的构造函数里已经传入了回调函数的地址:ResourceHandleManager::downloadTimerCallback。

 

9. 一路返回到Load()函数,并返回到调用源,函数执行完毕。

 

10.  ResourceHandleManager::downloadTimerCallback回调函数被定时器调用。

 

11. 可以看到downloadTimerCallback函数的代码:

         调用libcurl库的接口curl_multi_fdset,curl_multi_perform等查询数据。

 

以下调试时Loader执行流程的函数调用栈截图

   

 

 

webkit应用场景再举例:

用户给出一个 URL (直接输入或者点击链接或者 JavaScript解析等方式)。然后浏览器外壳调用 FrameLoader 来装载页面。 FrameLoader 首先检查一些条件(policyCheck()) ,如 URL 是否非空、 URL 是否可达,用户是否取消等等。然后通过DocumentLoader 启动一个 MainResourceLoader 来装载页面。MainResourceLoader 调用 network 模块中的接口来下载页面内容( ResourceHandle ),实际上这里的Resourcehandle已经是平台相关的内容了,接收到数据以后,会有回调函数,告诉MainResourceLoader数据已经接收到了。然后一路返回到 FrameLoader 开始调用 HTMLTokenizer 解析 HTML 文本。解析过程中,如果遇到 Javascript 脚本的话,也会调用Javascript 引擎( Webkit 中的 JavascriptCore , chrome 中的 V8 )来解析。数据被解析完了以后,生成了一个一个的node ,生成 DOM 树和 Render 树,然后通过FrameLoaderClient 调用外部的壳把内容显示出来。”

 

 

因此,总结Loader的功能:

Loader 是在WebKit里面一个很重要的连接器,通过loader发起IO下载网页,图片等数据,再通过loader发起解析,以及最后的渲染功能。

 

Loader的代码和架构清楚了没,没清楚?看代码吧,呵呵。文笔不好,或者加我QQ一起交流:270977395

 

 

 

 

Webkit Parser模块前篇

 

在讲parser模块之前,需要知道数据从curl怎么过来的,那就要先看看ResourceHandleManager.cpp里下面这几个函数:

headerCallback

writeCallback
readCallback

 

顾名思义,这三个函数是http请求发出以后的回调函数(为了实现异步操作),分别为写回调、http头回调、读回调,翻代码发现在ResourceHandleManager的initializeHandle函数里会调用以下函数,把这几个回调注册到libcurl里:

curl_easy_setopt(d->m_handle,CURLOPT_HEADERFUNCTION, headerCallback);

curl_easy_setopt(d->m_handle,CURLOPT_WRITEFUNCTION, writeCallback);   

 

curl_easy_setopt(d->m_handle,CURLOPT_READFUNCTION, readCallback);

重点先讲一下readCallback函数,它有点特殊,它是在ResourceHandleManager的setupPOST里函数里设置的,为什么呢?

下面解说之前,读者先要了解http协议的Get和Post才行,这是必须滴。因为一般请求URL地址,都只是Get请求,请求发出去后,只要接收http数据显示就行了。并不需要再发送啥数据了。

只有当Post(上传)一个文件或者提交Form表单的Post数据时,才需要读的回调,读回调被调用的时机是: socket的connect建立成功后,先发送的是post请求的http头部数据,再发送Body的数据,也就是说readCallback是用来发送body数据的。能理解吗?

下面分别列举一下Get和Post的数据:

 

Get

GET/books/?name=maxxiang HTTP/1.1

Host:www.qq.com

User-Agent:QQBrowser/1.3 (MTK 6235; U; MTK 1.3; ch-china; rv:1.7.6)

Connection:Keep-Alive

 

POST

POST /HTTP/1.1

Host:www.qq.com

User-Agent:QQBrowser/1.3 (MTK 6235; U; MTK 1.3; ch-china; rv:1.7.6)

Content-Type:application/x-www-form-urlencoded

Content-Length:29

Connection:Keep-Alive

(此处空一行)

name=maxxiang&publisher=china

注意: POST的http头和Body部分,中间要有2个”\r\n”来区分。

 

 

下面来分别跟踪一下代码:

1)      headerCallback



从上面的函数调用栈能看出,源头就是Loader章节讲到的downloadTimerCallback函数,它是读取libcurl数据的总控制函数。

Curl库的Curl_client_write函数分别把http头,按”\r\n”切隔分多次调用headerCallback传递给Loader,每次只传一个http字段,这样的做法好处是,curl需要自已解析并保存http头部字段的数据值,同时,也方便了webkit的处理,因为webkit也是需要处理这些数据的,它需要把http头部字段都set到m_response里,后面用得着。

 

2)      writeCallback


发现writeCallback的函数调用栈比headerCallback深了一些,是因为html body的数据是可以做压缩的,而curl发现http头部标识的是gzip压缩的数据,会先解压后,再传给webkit的writeCallback函数。

         在writeCallback里,首先会判断Http的返回Code, 200表示成功等。

if (CURLE_OK == err&& httpCode >= 300 && httpCode < 400)

        return totalSize;

    if (d->client())

        d->client()->didReceiveData(job,static_cast<char*>(ptr), totalSize, 0);

         可以看到,httpCode判断了300—400之间,如果属于这个区间,就直接返回了,为什么呢,

         这是因为300-400之间的代码,都是需要再处理的或再跳转请求的。

举例如下(目前有用到好像只有是300-307,具体可以去查看RFC规范,这里仅举两个例子):

如果301,302都表示跳转,就不用去解析body部分了,直接跳转。

         如果303表示请求是 POST,那么就要转为调用 GET再请求才能取到正确的数据。

 

3)      readCallback

readCallback里,直接调用的m_formDataStream.read(ptr,size, nmemb)函数来上传。

FormDataStream类可以重点看一下,它的read函数实现了上传文件和上传Form表单(httpbody)的两个分支流程。

 

 

WebkitParser模块

 

1.在介绍parser模块前,先介绍另一个基础,这个基础是理解Parser和DOM的核心点

 

    1).我们都知道一个HTML文件,都是以<HTML>开头。(WML是以<card>开头)在webcore中<html>标签对应的是HTMLHtmlElement节点。

但实际上<html>标签(HTMLHtmlElement)并不是DOM树的根节点,webkit中DOM树的真正根节点是:HTMLDocument。当我们在浏览器里打开一个空白页面的时候,webkit首先会生成DOM Tree的根节点HTMLDocument和Render Tree的根节点RenderView。也就是说,当前是空白页而没有html网页显示时,这个DOM Tree和Render Tree的根节点就已经存在了。只是这两棵Tree的子节点都是“空”的而已。理解了吗?

 

    2).当用户在同一个浏览器页面中,先后打开两个不同的HTML文本文件时,会发现HTMLDocument和RenderView两个根节点并没有发生改变,改变的是HTMLHtmlElement及下面的子树,以及对应的RenderingTree的子树。

 

         为什么这样设计?原因是HTMLDocument和RenderView服从于浏览器页面的设置,譬如页面的大小和在整个屏幕中的位置等等。这些设置与页面中要显示什么的内容无关。

同时HTMLDocument包含一个HTMLTokenizer的成员,而HTMLTokenizer绑定HTMLParser。(下面具体介绍这两个组件)

         这样设计的好处是:因为HTMLElement是一开始就创建的,不会随着打开一个新的URL页面而释放(free)再创建(malloc),节省了CPU时间。

        

 

2.回顾一下大学课程里的《编译原理》课

    1).语言的解析一般分为词法分析(lexical analysis)和语法分析(Syntaxanalysis)两个阶段,WebKit中的html解析也不例外。

词法分析的任务是对输入字节流进行逐字扫描,根据构词规则识别单词和符号,分词。

    2).状态机:有穷状态机是一个五元组 (Q,Σ,δ,q0,F),后面省略....

 

3.下面介绍Parser模块里两个“非常非常”重要的组件:

HTMLTokenizerHTMLParser

 

    1).HTMLTokenizer类理解为词法解析器,HTML词法解析器的任务,就是将输入的字节流解析成一个个的标记(HTMLToken),然后由语法解析器进行下一步的分析。

    2).HTMLParser类理解为语法分析器,它通过HTMLTokenizer识别出的一个一个的标识(tag)来创建Element(Node),把Element组织成一个DOM Tree,同时,同步生成RenderTree。

 

 

4.Parser模块代码走读(如何生成DOM树和Render树的):

 

 1). 上文提到的writeCallback接收到html数据后,首先调用ResourceLoader的didReceiveData。一路往下传:ResourceLoader::didReceiveData->MainResourceLoader::didReceiveData->..->FrameLoader::receivedData->DocumentLoader::receivedData->FrameLoader::committedLoad

 

  2). FrameLoader::committedLoad函数这里注意,它会判断ArchiveMimeType,如果是同类的,则不需要往下走。(也就是上面第1节说的,不需往下创建HTMLTokenizer了,因为可以复用嘛)

    注: 有兴趣可继续往下看代码,HTMLTokenizer是在HTMLDocument里被创建的。

 

  3). 接下来会进入FrameLoader::addData-->write函数。 write函数首先处理字符编码的问题,把解码后的html数据,继续往下丢给HTMLTokenizer的write函数:

    if (tokenizer) {

        ASSERT(!tokenizer->wantsRawData());

        tokenizer->write(decoded, true);

 

  4).真正的词法分析开始啦。

    因为webkit支持边解析边绘制,也支持多线程,所以HTMLTokenizer的write函数首先会判断上次丢过来的数据是否已解析完,否则追加到上次的数据后面。

    write函数里有一个大的while循环,用于逐个字符的解析,这里代码太多,只贴一下重点,我补上注释说明:

  while (!src.isEmpty() && (!frame ||!frame->loader()->isScheduledLocationChangePending())) {

        UChar cc = *src;                    //从html数据Buffer中取出一个字符

 

        bool wasSkipLF = state.skipLF();            //是否要跳过回车

        if (wasSkipLF)

            state.setSkipLF(false);

 

        if (wasSkipLF && (cc == '\n'))

            src.advance();

        else if(state.needsSpecialWriteHandling()) {

            if (state.hasEntityState())

                state = parseEntity(src, dest,state, m_cBufferPos, false, state.hasTagState());

            else if (state.inComment())     //注释文本,如:<!--这里是注释 -->

                state = parseComment(src,state);

            else if (state.inDoctype())         //HTML的DocType

                state = parseDoctype(src,state);

            else if (state.inServer())      //asp或jsp的服务端代码,如:<%***%>

                state = parseServer(src,state);

            else if (state.startTag()) {        //重点注意:这里是检测到'<'字符,

  在检测到'<'字符后,表示后面跟的就是标签(html Tag)啦,这条分支里主要有两个函数:

  processToken和parseToken。 这里是重点。。。。

  *: processToken的作用是,在开始一个新的Tag之前,先看一下上一个tag是否已经处理完毕了?因为webkit的兼容性非常好,举例如有“<begin>”而没有“</end>”时,这里能兼容到,而不会因为网页设计人员的失误,导致网页绘制失败。(该函数还有另外一个作用,下面会介绍)

  *: parseTag函数,看名字就知道啦,它就是真正开始词法分析一个html tag标签的函数。

 

  5).parseTag函数里也是一个大的while,状态机,state变量维护这个状态机,有如下几种状态:

    enum TagState {

        NoTag = 0,

        TagName = 1,

        SearchAttribute = 2,

        AttributeName = 3,

        SearchEqual = 4,

        SearchValue = 5,

        QuotedValue = 6,

        Value = 7,

        SearchEnd = 8

    };

    TagName: 一个HTML Tag的开始,它会把Tag的名字存在一个叫Token的成员变量里,它永远保存当前正在Parser的Tag的数据。

    AttributeName: 在处理这个状态时,会把所有的大写转为小写。因为html标准中的attribute是不区分大小写的,这样做的目的是加快后面字符比较的速度。

    SearchEqual: 循环到到'='时,会把attributeName添加到currToken这个Token里。

    SearchEnd: 表示当前的Tag全部解析完了,噢,终于完整地解析完一个Tag了,这里该干嘛了? 当然是生成DOM节点啦,

    这个时候,token成员类变里已经存下了:Tag的名字,所有的attribute和value,有了这些后,会调用:

     RefPtr<Node> n = processToken();

 

  6). processToken就是真正生成DOM节点和Render节点的函数。

   processToken函数会调用parser->parseToken(&currToken);  

   该函数定义:PassRefPtr<Node>HTMLParser::parseToken(Token* t)。 返回的就是一个Node的节点, Node类是所有DOM节点的父类。

 

  7). HTMLParser::parseToken函数重点代码介绍:

    RefPtr<Node> n = getNode(t);    //这里返回Node节点,  往里面跟,会发现它用了很多设计模式的东东

    if (!insertNode(n.get(), t->flat)) {        //会调用Node* newNode =current->addChild(n); 把当前的新节点加入到DOM Tree中。

 

  8).接下来会调用Element::attach,创建相对应的Render节点,代码如下:

void Element::attach()

{

    createRendererIfNeeded();

    ContainerNode::attach();

    if (ElementRareData* rd = rareData()) {

        if (rd->m_needsFocusAppearanceUpdateSoonAfterAttach){

            if (isFocusable() &&document()->focusedNode() == this)

               document()->updateFocusAppearanceSoon();

           rd->m_needsFocusAppearanceUpdateSoonAfterAttach = false;

        }

    }

 

  9:真正创建Render的地方:

   RenderObject::createObject(), 该函数会根据不同的type,而创建不同的Render节点。



 

 

Parser这块代码挺多的,上面只记录了重点。

从上面函数栈中,找到相应的函数,设置断点去调试吧,代码跟踪一遍后就清楚了。

 

 

Webkitlayout模块

 

 

 

 

WebkiJS引擎

 

 

 

Webkit 插件加载

 

RenderView:

    void addWidget(RenderObject*);

void removeWidget(RenderObject*);

void updateWidgetPositions();

 

    typedef HashSet<RenderObject*>RenderObjectSet;

RenderObjectSet m_widgets;

 

插件加载时调用的源头:

HTMLPlugInElement::updateWidgetCallback

 

插件来源判断加载重点函数:

RenderPartObject::updateWidget

 

真正加载插件的入口:

WebCore::PluginPackage::load函数

 

 

 

官方Webkit最新代码(r86499)在win7下编译时的注意事项:

 

最近在blog上写下关于webkit的笔记后,经常有朋友会问我, 在安装webkit.org官方的开发环境时,老是编译不过, 刚好最近我换了个新本本, 需要在新本本上也装上webkit的开发环境,我下载的是r86499的版本(最新), 故在此把需要注意的事项在这里总结下:

 

http://www.webkit.org/building/tools.html
官方已经给出了安装步骤,那这里就不再重复. 但官方给出的太粗, 我把有可能出错的地方记录下来了,大概是以下几点:

1.      环境变量

在系统环境变量中加入:
WEBKITOUTPUTDIR = %WEBKIT_HOME%\WebKitBuild  /*注意老的版本该目录是Build,名称不同,如果照网上的文章,就会出错*/

WEBKITLIBRARIESDIR = %WEBKIT_HOME%\WebKitLibraries\win

 

WEBKIT_HOME = E:\webkit\Webkit\WebKit-r86499  /*这里指你webkit源代码的路径, 不要照抄我的*/

2.      VS2005最好安装在C盘, 如果安装在别的盘, 需要加一个宏,webkit 的编译脚本才能找到正确的位置(不在C盘时,才加该宏):

VSINSTALLDIR = D:\ProgramFiles\Microsoft Visual Studio 8

不想加宏的话, 找到Tools\Scripts\webkitdirs.pm,这个文件里的setupCygwinEnv函数中的$programFilesPath变量,就是去识别路径的. 直接改也行.

 

3.      打开webkit.sln编译过程中,经常报某个文件的某句代码error, 解决方法有下面两种情况:

1)      用VS2005打开G:\Webkitsrc\WebKit\win\WebKit.vcproj\WebKit.sln文件,将各个工程属性中C++设置项中的将警告视作错误的选项值改为否

2)      直接找到该文件, 用notepad打开,保存成utf-8就行.

 

 

还有一点要注意的是, 网上写的开发环境安装,大多是基于老是webkit目录和代码写的, 在配置过程中 , 要注意采用最新的目录名. 比如:WebKitLibraries,以前不叫这个名字的.

 

主要就这几点, 整个编译过程,大概1小时左右,看机器性能啦.

 

在写的时候是基于2万多的代码, 现在官方最新已经9万多,代码的实现变化挺大的, 因为决定暂停,重新基于9万号的代码来写.

0 0
原创粉丝点击