浏览器家园·资讯

展开

用了十几年的浏览器,你却只会浏览

编辑:浏览器知识

来源:腾讯技术工程

导语:本文从市面主流的浏览器及相应的内核引擎开始,介绍了Chromium为代表的浏览器架构及Blink内核的功能架构。Chromium为多进程架构,用户从启动运行浏览器后,先后经过页面导航、渲染、资源加载、样式计算、布局、绘制、合成到栅格化,最后完成GPU展示。而页面渲染完成后,浏览器如何响应页面操作事件也进行了深入的介绍。良心推荐!

本文第二至五部分内容根据 Mariko Kosaka 的英文原版《Inside look at modern web browser》(见参考文献),进行翻译、理解、总结提炼、条理化、加入应用示例、进行相关知识补充扩展而来。

用了十几年的浏览器,你却只会浏览

一、浏览器概论

浏览器经历了很多年的发展,浏览器引擎也在不停地迭代和演进。从PC时代到移动端,以独立浏览器的形态还是以系统WebView组件内嵌的形态存在,在互联网的生态系统中一直扮演着重要的角色。了解浏览器及其原理可以让我们打开另一个世界。

1. 浏览器引擎

以下是市面留存的主流浏览器的引擎介绍。

1.1 浏览器引擎

1) Chromium:基于webkit,08年开始作为Chrome的引擎,Chromium浏览器是Chrome的实验版,实验新特性。

2) Webkit2:2010年随OS X Lion一起面世。WebCore层面实现进程隔离与Google的沙箱设计存在冲突。

3) Blink:基于Webkit2分支,13年谷歌开始作为Chrome 28的引擎集成在Chromium浏览器里。Android的WebView同样基于Webkit2。

1.2 微软浏览器

目前PC场景操作系统仍是windows一统天下,对桌面用户来说,虽然IE的市场份额在下降,但是IE曾经也风光过。IE内核以Trident为主,最新的Edge也兼容了Chromium内核。

Microsoft Edge:内核为:EDGE,Windows 10默认浏览器,不能单独下载安装。兼容Chromium内核,同时保留EDGE内核来兼容企业网站

2. 浏览器架构

目前chromium浏览器的架构主要由下以几个部分构成。

用了十几年的浏览器,你却只会浏览

以下为架构的介绍:

JavaScriptCore(用于Safari)


WebCore


相关资料

2.1 多进程架构

用了十几年的浏览器,你却只会浏览

图片引自chromium-design-doc

https://www.chromium.org/developers/design-documents/multi-process-architecture

2.1.1 Chromium多进程架构

早期的web浏览器页面行为不当、浏览器错误、浏览器插件错误都会引起整个浏览器或当前运行的选项卡关闭。因此将chromium应用程序放在相互隔离的独立的进程中:

2.1.2 架构组成

2.1.3 渲染过程管理

2.1.4 运行流程

2.1.5 插件扩展

第三方编写的NPAPI插件因存在不稳定,同时需控制对系统资源的访问,在各自独立的进程中运行,与渲染器分开。

插件设计文档:https://www.chromium.org/developers/design-documents/plugin-architecture

2.2 Webkit(Blink)架构

Blink是Web平台的渲染引擎,实现了浏览器选项卡中呈现的内容:

2.2.1 Blink的运行流程

多进程架构,有一个浏览器进程和N个沙盒渲染器进程,Blink在沙盒渲染中运行。浏览器选项卡、iframe可共享同个渲染器进程。

沙箱运行:在沙箱中,须通过父浏览器进程来调度使用资源(文件访问、网络、音视频播放、用户配置文件读取(cookie,密码)等。Blink将浏览器进程抽象为一组服务,使用Mojo与服务、浏览器进程交互。

2.2.2 渲染进程中的线程

跨线程通信:使用PostTask API,不鼓励共享内存编程除非性能原因。

用了十几年的浏览器,你却只会浏览

2.2.3 Blink的运行和退出

2.2.4 Blink的项目代码结构

用了十几年的浏览器,你却只会浏览

2.2.5 platform内部构成

1) WTF:统一编码原语,如WTF::Vector, WTF::HashSet, WTF::HashMap, WTF::String and WTF::AtomicString来代替std:vector 等。

2) 内存管理:a. PartitionAlloc b.Oilpan(Blink GC) c.malloc/free/new/delete

