浏览器工作原理与实践

本文是对极客时间课程”浏览器工作原理与实践”的记录. 作者是2019年做的文章, 可能部分内容已经有点旧了. 但是基础概念不影响.

仅仅打开1个网页, 为什么有4个进程

其实何止4个进程啊, 装了插件之后, 几个插件就多几个进程.按shift+esc快捷键可以快速打开chrome的任务管理器查看. 如图1所示

图1: chrome浏览器进程

进程与线程

进程: 一个进程就是一个程序的运行实例. 详细的说, 启动一个程序的时候, 操作系统会为该程序创建一块内存, 用来存放代码,运行中的数据和一个执行任务的主线程, 我们将这样的运行环境叫进程.

线程: 多线程可以并行处理任务, 但是线程不能单独存在, 它是由进程来启动和管理的.

线程依附于进程, 而进程中使用多线程并行处理能提升运算效率.

进程与线程之间有以下特点.

  1. 进程中的任意一线程执行出错, 都会导致整个进程的崩溃.
  2. 线程之间共享进程中的数据
  3. 当一个进程关闭之后, 操作系统会回收进程所占用的内存
  4. 进程之间的内容相互隔离

单进程浏览器

早在07年之前, 市面上的浏览器都是单进程的.即浏览器的所有功能模块都是运行在同一个进程里.由此导致单进程浏览器的不稳定, 不流畅和不安全.

单进程浏览器架构示意图问题1: 不稳定

早期浏览器需要借助插件来实现诸如Web视频, Web游戏等各种强大的功能. 但是插件是最容易出问题的模块, 并且还运行再浏览器进程中, 所以一个插件的意外崩溃会引起整个浏览器的崩溃.

除插件外, 渲染引擎模块也是不稳定的, 通常一些复杂的JavaScript代码也可能引起渲染引擎模块的崩溃. 和插件一样, 渲染引擎的崩溃也会导致整个浏览器的崩溃.

问题2: 不流畅

单进程浏览器的所有页面的渲染模块, JavaScript执行环境以及插件都是运行在同一个线程中的, 这就意味着同一时刻只能有一个模块可以执行. 一旦某个js代码产生bug引起了无限循环, 它就会独占整个线程, 导致其他运行在该线程中的模块没有机会被执行. 而又因为浏览器中所有的页面都运行在该线程中, 所以这些页面也都没有机会去执行任务, 这样就会导致整个浏览器失去响应, 变卡顿.

除此之外页面的内存泄漏也是单进程变慢的一个重要原因. 通常浏览器的内核是非常复杂的, 运行一个复杂点的页面再关闭页面, 会存在内存不能完全回收的情况. 这样就会导致, 使用时间越长, 内存占用越高, 浏览器会变得越慢.

问题3: 不安全

插件可以使用C/C++等代码编写, 通过插件可以获取到操作系统的任意资源, 当你在页面运行一个插件时也就意味着这个插件能完全操作你的电脑. 如果是个恶意插件, 那么它就可以释放病毒, 窃取账号密码等, 引发安全性问题.

另外就是页面脚本. 它可以通过浏览器的漏洞来获取系统权限, 从而引发安全问题.

多进程浏览器

多进程浏览器很好的解决了上面说的这些问题.

先看看2008年Chrome发布时的进程架构图.

早期Chrome进程架构图

图中可以看出, Chrome页面是运行在单独的渲染进程中的, 同时页面的插件也是运行在单独的插件进程中, 而进程之间是通过IPC机制进行通信.

解决1: 不稳定性

由于进程之间是相互隔离的, 所以当一个页面或者插件崩溃时, 仅仅影响到当前页面进程或插件进程, 并不会影响到浏览器和其他页面, 从而解决浏览器的不稳定性.

解决2: 不流畅性

JavaScript脚本是运行在渲染进程中的, 而每个页面都有其单独的渲染进程. 所以当其中一个由于JavaScript代码导致了死循环, 没有响应的也仅仅是当前的页面.

对于内存泄漏的问题, 当关闭一个页面时, 整个渲染进程都会被关闭.之后该进程所占用的内存都会被系统回收.也就轻松解决了因为内存泄漏导致的页面越用越卡顿的问题.

解决3: 安全性

