前面把Loader数据加载模块介绍完了,那有了数据以后就可以开始解析了,但在介绍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&bookname=webkit
注意: POST的http头和Body部分,中间要有2个”\r\n”来区分。没记错的话应该是RFC 2616规定的。
下面来分别跟踪一下代码:
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)的两个分支流程。
1).我们都知道一个HTML文件,都是以<HTML>开头。(WML是以<wml>开头)在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及下面的子树,以及对应的Rendering Tree的子树。
为什么这样设计?原因是HTMLDocument和RenderView服从于浏览器页面的设置,譬如页面的大小和在整个屏幕中的位置等等。这些设置与页面中要显示什么的内容无关。
同时HTMLDocument包含一个HTMLTokenizer的成员,而HTMLTokenizer绑定HTMLParser。(下面具体介绍这两个组件)
这样设计的好处是: 因为HTMLElement是一开始就创建的,不会随着打开一个新的URL页面而释放(free)再创建(malloc),节省了CPU时间。
1).语言的解析一般分为词法分析(lexical analysis)和语法分析(Syntax analysis)两个阶段,WebKit中的html解析也不例外。
词法分析的任务是对输入字节流进行逐字扫描,根据构词规则识别单词和符号,分词。
2).状态机:有穷状态机是一个五元组 (Q,Σ,δ,q0,F),后面省略....
3.下面介绍Parser模块里两个“非常非常”重要的组件:
HTMLTokenizer和HTMLParser。
1).HTMLTokenizer类理解为词法解析器,HTML词法解析器的任务,就是将输入的字节流解析成一个个的标记(HTMLToken),然后由语法解析器进行下一步的分析。
2).HTMLParser类理解为语法分析器,它通过HTMLTokenizer识别出的一个一个的标识(tag)来创建Element(Node),把Element组织成一个DOM Tree, 同时,同步生成Render Tree。
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节点。