3) 任务调度:为提高渲染引擎的响应,应执行异步。所有任务都应发布到Blink Scheduler任务队列,指定正确类型并设置优先级,以使得能巧妙地安排任务。

4) Page/Frame/Document/ExecutionContext/DOMWindow

分别对应选项卡、iframe、window.document、主线程和工作线程上下文、JavaScript中的窗口对象。

渲染进程中各种数量关系

5) 进程外iframe

站点隔离:为每个站点创建一个渲染器进程(相同一二级域名)。跨站点由两个渲染器托管。

6) 分离的iframe/文件

doc = iframe.contentDocument
iframe.remove() //iframe 与 dom 树分离
doc.createElement('div'); //仍可在分离的框架上运行脚本

左滑可查看完整代码,下同

7) Web IDL绑定

8) V8

关系:一个frame = N个窗口对象 = 用于N个world。Context对应该窗口对象

V8的API低级且难以使用,在platform/bindings中提供很多V8 API辅助类。每个C++ DOM对象,如Node都有其对应的V8包装器。V8包装器对应的C++ DOM对象具有强引用。C++ DOM对象只对V8包装器弱引用。

用了十几年的浏览器,你却只会浏览

2.3 V8

V8是Google的开源高性能JavaScript和WebAssembly引擎,用C++编写,它实现ECMAScript和WebAssembly,可独立运行或嵌入到任何C++应用程序中,如Chrome和Node.js。

用了十几年的浏览器,你却只会浏览

相关资料ECMAScript:https://tc39.es/ecma262/ WebAssembly:https://webassembly.github.io/spec/core/

二、Chrome的多进程架构

注意:以下内容根据 Mariko Kosaka 的英文原版《Inside look at modern web browser》(见参考文献),进行翻译、理解、总结提炼、条理化、加入应用示例、进行相关知识补充扩展而来。

1. 背景:计算机的核心是CPU和GPU

CPU:Center Processing Unit,同时支持并行、串行操作,需很强通用性处理不同数据类型、要支持复杂通用逻辑判断,需引入大量分支和中断处理,结构异常复杂。

GPU:Graphics Processing Uint,专为执行图形渲染必须的复杂的数学和几何计算而设计。

用了十几年的浏览器,你却只会浏览

图片引自Mariko Kosaka的《Inside look at modern web browser》

用了十几年的浏览器,你却只会浏览

图片引自Mariko Kosaka的《Inside look at modern web browser》

用了十几年的浏览器,你却只会浏览

三层计算机体系结构

图片引自Mariko Kosaka的《Inside look at modern web browser》

2. 基础:在Process和Thread执行程序

启动应用程序时,创建一个进程,并提供”slab”内存,所有应用程序状态保存在该专用内存中,关闭程序时,系统释放内存。

应用程序可能会创建多个线程完成工作任务。

用了十几年的浏览器,你却只会浏览

图片引自Mariko Kosaka的《Inside look at modern web browser》

用了十几年的浏览器,你却只会浏览

图片引自Mariko Kosaka的《Inside look at modern web browser》

3. 浏览器架构

浏览器架构没有统一标准规范,不同浏览器可能使用不同线程或多个不同进程来构建web。少数线程间通过IPC通信。

3.1 不同浏览器实现的体系结构

用了十几年的浏览器,你却只会浏览

图片引自Mariko Kosaka的《Inside look at modern web browser》

3.2 Chrome的多进程架构

用了十几年的浏览器,你却只会浏览

图片引自Mariko Kosaka的《Inside look at modern web browser》

4. 不同进程作用

用了十几年的浏览器,你却只会浏览

图片引自Mariko Kosaka的《Inside look at modern web browser》

5. 多进程架构

优点:

用了十几年的浏览器,你却只会浏览

图片引自Mariko Kosaka的《Inside look at modern web browser》

6. 服务化 – 节省更多内存

浏览器程序中相同的功能方法,正在将浏览器的每个部分作为一项服务运行,可以轻松拆分为不同进程或聚合成一个进程。

当Chrome在强大的硬件上运行时,它可能会将每个服务拆分为不同的流程,从而提供更高的稳定性,但如果它位于资源约束设备上,Chrome会将服务整合到一个流程中,从而节省内存占用。

