分享

现代浏览器的工作原理(中)

 shanyuxiyun 2016-01-11


原文:Tali Garsiel

译文:zzzaquarius

来源:http://blog.csdn.net/zzzaquarius/article/details/6532299


(接上文)


CSS解析 CSS parsing


还记得简介中提到的解析的概念吗,不同于html,css属于上下文无关文法,可以用前面所描述的解析器来解析。Css规范定义了css的词法及语法文法。


看一些例子:


每个符号都由正则表达式定义了词法文法(词汇表):


comment

///*[^*]*/*+([^/*][^*]*/*+)*//

num [0-9]+|[0-9]*”.”[0-9]+

nonascii [/200-/377]

nmstart [_a-z]|{nonascii}|{escape}

nmchar [_a-z0-9-]|{nonascii}|{escape}

name {nmchar}+

ident {nmstart}{nmchar}*


“ident”是识别器的缩写,相当于一个class名,“name”是一个元素id(用“#”引用)。


语法用BNF进行描述:


ruleset

: selector [ ',' S* selector ]*

‘{’ S* declaration [ ';' S* declaration ]* ‘}’ S*

;

selector

: simple_selector [ combinator selector | S+ [ combinator selector ] ]

;

simple_selector

: element_name [ HASH | class | attrib | pseudo ]*

| [ HASH | class | attrib | pseudo ]+

;

class

: ‘.’ IDENT

;

element_name

: IDENT | ‘*’

;

attrib

: ‘[' S* IDENT S* [ [ '=' | INCLUDES | DASHMATCH ] S*

[ IDENT | STRING ] S* ] ‘]’

;

pseudo

: ‘:’ [ IDENT | FUNCTION S* [IDENT S*] ‘)’ ]

;



说明:一个规则集合有这样的结构



div.error , a.error {

color:red;

font-weight:bold;

}


div.error和a.error时选择器,大括号中的内容包含了这条规则集合中的规则,这个结构在下面的定义中正式的定义了:


ruleset

: selector [ ',' S* selector ]*

‘{’ S* declaration [ ';' S* declaration ]* ‘}’ S*

;


这说明,一个规则集合具有一个或是可选个数的多个选择器,这些选择器以逗号和空格(S表示空格)进行分隔。每个规则集合包含大括号及大括号中的一条或多条以分号隔开的声明。声明和选择器在后面进行定义。


Webkit CSS 解析器 Webkit CSS parser


Webkit使用Flex和Bison解析生成器从CSS语法文件中自动生成解析器。回忆一下解析器的介绍,Bison创建一个自底向上的解析器,Firefox使用自顶向下解析器。它们都是将每个css文件解析为样式表对象,每个对象包含css规则,css规则对象包含选择器和声明对象,以及其他一些符合css语法的对象。



图12:解析css


脚本解析 Parsing scripts


本章将介绍Javascript。


处理脚本及样式表的顺序 The order of processing scripts and style sheets


脚本


web的模式是同步的,开发者希望解析到一个script标签时立即解析执行脚本,并阻塞文档的解析直到脚本执行完。如果脚本是外引的,则网络必须先请求到这个资源——这个过程也是同步的,会阻塞文档的解析直到资源被请求到。这个模式保持了很多年,并且在html4及html5中都特别指定了。开发者可以将脚本标识为defer,以使其不阻塞文档解析,并在文档解析结束后执行。Html5增加了标记脚本为异步的选项,以使脚本的解析执行使用另一个线程。


预解析 Speculative parsing


Webkit和Firefox都做了这个优化,当执行脚本时,另一个线程解析剩下的文档,并加载后面需要通过网络加载的资源。这种方式可以使资源并行加载从而使整体速度更快。需要注意的是,预解析并不改变Dom树,它将这个工作留给主解析过程,自己只解析外部资源的引用,比如外部脚本、样式表及图片。


样式表 Style sheets


样式表采用另一种不同的模式。理论上,既然样式表不改变Dom树,也就没有必要停下文档的解析等待它们,然而,存在一个问题,脚本可能在文档的解析过程中请求样式信息,如果样式还没有加载和解析,脚本将得到错误的值,显然这将会导致很多问题,这看起来是个边缘情况,但确实很常见。Firefox在存在样式表还在加载和解析时阻塞所有的脚本,而chrome只在当脚本试图访问某些可能被未加载的样式表所影响的特定的样式属性时才阻塞这些脚本。