采用多进程架构的额外好处是可以使用安全沙箱. 沙箱里的程序不能写入任何数据到硬盘. 也不能在敏感位置读取数据. Chrome把插件进程和渲染进程锁在沙箱里, 这样即使在渲染进程或插件进程执行了恶意程序, 也获取不到系统权限.

发展的多进程架构

随着Chrome的发展, 目前的架构又有了很多改变.

新Chrome进程架构图

图中我们可以发现, 新的Chrome浏览器包括: 1个浏览器(Browser)主进程, 1个GPU进程, 1个网络(NetWork)进程, 多个渲染进程和多个插件进程.

  • 浏览器进程: 主要负责界面显示, 用户交互, 子进程管理, 同时提供存储等功能.
  • 渲染进程: 核心任务是将HTML, CSS和JavaScript转换为用户可以与之交互的网页.排版引擎Blink和JavaScript引擎V8都运行再该进程中. 默认情况下, Chrome会为每个Tab标签创建一个渲染进程. 处于安全考虑, 渲染进程都是运行再沙箱模式下.
  • GPU进程: GPU的使用初衷是为了实现3D CSS效果, 但是随后网页, Chrome的UI界面都选择采用GPU来绘制, 这使得GPU成为浏览器普遍的需求. 最终Chrome在其多进程架构上引入了GPU进程.
  • 网络进程: 负责网页的网络资源加载.
  • 插件进程: 负责插件的运行

凡事都有两面性, 多进程模块提升了浏览器的稳定性, 流畅性和安全性的同时, 不可避免也带来了更高的资源占用,更复杂的体系架构问题. 针对这两个问题, 2016年Chrome团队使用”面向服务的架构”的思想设计了新的Chrome架构.

面向服务的架构

Chrome将原来的各种模块重构成独立的服务(Service), 每个服务(Service)都可以在独立的进程中运行, 访问服务(Service)必须使用定义好的接口, 通过IPC通信, 从而构建一个更内聚, 松耦合, 易于维护和扩展的系统.

Chrome最终要把UI, 数据库, 文件, 设备, 网络等模块重构未基础服务, 类似操作系统的底层服务.

面向服务的架构-进程模型图

同时Chrome还提供灵活的弹性架构, 在强大性能设备上会以多进程的方式运行基础服务, 但是如果在资源受限的设备上(如下图), Chrome会将很多服务整合到一个进程中, 从而节省内存占用

在资源不足的设备上, 将服务合并到浏览器进程中

TCP/IP是如何工作的

IP: 把数据包送达目的主机

数据包要在互联网上进行传输, 就要符合网际协议(Internet Protocol, 简称IP)标准.互联网上不同的在下设备都有唯一的地址, 即为IP地址. 访问任何网站实际上只是你的计算机向另一台计算机请求信息.

如果要想把一个数据包从主机A发送给主机B, 那么在传输之前, 数据包上会被附加上主机B的IP地址信息, 这样在传输过程中才能正确寻址. 额外的, 数据包上还会附加上主机A本身的IP地址, 有了这些信息主机B才可以回复信息给主机A. 这些附加的信息会被装进一个叫IP头的数据结构中. IP头是IP数据包开头的信息, 包含IP版本, 源IP地址, 目标IP地址, 生存时间等信息.

简化的IP网络三层传输模型

结合图片可以看下一个数据包从主机A到主机B的旅程:

  • 上层将含有”极客时间”的数据包交给网络层
  • 网络层再将IP头附加到数据包上, 组成新的IP数据包, 并交给底层
  • 底层通过物理网络将数据包传输给主机B
  • 数据包被传输到主机B的网络层, 在这里主机B拆开数据包的IP头信息, 并将拆开来的数据部分交给上层
  • 最终, 含有”极客时间”信息的数据包就到达了主机B的上层了

UDP: 把数据包送达应用程序

IP是非常底层的协议, 只负责把数据包传送到对方电脑. 但是对方电脑并不知道把数据包具体交给哪个程序. 因此, 需要基于IP之上开发能和应用打交道的协议. 最常见的是用户数据包协议(User Datagram Protocol, 简称UDP)

UDP中一个最重要的信息是端口号.每个想访问网络的程序都需要绑定一个端口号. 通过端口号UDP就能把指定的数据包发送给指定的程序. 所以, IP通过IP地址信息把数据包发送给指定电脑, 而UDP通过端口号把数据包分发给正确的程序.