Android的平台上已经使用了类似的方法来整合流程以减少内存使用。

用了十几年的浏览器,你却只会浏览

图片引自Mariko Kosaka的《Inside look at modern web browser》

7. 给Iframe分配单独渲染进程 – 站点隔离

站点隔离:因不同站点之间共享内存空间会存在同源策略绕过(Meltdown and Spectre)安全问题:https://blog.csdn.net/wlmnzf/article/details/79319509%22%20/t%20%22_blank 。因此为每个跨网站iframe运行单独的渲染器进程。

站点隔离难点:从根本上改变iframe的通信方式,包括ctrl+F查找、打开devtools等需在不同渲染器进程访问。【重大版本】。

用了十几年的浏览器,你却只会浏览

图片引自Mariko Kosaka的《Inside look at modern web browser》

三、页面导航过程

1. 浏览器进程运行

多进程架构启动多个进程处理不同的任务。选项卡外部的所有内容都由浏览器进程处理(包含UI线程、网络线程、存储线程)。在地址栏输入url时,由浏览器进程的UI线程处理。

用了十几年的浏览器,你却只会浏览

图片引自Mariko Kosaka的《Inside look at modern web browser》

2. 处理输入

当用户开始输入地址栏时,UI线程需判断是搜索查询还是URL。

3. 开始导航

用户点击进入时:

1) UI线程启动网络调用以获取站点内容,选项卡加载转圈

2) 网络线程通过DNS查找域名对应IP及建立http连接

3) 网络线程接收处理301重定向头。网络线程与请求重定向的UI线程通信,启动另一个URL请求

Service Worker

Service Worker注册后,保留其范围为参考。当导航时,网络线程根据注册的范围检查域名,若url已注册Service Worker,UI线程找到渲染进程执行ServiceWorker代码,从缓存加载数据或从网络加载新资源。生命周期见:https://developers.google.com/web/fundamentals/primers/service-workers/lifecycle

导航预加载

如果ServiceWorker最终决定从网络请求数据,浏览器进程与渲染进程间的往返可能导致延时,通过与ServiceWorker启动并行加载资源加速来减少延时,允许标记这些请求,允许服务器决定为这些请求发送不同的内容。

用了十几年的浏览器,你却只会浏览

图片引自上面ServiceWorker的生命周期

4. 读取响应结果

4.1 确定文件MIME类型

网络线程查看流的前几个字节,响应头中Content-Type头确定MIME数据类型。因此数据可能丢失,因此用MIME嗅探方式来查看资源。https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types

4.2 处理不同MIME文件

响应文件是HTML,则将数据传递给渲染器进程。如果为.zip或其他文件则将数据传递给下载管理器。

4.3 安全检查

5. 查找渲染进程

所有检查完成后,网络线程告知UI线程数据已准备就绪,UI线程找到渲染进程以继续渲染网页。

由于网络请求可能需要几百毫秒才能得到响应,为加速此过程,在开始导航网络线程发送url请求时,已经主动进行查找、启动渲染进程,数据接收完成后,渲染进程已备用。

6. 提交导航

现在数据和渲染器进程已准备就绪,IPC将从浏览器进程发送到渲染进程以提交导航。渲染进程确认提交完成,导航完成。文档加载开始。

1、UI更新:地址栏更新、安全指示器、站点设置UI会反映新页面站点信息

2、选项卡的会话历史记录更新(前进/后退),为便于关闭浏览器后恢复,历史记录到磁盘

7. 初始化 load complete

提交导航后,渲染器进程将继续加载资源并呈现页面,一旦渲染器进程“完成”(onload事件在所有帧上触发执行完成后)渲染,它就会将IPC发送回浏览器进程。

UI线程停止选项卡的加载转圈。

8. 导航到其他站点

导航完成后,再次将不同的URL放到地址栏导航,浏览器会检查当前渲染网站的beforeunload事件。如有设置导航或关闭选项卡时发出警报“离开这个网站吗?” 包含JavaScript代码的选项卡内的所有内容都由渲染进程处理。

渲染进程导航操作单击链接或客户端JavaScript已运行window.location = “https://newsite.com“ ,过程与流程器进程启动导航过程相同,不同点在于导航请求是从渲染进程启动到浏览器进程。