渲染树的构造 Render tree construction


当Dom树构建完成时,浏览器开始构建另一棵树——渲染树。渲染树由元素显示序列中的可见元素组成,它是文档的可视化表示,构建这棵树是为了以正确的顺序绘制文档内容。


Firefox将渲染树中的元素称为frames,webkit则用renderer或渲染对象来描述这些元素。


一个渲染对象直到怎么布局及绘制自己及它的children。


RenderObject是Webkit的渲染对象基类,它的定义如下:


class RenderObject{

virtual void layout();

virtual void paint(PaintInfo);

virtual void rect repaintRect();

Node* node;

//the DOM node

RenderStyle* style;

// the computed style

RenderLayer* containgLayer;

//the containing z-index layer

}


每个渲染对象用一个和该节点的css盒模型相对应的矩形区域来表示,正如css2所描述的那样,它包含诸如宽、高和位置之类的几何信息。盒模型的类型受该节点相关的display样式属性的影响(参考样式计算章节)。下面的webkit代码说明了如何根据display属性决定某个节点创建何种类型的渲染对象。


RenderObject* RenderObject::createObject(Node* node, RenderStyle* style)

{

Document* doc = node->document();

RenderArena* arena = doc->renderArena();

RenderObject* o = 0;

switch (style->display()) {

case NONE:

break;

case INLINE:

o = new (arena) RenderInline(node);

break;

case BLOCK:

o = new (arena) RenderBlock(node);

break;

case INLINE_BLOCK:

o = new (arena) RenderBlock(node);

break;

case LIST_ITEM:

o = new (arena) RenderListItem(node);

break;

}

return o;

}


元素的类型也需要考虑,例如,表单控件和表格带有特殊的框架。


在webkit中,如果一个元素想创建一个特殊的渲染对象,它需要复写“createRenderer”方法,使渲染对象指向不包含几何信息的样式对象。


渲染树和Dom树的关系 The render tree relation to the DOM tree


渲染对象和Dom元素相对应,但这种对应关系不是一对一的,不可见的Dom元素不会被插入渲染树,例如head元素。另外,display属性为none的元素也不会在渲染树中出现(visibility属性为hidden的元素将出现在渲染树中)。


还有一些Dom元素对应几个可见对象,它们一般是一些具有复杂结构的元素,无法用一个矩形来描述。例如,select元素有三个渲染对象——一个显示区域、一个下拉列表及一个按钮。同样,当文本因为宽度不够而折行时,新行将作为额外的渲染元素被添加。另一个多个渲染对象的例子是不规范的html,根据css规范,一个行内元素只能仅包含行内元素或仅包含块状元素,在存在混合内容时,将会创建匿名的块状渲染对象包裹住行内元素。


一些渲染对象和所对应的Dom节点不在树上相同的位置,例如,浮动和绝对定位的元素在文本流之外,在两棵树上的位置不同,渲染树上标识出真实的结构,并用一个占位结构标识出它们原来的位置。



图12:渲染树及对应的Dom树


创建树的流程 The flow of constructing the tree


Firefox中,表述为一个监听Dom更新的监听器,将frame的创建委派给Frame Constructor,这个构建器计算样式(参看样式计算)并创建一个frame。


Webkit中,计算样式并生成渲染对象的过程称为attachment,每个Dom节点有一个attach方法,attachment的过程是同步的,调用新节点的attach方法将节点插入到Dom树中。


处理html和body标签将构建渲染树的根,这个根渲染对象对应被css规范称为containing block的元素——包含了其他所有块元素的顶级块元素。它的大小就是viewport——浏览器窗口的显示区域,Firefox称它为viewPortFrame,webkit称为RenderView,这个就是文档所指向的渲染对象,树中其他的部分都将作为一个插入的Dom节点被创建。


样式计算 Style Computation


创建渲染树需要计算出每个渲染对象的可视属性,这可以通过计算每个元素的样式属性得到。


样式包括各种来源的样式表,行内样式元素及html中的可视化属性(例如bgcolor),可视化属性转化为css样式属性。