和IP头一样, 端口号会被装进UDP头里, UDP头再和原始数据包合并组成新的UDP数据包. UDP头中除了目的端口, 还有源端口号等信息.

简化的UDP网络四层传输模型

再来回顾下一个数据包从主机A到主机B的路线:

  • 上层将含有”极客时间”的数据包交给传输层
  • 传输层会在数据包前面附加上UDP头, 组成新的UDP数据包, 再将新的UDP数据包交给网络层
  • 网络层再将IP头附加到数据包上, 组成新的IP数据包, 并交给底层
  • 数据包被传输到主机B的网络层, 在这里主机B拆开IP头信息, 并将拆开来的数据部分交给传输层
  • 在传输层, 数据包中的UDP头会被拆开, 并根据UDP中所提供的端口号, 把数据部分交给上层的应用程序
  • 最终, 含有”极客时间”信息的数据包就到达主机B上层应用程序了

缺点: UDP不能保证数据可靠性. UDP发送数据时, 在各种因素影响下会导致数据包出错. 虽然UDP可以校验数据是否正确, 但是对于错误的数据包, UDP并不提供重发机制, 只是丢弃当前包, 而且UDP在发送之后也无法知道是否能到达目的地

优点: 传输速度快. UDP应用一般应用在一些关注速度, 但不那么严格要求数据完整性的领域. 如在线视频, 互动游戏等.

TCP: 把数据完整的送达应用程序

对于邮件这种要求数据传输可靠性的应用, UDP传输会存在两个问题

  • 数据包在传输过程中容易丢失
  • 大文件会被拆分很多小的数据包来传输, 这些小的数据包会经过不同的路由, 并在不同的时间到达接收端, 而UDP协议并不知道如何组装这些数据包, 从而把这些数据包还原成完整的文件

为了解决这些问题, 我们引入了TCP. TCP(Transmission Control Protocol, 传输控制协议)是一种面向连接的, 可靠的, 基于字节流的传输层通信协议.

相对于UDP, TCP有以下两个特点

  • 对于数据包丢失的情况, TCP提供重传机制
  • TCP引入了数据包排序机制, 用来保证把乱序的数据包组合成一个完整的文件

TCP除了包含目标端口和本机端口外, 还提供了用于排序的序列号, 以便接收端通过序号来重排数据包.

下面看看TCP下的单个数据的传输流程:

简化的TCP网络四层传输模型

TCP单个数据包的传输流程和UDP流程差不多, 不同之处在于, 通过TCP头的信息保证了一块大的数据传输的完整性.

下面我们来了解下完整的TCP连接过程

一个TCP连接的生命周期

一个完整的TCP连接的生命周期包含了”建立连接”, “传输数据”和”断开连接”三个阶段.

  1. 建立连接
    这个阶段是通过”三次握手”来建立客户端和服务器之间的连接. TCP提供面向连接的通信传输. 面向连接是指在数据通信开始之前先做好两端之间的准备工作. 三次握手是指在建立一个TCP连接时, 客户端和服务器之间总共要发送三个数据包以确认连接的建立.
  2. 传输数据阶段
    在该阶段, 接收端需要对每个数据包进行确认操作, 也就是接收端在接收到数据包后, 需要发送确认数据包个发送端. 所以当发送端发送了一个数据包之后, 在规定时间内没有接收到接收端反馈的确认信息, 则判断为数据包丢失, 并触发发送端的重发机制. 同样, 一个大文件在传输过程中会被拆分成很多小的数据包, 这些数据包到达接收端后, 接收端会按照TCP头中的序号为其排序, 从而保证组成完整的数据
  3. 断开连接阶段
    数据传输完毕之后, 需要四次挥手来保证双方都能断开连接

到这就能明白, TCP为了保证数据传输的可靠性, 牺牲了数据包的传输速度, 因为”三次握手”和”数据包校验机制”等把传输过程中的数据包的数量提高了一倍.

HTTP请求流程

HTTP是一种允许浏览器向服务器获取资源的协议, 是Web的基础.

HTTP和TCP的关系

浏览器使用HTTP协议作为应用层协议, 用来封装请求的文本信息.并使用TCP/IP作传输层协议将它发到网络上.

