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版本。
如果您在阅读过程中发现了什么问题,请您联系[email protected],十分感谢。
简介:
在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 if (NS_FAILED(result)) { //如果创建Parser失败 printf("\nUnable to create aparser\n"); //弹出错误信息 return result; } // Create a sink nsCOMPtr 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: 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的状态能够确定出正确的解析上下文。即是说,比如我们看到了一个
我们首先来分析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 * @paramaSink is the new sink to be used by parser * @returnold sink, or NULL */ NS_IMETHOD_(void)SetContentSink(nsIContentSink* aSink); //为该Parser设置对应的ContentSink,ContentSink就是用来建立DOM树所用到的模块 /** * retrive the sink set into the parser * @update gess5/11/98 * @paramaSink is the new sink to be used by parser * @returnold 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 * @paramaURL is a descriptor for source document * @paramaListener is a listener to forward notifications to * @returnTRUE 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 * @paramanHTMLString contains a string-full of real HTML * @paramappendTokens tells us whether we should insert tokens inline, or appendthem. * @returnTRUE 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 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 * @returnYES 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_IMETHODContinueParsing(); //让parser继续工作 NS_IMETHODContinueInterruptedParsing(); //让被打断的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 * @paramaState 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 * @returnTRUE 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); //在给定的缓冲字符串中寻找标签,返回是否找到 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 //建立一组数据监听器 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 //用来存放一个指向当前所用DTD对象的指针 nsCOMPtr //用来观察并接收nsIRequest的监听器 nsCOMPtr //当前parser所用的ContentSink nsIRunnable*mContinueEvent; // weak ref //设置一个指向nsIRunnable类型的指针,该指针指向的函数就是当解析结束的时候所要执行的函数。 nsRefPtr //当前负责进行资源预读取的线程 nsCOMPtr //设置一个指针,指向当前解析器的Filter nsTokenAllocatormTokenAllocator; //当前解析器的Token分配器 eParserCommands mCommand; //当前解析器的指令 nsresultmInternalState; //当前解析器的(内部)状态 PRInt32 mStreamStatus; //当前解析器解析流的状态 PRInt32mCharsetSource; //当前的字符集类型(来源) 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_ENABLED0x00000002 #defineNS_PARSER_FLAG_OBSERVERS_ENABLED0x00000004 #defineNS_PARSER_FLAG_PENDING_CONTINUE_EVENT 0x00000008 #defineNS_PARSER_FLAG_CAN_INTERRUPT0x00000010 #defineNS_PARSER_FLAG_FLUSH_TOKENS0x00000020 #defineNS_PARSER_FLAG_CAN_TOKENIZE0x00000040 //首先它定义了几个全局用的值,仔细看可以发现,前三个分别是二进制的第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 //这个方法声明了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()的时候,文件读取工作还没有结束,因为parser可能在最后一部分数据传输到的时候被打断。文档只有在所有的token都被处理过,并且也没有等待处理的nsParserContinueEvents时才算被处理完毕。如果一些应用程序认为它能够通过监视文档的读取请求来判断文档是否读取结束的话,会造成不小的问题。这种问题在Mozilla里就会发生。当所有的文档读取请求都已经被满足时,文档就被认为已经全部读取完毕了。为了拖延文档读取直到所有的解析工作完毕,nsHTMLContentSink加入了一个很笨的解析器读取请求,这个请求始终不会被满足,直到nsHTMLContentSink的DidBuildModel方法被调用了为止。而CNavDTD则能够保证直到最后一块数据通过OnDataAvailable()被传输到解析器中,并且没有任何等待满足的nsParserContinueEvent时,才会去调用DidBuildModel。 //目前Parser在处理script的时候会屏蔽所有中断的请求。这是因为JavaScript修改DOM树的document.write()方法如果被打断,则可能会出现一些错误。 //如果想得到更多的信息,请访问bugzilla76772。 //下面,我们就开始分析nsParser代码的逻辑实体部分,首先是声明前面提到过的nsParserContinueEvent。 classnsParserContinueEvent : public nsRunnable { public: nsRefPtr nsParserContinueEvent(nsParser* aParser) //初始化方法,将mParser赋值 : mParser(aParser) {} NS_IMETHOD Run() //该Event的运行方法 { mParser->HandleParserContinueEvent(this); //调用关联的Parser的方法进行处理 return NS_OK; //返回成功的正确值 } }; //下面是个模板类Holder,用来存放classType的,并提供了一个GET方法返回存放的值,其析构方法就是使用Reaper定义的值进行替代,很简单的实现,主要为主函数提供支持。 template classHolder { public: typedef void(*Reaper)(Type *); //定义一个函数指针类型reaper,该类型指针指向void function(Type *)类型的函数 Holder(Reaper aReaper) //构造方法 : mHoldee(nsnull), mReaper(aReaper) //将mReaper赋初值 { } ~Holder() { //析构方法 if (mHoldee) { mReaper(mHoldee); //将mReaper里赋上mHoldee的值,即用mHoldee的值去替换当前mReaper所指向的值 } } Type *get() { return mHoldee; //返回mHoldee的值 } const Holder &operator=(Type *aHoldee) { //重载操作符 if (mHoldee && aHoldee !=mHoldee) { //如果mHoldee不为空且和新的Holdee不同 mReaper(mHoldee); //则mReaper赋值为原先的Holdee } mHoldee = aHoldee; //用新的aHoldee代替原来的mHoldee return *this; //将本Holder返回 } private: //前面用到的两个数据成员 Type *mHoldee; Reaper mReaper; }; //下面,是预读取资源的解析线程的类声明部分: classnsSpeculativeScriptThread : public nsIRunnable{ //注意它也是继承自nsIRunnable public: nsSpeculativeScriptThread() //构造方法,将各个数据成员赋初值 : mLock(nsAutoLock::DestroyLock), mCVar(PR_DestroyCondVar), mKeepParsing(PR_FALSE), mCurrentlyParsing(PR_FALSE), mNumConsumed(0), mContext(nsnull), mTerminated(PR_FALSE) { } ~nsSpeculativeScriptThread() { //析构方法 NS_ASSERTION(NS_IsMainThread() || !mDocument, //确保不是主线程或解析文档为空 "Destroyingthe document on the wrong thread"); } NS_DECL_ISUPPORTS //详见nsISupportsImpl.h文件 NS_DECL_NSIRUNNABLE nsresult StartParsing(nsParser *aParser); //开始进行解析 void StopParsing(PRBool aFromDocWrite); //停止进行解析 enum PrefetchType { NONE, SCRIPT, STYLESHEET, IMAGE};//枚举类型,定义预读取数据的类型 struct PrefetchEntry { //定义一个结构体,为解析过程使用,主要是为了存放一些需要预先读取的数据内容 PrefetchType type; nsString uri; nsString charset; nsString elementType; }; nsIDocument *GetDocument() { //私有成员变量访问接口,获取该Thread所关联的Document NS_ASSERTION(NS_IsMainThread(), "Potentialthreadsafety hazard"); return mDocument; } PRBool Parsing() { //私有成员变量访问接口,获取该Thread是否正在Parsing return mCurrentlyParsing; } CParserContext *Context() { //私有成员变量访问接口,获取该Thread的ParserContext return mContext; } typedef nsDataHashtable //定义一个Hashtable类型 PreloadedType& GetPreloadedURIs() { //定义一个获取该Hashtable的GET方法 return mPreloadedURIs; //返回mPreloadedURIs } void Terminate() { //销毁Thread mTerminated = PR_TRUE; //设置销毁标志位 StopParsing(PR_FALSE); //停止解析 } PRBool Terminated() { //获取当前Thread是否处于销毁状态 return mTerminated; } //下面是私有部分 private: void ProcessToken(CToken *aToken); //处理Token void AddToPrefetchList(constnsAString &src, //将一个新的需要进行预读取的URL添加到mURLs中去 const nsAString &charset, const nsAString &elementType, PrefetchType type); void FlushURIs(); //将当前缓冲区内的所有的URI数据传输到主线程中进行处理 // These members are only accessed on the speculativelyparsing thread. nsTokenAllocator mTokenAllocator; //私有数据成员,当前线程的TokenAllocator // The following members are shared across the main threadand the // speculatively parsing thread. //以下这些函数会在主线程和次级线程中进行使用 Holder Holder volatile PRBool mKeepParsing; //BOOL变量,标示是否继续进行解析 volatile PRBool mCurrentlyParsing; //BOOL变量,标示当前是否正在解析 nsRefPtr nsAutoPtr enum { kBatchPrefetchURIs = 5 }; //枚举类型,设置缓冲区的大小 nsAutoTArray // Number of characters consumed by the last speculativeparse. //用来存放上一次解析时消耗的字符数 PRUint32 mNumConsumed; // These members are only accessed on the main thread. //下面这些数据成员只有在主线程中才会被调用 nsCOMPtr CParserContext *mContext; //当前线程的解析上下文mContext。 PreloadedType mPreloadedURIs; //Hash数据表,存放当前解析线程所有的URI PRBool mTerminated; //标示该线程是否被销毁 }; //下面这个CSSLoaderObserver是用来注册一个监听器来接收CSS信息,但是实际上并没有什么作用,只在没有CSS的时候才用到。(即没有CSS文件的时候,同样要实现一个CSSLoaderOberserver接口) /** * Used if we need to pass annsICSSLoaderObserver as parameter, * but don't really need its services */ classnsDummyCSSLoaderObserver : publicnsICSSLoaderObserver { public: NS_IMETHOD StyleSheetLoaded(nsICSSStyleSheet* aSheet, PRBool aWasAlternate,nsresult aStatus) { return NS_OK; } NS_DECL_ISUPPORTS //事先#define了一些接口的定义,参见nsISupportImpl.h }; //下面这个方法是一个很有意思的方法,通过一个定义好的构件模板,可以为某个内部类添加一个新的方法。具体方法暂不在这篇文档中介绍了,有兴趣的可以去看nsISupportImpl.h文档。 NS_IMPL_ISUPPORTS1(nsDummyCSSLoaderObserver,nsICSSLoaderObserver) //下面是对nsPreloadURIs的定义: classnsPreloadURIs : public nsIRunnable { public: //构造方法,用构造参数aURIs和aSriptThread对两个数据成员进行赋值 nsPreloadURIs(nsAutoTArray nsSpeculativeScriptThread*aScriptThread) : mURIs(aURIs), mScriptThread(aScriptThread) { } NS_DECL_ISUPPORTS //事先#define了一些接口的定义,参见nsISupportImpl.h NS_DECL_NSIRUNNABLE static voidPreloadURIs(const nsAutoTArray private: nsAutoTArray nsRefPtr }; //下面是利用预先#define好的语句添加线程安全性的支持 NS_IMPL_THREADSAFE_ISUPPORTS1(nsPreloadURIs,nsIRunnable) //之后的Run()方法很简单,就是直接调用内部的PreloadURIs()方法。 NS_IMETHODIMP nsPreloadURIs::Run() //运行方法 { PreloadURIs(mURIs, mScriptThread); //直接调用PreloadURIs,将本身的两个成员变量作为参数传递过去 return NS_OK; //返回成功的值 } //下面我们就来看它本体调用的方法,PreloadURIs()的具体执行方法。 void nsPreloadURIs::PreloadURIs(constnsAutoTArray nsSpeculativeScriptThread *aScriptThread) { //首先判断是否是主线程 NS_ASSERTION(NS_IsMainThread(), "Touchingnon-threadsafe objects off thread"); if (aScriptThread->Terminated()) { return; //如果当前线程已经处于销毁状态,则什么事情都不作,直接返回。 } //获取当前线程所对应得nsIDocument对象 nsIDocument *doc = aScriptThread->GetDocument(); //如果读取失败,即doc对象为空则报错 NS_ASSERTION(doc, "We shouldn't havestarted preloading without a document"); // Note: Per the code in the HTML content sink, we shouldbe keeping track // of each // parsing off the main thread, this is hard to emulate.For now, just load // the URIs using the document's base URI at the potentialcost of being // wrong and having to re-load a given relative URI later. //对于HTMLcontent sink中的每一个节点代码,我们都应当跟踪所有的 //首先获取当前doc的BaseURI nsIURI *base = doc->GetBaseURI(); //获取文档的编码集 const nsCString &charset = doc->GetDocumentCharacterSet(); //用一个指针的形式,获取当前线程的PreloadedURIs的地址 nsSpeculativeScriptThread::PreloadedType &alreadyPreloaded = aScriptThread->GetPreloadedURIs(); //获取需要preload的URI //对于每一个参数传递进来的URIs进行处理 for (PRUint32 i = 0, e = aURIs.Length(); i < e;++i) { //获取该数据类型的起始地址 constnsSpeculativeScriptThread::PrefetchEntry &pe = aURIs[i]; //一个指向nsIURI类型的指针 nsCOMPtr //建立一个新的uri,并调用IO模块去进行读取 nsresult rv = NS_NewURI(getter_AddRefs(uri), pe.uri, charset.get(),base); if (NS_FAILED(rv)) { //如果读取失败 NS_WARNING("Failed to create aURI"); //则报错 continue; //执行下一个循环 } nsCAutoString spec; //新申请一个字符串 uri->GetSpec(spec); //调用GetSpec,获取其URL scheme并将其添加至spec字符串之前 PRBoolanswer; //从当前的alreadyPreloaded的hash列表中查找该spec的URI,是否已经被读取了,如果是则不用再次读取(比如页面上有两张一样的图片,那么只需要读取一次) if (alreadyPreloaded.Get(spec,&answer)) { // Already preloaded. Don't preload again. continue; } //将spec放入已经读取的hash表中,记录其已经被读取 alreadyPreloaded.Put(spec, PR_TRUE); //根据pe的类型,进行不同的读取 switch (pe.type) { case nsSpeculativeScriptThread::SCRIPT: //如果类型是SCRIPT doc->ScriptLoader()->PreloadURI(uri, pe.charset, pe.elementType); //调用doc对象的ScriptLoader对其进行读取 break; case nsSpeculativeScriptThread::IMAGE: //如果是IMAGE doc->MaybePreLoadImage(uri); //调用MaybePreLoadImage(怪不得图像不一定显示出来呢) break; casensSpeculativeScriptThread::STYLESHEET: { //如果是STYLESHEET类型 nsCOMPtr //还记得前面的nsDummyCSSLoaderOberver么? doc->CSSLoader()->LoadSheet(uri, doc->NodePrincipal(), NS_LossyConvertUTF16toASCII(pe.charset), obs); //调用doc对象的CSSLoader对该uri进行读取 break; } case nsSpeculativeScriptThread::NONE: //如果是空类型(这种情况不应当发生) NS_NOTREACHED("Uninitialized preloadentry?"); //则报错 break; } } } //以上代码主要用来对文档中需要进行预读取的图像,Script代码,CSS格式表等URL进行读取的处理函数。下面,我们来看nsSpeculativeScriptThread的一些具体方法。首先是调用构件的方法,为该线程提供一些线程安全的支持。 NS_IMPL_THREADSAFE_ISUPPORTS1(nsSpeculativeScriptThread,nsIRunnable) //之后是该线程的运行方法Run() NS_IMETHODIMP nsSpeculativeScriptThread::Run() { //判断,预读取行为不能够在主线程上进行 NS_ASSERTION(!NS_IsMainThread(), "Speculativeparsing on the main thread?"); //初始化当前已经解析的数目为0 mNumConsumed = 0; //调用mTokenizer的初始化方法,进行一些初始化 mTokenizer->WillTokenize(PR_FALSE, &mTokenAllocator); //通过对mKeepParsing进行判断,逐步地进行分词,也就是说通过设置这个变量可以打断分词的过程 while (mKeepParsing) { PRBool flushTokens = PR_FALSE; //设置一个布尔变量 nsresult rv = mTokenizer->ConsumeToken(*mScanner, flushTokens); //调用ConsumeToken对词条进行读取处理,注意传递进去的参数,一个为使用的扫描器,一个为刚刚设置的FALSE的变量 if (NS_FAILED(rv)) { //如果分词失败 break; //则跳出while循环 } mNumConsumed += mScanner->Mark(); //获取当前已经分词过的词条数 // TODO Don't pop the tokens. CToken*token; //当mKeepParsing为真并且 while (mKeepParsing && (token =mTokenizer->PopToken())) { //逐token读取 ProcessToken(token); //对token进行处理,后面有详细解析 } } mTokenizer->DidTokenize(PR_FALSE); //分词结束,调用DidTokenize进行一些收尾工作 if (mKeepParsing) { // Ran out of room in this part of thedocument -- flush out the URIs we // gathered so far so we don't end up waitingfor the parser's current // load to finish. //Doucment的当前这部分已经没有剩余空间了---将我们所收集来的URIs传递出去,以便我们不用一直等到parser的当前读取过程完成。 if (!mURIs.IsEmpty()) { //如果当前URIs不为空 FlushURIs(); //传递出去URIs } } { nsAutoLock al(mLock.get()); //获取互斥锁 mCurrentlyParsing = PR_FALSE; //设置标示当前正在处理的变量为FALSE PR_NotifyCondVar(mCVar.get()); //通知正在当前条件变量上等待的线程 } return NS_OK; } //下面是开始进行处理的函数start()方法: nsresult nsSpeculativeScriptThread::StartParsing(nsParser*aParser) { //判断当前线程是否是主要线程,如果是则报错,因为这是次级线程 NS_ASSERTION(NS_IsMainThread(), "Calledon the wrong thread"); //判断当前线程是否已经开始处理,如果是则报错 NS_ASSERTION(!mCurrentlyParsing, "Badrace happening"); if (!aParser->ThreadPool()) { //如果作为参数传递进来的parser根本没有线程池 return NS_OK; //则返回 } //获取参数传递进来的parser的contentSink nsIContentSink *sink = aParser->GetContentSink(); if (!sink) { //如果获取不到 return NS_OK; //则返回 } nsCOMPtr if (!doc) { //如果获取不到 return NS_OK; //则返回 } nsAutoString toScan; CParserContext *context = aParser->PeekContext(); //获取当前parser中位于栈顶的context if (!mLock.get()) { //如果当前没有获取到异步锁,应该说明没有其他线程正在解析 mLock = nsAutoLock::NewLock("nsSpeculativeScriptThread::mLock"); //则申请一个锁 if (!mLock.get()) { //如果申请失败 return NS_ERROR_OUT_OF_MEMORY; //估计是没内存了 } mCVar = PR_NewCondVar(mLock.get()); //申请一个新的条件变量,锁和条件变量需配合使用 if (!mCVar.get()) { //如果申请失败 return NS_ERROR_OUT_OF_MEMORY; } //估计是没内存了 if (!mPreloadedURIs.Init(15)) { //尝试初始化一下mPreloadedURIs的hashtable return NS_ERROR_OUT_OF_MEMORY; //失败估计是没有内存了 } //申请一个新的分词器,使用当前的Context的数据作为初始化参数 mTokenizer = newnsHTMLTokenizer(context->mDTDMode, context->mDocType, context->mParserCommand, 0); if (!mTokenizer) { //如果申请失败 return NS_ERROR_OUT_OF_MEMORY; //估计是没有内存了 } mTokenizer->CopyState(context->mTokenizer); //将该context所对应的Tokenizer中的mFlag,即状态标示变量拷贝过来,拷到现在所新申请的Tokenizer中 context->mScanner->CopyUnusedData(toScan); //并且将改context中未解析完的数据拷贝到toScan变量中 if (toScan.IsEmpty()) { //如果toScan为空,说明要么拷贝失败,要么已经没有未拷贝数据 return NS_OK; } } else if(context == mContext) { //如果获取到了锁,并且当前线程的context等于parser栈顶的Context // Don't parse the same part of the document twice. //避免重复解析 nsScannerIteratorend; context->mScanner->EndReading(end); //获取当前Scanner的结尾位置,并赋值给end nsScannerIterator start; context->mScanner->CurrentPosition(start); //获取当前Scanner的当前位置,并赋值给start if (mNumConsumed >context->mNumConsumed) { // We consumed more the last time we triedspeculatively parsing than we // did the last time we actually parsed. //如果判定成功,说明上次我们读取的数据多于我们上次实际解析了的数据 PRUint32distance = Distance(start, end); //计算start和end之间的距离,并放到distance中 start.advance(PR_MIN(mNumConsumed - context->mNumConsumed,distance)); //将start前进一段距离,这个距离取distance和上次读取数据和上次解析数据之差的最小值 } if (start == end) { //如果start和end相等,说明现在已经解析完毕了,返回即可 // We're at the end of this context's buffer,nothing else to do. return NS_OK; } //将start和end之间的这段数据拷贝至toScan字符串 CopyUnicodeTo(start, end, toScan); }else { // Grab all of the context. //将mScanner中所有未使用的数据拷贝至toScan中 context->mScanner->CopyUnusedData(toScan); if (toScan.IsEmpty()) { // Nothing to parse, don't do anything. //如果此时toScan还为空,那么说明待解析的内容一点也没有了,直接返回 return NS_OK; } } nsCAutoString charset; PRInt32 source; aParser->GetDocumentCharset(charset, source); //获取doucment的字符集 mScanner = new nsScanner(toScan,charset, source); //申请一个新的scanner if (!mScanner) { //如果失败 return NS_ERROR_OUT_OF_MEMORY; //估计是内存不够 } mScanner->SetIncremental(PR_TRUE); //将mScaaner的mIncremental的值设置为TRUE,增量式扫描 mDocument.swap(doc); //交换指针,将doc的值赋值给mDocument mKeepParsing = PR_TRUE; //设置持续解析为TRUE mCurrentlyParsing = PR_TRUE; //设置当前正在解析为TRUE mContext = context; //设置当前线程的mContext为context return aParser->ThreadPool()->Dispatch(this, NS_DISPATCH_NORMAL); //调用组件,将当前线程放入解析器的线程池 } //下面是让解析线程停止的StopParsing()方法。 void nsSpeculativeScriptThread::StopParsing(PRBool/*aFromDocWrite*/) { //判断是否是主线程,如果是则报错 NS_ASSERTION(NS_IsMainThread(), "Can'tstop parsing from another thread"); //如果获取不到当前的锁变量 if (!mLock.get()) { // If we bailed early out of StartParsing,don't do anything. return; //直接返回 } { nsAutoLock al(mLock.get()); //获取锁 mKeepParsing = PR_FALSE; //设置继续解析位为FALSE if (mCurrentlyParsing) { //如果当前正在解析 PR_WaitCondVar(mCVar.get(), PR_INTERVAL_NO_TIMEOUT); //在条件变量上等待 NS_ASSERTION(!mCurrentlyParsing, "Didn'tactually stop parsing?"); //如果当前不是正在进行解析,则报错 } } // The thread is now idle. if (mTerminated) { //如果设置了销毁标示位 // If we're terminated, then we need to ensurethat we release our document // and tokenizer here on the main thread sothat our last reference to them // isn't our alter-ego rescheduled on another thread. //如果销毁了,我们必须要清空我们的分词器,文档引用对象,这样我们就不会错误地引用他们。 mDocument = nsnull; mTokenizer = nsnull; mScanner = nsnull; } else if(mURIs.Length()) { //如果mURIs的长度不为空 // Note: Don't do this if we're terminated. //读取当前已经解析出来的URIs nsPreloadURIs::PreloadURIs(mURIs,this); //清空当前已经解析出来的URIs mURIs.Clear(); } // Note: Currently, we pop the tokens off (see the commentin Run) so this // isn't a problem. If and when we actually use the tokenscreated // off-thread, we'll need to use aFromDocWrite for real. //因为目前我们是将词条们采用出栈的方式进行处理,因此目前这样做(指以上的操作)不会产生什么问题。但是如果我们想使用多线程情况下产生的tokens的话,就需要使用aFromDocWrite了。 } //下面我们来看一下前面用到过的,对词条进行处理的ProcessToken()方法。 void nsSpeculativeScriptThread::ProcessToken(CToken*aToken) { // Only called on the speculative script thread. //这个方法只会在非主线程中被调用 CHTMLToken *token = static_cast //之后获取该词条的类型,同样需要进行一下强制类型转换 switch (static_cast case eToken_start: { //如果是开始型词条,比如 CStartToken *start = static_cast //获取该词条的类型ID nsHTMLTag tag = static_cast //获取该词条的属性总数 PRInt16 attrs = start->GetAttributeCount(); PRInt16 i = 0; //申请几个字符串变量,从变量名应该就能看出是用来存放什么的 nsAutoString src; nsAutoString elementType; nsAutoString charset; nsAutoString href; nsAutoString rel; //申请一个prefetchType类型 PrefetchType ptype = NONE; //下面根据tagID进行判断 switch (tag) { case eHTMLTag_link: //如果是eHTMLTag_link ptype = STYLESHEET; //将前面申请的ptype设置为STYLESHEET,即样式表 break; case eHTMLTag_img: //如果是Tag_img ptype = IMAGE; //将ptype设置为IMAGE break; case eHTMLTag_script: //如果是Tag_script ptype = SCRIPT; //将ptype设置为SCRIPT break; default: //其他情况下不需做这种prefetch的处理 break; } // We currently handle the followingelement/attribute combos : // // //目前我们只处理如下这些元素/属性的集合: // // //也就是说只能够识别以上这些形式的超链接 if (ptype != NONE) { //如果ptype不为空,说明可能需要进行prefetch // loopover all attributes to extract relevant info //循环遍历所有的属性,来取到相关的信息 for(; i < attrs ; ++i) { CAttributeToken *attr = static_cast //首先判断token_type,如果不为属性类型的token,则报错 NS_ASSERTION(attr->GetTokenType()== eToken_attribute, "Weird token"); //如下的语句就是分别对attr的类型进行判断,并且获取其属性值,如果没有该属性,那么该属性所对应的变量就为空 if(attr->GetKey().EqualsLiteral("src")){ src.Assign(attr->GetValue()); } elseif (attr->GetKey().EqualsLiteral("href")) { href.Assign(attr->GetValue()); } elseif (attr->GetKey().EqualsLiteral("rel")) { rel.Assign(attr->GetValue()); } elseif (attr->GetKey().EqualsLiteral("charset")) { charset.Assign(attr->GetValue()); } elseif (attr->GetKey().EqualsLiteral("type")) { elementType.Assign(attr->GetValue()); } IF_FREE(attr,&mTokenAllocator); //前面#define的方法,用来回收attr指针 } // ensurewe have the right kind if it's a link-element if(ptype == STYLESHEET) { //如果是STYLESHEET类型,我们还需要对其进行一下验证和特殊处理,确保后面的操作能够顺利进行 if(rel.EqualsLiteral("stylesheet")){ //判断如果rel的值为stylesheet //将href的值赋值给src,因为src是后面将会用到的一个很重要的变量 src = href; // src is the important variable below } else{ //其他情况下,清空src的值 src.Truncate(); // clear src if wrong kind of link } } // add tolist if we have a valid src //如果我们的src是正确的,那么就将其加入到prefetch的list中 if(!src.IsEmpty()) { //判断src不为空 //将其添加到prefetchlist中 AddToPrefetchList(src, charset,elementType, ptype); } } else { //Irrelevant tag, but pop and free all its attributes in any case //可能是无关的tag,为了保险起见要将其所有的属性值取出 for(; i < attrs ; ++i) { CToken *attr =mTokenizer->PopToken(); //取出Token IF_FREE(attr,&mTokenAllocator); //回收该attr指针 } } break; } default: break; } IF_FREE(aToken, &mTokenAllocator); //回收aToken } //下面就是我们前面所用到的AddToPrefetchList()了,即将需要预载入的URI放入一个清单中。 void nsSpeculativeScriptThread::AddToPrefetchList(const nsAString &src, const nsAString &charset, const nsAString &elementType, PrefetchType type) {//注意传递的参数及类型 PrefetchEntry *pe = mURIs.AppendElement(); //在mURIs中新增一个元素,并返回指向该元素的一个指针 pe->type = type; //用参数type为其赋值 pe->uri = src; //用参数src为其赋值 pe->charset = charset; //用参数charset为其赋值 pe->elementType = elementType; //用参数elementType为其赋值 if (mURIs.Length() == kBatchPrefetchURIs) { //如果mURIs的列表长度到达了我们定义的阈值(目前为5),我们就会调用下面的方法将URIs抛出 FlushURIs(); //清空URIs } } //下面我们看一下上面这个FlushURIs()方法。 void nsSpeculativeScriptThread::FlushURIs() { nsCOMPtr if (!r) { //如果申请失败 return; //则返回 } mURIs.Clear(); //清空mURIs中的数据 NS_DispatchToMainThread(r, NS_DISPATCH_NORMAL); //将r放到主线程(应该是线程池)中去 } //上面就是一些和Parser有关的支持函数了,下面我们来看解析器真正的主类部分: //首先初始化并清空几个值,前两个是字符集转换用的,后一个方前面次级线程用的线程池 nsICharsetAlias*nsParser::sCharsetAliasService = nsnull; nsICharsetConverterManager*nsParser::sCharsetConverterManager = nsnull; nsIThreadPool*nsParser::sSpeculativeThreadPool = nsnull; //首先是nsParser的一个初始化方法,在构造方法中被调用。 /** * Thisgets called when the htmlparser module is initialized. */ // static //这段方法在htmlparser模块进行初始化的时候被调用 nsresult nsParser::Init() { nsresult rv; nsCOMPtr do_GetService(NS_CATEGORYMANAGER_CONTRACTID, &rv); NS_ENSURE_SUCCESS(rv, rv); //判断是否获取成功 nsCOMPtr rv = cm->EnumerateCategory("Parserdata listener", getter_AddRefs(e)); //获取到名为”Parser data listener”的Category,并将e作为其遍历器,此处尚不清楚该目录服务是怎样注册的 NS_ENSURE_SUCCESS(rv, rv); //确保操作成功 //下面申请几个变量 nsCAutoString categoryEntry; nsXPIDLCString contractId; nsCOMPtr //用e进行遍历,并将查询到的key放入entry while(NS_SUCCEEDED(e->GetNext(getter_AddRefs(entry)))) { nsCOMPtr //使用entry进行查询 if (!category) { //如果查询失败,则推出本次循环,继续进行下一个循环 NS_WARNING("Category entry not annsISupportsCString!"); continue; } //通过category获取其对应的Data,并放到categoryEntry中 rv = category->GetData(categoryEntry); NS_ENSURE_SUCCESS(rv, rv); //通过categoryEntry,获取其对应的contractID rv = cm->GetCategoryEntry("Parserdata listener", categoryEntry.get(), getter_Copies(contractId)); NS_ENSURE_SUCCESS(rv, rv); //通过contractID,创建一个新的字节流监听器listener nsCOMPtr do_CreateInstance(contractId.get()); if (listener) { //如果成功创建了listener if (!sParserDataListeners) { //初始化的时候,这个变量应该为空 //创建一个新的数组,存放nsIUnicharStreamListener类型的监听器 sParserDataListeners = newnsCOMArray if (!sParserDataListeners) //如果创建数组失败 return NS_ERROR_OUT_OF_MEMORY; //说明内存不够了 } sParserDataListeners->AppendObject(listener); //将新的listener加入到sParserDataListeners数组中去 } } //可见,listener的数量有很多,并且会通过一个数组来对其进行管理。 nsCOMPtr do_GetService(NS_CHARSETALIAS_CONTRACTID, &rv); NS_ENSURE_SUCCESS(rv, rv); //确保获取操作成功 nsCOMPtr do_GetService(NS_CHARSETCONVERTERMANAGER_CONTRACTID, &rv); NS_ENSURE_SUCCESS(rv, rv); //确保获取操作成功 //使用swap操作,将新申请的两个服务赋值给当前parser的两个相应数据成员中去 charsetAlias.swap(sCharsetAliasService); charsetConverter.swap(sCharsetConverterManager); nsCOMPtr do_CreateInstance(NS_THREADPOOL_CONTRACTID, &rv); NS_ENSURE_SUCCESS(rv, rv); //确保操作成功 rv = threadPool->SetThreadLimit(kSpeculativeThreadLimit); //设置该线程池的最大线程数量(默认15) NS_ENSURE_SUCCESS(rv, rv); //确保操作成功 rv = threadPool->SetIdleThreadLimit(kIdleThreadLimit); //设置该线程池空闲线程的最大数目(默认为0) NS_ENSURE_SUCCESS(rv, rv); //确保操作成功 rv = threadPool->SetIdleThreadTimeout(kIdleThreadTimeout); //设置该线程池空闲线程的超时时间限制(默认为50) NS_ENSURE_SUCCESS(rv, rv); //确保操作成功 threadPool.swap(sSpeculativeThreadPool); //同样通过swap操作,用新申请的变量对本parser的数据成员进行赋值 return NS_OK; } //下面是shutdown()方法,用来关闭parser。 /** * Thisgets called when the htmlparser module is shutdown. */ // static voidnsParser::Shutdown() //这个方法会在parser关闭的时候被调用 { delete sParserDataListeners; //删除相应的sParserDataListeners数组,回收其内存空间 sParserDataListeners = nsnull; //设置本parser的指针为空 NS_IF_RELEASE(sCharsetAliasService); //构件回收方法 NS_IF_RELEASE(sCharsetConverterManager); //构件回收方法 if (sSpeculativeThreadPool) { //如果线程池存在 sSpeculativeThreadPool->Shutdown(); //关闭 NS_RELEASE(sSpeculativeThreadPool); //回收线程池 } } //下面是默认的构造方法,很简单,直接调用写好的方法 /** *default constructor */ nsParser::nsParser() //构造方法 { Initialize(PR_TRUE); //调用Initialize } nsParser::~nsParser() //析构方法 { Cleanup(); //调用Cleanup() } //接下来我们看一下上面构造方法中调用的Initialize(PR_TRUE)方法的实现。 void nsParser::Initialize(PRBool aConstructor) //参数为布尔型的aConstructor { #ifdefNS_DEBUG //如果是调试模式,输出调试信息 if (!gDumpContent) { gDumpContent =PR_GetEnv("PARSER_DUMP_CONTENT") != nsnull; } #endif if (aConstructor) { // Raw pointer //说明用的是普通指针 mParserContext = 0; //设置当前的ParserContext为0,即没有解析上下文 } else { //否则说明用的是nsCOMPtr构件指针 mObserver = nsnull; //初始化mOberver变量 mParserFilter = nsnull; mUnusedInput.Truncate(); //清空待处理的数据 } mContinueEvent = nsnull; //初始化解析结束时触发的时间,默认为空 mCharsetSource = kCharsetUninitialized; //设置字符集来源设置,此处的默认值即为0,在头文件中声明的 mCharset.AssignLiteral("ISO-8859-1"); //为mCharset mInternalState = NS_OK; //目前运行状态,默认为TRUE即正常 mStreamStatus = 0; mCommand = eViewNormal; //注意这里,默认初始化时是ViewNormal模式 mFlags = NS_PARSER_FLAG_OBSERVERS_ENABLED | //位或操作 NS_PARSER_FLAG_PARSER_ENABLED | //结果值应为0x0000000E NS_PARSER_FLAG_CAN_TOKENIZE; MOZ_TIMER_DEBUGLOG(("Reset: Parse Time:nsParser::nsParser(), this=%p\n", this)); MOZ_TIMER_RESET(mParseTime); //重设时间,具体请参考nsTimer.h,以及stopwatch.h MOZ_TIMER_RESET(mDTDTime); //主要作用就是计时,因为Mozilla FireFox是一款注重 MOZ_TIMER_RESET(mTokenizeTime); //人机交互的浏览器,对于任何响应时间都要进行计算 } //下面,是用来清除Context的parser::CleanUp()方法 void nsParser::Cleanup() { #ifdefNS_DEBUG //这些调试的输出信息就不解释了 if (gDumpContent) { if (mSink) { // Sink (HTMLContentSink at this time)supports nsIDebugDumpContent // interface. We can get to the contentmodel through the sink. nsresult result = NS_OK; nsCOMPtr if (NS_SUCCEEDED(result)) { trigger->DumpContentModel(); } } } #endif #ifdefDEBUG if (mParserContext &&mParserContext->mPrevContext) { NS_WARNING("Extra parser contextsstill on the parser stack"); } #endif while (mParserContext) { //如果当前parserContext不为空,则循环进行清除,直到为空为止 CParserContext *pc = mParserContext->mPrevContext; //获取前一个Context delete mParserContext; //删除当前 mParserContext = pc; //将当前的Context赋为前一个Context } // It should not be possible for this flag to be set whenwe are getting // destroyed since this flag implies a pendingnsParserContinueEvent, which // has an owning reference to |this|. //当我们正在被销毁的时候,下面这个判断是不应当发生的,因为下面这个标示位表示有一个nsParserContinueEvent被挂起,并且有一个关联到当前parser的关系。 //位操作,很好理解 NS_ASSERTION(!(mFlags & NS_PARSER_FLAG_PENDING_CONTINUE_EVENT), "bad"); if (mSpeculativeScriptThread) { //如果有当前的次级线程存在 mSpeculativeScriptThread->Terminate(); //销毁该线程 mSpeculativeScriptThread = nsnull; //并将关联指针赋为空 } } //下面有一大段代码,全部使用之前#define的方法进行了定义,是一种很有意思的代码声明方式。这段代码我们这里暂且不进行解析,(有点麻烦,写完后面再回来写这部分吧)有兴趣的读者可以自己去跟踪源代码,很有意思,也是一种统一地复用化写代码的方法。 NS_IMPL_CYCLE_COLLECTION_CLASS(nsParser) NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(nsParser) NS_IMPL_CYCLE_COLLECTION_UNLINK_NSCOMPTR(mDTD) NS_IMPL_CYCLE_COLLECTION_UNLINK_NSCOMPTR(mSink) NS_IMPL_CYCLE_COLLECTION_UNLINK_NSCOMPTR(mObserver) NS_IMPL_CYCLE_COLLECTION_UNLINK_END NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(nsParser) NS_IMPL_CYCLE_COLLECTION_TRAVERSE_NSCOMPTR(mDTD) NS_IMPL_CYCLE_COLLECTION_TRAVERSE_NSCOMPTR(mSink) NS_IMPL_CYCLE_COLLECTION_TRAVERSE_NSCOMPTR(mObserver) CParserContext *pc = tmp->mParserContext; while (pc) { cb.NoteXPCOMChild(pc->mTokenizer); pc = pc->mPrevContext; } NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END NS_IMPL_CYCLE_COLLECTING_ADDREF_AMBIGUOUS(nsParser,nsIParser) NS_IMPL_CYCLE_COLLECTING_RELEASE_AMBIGUOUS(nsParser,nsIParser) NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsParser) NS_INTERFACE_MAP_ENTRY(nsIStreamListener) NS_INTERFACE_MAP_ENTRY(nsIParser) NS_INTERFACE_MAP_ENTRY(nsIRequestObserver) NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIParser) NS_INTERFACE_MAP_END //下面我们继续看后面的代码,如果有印象的读者会记得,前面说过,下面这个PostContinueEvent是在解析结束后触发的事件,且只有可能在两种情况下出现。我们来看一下它的具体实现: // The parser continue eventis posted only if // all of the data to parsehas been passed to ::OnDataAvailable // and the parser has beeninterrupted by the content sink // because the processing oftokens took too long. //parser的触发事件只有在所有的数据都传递到OnDataAvailable时,或者由于token的处理使劲过长导致parser被contentsink打断时才会发生。 nsresult nsParser::PostContinueEvent() { if (!(mFlags &NS_PARSER_FLAG_PENDING_CONTINUE_EVENT)) { //位与操作,即判断二进制低位起第3个bit位是否被设置,如果没设置,那么就不应当出现continueEvent,报错 // If this flag isn't set, then thereshouldn't be a live continue event! NS_ASSERTION(!mContinueEvent, "bad"); // This creates a reference cycle between thisand the event that is // broken when the event fires. //下面这将会创建一段代码,主要是注册事件用的,其中会用到前面解析过的nsParserContinueEvent,该Event中提供了Run()方法,Run方法中又调用了Parser::HandleParserContinueEvent,后面会进行解析 nsCOMPtr //创建事件 if(NS_FAILED(NS_DispatchToCurrentThread(event))){ //将事件分发至主线程 NS_WARNING("failed to dispatch parsercontinuation event"); } else { //成功分发后 mFlags |= NS_PARSER_FLAG_PENDING_CONTINUE_EVENT; //设置Event挂起位 mContinueEvent = event; //将本parser的相应变量mContinueEvent设置为该event } } return NS_OK; } //接下来是两个小方法,用来设置成员变量 NS_IMETHODIMP_(void) nsParser::SetParserFilter(nsIParserFilter* aFilter) //为Parser设置过滤器 { mParserFilter = aFilter; //具体的filter解析我们放在对ParserFilter文件解析时再进行 } NS_IMETHODIMP_(void) nsParser::GetCommand(nsCString&aCommand) //获取Parser的Command { aCommand = mCommandStr; //用当前的command去赋值参数传递过来的地址 } /** * Callthis method once you've created a parser, and want to instruct it * aboutthe command which caused the parser to be constructed. For example, * thisallows us to select a DTD which can do, say, view-source. * *@param aCommand the commandstring to set */ //一旦你创建了parser后,就可以调用这个方法来指示它用某种命令模式进行构建了。比如,我们可以通过这个方法来设置DTD对象的权限和功能,比如view-source模式。 NS_IMETHODIMP_(void) nsParser::SetCommand(const char* aCommand) { mCommandStr.Assign(aCommand); //将该Command赋值给 if (mCommandStr.Equals(kViewSourceCommand)) { //判断是否是”view-source” mCommand = eViewSource; //设置相应命令 } else if(mCommandStr.Equals(kViewFragmentCommand)) { //判断是否是”view-fragment” mCommand = eViewFragment; //设置相应命令 } else { mCommand = eViewNormal; //其他情况设置为默认值 } } //下面这个方法作用和上面这个一样,只不过参数换成了eParserCommand类型而已 /** * Callthis method once you've created a parser, and want to instruct it * aboutthe command which caused the parser to be constructed. For example, * thisallows us to select a DTD which can do, say, view-source. * *@param aParserCommand the commandto set */ NS_IMETHODIMP_(void) nsParser::SetCommand(eParserCommandsaParserCommand) { mCommand = aParserCommand; //因为参数已经是aParserCommands,直接赋值就行了 } 下面的方法,是用来进行一些变量值设置的。 /** * Callthis method once you've created a parser, and want to instruct it * aboutwhat charset to load * *@param aCharset- the charset ofa document *@param aCharsetSource- thesource of the charset */ //一旦你创建了parser之后,并且希望为它设置所要使用的字符集时可以调用这个方法 NS_IMETHODIMP_(void) nsParser::SetDocumentCharset(const nsACString& aCharset, PRInt32aCharsetSource) { mCharset = aCharset; //设置当前的charset mCharsetSource = aCharsetSource; //设置当前的charsetSource if (mParserContext &&mParserContext->mScanner) { //如果当前的解析上下文存在并且对应的扫描器也存在 mParserContext->mScanner->SetDocumentCharset(aCharset,aCharsetSource); //那么需要为扫描器也设置一下字符集charset } } //下面这个方法是设置本parser相应的contentSink的字符集的,很简单不多做解释了 void nsParser::SetSinkCharset(nsACString&aCharset) { if (mSink) { mSink->SetDocumentCharset(aCharset); } } /** * Thismethod gets called in order to set the content * sinkfor this parser to dump nodes to. * *@param nsIContentSink interfacefor node receiver */ NS_IMETHODIMP_(void) //这个方法就是为该parser设置其输出所用的contentSink的 nsParser::SetContentSink(nsIContentSink*aSink) { NS_PRECONDITION(aSink, "sink cannot benull!"); //判断参数给的contentSink不为空 mSink = aSink; //用参数的sink赋值给当前parser的mSink if (mSink) { //如果mSink不为空 mSink->SetParser(this); //同时需要设置一下,确保该sink的对应parser是自己 } } //下面这个方法用来获取本parser所对应的contentsink /** * retrieve the sink set into the parser * @returncurrent sink */ NS_IMETHODIMP_(nsIContentSink*) nsParser::GetContentSink() { return mSink; //只需要返回msink的值 } //下面的这些方法需要注意一下,主要因为其重要性比较高。 //该方法主要用来确定应当使用哪种DTD来进行解析。因为DTD对象才是真正进行文法比对的对象。对于不同格式的HTML,需要使用不同的DTD,或者说需要对DTD进行不同的设置。因此确定应当使用哪种DTD就显得格外重要。 //首先我们来看一下它代码中的注释部分所说的: //确定为这份文档使用哪种DTD模式(同时也就确定了使用哪种layout兼容模式)主要是基于从网络部分所接收的第一块数据块(每一个解析上下文parsercontext都可以拥有它自己的DTD)。目前这还不是最优的解决方案,我们在接收到DOCTYPE之前其实都不需要对其做太多的考虑,并且如果本parser能够设计得更加方便进行正则表达式的判定的话,这个过程可能可以设计得简单得多。 //下面我们就来看看它的代码,首先是一个支持用的方法ParsePS() // Parse the PS productionin the SGML spec (excluding the part dealing // with entity references)starting at theIndex into theBuffer, and // return the first indexafter the end of the production. //这段代码的输入是一个字符串的地址,外带一个整形的地址,实际上就是从参数aIndex的位置开始,在aBuffer中寻找SGML定义语言的起始部分,其中要跳过所有的空格以及\t \n \r等字符,并且跳过所有-- ….--形式的内容(即注释)并返回第一个不符合以上条件这样字符的位置,相当于一个复杂一点的string.trim()函数 staticPRInt32 ParsePS(constnsString& aBuffer, PRInt32 aIndex) { for (;;) { //无限制循环 PRUnichar ch = aBuffer.CharAt(aIndex); //首先找到位于aIndex的字符并进行判断 if ((ch == PRUnichar(' ')) || (ch == PRUnichar('\t'))|| (ch == PRUnichar('\n')) || (ch ==PRUnichar('\r'))) { //如果为以上这四种字符 ++aIndex; //那么将aIndex加一 } else if(ch == PRUnichar('-')) { //否则判断是否为’-’ PRInt32 tmpIndex; if (aBuffer.CharAt(aIndex+1) ==PRUnichar('-') && //如果是的话则在其之后再寻找结尾的--符 kNotFound != (tmpIndex=aBuffer.Find("--",PR_FALSE,aIndex+2,-1))){ aIndex = tmpIndex + 2; } else { return aIndex; //其他情况下则返回该aIndex的值 } } else { returnaIndex; //同样返回aIndex的值 } } } //之后定义了几个需要用到的标示位,这次不用十六进制表示了,采取的是移位的方法使其互不冲突。从其定义的名称就可以大概看出其作用。 #definePARSE_DTD_HAVE_DOCTYPE(1<<0) #definePARSE_DTD_HAVE_PUBLIC_ID(1<<1) #definePARSE_DTD_HAVE_SYSTEM_ID(1<<2) #definePARSE_DTD_HAVE_INTERNAL_SUBSET(1<<3) //下面的方法,就是通过解析相应字段,来判断DTD的类型的代码: // return PR_TRUE on success(includes not present), PR_FALSE on failure staticPRBool ParseDocTypeDecl(const nsString &aBuffer, PRInt32 *aResultFlags, nsString &aPublicID, nsString &aSystemID) { PRBool haveDoctype = PR_FALSE; //初始化设置是否有DTD设置项为FALSE *aResultFlags = 0; //初始化Flag,为空 // Skip through any comments and processing instructions // The PI-skipping is a bit of a hack. PRInt32 theIndex = 0; //初始化解析位置 do { //本循环是跳过所有的注释部分即 你可能感兴趣的:(Mozilla FireFox Gecko内核源代码解析(1.nsParser)) |