样式表来源于浏览器默认样式表,及页面作者和用户提供的样式表——有些样式是浏览器用户提供的(浏览器允许用户定义喜欢的样式,例如,在Firefox中,可以通过在Firefox Profile目录下放置样式表实现)。


计算样式的一些困难:


1. 样式数据是非常大的结构,保存大量的样式属性会带来内存问题


2. 如果不进行优化,找到每个元素匹配的规则会导致性能问题,为每个元素查找匹配的规则都需要遍历整个规则表,这个过程有很大的工作量。选择符可能有复杂的结构,匹配过程如果沿着一条开始看似正确,后来却被证明是无用的路径,则必须去尝试另一条路径。


例如,下面这个复杂选择符


div div div div{…}


这意味着规则应用到三个div的后代div元素,选择树上一条特定的路径去检查,这可能需要遍历节点树,最后却发现它只是两个div的后代,并不使用该规则,然后则需要沿着另一条路径去尝试


3. 应用规则涉及非常复杂的级联,它们定义了规则的层次


我们来看一下浏览器如何处理这些问题:


共享样式数据


webkit节点引用样式对象(渲染样式),某些情况下,这些对象可以被节点间共享,这些节点需要是兄弟或是表兄弟节点,并且:


  • ▲这些元素必须处于相同的鼠标状态(比如不能一个处于hover,而另一个不是)

  • ▲不能有元素具有id

  • ▲标签名必须匹配

  • ▲class属性必须匹配

  • ▲对应的属性必须相同

  • ▲链接状态必须匹配

  • ▲焦点状态必须匹配

  • ▲不能有元素被属性选择器影响

  • ▲元素不能有行内样式属性

  • ▲不能有生效的兄弟选择器,webcore在任何兄弟选择器相遇时只是简单的抛出一个全局转换,并且在它们显示时使整个文档的样式共享失效,这些包括+选择器和类似:first-child和:last-child这样的选择器。


Firefox规则树 Firefox rule tree


Firefox用两个树用来简化样式计算-规则树和样式上下文树,webkit也有样式对象,但它们并没有存储在类似样式上下文树这样的树中,只是由Dom节点指向其相关的样式。



图14:Firefox样式上下文树


样式上下文包含最终值,这些值是通过以正确顺序应用所有匹配的规则,并将它们由逻辑值转换为具体的值,例如,如果逻辑值为屏幕的百分比,则通过计算将其转化为绝对单位。样式树的使用确实很巧妙,它使得在节点中共享的这些值不需要被多次计算,同时也节省了存储空间。


所有匹配的规则都存储在规则树中,一条路径中的底层节点拥有最高的优先级,这棵树包含了所找到的 所有规则匹配的路径(译注:可以取巧理解为每条路径对应一个节点,路径上包含了该节点所匹配的所有规则)。规则树并不是一开始就为所有节点进行计算,而是 在某个节点需要计算样式时,才进行相应的计算并将计算后的路径添加到树中。


我们将树上的路径看成辞典中的单词,假如已经计算出了如下的规则树:


假如需要为内容树中的另一个节点匹配规则,现在知道匹配的规则(以正确的顺序)为B-E-I,因为我们已经计算出了路径A-B-E-I-L,所以树上已经存在了这条路径,剩下的工作就很少了。


现在来看一下树如何保存。


结构化


样式上下文按结构划分,这些结构包括类似border或color这样的特定分类的样式信息。一个结构中的所有特性不是继承的就是非继承的,对继承的特性,除非元素自身有定义,否则就从它的parent继承。非继承的特性(称为reset特性)如果没有定义,则使用默认的值。


样式上下文树缓存完整的结构(包括计算后的值),这样,如果底层节点没有为一个结构提供定义,则使用上层节点缓存的结构。


使用规则树计算样式上下文


当为一个特定的元素计算样式时,首先计算出规则树中的一条路径,或是使用已经存在的一条,然后使 用路径中的规则去填充新的样式上下文,从样式的底层节点开始,它具有最高优先级(通常是最特定的选择器),遍历规则树,直到填满结构。如果在那个规则节点 没有定义所需的结构规则,则沿着路径向上,直到找到该结构规则。


如果最终没有找到该结构的任何规则定义,那么如果这个结构是继承型的,则找到其在内容树中的parent的结构,这种情况下,我们也成功的共享了结构;如果这个结构是reset型的,则使用默认的值。