在HTTP开始工作之前, 浏览器需要通过TCP与服务器建立连接.也就是说, HTTP的内容是通过TCP的传输数据阶段来实现的. 结合下图理解

TCP和HTTP的关系示意图

浏览器发送HTTP请求

当我们在浏览器输入一个网站的地址, 浏览器到底会完成哪些动作呢?

  1. 构建请求

    首先, 浏览器构建请求行信息, 构建好后, 浏览器准备发送网络请求.

    1
    GET /index.html HTTP1.1
  2. 查找缓存

    如果发现请求的资源已经在浏览器缓存中, 就会拦截请求, 返回该资源的副本, 并结束请求.

  3. 准备IP地址和端口
    一般我们访问网站是输入网站的域名, 所以首先浏览器会请求NDS(Domain Name System, 域名系统)返回域名对应的IP. 当然浏览器还提供了DNS数据缓存服务, 如果某个域名已经解析过了, 那么浏览器会缓存解析的结果, 以供下次查询时直接使用, 减少一次网络请求.
    至于端口, 通常情况下, 如果URL没有特别指明端口号, 那么HTTP协议默认是80端口(HTTPS默认端口是443).

  4. 等待TCP队列
    准备好端口和IP地址后并不能立马建立TCP连接. 因为Chrome有个机制, 同一个域名同时最多只能建立6个TCP连接, 如果在同一个域名下同时有10个请求, 那么其中4个请求会进入排队等待状态, 直至进行中的请求完成.
  5. 建立TCP连接
    排队结束, 就可以和服务器握手了.
  6. 发送HTTP请求
    一旦建立了TCP连接, 浏览器就可以和服务器进行通信了.
    首先浏览器会向服务器发送请求行, 它包含了请求方法, 请求URI和HTTP版本协议. 发送请求行, 就是告诉服务器浏览器需要什么资源
    接着再以请求头形式发送其他一些信息(浏览器的一些基础信息)给服务器.比如浏览器所用的操作系统, 浏览器内核等信息. 以及当前请求的域名信息, 浏览器端的Cookie信息等.
    HTTP请求数据格式

服务端处理HTTP请求

对应的, 服务器也会先返回响应码, 包含协议版本和状态码.

随后再随同响应向浏览器发送响应头. 响应头包含了服务器自身的一些信息, 比如服务器生成返回数据的时间, 返回的数据类型, 以及服务器要在客户端保存的Cookie等信息.

发送完响应头后, 服务器再继续发送响应体的数据.
服务器响应的数据格式

通常情况下, 一旦服务器向客户的返回了请求数据, 它就要关闭TCP连接. 不过如果浏览器或者服务器在其头信息中加入了Connection: Keep-Alive, 那么TCP连接在发送后将仍然保持打开状态.这时候浏览器就可以继续通过同一个TCP连接发送请求.保持TCP连接可以省去下次请求时需要建立连接的时间, 提升资源加载速度.

另外再附一张强制缓存的图, 方便记忆.

缓存查找流程示意图

总结

浏览器中的HTTP请求从发送到结束一共经历八个阶段: 构建请求, 查找缓存, 准备IP和端口, 等待TCP队列, 建立TCP队列, 发送HTTP请求, 服务器处理请求, 服务器返回请求, 断开连接.

HTTP请求流程示意图

在浏览器里, 从输入URL到页面展示, 这中间发生了什么

大致流程

下面是从输入URL到页面展示的额完整流程示意图, 从图中可以看出, 整个过程中需要各个进程之间的配合.

从输入URL到页面展示完整流程示意图

图中蓝色背景为核心节点. 整个流程大致描述如下:

  • 首先, 浏览器进程接收到用户输入的URL请求, 浏览器进程便将该URL转发给网络进程.
  • 然后, 在网络进程中发起真正的URL请求.
  • 接着网络进程接收到了响应头数据, 便解析响应头数据, 并将数据转发给浏览器进程.
  • 浏览器进程接收到网络进程的响应头数据之后, 发送”提交导航(CommitNavigation)”消息到渲染进程.
  • 渲染进程接收到”提交导航”的消息之后, 便开始准备接收HTML数据, 接收数据的方法是直接和网络进程建立数据管道.
  • 最后渲染进程会向浏览器进程”确认提交”, 这是告诉浏览器进程: “已经准备好接收和解析页面数据了”.
  • 浏览器进程接收到渲染进程”提交文档”的消息之后, 便开始移除之前旧的文档, 然后更新浏览器进程中的页面状态.

