Mozilla FireFox Gecko内核源代码解析
(1.nsParser)
中科院计算技术研究所网络数据科学与工程研究中心-信息抽取小组
耿耘
前言:
在Web信息抽取的工作过程中,我们主要处理的都是经过各种处理HTML格式文档,而无论是DOM方式还是视觉方式的信息抽取,都需要对HTML进行解析,而最标准的解析器莫过于浏览器内核引擎,因此,对于浏览器内核进行研究会对我们的工作和学习带来很大的帮助。
Mozilla FireFox浏览器的内核Gecko是一款非常成功的开源浏览器内核引擎,但其公认的弊病是XPCOM和XUL复杂的体系让许多开发人员望而却步,本系列文档主要针对Gecko内核工作原理和工作方式进行了逐行代码的详细解析,从工作流程上来讲就是从负责HTML分析的代码开始,直到渲染视觉模型模块为止。
本文档单纯地对源代码进行了注释型解释,并在适当位置加入了一些说明信息,其中不包括浏览器的整体结构等信息,在阅读本文档前,读者可以先对MDN,Bugzilla等网站上的文档进行阅读和调研,大体掌握Gecko以及FireFox浏览器的工作原理,以及一些基本的XPCOM组件知识(类似于微软的COM),这样会对理解本文档带来很大帮助。同时本文档的编码中使用了大量的类型名都是经过重定义的,如nsresult实际上是int,以及Int32,nsCOMPtr等,我在相应的地方加上了一些定义它们的.h文件的名称,希望能帮助大家理解。
请注意本文档只包括Gecko代码的解析,不包括:网络通讯模块Necko,浏览器界面生成组件XULRunner,构件支持模块XPCOM,JS引擎SpiderMonkey等非内核模块等。
本系列文档代码针对Mozilla 1.8.2版本。
如果您在阅读过程中发现了什么问题,请您联系gengyun@sohu.com,十分感谢。
简介:
在Gecko中,包含了一个对于HTML文档进行解析并生成DOM树(Gecko中称为内容模型ContentModel)的模块,这个模块可以统称为nsHTMLParser,它由多个组件构成,如负责字符串扫描的nsScanner,负责分词的nsTokenizer,负责语法检查的DTD,以及负责建立DOM树的ContentSink等。我们这篇文档首先针对其主要的流程控制文件nsParser.h(.cpp)进行解析。
读者在刚开始上手理解HtmlParser的时候,可能会比较困难,因为它和其他的模块进行了很密切的交互和耦合,这篇文档希望能够帮助读者更加容易地理解parser的结构和行为,当读者了解了其他模块后,再去理解这个模块可能就会容易很多。
在阅读Mozilla源代码的时候,需要注意它为了跨硬件跨平台的考虑,重新定义了许多数据类型,如32位机器下的int,会被定义为PRInt32等。以及一些对变量进行Bool结果判断的NS_FAILED(),NS_ASSERTION()等。这些函数最好的了解方式是去看.h文件中的声明,如prtype.h,nscore.h等。
Mozilla FireFox的前身是Netscape浏览器,大部分核心代码都是Netscape的代码,因此大部分代码前面都有ns字样,而接口类型的类则一般声明为nsI字样。
源代码解析:
如果想快速地了解Parser的使用流程,可以查看parser/htmlparser/tests/html下的TestParser.cpp文件。这个文件实际上是用来测试Parser模块功能的。
该模块通过用户输入的参数,单纯地读取某个Html文件并进行解析。需要注意的是,标准的Html解析并不是仅仅打开一个文件或者获取一个输入流并进行解析这么简单,不过这个我们放在后面进行解释。
这里我们先通过分析这部分源代码进行一下大体了解。首先从其主函数入手:
nsresult ParseData(char* anInputStream,char*anOutputStream) {
NS_ENSURE_ARG_POINTER(anInputStream); //确保anInputStream参数正确
NS_ENSURE_ARG_POINTER(anOutputStream); //确保anOutputStream参数正确
nsresult result = NS_OK; //nsresult数据类型和NS_OK数据类型都是ns中自定义的数据类型,请参考nscore.h
// Create a parser
nsCOMPtr<nsIParser> parser(do_CreateInstance(kParserCID,&result)); //创建一个parser
if (NS_FAILED(result)) { //如果创建Parser失败
printf("\nUnable to create aparser\n"); //弹出错误信息
return result;
}
// Create a sink
nsCOMPtr<nsILoggingSink> sink(do_CreateInstance(kLoggingSinkCID,&result)); //创建一个LoggingSink
if (NS_FAILED(result)) { //如果创建Sink失败
printf("\nUnable to create asink\n"); //弹出错误信息
return result;
}
int main(int argc, char**argv)
{
if (argc < 3) { //如果参数数量小于3,说明输入错误
printf("\nUsage: <inputfile><outputfile>\n"); //提示用户参数输入方式
return -1;
}
nsresult rv = NS_InitXPCOM2(nsnull, nsnull, nsnull); //这里测试一下NS的组件机制是否能够正确初始化
if (NS_FAILED(rv)) { //如果不能
printf("NS_InitXPCOM2 failed\n"); //报错
return -1;
}
ParseData(argv[1],argv[2]); //这是解析函数的主体,并将用户输入的第一个和第二个参数传递给函数。
return 0;
}
PRFileDesc* out = PR_Open(anOutputStream,
PR_CREATE_FILE|PR_TRUNCATE|PR_RDWR, 0777);
if (!out) { //如果无法打开输出流
printf("\nUnableto open output file - %s\n", anOutputStream); //则报错
returnresult;
}
nsString stream;
charbuffer[1024] = {0}; // XXX Yikes! //用来存放读取的Html流
PRBool done = PR_FALSE;
PRInt32 length = 0;
while(!done){ //循环地将html字段都写入stream中,每次只读1024字节,可能为了模拟缓冲区大小吧
length = PR_Read(in, buffer, sizeof(buffer)); //读取参数
if (length!= 0) { //如果确实读进来了字节
stream.Append(NS_ConvertUTF8toUTF16(buffer, length)); //
}
else { //如果读进来的是空内容
done=PR_TRUE; //说明全部读取完毕,退出循环
}
}
sink->SetOutputStream(out); //设置输出流
parser->SetContentSink(sink); //为parser设置配合其工作的contentsink
result = parser->Parse(stream, 0,NS_LITERAL_CSTRING("text/html"),PR_TRUE);
//这句就是调用Parser::Parse()方法执行解析的语句了,具体方法我们放在后面进行分析。
PR_Close(in); //关闭输入流
PR_Close(out); //关闭输出流
returnresult;
}
因为每一个HtmlParser都要有一个ContentSink来接收输出,这里创建的LoggingSink实际上就是ContentSink,只不过将ContentSink的输出改为直接输出消息到输出流中,而不是标准地输入到后面的模块,这是专门为测试而建立的Sink,具体可见nsILoggingSink.h的代码说明。
下面我们分析重要的htmlparser部分代码。
打开htmlparser文件夹,可以看到很清晰的三个文件夹:public,src,tests。其中public中包含的大部分是公用的一些头文件,以及一些parser所引用的其他模块的头文件,如nsIContentSink.h等。而tests中则是一些测试用的相关内容,包括了一个随机的html文件生成器,一些html测试用例页面,以及一些测试结果等。开源代码的作者很有意思,自己的很多工作痕迹都上传在SVN上,我们可以利用这些结果去帮助我们进行分析。
Parser类说明:
首先我们可以看一下nsParser.h的开头注释,可知Parser类主要提供两项主要功能:
1) 它遍历在分词过程(tokenization process)中产生的词条(tokens),识别出各个元素的起始和结束(进行验证和标准化)。
2) 它控制并协调一个IContentSink的接口,来产生内容模型(content model)。
这个类在解析Html的时候,不会默认Html文档是有结构的(即不会认为Html文档一定包含BODY,HEAD等模块内容),因此也就不包含一些类似DoBody(),DoHead()之类的方法。
另外,为了让我们的解析过程能够自后向前兼容(即是说和Html流的顺序无关),我们必须扫描每个Token并且实施以下一些基本操作:
1) 确定每个Token的类型(这个很简单,因为每个Token中就包含了这个信息)
2) 确定每个Token所应当处在Html文档中的哪个位置(是在BODY,HEAD,还是FRAMESET等)
3) 将解析好的Content通过ContentSink插入到Document的合适位置。
4) 对于属于BODY部分的tags,我们必须确保通过Document的状态能够确定出正确的解析上下文。即是说,比如我们看到了一个<TR>标签,那么我们必须确定我们的Document中包含了一个table,能够让该<TR>正确地插入进去。这潜在地起到了“容器”的作用,即保证我们的Html是结构正确的。
我们首先来分析nsParser.h(.cpp)。该类是解析器的主体类。
#ifndefNS_PARSER__
#defineNS_PARSER__
#include "nsIParser.h"
#include "nsDeque.h"
#include "nsParserNode.h"
#include "nsIURL.h"
#include "CParserContext.h"
#include "nsParserCIID.h"
#include "nsITokenizer.h"
#include "nsHTMLTags.h"
#include "nsDTDUtils.h"
#include "nsTimer.h"
#include "nsThreadUtils.h"
#include "nsIContentSink.h"
#include "nsIParserFilter.h"
#include "nsCOMArray.h"
#include "nsIUnicharStreamListener.h"
#include "nsCycleCollectionParticipant.h"
classnsICharsetConverterManager;
classnsICharsetAlias;
classnsIDTD;
classnsScanner;
classnsSpeculativeScriptThread;
classnsIThreadPool;
#ifdef_MSC_VER
#pragma warning( disable :4275 )
#endif
//这段代码主要是一些头文件包含声明,以及一些前置声明。我们跳过这段代码直接看后面的
classnsParser : public nsIParser,
publicnsIStreamListener
{
//nsParser继承自两个基类:nsIParser,nsIStreamListener,前者是基本接口,而后者则是为了和Necko进行通讯所用的基类。
public:
/**
* Called on module init
*/
static nsresult Init(); //初始化的方法
/**
* Called on module shutdown
*/
static void Shutdown(); //关闭方法
NS_DECL_CYCLE_COLLECTING_ISUPPORTS //这两个是在前面的nsISupportImpl.h中#Define过的,主要定义了一些接口
NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(nsParser, nsIParser)
/**
* default constructor
* @update gess5/11/98
*/
nsParser(); //构造方法
/**
* Destructor
* @update gess5/11/98
*/
virtual ~nsParser(); //析构方法
/**
* Select given content sink into parserfor parser output
* @update gess5/11/98
* @param aSink is the new sink to be used by parser
* @return old sink, or NULL
*/
NS_IMETHOD_(void)SetContentSink(nsIContentSink* aSink); //为该Parser设置对应的ContentSink,ContentSink就是用来建立DOM树所用到的模块
/**
* retrive the sink set into the parser
* @update gess5/11/98
* @param aSink is the new sink to be used by parser
* @return old sink, or NULL
*/
NS_IMETHOD_(nsIContentSink*)GetContentSink(void); //获取该Parser所对应的ContentSink
/**
* Call this method once you've created a parser, and want to instruct it
* about the command which caused the parser to be constructed. Forexample,
* this allows us to select a DTD which can do, say, view-source.
*
* @update gess 3/25/98
* @param aCommand -- ptrs tostring that contains command
* @return nada
*/
NS_IMETHOD_(void)GetCommand(nsCString& aCommand); //获取当前Parser的指令方式
NS_IMETHOD_(void) SetCommand(const char*aCommand); //为当前的Parser进行指令设置
NS_IMETHOD_(void)SetCommand(eParserCommands aParserCommand); //同上,形参不同
//根据程序注释,这里主要是设定Parser的工作方式,解析器有多种工作模式,HTML模式,查看源代码模式,这里可以对其进行设置,还有可以对后面我们会用到的DTD进行设置,Parser在对不同的Html文档进行解析时需要进行不同的操作,这些我们后面再进行解释。
/**
* Call this method once you've created a parser, and want to instruct it
* about what charset to load
*
* @update ftang 4/23/99
* @param aCharset- the charset ofa document
* @param aCharsetSource- thesource of the charset
* @return nada
*/
NS_IMETHOD_(void) SetDocumentCharset(const nsACString& aCharset, PRInt32 aSource);
//设置Parser进行文档解析时使用的字符集
NS_IMETHOD_(void) GetDocumentCharset(nsACString& aCharset,PRInt32& aSource)
//获取Parser进行文档解析时使用的字符集
{
aCharset = mCharset;
aSource = mCharsetSource;
}
NS_IMETHOD_(void) SetParserFilter(nsIParserFilter* aFilter); //为Parser设置过滤器
/**
* Cause parser to parse input from givenURL
* @update gess5/11/98
* @param aURL is a descriptor for source document
* @param aListener is a listener to forward notifications to
* @return TRUE if all went well -- FALSE otherwise
*/
NS_IMETHOD Parse(nsIURI* aURL,
nsIRequestObserver*aListener = nsnull,
void*aKey = 0,
nsDTDMode aMode =eDTDMode_autodetect);
//这个方法能够从给定的URL参数中,获取Html文档并进行解析
/**
* @update gess5/11/98
* @param anHTMLString contains a string-full of real HTML
* @param appendTokens tells us whether we should insert tokens inline, or appendthem.
* @return TRUE if all went well -- FALSE otherwise
*/
NS_IMETHOD Parse(const nsAString&aSourceBuffer,
void*aKey,
constnsACString& aContentType,
PRBool aLastCall,
nsDTDMode aMode =eDTDMode_autodetect);
//这个方法能够从给定的aSourceBuffer中获取Html文档并进行解析
NS_IMETHOD_(void *) GetRootContextKey(); //获取位于根部的ParseContext的Key,ParserContext是解析上下文,在解析的过程中为解析提供支持所用的
//以上两个Parser方法有很大不同,虽然都是对Html流进行解析,但是还是有很多区别,这个在对该方法进行解析的时候会进行说明。而对于GetRootContextKey()方法,由于我们的ParserContext们采用的是栈式数据结构,并且用链表方式进行存储,且每个Context都有一个唯一的Key,这个GetRootContextKey()主要是为了获取栈底元素的Key值。
/**
* This method needs documentation
*/
NS_IMETHOD ParseFragment(constnsAString& aSourceBuffer,
void* aKey,
nsTArray<nsString>& aTagStack,
PRBool aXMLMode,
const nsACString& aContentType,
nsDTDMode aMode =eDTDMode_autodetect);
NS_IMETHOD ParseFragment(constnsAString& aSourceBuffer,
nsISupports*aTargetNode,
nsIAtom*aContextLocalName,
PRInt32aContextNamespace,
PRBool aQuirks);
//上面这两个方法是主要针对HTML FRAGMENT进行解析的,也就是进行一些简单的HTML TO DOM的解析。其中,第一个方法还可以用来解析XML文档,而第二个方法在目前版本的FireFox里还没有实现。
/**
* This method gets called when the tokenshave been consumed, and it's time
* to build the model via the content sink.
* @update gess5/11/98
* @return YES if model building went well -- NO otherwise.
*/
NS_IMETHOD BuildModel(void);
//上面这个方法是在分词过程结束后,需要调用ContentSink进行输出和建立Content Model的时候调用的方法。
/**
* Call this when you want control whether or not the parser will parse
* and tokenize input (TRUE), or whether it just caches input to be
* parsed later (FALSE).
*
* @update gess 9/1/98
* @param aState determines whetherwe parse/tokenize or just cache.
* @return current state
*/
NS_IMETHOD ContinueParsing(); //让parser继续工作
NS_IMETHOD ContinueInterruptedParsing(); //让被打断的Parser继续工作
NS_IMETHOD_(void) BlockParser(); //阻塞parser的工作
NS_IMETHOD_(void) UnblockParser(); //解除parser的阻塞
NS_IMETHOD Terminate(void); //结束parser工作
//这几个方法主要是对Parser进行控制的,从字面就很好理解他们的作用。其中parser的阻塞原因可能有很多种,如时间过长等
/**
* Call this to query whether the parser isenabled or not.
*
* @update vidur 4/12/99
* @return current state
*/
NS_IMETHOD_(PRBool) IsParserEnabled(); //返回paser是否当前可用
/**
* Call this to query whether the parserthinks it's done with parsing.
*
* @update rickg 5/12/01
* @return complete state
*/
NS_IMETHOD_(PRBool) IsComplete(); //返回paser是否认为自己完成了工作
//需要注意的是,IsComplete()返回的只是从parser本身出发认为自己是否完成了工作。
/**
* This rather arcane method (hack) is used as a signal between the
* DTD and the parser. It allows the DTD to tell the parser that content
* that comes through (parser::parser(string)) but not consumed should
* propagate into the next string based parse call.
*
* @update gess 9/1/98
* @param aState determines whether we propagate unused string content.
* @return current state
*/
void SetUnusedInput(nsString&aBuffer); //这个方法主要是设置一个字符串,该字符串中存放的是当前还未处理的字符流,这些字符流只有在下一个parser的调用中才能够被解析
/**
* This method gets called (automatically)during incremental parsing
* @update gess5/11/98
* @return TRUE if all went well, otherwise FALSE
*/
virtual nsresult ResumeParse(PRBoolallowIteration = PR_TRUE,
PRBool aIsFinalChunk = PR_FALSE,
PRBoolaCanInterrupt = PR_TRUE);
//这个方法是在进行增量式解析的时候自动被调用的(其实在其他地方也有调用)。
//*********************************************
// These methods are callback methods used by
// net lib to let us know about ourinputstream.
//*********************************************
// nsIRequestObserver methods:
NS_DECL_NSIREQUESTOBSERVER
// nsIStreamListener methods:
NS_DECL_NSISTREAMLISTENER
//以上两个方法是预先#define好的,用来提供parser的输入用的,让Necko可以通过调用这两个模块来提醒parser有新的输入流了。
void PushContext(CParserContext&aContext); //将Context压栈
CParserContext* PopContext(); //将Context出栈
CParserContext* PeekContext() {return mParserContext;} //查看栈顶的Context
//这三个方法很显然是对栈进行操作,而栈中的元素则是Context,我们前面提到过ParserContext是以栈的形式存放的,用来对解析的过程进行支持。
/**
* Get the channel associated with thisparser
* @update harishd,gagan 07/17/01
* @param aChannel out param that willcontain the result
* @return NS_OK if successful
*/
NS_IMETHOD GetChannel(nsIChannel** aChannel); //获取该Parser的数据通道,这个方法主要是获取和该Parser相连的Channel,该Channel是parser获取输入流的来源。
/**
* Get the DTD associated with this parser
* @update vidur 9/29/99
* @param aDTD out param that will containthe result
* @return NS_OK if successful,NS_ERROR_FAILURE for runtime error
*/
NS_IMETHOD GetDTD(nsIDTD** aDTD); //获取该Parser的DTD。
/**
* Detects the existence of a META tag withcharset information in
* the given buffer.
*/
PRBool DetectMetaTag(const char* aBytes,
PRInt32 aLen,
nsCString&oCharset,
PRInt32&oCharsetSource);
//在给定的缓冲字符串中寻找<META>标签,返回是否找到
void SetSinkCharset(nsACString&aCharset);
//为Sink设置让其使用的字符集
/**
* Removes continue parsing events
* @update kmcclusk 5/18/98
*/
NS_IMETHODIMP CancelParsingEvents();
//删除解析结束时所触发的事件(其实就是清空当前parser里mContinueEvent的值)
/**
* Indicates whether the parser is in a state where it
* can be interrupted.
* @return PR_TRUE if parser can be interrupted, PR_FALSE if it can not beinterrupted.
* @update kmcclusk 5/18/98
*/
virtual PRBool CanInterrupt();
//返回该parser在解析的时候是否能够被外来事件打断。返回TRUE表示能,返回FALSE表示不能。
/**
* Set to parser state to indicate whether parsing tokens can beinterrupted
* @param aCanInterrupt PR_TRUE if parser can be interrupted, PR_FALSE ifit can not be interrupted.
* @update kmcclusk 5/18/98
*/
voidSetCanInterrupt(PRBool aCanInterrupt);
//设置该parser在进行解析的时候能否被外来事件打断。
/**
* This is called when the final chunk hasbeen
* passed to the parser and the contentsink has
* interrupted token processing. Itschedules
* a ParserContinue PL_Event which will askthe parser
* to HandleParserContinueEvent when it ishandled.
* @update kmcclusk6/1/2001
*/
nsresult PostContinueEvent(); //触发让parser继续的Event
//需要注意的是,上面PostContinueEvent()只能在两种情况下被调用,一个是当所有的数据都输入完毕的时候,还有就是在Parser已经被ContentSink因为处理时间过长而阻塞的时候。
/**
* Fired when the continue parse event is triggered.
* @update kmcclusk 5/18/98
*/
voidHandleParserContinueEvent(classnsParserContinueEvent *);
//这个是在上面那个nsContinueEvent被触发的时候进行调用的,具体请见nsContinueEvent的类定义
/**
* Called by top-level scanners when datafrom necko is added to
* the scanner.
*
//下面这些代码是为了给高层的扫描器提供一个借口,当数据从necko传输到扫描器的时候被调用
nsresultDataAdded(const nsSubstring& aData,nsIRequest *aRequest);
//aData是数据,aRequest是数据的请求
staticnsCOMArray<nsIUnicharStreamListener> *sParserDataListeners;
//建立一组数据监听器
static nsICharsetAlias*GetCharsetAliasService() {
return sCharsetAliasService;
} //获取字符集编码设置等值
staticnsICharsetConverterManager* GetCharsetConverterManager() {
return sCharsetConverterManager;
} //获取字符集编码转换等功能的服务器
virtual voidReset() {
Cleanup();
Initialize();
}
//通过调用Cleanup()来清除解析器状态,并通过调用Initialize()来初始化解析器,用来重设解析器的值
nsIThreadPool* ThreadPool() {
return sSpeculativeThreadPool;
}
//这个SpeculativeThread是用来进行预读取用的线程,当Gecko的Html解析被打断时,这个线程会自动地并行去读取HTML文档中以src = URL形式给出的一些应当会用到的CSS,脚本语言文件等数据,这样来提高运行效率
PRBool IsScriptExecuting() {
return mSink &&mSink->IsScriptExecuting();
}
//通过调用当前解析器所属的ContentSink的IsScriptExecuting()方法来判断是否该ContentSink是否正在进行脚本解析
//下面是protected的一些方法:
protected:
void Initialize(PRBoolaConstructor = PR_FALSE);
//初始化方法
void Cleanup();
//清除解析器状态的方法
/**
*
* @update gess5/18/98
* @param
* @return
*/
nsresult WillBuildModel(nsString& aFilename);
//在解析器即将调用ContentSink进行ContentModel建模之前进行调用,做一些准备工作,Mozilla中经常可见这种三部曲式的代码,即以WillDoSomething-DoSomething-DidDoSomething的形式和顺序出现,用来进行运行准备,运行,运行收尾的三步工作。
/**
*
* @update gess5/18/98
* @param
* @return
*/
nsresult DidBuildModel(nsresult anErrorCode);
//调用ContentSink进行ContentModel的建立。
void SpeculativelyParse();
//并行进行读取解析
//下面是一些private的分词(tokenization)方法:
private:
/*******************************************
These are the tokenization methods...
*******************************************/
/**
* Part of the code sandwich, this gets called right before
* the tokenization process begins. The main reason for
* this call is to allow the delegate to do initialization.
*
* @update gess 3/25/98
* @param
* @return TRUE if it's ok toproceed
*/
PRBool WillTokenize(PRBool aIsFinalChunk = PR_FALSE);
//这个是在进行分词之前进行准备工作的方法
/**
* This is the primary control routine. It iteratively
* consumes tokens until an error occurs or you run out
* of data.
*
* @update gess 3/25/98
* @return error code
*/
nsresult Tokenize(PRBool aIsFinalChunk = PR_FALSE);
//这个就是进行分词的操作,它会不断地对tokens进行处理,直到出错或者处理完毕
/**
* This is the tail-end of the code sandwich for the
* tokenization process. It gets called once tokenziation
* has completed.
*
* @update gess 3/25/98
* @param
* @return TRUE if all went well
*/
PRBoolDidTokenize(PRBool aIsFinalChunk = PR_FALSE);
//这个是在tokenize处理之后进行收尾的操作
//最后,我们来看一下parser的全部数据成员,对这些数据成员的理解可以帮助我们去分析parser的结构。
protected:
//*********************************************
// And now, some data members...
//*********************************************
CParserContext* mParserContext;
//用来存放解析的上下文,注意这些上下文之间是以链表的方式进行存储的
nsCOMPtr<nsIDTD> mDTD;
//用来存放一个指向当前所用DTD对象的指针
nsCOMPtr<nsIRequestObserver>mObserver;
//用来观察并接收nsIRequest的监听器
nsCOMPtr<nsIContentSink> mSink;
//当前parser所用的ContentSink
nsIRunnable* mContinueEvent; // weak ref
//设置一个指向nsIRunnable类型的指针,该指针指向的函数就是当解析结束的时候所要执行的函数。
nsRefPtr<nsSpeculativeScriptThread>mSpeculativeScriptThread;
//当前负责进行资源预读取的线程
nsCOMPtr<nsIParserFilter>mParserFilter;
//设置一个指针,指向当前解析器的Filter
nsTokenAllocator mTokenAllocator;
//当前解析器的Token分配器
eParserCommands mCommand; //当前解析器的指令
nsresult mInternalState; //当前解析器的(内部)状态
PRInt32 mStreamStatus; //当前解析器解析流的状态
PRInt32 mCharsetSource; //当前的字符集类型(来源)
PRUint16 mFlags; //用于对解析器进行一些设置的标志位,如是否启用了Observer等,在后面的函数中会用到,主要是进行一些bit位操作,注意是PRUint16,该类型不同机器下不一样,一般使用unsigned short,也就是占2个字节,16位。
nsString mUnusedInput; //未解析的字符串
nsCString mCharset; //当前解析器的字符集
nsCString mCommandStr; //当前解析器的指令字符
static nsICharsetAlias* sCharsetAliasService; //解析器所用的字符集
static nsICharsetConverterManager*sCharsetConverterManager; //解析器所用的字符集类型转换器
static nsIThreadPool* sSpeculativeThreadPool; //并行预读取资源线程的线程池
enum {
kSpeculativeThreadLimit = 15, //设置线程池的上限
kIdleThreadLimit = 0, //设置空闲线程的上限
kIdleThreadTimeout = 50 //设置空闲线程超时的上限阈值
};
public:
//设置几个计时器,因为Mozilla Firefox是一款注重人机交互的软件,它非常注重程序的响应时间,因此设置了一些计时器
MOZ_TIMER_DECLARE(mParseTime) //用来测量解析时间
MOZ_TIMER_DECLARE(mDTDTime) //用来测量DTD的处理时间
MOZ_TIMER_DECLARE(mTokenizeTime) //用来测量Tokenize分词过程的处理时间
};
以上就是nsParser.h的代码,下面我们来看nsParser.cpp的代码。
//我们省略它的#include部分
#defineNS_PARSER_FLAG_PARSER_ENABLED 0x00000002
#defineNS_PARSER_FLAG_OBSERVERS_ENABLED 0x00000004
#defineNS_PARSER_FLAG_PENDING_CONTINUE_EVENT 0x00000008
#defineNS_PARSER_FLAG_CAN_INTERRUPT 0x00000010
#defineNS_PARSER_FLAG_FLUSH_TOKENS 0x00000020
#defineNS_PARSER_FLAG_CAN_TOKENIZE 0x00000040
//首先它定义了几个全局用的值,仔细看可以发现,前三个分别是二进制的第1,2,3位为1,其他位为零,也就是说这几个值不会互相干涉,这也是一种常用的比特标志位赋值方法,用它就可以对我们前面的mFlag标志位进行标示,来标示parser的一些基本状态。而至于这几个16进制值,读者可以自己观察他们的特点和之间的关系。
staticNS_DEFINE_IID(kISupportsIID, NS_ISUPPORTS_IID);
staticNS_DEFINE_CID(kCParserCID, NS_PARSER_CID);
staticNS_DEFINE_IID(kIParserIID, NS_IPARSER_IID);
//以上这三个方法是在nsID.h中定义的多重#DEFINE的方法,读者可以自己去看一下很简单,另外需要注意NS_ISUPPORTS_IID,NS_PARSER_CID和NS_IPARSER_IID的值的特点。
//-------------------------------------------------------------------
nsCOMArray<nsIUnicharStreamListener>*nsParser::sParserDataListeners;
//这个方法声明了Parser的sParserDataListener指向一个流监听器类型
//源文件中接下来有一段很长的关于nsParser的注释说明,介绍了Parser工作原理的特点。这里对其进行翻译并加以解释一下:
//Parser可以被在执行BuildModel()方法时所返回的NS_ERROR_HTMLPARSER_INTERRUPTED值所打断。这会使得Parser停止对当前内容的解析并返回到原先的事件循环中去。此时,Parser中所剩下的未解析的字符串则会被保留下来,直到下一次网络模块的OnDataAvailable()(即有新的数据被接收到时)被调用时再继续解析。然而,如果当所有的Html数据流都已经被接收到,那么则不会再产生新的OnDataAvailable()事件(此时如果parser被打断且还有剩下的未处理数据则会出现问题),因此Parser会设置一个nsParserContinueEvent,这个事件将会在Parser被打断并返回原先的时间循环后被再次调用(使得Parser能够继续处理未处理的数据),而如果此时Parser再次被打断,则他会再给自己加一个nsParserContinueEvent。这一过程会一直持续,直到以下两个情况之一发生为止:
// 1)所有剩下的数据能够不被打断地处理到结束
// 2) Parser被撤销
//这一功能目前在CNavDTD和nsHTMLContentSink中所使用。当新的数据块到达并需要进行处理的时候,nsHTMLSink是由CNavDTD进行通知的。当开始进行处理时,nsHTML content sink会记录下开始处理的时间,并且如果处理的时间超过了一个叫做最大tokenizing时间的阈值的话,则会返回一个NS_ERROR_HTMLPARSER_INTERRUPTED的错误。这将允许content sink对一个chunk中一次处理多少数据进行限定,从而也就限定了在事件循环之外的处理最多能耗费多少时间。处理小数据块同样可以减少在低层的reflows(浏览器回流操作,后面会介绍)操作的时间耗费。
//这一功能在读取大文件的时候作用尤其明显。如果最大tokenizing时间设置的足够小,那么浏览器在处理文档时候就能够始终保持和用户的可交互性。
//然而这一功能的一个副作用就是:当最后一部分数据传输到OnDataAvailable()的时候,文件读取工作还没有结束,因为