接解析HTML起源篇
起源篇提到数据的处理会调用到如下的两个函数处
DocumentParser::appendBytes
DocumentParser::finish
一个是解析过程中的,一个是解析完成的。而解析开始时,就是起源篇讲的Document,RenderView,DocumentParser的创建,注意当前是html文件,所以创建的是HTMLDocument和HTMLDocumentParser。
测试页面:
<html>
<body>
<p>First name: </p>
<input type="text"name="fname" />
Last name: <input type="text"name="lname" />
</body>
</html>
DocumentParser::appendBytes
首先回顾一下调用到该函数的调用栈
#0 WebCore::DecodedDataDocumentParser::appendBytes
#1WebCore::DocumentWriter::addData
#2WebCore::DocumentLoader::commitData
#3android::FrameLoaderClientAndroid::committedLoad
#4WebCore::DocumentLoader::commitLoad
#5WebCore::DocumentLoader::receivedData
#6WebCore::MainResourceLoader::addData
#7WebCore::ResourceLoader::didReceiveData
#8WebCore::MainResourceLoader::didReceiveData
#9WebCore::ResourceLoader::didReceiveData
#10android::WebUrlLoaderClient::didReceiveData
注意DocumentParser本身并没有实现appendBytes方法,这里调用的是其祖先类DecodedDataDocumentParser的appendBytes。
该函数有参数DocumentWriter,即DocumentWriter调用该函数时把自己作为参数传入了。
首先利用DocumentWriter创建一个TextResourceDecoder,使用函数为DocumentWriter::createDecoderIfNeed。根据名字可见,如果需要才创建,那么什么时候是需要呢?先看下创建的TextResourceDecoder是由谁维护的,在DocumentWriter中有成员RefPtr<TextResourceDecoder> m_decoder;在DocumentWriter构造时并没有创建该TextResourceDecoder的实例,只有在执行DocumentWriter::createDecoderIfNeed时,如果m_decoder为空,则创建一个实例,如果非空,则直接返回。所以这里的IfNeed就指DocumentWriter::m_decoder是否为空了。
解码
TextResourceDecoder
当前第一次执行到这里,所以DocumentWriter::m_decoder为空,需要创建一个TextResourceDecoder,创建时会传入MimeType,TextEncoding等信息。创建的TextResourceDecoder会赋值给DocumentWriter::m_decoder。之后又做了些设置Encoding的操作。
再之后把这个新创建的TextResourceDecoder又设置给了Document(通过Frame找到Document)。看下Document的成员,也有一个RefPtr<TextResourceDecoder> m_decoder;这里把新创建的TextResourceDecoder又赋值给了Document::decoder。最后将创建的TextResourceDecoder返回,完成了DocumentWriter::createDecoderIfNeed。
回到DocumentParser::appendBytes。调用TextResourceDecoder::decode。
TextResourceDecoder::decode
当前是HTML页面,所以这里只看下对HTML的处理情况。
这里先调用了TextResourceDecoder::checkForHeadCharset,该函数是个检查HTML头信息中是否有编码的信息,一般HTML的页面中如果指定了编码信息,那么编码信息会放在<head>标签中。该函数就是做这样的检查的。
该函数中有个重要的操作是把收到的字符串转存到TextResourceDecoder:: m_buffer中。
每次新收到的字符串数据都会追加到这个TextResourceDecoder:: m_buffer中,用于TextResourceDecoder的处理。
之后会创建一个HTMLMetaCharsetParser,并赋值给TextResourceDecoder::m_charsetParser通过HTMLMetaCharsetParser::checkForMetaCharset方法来执行对编码的检测,如果检测到,则把获取到的编码TextEncoding类型设置给TextResourceDecoder。
在TextResourceDecoder中有成员TextEncoding m_encoding;和EncodingSource m_source;分别记录了Encodeing具体的类型和来源。
这个过程的调用栈情况:
#0WebCore::TextResourceDecoder::checkForMetaCharset
#1WebCore::TextResourceDecoder::checkForHeadCharset
#2WebCore::TextResourceDecoder::decode
#3WebCore::DecodedDataDocumentParser::appendBytes
#4 WebCore::DocumentWriter::addData
#5WebCore::DocumentLoader::commitData
具体的检测编码的过程就不看了。这里要注意下,经过了TextResourceDecoder::checkForHeadCharset后,数据已经存在了TextResourceDecoder::m_buffer中。
当前没有设置编码,所以该函数返回false,之后WebCore::TextResourceDecoder::decode此处也直接返回””,即空字符串了。之后DecodedDataDocumentParser::appendBytes也返回了,即此时不再做处理了。
这样解析一直不会进行,直到进入finishedLoading调用链时,调用栈如下:
#0WebCore::DecodedDataDocumentParser::appendBytes
#1WebCore::DocumentWriter::addData
#2WebCore::DocumentWriter::endIfNotLoadingMainResource
#3WebCore::DocumentWriter::end
#4WebCore::DocumentLoader::finishedLoading
#5WebCore::FrameLoader::finishedLoading
#6WebCore::MainResourceLoader::didFinishLoading
#7WebCore::ResourceLoader::didFinishLoading
#8android::WebUrlLoaderClient::didFinishLoading
这里也会调用到WebCore::DocumentWriter::addData,但是这里传的参数是addData(0,0, true);可见并没有传入数据,注意第三个参数,表示是否flush,如果为true,则在DecodedDataDocumentParser::appendBytes中,会执行TextResourceDecoder::flush。
TextResourceDecoder::flush
该函数会执行解码的操作,并把TextResourceDecoder::m_buffer中的数据都进行解码,然后把结果输出,并做清理的操作。
那么之前一直没有讲到解码的操作,是因为之前一直没有进行解码,解码需要有编码格式,但是由于在当前的测试页面中没有给出编码格式所以一直都没有进行解码。但是这里当强制进行刷新时,则会强制的执行解码。
TextResourceDecoder中有成员OwnPtr<TextCodec>m_codec;它是负责真正的解码操作的,这里会判断它是否为空,为空就会创建它的实例,并把编码类型作为参数传入。由于页面中没有指定编码格式,这里使用的就是默认的编码格式了。
之后通过TextCodec:: decode进行解码,把TextResourceDecoder::m_buffer的数据传入,解码后会得到一个String类型的数据。之后会把TextResourceDecoder::m_buffer清空。
具体的解码过程这里就不看了。
看来html中有个<head>标签还是有用的。那么我们修改下测试页面,加上一个<head>标签看看。
<head>
<metahttp-equiv="content-type" content="text/html;charset=UTF-8" />
</head>
这样在第一次执行HTMLMetaCharsetParser:: checkForMetaCharset时就会检测到编码了,这里会检测到UTF-8编码,会通过TextResourceDecoder::setEncoding把检测到的编码格式设置给TextResourceDecoder中。调用栈如下:
#0WebCore::TextResourceDecoder::setEncoding
#1WebCore::TextResourceDecoder::checkForMetaCharset
#2 WebCore::TextResourceDecoder::checkForHeadCharset
#3WebCore::TextResourceDecoder::decode
#4WebCore::DecodedDataDocumentParser::appendBytes
#5WebCore::DocumentWriter::addData
#6WebCore::DocumentLoader::commitData
在检测到编码后,返回到TextResourceDecoder::decode中。
在TextResourceDecoder::decode中会执行跟TextResourceDecoder::flush类似的解码过程,判断TextResourceDecoder::m_codec是否为空,为空就会创建它的实例,这里是创建的它的子类的对象。并把编码类型作为参数传入,当前的编码类型为刚才设置的编码类型,即从<head>中找到的编码类型“UTF-8”。这里创建的就是TextCodecUTF8这个子类了。之后通过TextCodec:: decode进行解码,把TextResourceDecoder::m_buffer的数据传入,解码后会得到一个String类型的数据。之后会把TextResourceDecoder::m_buffer清空。
由此可见,当<head>中明确指明了编码格式后,当浏览器获取到明确的编码格式后,接收到数据,立刻就会开始执行解码了。
看下调用栈:
#0WebCore::TextCodecUTF8::decode
#1WebCore::TextResourceDecoder::decode
#2WebCore::DecodedDataDocumentParser::appendBytes
#3WebCore::DocumentWriter::addData
在经过了TextResourceDecoder::decode处理后,我们得到了解码后的字符串,回到DecodedDataDocumentParser::appendBytes中。回顾一下,这里实际上调用的是HTMLDocumentParser:: appendBytes,DecodedDataDocumentParser是HTMLDocumentParser的基类,HTMLDocumentParser本身没实现该函数。
在DecodedDataDocumentParser::appendBytes中,当执行TextResourceDecoder::decode得到数据后,会执行append操作。该函数在HTMLDocumentParser中有实现,所以这里调用的是HTMLDocumentParser::append。并把解码后的String作为参数传入。
HTMLDocumentParser::append
在HTMLDocumentParser中有成员HTMLInputStream m_input;此处,把参数传入的String数据追加到HTMLDocumentParser::m_input,这样HTMLDocumentParser中已经保存了解码后的字符串了。
至次,解码过程已经完毕,我们又处于HTMLDocumentParser中,并且已经保存了解码后的输入数据。
词法解析
HTMLDocumentParser::append中接下来调用HTMLDocumentParser::pumpTokenizerIfPossible。
pump这个单词翻译成泵,我一直不明白程序中出现pump到底什么意思,是不是一下一下的解决一连串的东西啊?
Tokenizer一般都是词法分析时用到的词,网上找到有段解释是这样的, StreamTokenizer类根据用户定义的规则,从输入流中提取可识别的子串和标记符号,这个过程称为令牌化 (tokenizing),因为流简化为了令牌符号。令牌(token)通常代表关键字、变量名、字符串、直接量和大括号等 语法标点。token:令牌,tokenize:令牌化,tokenizer:令牌解析器。还有把token翻译成标记。这里我理解就是类似分词一样,把一句话,划分成几段有效的词。这句话是一个输入,这些有效的词是token,这个过程是tokenizing,处理这个过程的工具是tokenizer。
这个函数做了一些判断后,当前告诉我们,Possible,于是调用了HTMLDocumentParser::pumpTokenizer。
HTMLDocumentParser::pumpTokenizer
这是一个重要的函数。
这里首先创建了一个PumpSession。
之后进入一个循环。循环的进入条件需要检查PumpSession的情况
PumpSession与HTMLParserScheduler
PumpSession的结构很简单,有如下成员
int processedTokens;
double startTime;
boolneedsYield;
HTMLParserScheduler中有函数checkForYieldBeforeToken(PumpSession&session),这个函数内会根据情况改变PumpSession::needsYield的值。是什么情况呢,判断当前的时间减去PumpSession::startTime的时间,如果大于某个设置好的极限时间(当前是0.5秒),那么设置PumpSession:: needsYield为true。如果PumpSession::needsYield为true,那么暂时就不进行解析,即上面的循环不执行。当PumpSession初始构造时,PumpSession:: startTime为0,如果是0,这里会先用当前时间给予赋值,然后再判断当前的时间减去PumpSession:: startTime的时间。这里似乎是为了判断一下超时的问题,具体原因不明。但是大家用gdb跟踪这块代码时要注意,当gdb跟到用当前时间赋值给PumpSession:: startTime时,接下来的语句还会获取下当前时间,然后做减法。调试时要快点跟到做减法处。否则两次获取的当前时间不同,可能就导致两次获取的当前时间的差已经大于极限时间(当前是0.5秒)了,就不会执行上述的循环了。而如果在此处不单步执行的话,一般而言这里两次获取的当前时间很短,所以会进入循环的。即单步调试导致了结果的不一致。
循环处的条件判断是调用HTMLDocumentParser::canTakeNextToken的,该函数会判断当前是否被停止,判断刚刚说的HTMLParserScheduler:: checkForYieldBeforeToken过程。HTMLParserScheduler::checkForYieldBeforeToken过程中由于时间差很短,所以PumpSession:: needsYield不会被设置,仍然是默认的false.这样整体条件符合进入循环。
接下来在循环内调用了重要的函数HTMLTokenizer::nextToken。
HTMLTokenizer::nextToken
回顾一下,当前处于HTMLDocumentParser::pumpTokenizer中,HTMLDocumentParser::m_input中已经存有解码后的输入字符串。HTMLDocumentParser中有成员OwnPtr<HTMLTokenizer> m_tokenizer;该成员在HTMLDocumentParser的构造函数中一并被创建出来。在HTMLDocumentParser中有成员HTMLToken m_token;
由此可见待解析的字符串有了,Tokenizer有了,Token作为一个中间变量,当前其类型还处于HTMLToken::Uninitialized状态。接下来就可以做所谓的tokenizing了。
看下当前完整的调用栈:
#0 WebCore::HTMLTokenizer::nextToken
#1WebCore::HTMLDocumentParser::pumpTokenizer
#2WebCore::HTMLDocumentParser::pumpTokenizerIfPossible
#3WebCore::HTMLDocumentParser::append
#4WebCore::DecodedDataDocumentParser::appendBytes
#5 WebCore::DocumentWriter::addData
#6WebCore::DocumentLoader::commitData
#7android::FrameLoaderClientAndroid::committedLoad
#8 WebCore::DocumentLoader::commitLoad
#9 WebCore::DocumentLoader::receivedData
#10 WebCore::MainResourceLoader::addData
#11WebCore::ResourceLoader::didReceiveData
#12 WebCore::MainResourceLoader::didReceiveData
#13WebCore::ResourceLoader::didReceiveData
#14android::WebUrlLoaderClient::didReceiveData
HTMLTokenizer::nextToken是HTMLTokenizer中最重要的函数。
HTML的有穷状态自动机
在进入HTMLTokenizer::nextToken的分析前,有必要学习下有穷状态自动机。以下是我从网上找的一段还算清晰的定义:
有穷状态自动机(finite automaton, FA) M=(Q,S, δ, q0, F) :
Q:状态的非空有穷集合。∀q∈Q,q称为M的一个状态(state)
S:输入字母表(Inputalphabet)。输入字符串都是S上的字符串
δ:状态转移函数(transitionfunction),有时候又叫做状态转换函数或者移动函数。δ:Q´S®Q, 对∀(q, a)∈Q´S,δ(q,a)=p表示:M在状态q读入字符a,将状态变成p,并将读头向右移动一个带方格而指向输入字符串的下一个字符。
q0:q0∈Q,是M的开始状态(initialstate),也可叫做初始状态或者启动状态
F:F是Q的子集,是M的终止状态(finalstate)集合,∀q∈F,q成为M的终止状态,又称为接受状态(accept state)。
对于HTML的各个状态的定义以及状态的转换情况,参考http://www.w3.org/TR/html5/tokenization.html#tokenization
Q:在参考链接的规范中明确定义了状态集。即每个小节的标题XXX state。
S:所有的字符。
δ:在参考链接的规范中每个状态都明确定义了状态转换时的输入和输出,例如规范中“Data state :U+003C LESS-THAN SIGN (<) Switch tothe tag openstate.”表明δ(q,a)=p中,q: Data state,a: U+003C LESS-THAN SIGN (<),p: tag openstate。而这个处理过程就是δ。
q0: data state(规范中描述“ Thestate machine must start in the data state”)
F: data state,rcdata state,rawtext state,script data state,plaintext在该状态下接受到EOF,Emit anend-of-file token。完成解析。
在该状态机下,在解析出一个完整的token时,会将该token发出,并通过发出的token构建DOM树。
即有穷状态自动机是Tokenizer的一个内部机制。Tokenizer的输入是一个字符串的输入流,输出是一个一个的token。
这些token是在Tokenizer的状态机的转换函数中,每次完整的获取到一个token,就将这个token输出。
有了上述的知识,接下来可以看HTMLTokenizer::nextToken的处理了。
HTMLTokenizer中状态机的处理
HTMLTokenizer定义了枚举类型HTMLTokenizer::State,这些枚举值就是对应规范中给出的状态,即这些状态集合就是上面说的Q。
该类中定义了成员State m_state;用于标识当前的状态。
定义了成员HTMLToken* m_token;用于标识输出的Token。
输入通过HTMLTokenizer::nextToken的参数获取。输入中的每一个字符都属于S的元素。
在构造函数中调用了HTMLTokenizer::reset函数,该函数内定义了一些成员的初始值,通过该函数可以看到m_token的初始值为DataState,即q0。
通过以上有了输入,有了输出的数据结构,有了Q ,S,q0,剩下的就是状态转换函数δ和终止状态,由规范可知终止状态的集合,那么关键就在于状态转换函数δ了。
另外有个内部类InputStreamPreprocessor用于维护输入流的,即把输入流交给它来处理,通过它的InputStreamPreprocessor::nextInputCharacter能够获取到下一个字符。在HTMLTokenizer有成员InputStreamPreprocessorm_inputStreamPreprocessor;
回到HTMLTokenizer::nextToken
首先会通过InputStreamPreprocessor::peek把输入流交给InputStreamPreprocessor::m_inputStreamPreprocessor。
通过InputStreamPreprocessor::nextInputCharacter获取一个输入字母表中的字符,即δ(q,a)=p中的a。
在HTMLTokenizer::nextToken中有个大的switch语句,根据m_state进入不同的case处理。
接下来对每个状态的case中都执行对应的状态处理函数,即δ,当前对于δ(q,a)=p已经有了状态q(即HTMLTokenizer::m_state),a(即InputStreamPreprocessor::nextInputCharacte),那么通过一套处理得到一个新的状态就是状态转换函数了。
HTMLTokenizer::nextToken中状态转换的过程是通过goto语句直接在switch的不同的case间跳转的,那么既然跳转则需要2步,第一步定义跳转的地方,通过宏BEGIN_STATE来定义,该宏定义了case语句以及跳转的标志。第二步执行goto语句进行跳转,通过宏RECONSUME_IN,ADVANCE_TO,SWITCH_TO这三个都可以执行跳转,他们在跳转前都先把当前状态HTMLTokenizer::m_state设置为跳转后的状态,RECONSUME_IN宏设置完状态后直接跳转,不消耗输入流中的字符,而ADVANCE_TO,SWITCH_TO在跳转时都消耗掉当前的输入流中的字符,然后取出下一个字符后,再跳转,但是这两个函数间的区别我还没看明白。
另外还定义了一个宏END_STATE用于执行 break语句。有了以上的宏和规范中对状态转换的定义,我们可以得出下面的伪代码:
对每一个状态执行类似如下的操作:
已知字符cc为输入流中当前待处理的字符,即δ(q,a)=p中的a
BEGIN_STATE(某个待处理的状态) //此处定义了case与跳转标志
if(cc是该状态可处理的字符1){
执行对字符1的处理;
通过RECONSUME_IN/ADVANCE_TO/SWITCH_TO中的一个执行跳转
} else (cc是该状态可处理的字符2){
执行对字符2的处理;
通过RECONSUME_IN/ADVANCE_TO/SWITCH_TO中的一个执行跳转
}
……
END_STATE() //此处定义了break;
注意ADVANCE_TO/SWITCH_TO执行后会,输入流中当前的字符被消耗,下一个字符会赋值给cc。
通过以上的伪代码可知,在一个状态q(case中的状态)下,消耗了一个字符a(cc),对该字符执行了一个处理后,通过goto跳转到新的状态case中。在跳转前会设置新的状态p,并根据情况决定是否获取新的a(cc)。
以上就完成了一个状态转换函数δ(q,a)=p。
而这个处理的过程中,最重要的就是生成并配置一个HTMLToken,这个HTMLToken将作为HTMLTokenizer的输出。这个过程就类似把一段文章进行分词,整段文章是HTMLTokenizer的输入,分出的一个个的词就是HTMLTokenizer的输出,也就是一个个的HTMLToken。这个过程就是词法分析。
经过以上,我们知道了解码的过程,进入到HTMLTokenizer的过程,以及HTMLTokenizer中有穷状态自动机的处理流程。并且知道该状态机的重要作用就是做词法分析,会输出HTMLToken,至于具体的HTMLToken的解析之后再学习。