其中, 用户发出URL请求到页面开始解析的过程, 叫做导航

用户输入

当用户在地址栏输入一个查询关键字时, 地址栏会判断输入的关键字时搜索内容还是请求的URL.

  • 如果是搜索内容, 地址栏会使用浏览器默认的搜索引擎, 来合成新的带搜索关键字的URL.
  • 如果判断输入内容符合URL规则, 如: baidu.com, 地址栏会根据规则, 把这段内容加上协议, 合成为完整的URL. 如: https://www.baidu.com

当用户输入关键字并按下回车之后, 就意味着当前页面即将要被替换成新的页面, 不过在这个流程继续之前, 浏览器还给了当前页面一次执行beforeunload事件的机会. beforeunload事件允许页面在退出之前执行一些数据清理操作, 还可以询问用户是否要离开当前页面. 比如当前页面可能有未提交完成的表单等情况, 因此用户可以通过beforeunload事件来取消导航, 让浏览器不再执行任何后续工作.

通过了beforeunload事件之后, 浏览器从刚开始加载一个地址之后, 标签页的图标便进入了加载状态. 但是此时页面显示的依然是之前打开的页面内容. 因为还需要等待提交文档阶段, 页面内容才会被替换.

URL请求过程

接下来就是页面资源请求过程. 这时, 浏览器进程会通过进程间通信(IPC)把URL请求发送至网络进程, 网络进程接收到URL请求后, 会在这里发起真正的URL请求流程.

首先, 网络进程会查找本地缓存是否缓存了该资源. 如果有缓存资源, 那么直接返回资源给浏览器进程;如果在缓存中没有查找到资源, 那么直接进入网络请求流程. 这请求前的第一步是要进行DNS解析, 以获取请求域名的服务器IP地址. 如果请求协议是HTTPS, 那么还需要建立TLS连接.

接下来就是利用IP地址和服务器建立TCP连接. 连接建立之后, 浏览器端会构建请求行, 请求头等信息, 并把和该域名相关的Cookie等数据附加到请求头中, 然后向服务器发送构建的请求信息.

服务器接收到请求信息后, 会根据请求信息生成响应数据(包括响应行, 响应头和响应体等信息), 并发给网络进程. 等网络进程接收了响应行和响应头之后, 就开始解析响应头的内容了.

重定向

在接收到服务器返回的响应头后, 网络进程开始解析响应头, 如果发现返回的状态码是301或302, 说明服务器需要浏览器重定向到其他URL. 这时网络进程会从响应头的Location字段里面读取重定向的地址, 然后再发起新的请求, 重新开始上述过程.

响应行返回状态码301

如果响应行是200, 那么表示浏览器可以继续处理该请求.

响应数据类型处理

URL请求的数据类型, 可能是一个下载类型, 也可能是正常的HTML页面. 浏览器通过Content-Type来区分二者.

Content-Type是HTTP头中一个非常重要的字段, 它告诉浏览器服务器返回的响应体数据是什么类型.然后浏览器会根据Content-Type的值来决定如何显示响应体的内容.

不同的Content-Type的后续处理流程也不同. 如果是下载类型, 请求会被提交给浏览器的下载管理器, 同时该URL请求的导航流程就此结束. 如果是HTML, 那么浏览器则会继续进行导航流程.

由于Chrome的页面渲染时运行在渲染进程中的, 所以接下来需要准备渲染进程.

准备渲染进程

默认情况下, Chrome会为每个页面分配一个渲染进程.但是如果是同一站点(same-site), 则共用同一个渲染进程.

同一站点: 根域名加上协议相同, 还有该根域名下的所有子域名和不同端口, 都属于同一站点.如:

1
2
3
https://time.geekbang.org
https://www.geekbang.org
https://www.geekbang.org:8080

Chrome的默认策略是, 每个标签对应一个渲染进程, 但如果从一个页面打开了另一个新页面, 而新页面和当前页面属于同一站点的话, 那么新页面会复用父页面的渲染进程.(process-per-site-instance)

渲染进程准备好之后, 还不能立即进入文档解析状态, 因为此时的文档数据还在网络进程中, 并没有提交给渲染进程. 所以下一步就进入了提交文档阶段.