页面生命周期:https://developers.google.com/web/updates/2018/07/page-lifecycle-api#overview_of_page_lifecycle_states_and_events

用了十几年的浏览器,你却只会浏览

图片引自上面的页面生命周期

四、页面渲染

1. 渲染进程处理页面内容

渲染进程负责选项卡内发生的所有事情。在渲染器进程中

用了十几年的浏览器,你却只会浏览

图片引自Mariko Kosaka的《Inside look at modern web browser》

2. 解析

2.1 构建DOM

当渲染进程接收提交的导航消息和HTML数据,主线程开始解析文本串(HTML),使之成为一个DOM。解析中遇到html能优雅容错。

DOM:浏览器页面内部表示,提供给开发人员通过JS与DOM交互的数据结构和API。

用了十几年的浏览器,你却只会浏览

图片引自Mariko Kosaka的《Inside look at modern web browser》

2.2 子资源加载

网站通常使用图像,CSS和JavaScript等外部资源,需要从网络或缓存加载。在解析构建DOM时,主线程可以逐个请求它们。为了加快速度“预加载扫描器”同时运行。

2.3 JavaScript阻塞解析

当遇到<script>时,暂停HTML解析,加载解析执行JS代码。因为JS可能会改变Html的结构导致重新reflow和repaint。

解析模型见:https://html.spec.whatwg.org/multipage/parsing.html#overview-of-the-parsing-model

V8相关讨论:https://mathiasbynens.be/notes/shapes-ics

用了十几年的浏览器,你却只会浏览

3. 确定加载资源方式

1) async:指示浏览器尽可能异步加载脚本,默认同步加载脚本(async=false)

2) defer:指示脚本要在解析文档之后但在触发DOMContentLoaded之前执行。

用了十几年的浏览器,你却只会浏览

上面图片引自https://v8.dev/features/modules

4. 样式计算

主线程解析CSS并确定每个DOM节点的计算样式,再根据CSS选择器将哪种样式应用于哪个元素。

用了十几年的浏览器,你却只会浏览

图片引自Mariko Kosaka的《Inside look at modern web browser》

未提供任何样式时,每个DOM节点都具有默认的Computed样式。见:https://cs.chromium.org/chromium/src/third_party/blink/renderer/core/html/resources/html.css

5. 布局 – layout

渲染进程知道每个节点的文档结构和样式。布局是查找元素几何的过程。

1) 布局过程

主线程 遍历DOM并计算样式,并创建布局树(layout tree, 包含坐标和边界框大小等信息)。

布局树的特殊情况

用了十几年的浏览器,你却只会浏览

图片引自Mariko Kosaka的《Inside look at modern web browser》

确定页面布局的挑战:

6. 绘制 – Paint

知道元素的大小,形状和位置,但是不知道绘制的顺序。主线程遍历布局树以创建绘制记录,绘制记录是绘画过程的一个注释。

用了十几年的浏览器,你却只会浏览

图片引自Mariko Kosaka的《Inside look at modern web browser》

渲染中的难点

用了十几年的浏览器,你却只会浏览

图片引自Mariko Kosaka的《Inside look at modern web browser》

用了十几年的浏览器,你却只会浏览

图片引自Mariko Kosaka的《Inside look at modern web browser》

例如用时间不确定的 setTimeout() 只会更新内存中的属性变化,由于期间隔时间和屏幕刷新时间不同步,可能导致某些帧的操作被跨跃,直接更新下一帧的图像。

用了十几年的浏览器,你却只会浏览

图片引自Mariko Kosaka的《Inside look at modern web browser》

用了十几年的浏览器,你却只会浏览

图片引自Mariko Kosaka的《Inside look at modern web browser》

用了十几年的浏览器,你却只会浏览

图片引自Mariko Kosaka的《Inside look at modern web browser》

解决方法

优点:

1) 省CPU时间:页面隐藏最小化时停止渲染,setTimeout持续运行。

2) 函数节流:高频率事件(resize/scroll)为防止刷新间隔内多次执行函数,只执行一次更流畅,省开销。

// 节流(throttle) + 去抖(debounce)
// 把要执行的函数放在延时器中执行且只执行最后一次,且判断距上次执行的时间大于延时执行时间
 
function show() {}
 