如果特定的节点添加了值,那么需要做一些额外的计算以将其转换为实际值,然后在树上的节点缓存该值,使它的children可以使用。


当一个元素和它的一个兄弟元素指向同一个树节点时,完整的样式上下文可以被它们共享。


来看一个例子:假设有下面这段html


this is a

big error

this is also a

very big error

error

another error



以及下面这些规则


1. div {margin:5px;color:black}

2. .err {color:red}

3. .big {margin-top:3px}

4. div span {margin-bottom:4px}

5. #div1 {color:blue}

6. #div2 {color:green}



简化下问题,我们只填充两个结构——color和margin,color结构只包含一个成员-颜色,margin结构包含四边。


生成的规则树如下(节点名:指向的规则)



上下文树如下(节点名:指向的规则节点)



假设我们解析html,遇到第二个div标签,我们需要为这个节点创建样式上下文,并填充它的样式结构。


我们进行规则匹配,找到这个div匹配的规则为1、2、6,我们发现规则树上已经存在了一条我们可以使用的路径1、2,我们只需为规则6新增一个节点添加到下面(就是规则树中的F)。


然后创建一个样式上下文并将其放到上下文树中,新的样式上下文将指向规则树中的节点F。


现在我们需要填充这个样式上下文,先从填充margin结构开始,既然最后一个规则节点没有添加margin结构,沿着路径向上,直到找到缓存的前面插入节点计算出的结构,我们发现B是最近的指定margin值的节点。因为已经有了color结构的定义,所以不能使用缓存的结构,既然color只有一个属性,也就不需要沿着路径向上填充其他属性。计算出最终值(将字符串转换为RGB等),并缓存计算后的结构。


第二个span元素更简单,进行规则匹配后发现它指向规则G,和前一个span一样,既然有兄弟节点指向同一个节点,就可以共享完整的样式上下文,只需指向前一个span的上下文。


因为结构中包含继承自parent的规则,上下文树做了缓存(color特性是继承来的,但Firefox将其视为reset并在规则树中缓存)。


例如,如果我们为一个paragraph的文字添加规则:


p {font-family:Verdana;font size:10px;font-weight:bold}


那么这个p在内容树中的子节点div,会共享和它parent一样的font结构,这种情况发生在没有为这个div指定font规则时。


Webkit中,并没有规则树,匹配的声明会被遍历四次,先是应用非important的高优先级属性(之所以先应用这些属性,是因为其他的依赖于它们-比如display),其次是高优先级important的,接着是一般优先级非important的,最后是一般优先级important的规则。这样,出现多次的属性将被按照正确的级联顺序进行处理,最后一个生效。


总结一下,共享样式对象(结构中完整或部分内容)解决了问题1和3,Firefox的规则树帮助以正确的顺序应用规则。


对规则进行处理以简化匹配过程


样式规则有几个来源:


· 外部样式表或style标签内的css规则


· 行内样式属性


· html可视化属性(映射为相应的样式规则)


后面两个很容易匹配到元素,因为它们所拥有的样式属性和html属性可以将元素作为key进行映射。


就像前面问题2所提到的,css的规则匹配可能很狡猾,为了解决这个问题,可以先对规则进行处理,以使其更容易被访问。


解析完样式表之后,规则会根据选择符添加一些hash映射,映射可以是根据id、class、标签名或是任何不属于这些分类的综合映射。如果选择符为id,规则将被添加到id映射,如果是class,则被添加到class映射,等等。


这个处理是匹配规则更容易,不需要查看每个声明,我们能从映射中找到一个元素的相关规则,这个优化使在进行规则匹配时减少了95+%的工作量。


来看下面的样式规则:


p.error {color:red}


#messageDiv {height:50px}


div {margin:5px}


第一条规则将被插入class映射,第二条插入id映射,第三条是标签映射。


下面这个html片段:


an error occurred

this is a message


我们首先找到p元素对应的规则,class映射将包含一个“error”的key,找到p.error的规则,div在id映射和标签映射中都有相关的规则,剩下的工作就是找出这些由key对应的规则中哪些确实是正确匹配的。


例如,如果div的规则是


table div {margin:5px}


这也是标签映射产生的,因为key是最右边的选择符,但它并不匹配这里的div元素,因为这里的div没有table祖先。


Webkit和Firefox都会做这个处理。