提交文档

所谓提交文档, 就是指浏览器进程将网络进程接收到的HTML数据提交给渲染进程.

具体流程:

  • 首先当浏览器进程接收到网络进程的响应头数据后, 向渲染进程发起”提交文档”的消息.
  • 渲染进程接收到”提交文档”的消息后, 会和网络进程建立传输数据的”管道”
  • 等文档数据传输完成后, 渲染进程会返回”确认提交”的消息给浏览器进程
  • 浏览器进程在收到”确认提交”的消息后, 会更新浏览器界面状态, 包括了安全状态, 地址栏的URL, 前进后退的历史状态, 并更新web页面.

当渲染进程确认提交之后, 更新的内容如图所示.

导航完成状态

这就解释了为什么在浏览器的地址栏里面输入了一个地址后, 之前的页面没有立马消失, 而是要加载一会才会更新页面.

到这里, 一个完整的导航流程就走完了, 下面就进入渲染阶段了.

渲染阶段

一旦文档被提交, 渲染进程便开始页面解析和子资源加载了. 具体过程下一节描述. 一旦页面生成完成, 渲染进程会发送一个消息给浏览器进程, 浏览器接收到消息后, 会停止标签图标上的加载动画.

至此, 一个完整的页面就生成了.

渲染流程

渲染流程是一个复杂的过程, 渲染模块在执行过程中会被划分为很多子阶段, 输入的HTML经过这些子阶段, 最后输出像素. 我们将这样的处理流程叫做渲染流水线.

渲染流水线包含以下几个子阶段: 构建DOM树, 样式计算, 布局阶段, 分层, 绘制, 分块, 光栅化和合成.

对于每个子阶段都有3个特点:

  • 开始每个子阶段都有其输入的内容
  • 然后每个子阶段有其处理过程
  • 最终每个子阶段会生成输出内容

构建DOM树

浏览器无法直接理解和使用HTML, 所以需要将HTML转换为浏览器能够理解的结构—DOM树.

构建DOM树的输入内容是一个HTML文件, 然后经由HTML解析器解析, 最终输出树状结构的DOM.

DOM树构建过程示意图

DOM和HTML内容几乎是一样的, 区别在于, DOM是保存在内存中的树状结构, 可以通过JavaScript来查询或修改.

样式计算

样式计算大体可以分成三步完成.

把css转换成浏览器能够理解的结构

css样式主要有3个来源

  • 通过link引用外部css文件
  • \标记内的css
  • 行内样式

和HTML文件一样, 浏览器自身无法理解纯文本的css样式, 所以当渲染引擎接收到css文本时, 会执行一个转换操作, 将css文本转换成浏览器可以理解的结构—styleSheets. 该结构同时具备了查询和修改功能, 为后面样式操作提供基础. styleSheets结构如图所示

styleSheets

转换样式表中的属性值, 使其标准化

什么是属性值标准化?

1
2
3
4
5
6
body { font-size: 2em }
p {color:blue;}
span {display: none}
div {font-weight: bold}
div p {color:green;}
div {color:red; }

在上面的示例代码中, 诸如2em, blue, bold. 这些类型数值不容易被渲染引擎理解, 所以需要将所有值转换为渲染引擎容易理解的, 标准化的计算值.

标准化属性值

计算出DOM树种每个节点的具体样式

如何计算DOM树中每个节点的样式属性, 涉及到CSS的继承规则和层叠规则.

CSS继承: 每个DOM节点都包含有父节点的样式.

样式层叠: 是CSS的一个基本特性, 它是一个定义了如何合并来自多个源的属性值的算法.

遵循上面两个原则, 计算出DOM节点每个元素的具体样式后, 最终输出每个DOM节点的样式, 并保存在ComputedStyle的结构中.

布局阶段

有了DOM树和DOM树中元素的样式后, 接下来就是计算DOM树中可见元素的几何位置. 这个计算过程叫做布局.

创建布局树

DOM树种含有很多不可见元素, 比如head标签, 比如display:none的元素. 所以在显示之前, 需要额外构建一棵只包含可见元素布局树.所有不可见元素都没有包含在此布局树中.

布局树构造过程示意图

布局计算

在执行布局操作时, 会把布局运算的结果重新写回布局树中. 所以布局树即是输入内容也是输出内容.