let lastTime = new Date();
let delay = 200;
let timer = null;
document.body.onscroll = function () {
 if (document.documentElement.scrollTop >= 1000) {
 let now = new Date();
 if (now - lastTime > delay) {
 // 去抖
 timer = setTimeout(show, 200);
 } else {
 clearTimeout(timer); // 节流
 timer = setTimeout(show, 200);
 }
 
 lastTime = new Date();
 }
};
// 任务文件 demo_workers.js
var i = 0;
 
function timedCount() {
 i = i + 1;
 postMessage(i); // 向页面发送数据
}
setInterval(timedCount, 500);
 
// 包含dom的页面 index.html
let w = new window.Worker('demo_workers.js'); // 开始
w.onmessage = function (event) {
 document.getElementById('result').innerHTML = event.data;
};
 
w.terminate(); // 停止

7. 合成

浏览器知道文档的结构,每个元素的样式,页面的几何形状和绘制顺序,需将信息转换为屏幕上的像素,称为光栅化。

在视口内部使用栅格部件 – chrome首次发布时处理栅格化的方式用户滚动页面,则移动光栅框架,并通过更多光栅填充缺失的部分

合成是一种将页面的各个部分分层,分别栅格化,并在合成器线程的单独线程中合成为页面的技术。如果发生滚动,图层已经被栅格化需要合成一个新帧。通过移动图层和合成新帧,可以以相同的方式实现动画。

7.1 分层

为了找出哪些元素需要在哪些层中,主线程遍历布局树以创建层树。如果页面的某些部分应该是单独的图层(如滑入式侧面菜单)但没有得到单独图层,可以使用CSS属性will-change提示浏览器。https://developer.mozilla.org/zh-CN/docs/Web/CSS/will-change

过多的图层:过多图层合成可能会导致比页面小部分每帧光栅化更慢。

用了十几年的浏览器,你却只会浏览

图片引自Mariko Kosaka的《Inside look at modern web browser》

7.2 光栅和复合关闭主线程

1) 提交合成:一旦创建了层树并确定了绘制顺序,主线程就会将该信息提交给合成器线程。

2) 栅格化:合成器线程然后栅格化每个层。一个图层可能像页面的整个长度一样大,因此合成器线程将它们分成多个图块并将图块发送到栅格线程。

3) 栅格存储:栅格线程栅格化每个图块并将它们存储在GPU内存中。

4) 绘制四边形:一旦图块被光栅化,绘制四边形的图块信息(图块在内存中的位置、绘制图块页面中的位置)

5) 合成框架:合成器线程可以优先考虑视口(或附近)内的删格线程,以便优先被光栅化。图层还有不同分辨率的倾斜度,可以处理放大操作。

6) 创建合成器帧:收集绘制四边形的图块信息,通过IPC将合成器框架提交给浏览器进程

7) 浏览器UI合成:UI线程添加另一个合成器框架以用于浏览器UI更改,或者从其他渲染器进程添加扩展。

8) GPU展示:合成器帧被发送到GPU以在屏幕上显示。

9) 滚动事件:合成器线程会创建另一个合成器帧以发送到GPU

用了十几年的浏览器,你却只会浏览

图片引自Mariko Kosaka的《Inside look at modern web browser》

创建磁贴位图并发送到GPU的栅格线程

用了十几年的浏览器,你却只会浏览

图片引自Mariko Kosaka的《Inside look at modern web browser》

合成器线程创建合成框架,将名称发送到浏览器进程然后发送到GPU

合成的好处

因此合成动画 被认为是平滑性能的最佳选择。如果需要再次计算布局或绘图,则必须涉及主线程。

五、页面操作

1. 接收输入事件

1) 浏览器进程接收 键入、鼠标事件、触摸手势等输入事件。浏览器进程仅知道手势发生位置,选项卡内部内容由渲染进程处理。

2) 浏览器进程将事件类型、坐标发送给渲染进程

3) 渲染进程通过查找事件目标并运行附加的事件侦听器来适当地处理事件

用了十几年的浏览器,你却只会浏览

4) 合成器接入输入事件

2. 非快速可滚动区域

1) 合成页面时,合成器线程标记页面的一个区域,该区域将事件处理程序附加为“非快速可滚动区域”。

2) 通过获取此信息,合成器线程可以确保在该区域中发生事件时将输入事件发送到运行JavaScript的主线程。如果输入事件来自该区域之外,则合成器线程在不等待主线程的情况下继续合成新帧。