以正确的级联顺序应用规则


样式对象拥有对应所有可见属性的属性,如果特性没有被任何匹配的规则所定义,那么一些特性可以从parent的样式对象中继承,另外一些使用默认值。


这个问题的产生是因为存在不止一处的定义,这里用级联顺序解决这个问题。


样式表的级联顺序


一个样式属性的声明可能在几个样式表中出现,或是在一个样式表中出现多次,因此,应用规则的顺序至关重要,这个顺序就是级联顺序。根据css2的规范,级联顺序为(从低到高):


1. 浏览器声明

2. 用户声明

3. 作者的一般声明

4. 作者的important声明

5. 用户important声明


浏览器声明是最不重要的,用户只有在声明被标记为important时才会覆盖作者的声明。具有同等级别的声明将根据specifity以及它们被定义时的顺序进行排序。Html可视化属性将被转换为匹配的css声明,它们被视为最低优先级的作者规则。


Specifity


Css2规范中定义的选择符specifity如下:


  • · 如果声明来自style属性,而不是一个选择器的规则,则计1,否则计0(=a)

  • · 计算选择器中id属性的数量(=b)

  • · 计算选择器中class及伪类的数量(=c)

  • · 计算选择器中元素名及伪元素的数量(=d)


连接a-b-c-d四个数量(用一个大基数的计算系统)将得到specifity。这里使用的基数由分类中最高的基数定义。例如,如果a为14,可以使用16进制。不同情况下,a为17时,则需要使用阿拉伯数字17作为基数,这种情况可能在这个选择符时发生html body div div …(选择符中有17个标签,一般不太可能)。


一些例子:


* {}

/* a=0 b=0 c=0 d=0 -> specificity = 0,0,0,0 */

li {}

/* a=0 b=0 c=0 d=1 -> specificity = 0,0,0,1 */

li:first-line {}

/* a=0 b=0 c=0 d=2 -> specificity = 0,0,0,2 */

ul li {}

/* a=0 b=0 c=0 d=2 -> specificity = 0,0,0,2 */

ul ol+li {}

/* a=0 b=0 c=0 d=3 -> specificity = 0,0,0,3 */

h1 + *[rel=up]{}

/* a=0 b=0 c=1 d=1 -> specificity = 0,0,1,1 */

ul ol li.red {}

/* a=0 b=0 c=1 d=3 -> specificity = 0,0,1,3 */

li.red.level {}

/* a=0 b=0 c=2 d=1 -> specificity = 0,0,2,1 */

#x34y {} /* a=0 b=1 c=0 d=0 -> specificity = 0,1,0,0 */

style=”'

/* a=1 b=0 c=0 d=0 -> specificity = 1,0,0,0 */


规则排序


规则匹配后,需要根据级联顺序对规则进行排序,webkit先将小列表用冒泡排序,再将它们合并为一个大列表,webkit通过为规则复写“>”操作来执行排序:


static bool operator >(CSSRuleData& r1, CSSRuleData& r2)

{

int spec1 = r1.selector()->specificity();

int spec2 = r2.selector()->specificity();

return (spec1 == spec2) : r1.position() > r2.position() : spec1 > spec2;

}


逐步处理 Gradual process


webkit使用一个标志位标识所有顶层样式表都已加载,如果在attch时样式没有完全加载,则放置占位符,并在文档中标记,一旦样式表完成加载就重新进行计算。


布局 Layout


当渲染对象被创建并添加到树中,它们并没有位置和大小,计算这些值的过程称为layout或reflow。


Html使用基于流的布局模型,意味着大部分时间,可以以单一的途径进行几何计算。流中靠后的元素并不会影响前面元素的几何特性,所以布局可以在文档中从右向左、自上而下的进行。也存在一些例外,比如html tables。


坐标系统相对于根frame,使用top和left坐标。


布局是一个递归的过程,由根渲染对象开始,它对应html文档元素,布局继续递归的通过一些或所有的frame层级,为每个需要几何信息的渲染对象进行计算。


根渲染对象的位置是0,0,它的大小是viewport-浏览器窗口的可见部分。


所有的渲染对象都有一个layout或reflow方法,每个渲染对象调用需要布局的children的layout方法。


Dirty bit 系统


