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模块里两个“非常非常”重要的组件:
HTMLTokenizer和HTMLParser。
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模块
Webkit JS引擎
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万号的代码来写.
- webkit之技术详解
- WebKit之webIDL详解
- WebKit之V8技术优化分析
- webkit 详解
- Webkit之webkit
- WebKit技术分析
- WebKit技术内幕
- webkit分析详解
- android webkit 详解
- 基于webkit技术的爬虫
- webkit技术浅析系列---DNS
- android技术之SQLite技术详解
- webkit 入门之准备
- WebKit之Port
- WebKit之WebCore篇
- webkit 学习之目录
- WebKit之WebCore
- WebKit之Port
- Codeforces 456A Laptops(水题)
- Java中Synth外观学习(九)--ComboBox的定制
- hdu 4768 Flyer(二分查找)
- 一字段含有逗号分隔的串,如何把这条记录按分隔符分成多
- 【c++primer——15】面向对象编程01——虚函数与默认实参
- webkit之技术详解
- TortoiseSVN常用批处理命令
- linux高级技巧:rsync同步(一)
- hdu 1133 Buy the Ticket(Catalan数)
- Codeforces Round #260 (Div. 2) B Fedya and Maths
- 1 LoadRunner性能测试流程
- C++中delete和delete[]的区别
- vim使用(二):常用功能
- doc 转换pdf swf