用了十几年的浏览器,你却只会浏览

图片引自Mariko Kosaka的《Inside look at modern web browser》

3. 事件处理

下面程序中,整个页面都被标记为非快速可滚动区域,合成器线程也必须与主线程通信,并在每次输入事件进入时等待它,最终影响合成器平滑滚动能力。

document.body.addEventListener('touchstart', event = > {
 if (event.target === area) {
 event.preventDefault();
 }
});

解决方案

在事件监听器中传递 passive: true 选项,提示浏览器在主线程中监听事件,合成器线程也可以继续合成新帧。

document.body.addEventListener(
 'touchstart',
 event = > {
 if (event.target === area) {
 event.preventDefault();
 }
}, {
 passive: true
});

4. 检查取消事件

通过 event.cancelable和event.preventDefault()检查取消事件。也可通过CSS来完全消除事件处理程序。

document.body.addEventListener(
 'pointermove',
 event = > {
 if (event.cancelable) {
 event.preventDefault();
 }
}, {
 passive: true
});
#area {
 touch - action: pan - x;
}

5. 查找event.target

当合成器线程向主线程发送输入事件时,首先要运行的是命中测试以查找事件目标。命中测试使用在渲染过程中生成的绘制记录数据来找出事件发生的点坐标下面的内容。

用了十几年的浏览器,你却只会浏览

图片引自Mariko Kosaka的《Inside look at modern web browser》

6. 最小化事件派发到主线程

输入事件具有比屏幕刷新更高的保真度。主线程中触发过快的连续事件,会触发过多的命中测试和JS执行,导致页面抖动。为减少对主线程过度调用,Chrome合并连续事件(如 wheel,mousewheel,mousemove,pointermove, touchmove)并延迟调度,直到下一个requestAnimationFrame执行。

离散事件则立即执行,如keydown,keyup,mouseup,mousedown,touchstart,touchend

用了十几年的浏览器,你却只会浏览

(图片引自Mariko Kosaka的《Inside look at modern web browser》)

用了十几年的浏览器,你却只会浏览

图片引自Mariko Kosaka的《Inside look at modern web browser》

7. 使用getCoalescedEvents得到帧内事件

大多数Web应用程序,合并事件应足以提供良好的用户体验。构建绘制应用程序并根据touchmove坐标放置路径等可能会丢失中间坐标以绘制平滑线,可以使用getCoalescedEvents指针事件中的方法来获取这些合并事件的信息。

用了十几年的浏览器,你却只会浏览

图片引自Mariko Kosaka的《Inside look at modern web browser》

window.addEventListener('pointermove', event = > {
 const events = event.getCoalescedEvents();
 for (let event of events) {
 const x = event.pageX;
 const y = event.pageY;
 // draw a line using x and y coordinates.
 }
});

六、小结

浏览器是一个复杂的系统,这里介绍的只是冰山一角,chromium项目也在不停地迭代更新,所以可能一段时间后,某些功能已经发生了变化。更加细节及最新的可以关注一下最新的chromium源码。

参考文献

1、Inside look at modern web browser:https://developers.google.com/web/updates/2018/09/inside-browser-part1

2、V8:https://v8.dev

3、JavaScript module:https://v8.dev/features/modules

4、Gpu-accelerated-compositing:https://www.chromium.org/developers/design-documents/gpu-accelerated-compositing-in-chrome

5、Chrominum-design-documents:https://www.chromium.org/developers/design-documents

6、How Blink works:https://docs.google.com/document/d/1aitSOucL0VHZa9Z2vbRJSyAIsAz24kX8LFByQ5xQnUg/edit

7、Life of a Pixel 2018:https://docs.google.com/presentation/d/1boPxbgNrTU0ddsc144rcXayGA_WF53k96imRH8Mp34Y/edit

作者简介:

龙付成,15年加入腾讯,资深高级前端工程师。负责过QQ浏览器游戏平台、天宫活动系统、搜索页面生成引擎、领域组件库等项目。爱好技术研究、总结和分享,曾在腾讯课堂直播《Web前端安全与实践》课程。

文章TAG:十几  浏览  浏览器  用了十几年的浏览器  

加载全部内容

相关教程
猜你喜欢
大家都在看