为了不因为每个小变化都全部重新布局,浏览器使用一个dirty bit系统,一个渲染对象发生了变化或是被添加了,就标记它及它的children为dirty-需要layout。存在两个标识-dirty及children are dirty,children are dirty说明即使这个渲染对象可能没问题,但它至少有一个child需要layout。


全局和增量 layout


当layout在整棵渲染树触发时,称为全局layout,这可能在下面这些情况下发生:


1. 一个全局的样式改变影响所有的渲染对象,比如字号的改变


2. 窗口resize


layout也可以是增量的,这样只有标志为dirty的渲染对象会重新布局(也将导致一些额外的布局)。增量 layout会在渲染对象dirty时异步触发,例如,当网络接收到新的内容并添加到Dom树后,新的渲染对象会添加到渲染树中。


图20:增量 layout


异步和同步layout


增量layout的过程是异步的,Firefox为增量layout生成了reflow队列,以及一个调度执行这些批处理命令。Webkit也有一个计时器用来执行增量layout-遍历树,为dirty状态的渲染对象重新布局。


另外,当脚本请求样式信息时,例如“offsetHeight”,会同步的触发增量布局。


全局的layout一般都是同步触发。


有些时候,layout会被作为一个初始layout之后的回调,比如滑动条的滑动。


优化


当一个layout因为resize或是渲染位置改变(并不是大小改变)而触发时,渲染对象的大小将会从缓存中读取,而不会重新计算。


一般情况下,如果只有子树发生改变,则layout并不从根开始。这种情况发生在,变化发生在元素自身并且不影响它周围元素,例如,将文本插入文本域(否则,每次击键都将触发从根开始的重排)。


layout过程


layout一般有下面这几个部分:


1. parent渲染对象决定它的宽度


2. parent渲染对象读取chilidren,并:


1. 放置child渲染对象(设置它的x和y)


2. 在需要时(它们当前为dirty或是处于全局layout或者其他原因)调用child渲染对象的layout,这将计算child的高度


3. parent渲染对象使用child渲染对象的累积高度,以及margin和padding的高度来设置自己的高度-这将被parent渲染对象的parent使用


4. 将dirty标识设置为false


Firefox使用一个“state”对象(nsHTMLReflowState)做为参数去布局(firefox称为reflow),state包含parent的宽度及其他内容。


Firefox布局的输出是一个“metrics”对象(nsHTMLReflowMetrics)。它包括渲染对象计算出的高度。


宽度计算


渲染对象的宽度使用容器的宽度、渲染对象样式中的宽度及margin、border进行计算。例如,下面这个div的宽度:



webkit中宽度的计算过程是(RenderBox类的calcWidth方法):


· 容器的宽度是容器的可用宽度和0中的最大值,这里的可用宽度为:contentWidth=clientWidth()-paddingLeft()-paddingRight(),clientWidth和clientHeight代表一个对象内部的不包括border和滑动条的大小


· 元素的宽度指样式属性width的值,它可以通过计算容器的百分比得到一个绝对值


· 加上水平方向上的border和padding


到这里是最佳宽度的计算过程,现在计算宽度的最大值和最小值,如果最佳宽度大于最大宽度则使用最大宽度,如果小于最小宽度则使用最小宽度。最后缓存这个值,当需要layout但宽度未改变时使用。


Line breaking


当一个渲染对象在布局过程中需要折行时,则暂停并告诉它的parent它需要折行,parent将创建额外的渲染对象并调用它们的layout。


绘制 Painting


绘制阶段,遍历渲染树并调用渲染对象的paint方法将它们的内容显示在屏幕上,绘制使用UI基础组件,这在UI的章节有更多的介绍。


全局和增量


和布局一样,绘制也可以是全局的-绘制完整的树-或增量的。在增量的绘制过程中,一些渲染对象以不影响整棵树的方式改变,改变的渲染对象使其在屏幕上的矩形区域失效,这将导致操作系统将其看作dirty区域,并产生一个paint事件,操作系统很巧妙的处理这个过程,并将多个区域合并为一个。Chrome中,这个过程更复杂些,因为渲染对象在不同的进程中,而不是在主进程中。Chrome在一定程度上模拟操作系统的行为,表现为监听事件并派发消息给渲染根,在树中查找到相关的渲染对象,重绘这个对象(往往还包括它的children)。


    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多