webview加载的页面和浏览器渲染的页面不一致_解析浏览器原理

d45b6f80f4ebb397a3e67074986bd974.png

浏览器组成

浏览器主要分为以下几大部分:

  • 用户界面

  • 浏览器引擎(负责窗口管理、Tab进程管理等)

  • 渲染引擎(又叫内核,负责HTML解析、页面渲染)

  • JS引擎(JS解释器,如Chrome和Nodejs采用的V8)

98a454148f5b0b31e45971b21b0f80f8.png

浏览器内核

浏览器内核分为两部分:渲染引擎、js引擎,随着js引擎越来越独立,内核就倾向于只指渲染引擎

  • 渲染引擎:负责对网页语法的解释(HTML、javaScript、引入css等),并渲染(显示)网页

  • js引擎:javaScript的解释、编译、执行(V8、SpiderMonkey、JavaScriptCore)

主流内核:Trident(IE)、Gecko(FireFox)、Webkit(Safari)、blink(Chrome)、Presto(opera前内核、已废弃)

2008 年,谷歌公司发布了 chrome 浏览器,浏览器使用的内核被命名为 chromium,基于苹果开源的Webkit

2013 年 ,谷歌Webkit分道扬镳,在 Chromium 项目中研发 Blink 渲染引擎,内置于 Chrome 浏览器之中

移动端webview内核

  • Android

    在Android4.4以前,webview是Android webkit 浏览器内核,很多HTML5标准语法不支持,比如indexeddb、webgl等,canvas性能也非常差 Android4.4起,变成了chromium内核,内核版本是chrome30,性能和现代语法支持大幅提升 从Android5开始,webview脱离rom可单独更新,伴随着chrome的发版,google会在google play store上同步更新Android system webview(国内存在无法更新问题)

    个别厂商会定制webview;腾讯使用X5内核(APP也可以集成,支持webrtc网页浏览器进行实时语音对话或视频对话)

    不同于PC端chromium内核的浏览器,默认与APP在同一进程,可开启独立进程运行

  • iOS

    从iOS8起,Apple推出了wkwebview,在app外的独立进程执行,使用更快的Nitro JavaScript引擎(2008 年苹果重写了JSCore,叫做 SquirrelFish,后来是 SquirrelFish Extreme,又叫 Nitro,但早期uiwebview被限制使用),由于iOS13将uiwebview列入非公开api,未来会禁止使用uiwebview的应用上架

浏览器进程架构(Chrome)

chrome提供了任务管理器,从中可以查看到正在运行的进程:

b7985af643caa45c1e04ec36002429df.png

从上图可以看出浏览器只打开了一个标签页,但是却有十多个进程,抛开插件进程外还有5个进程

浏览器进程并不是一开始就是这样的结构,这里先不解释每一个进程的作用,而是回顾一下浏览器进程架构的演化历史

单进程浏览器时代

浏览器的所有功能模块都是运行在同一个进程里,包含了网络、插件、JavaScript 运行环境、渲染引擎和页面等。

7baa67d4e8ca3b5d0b3348ef6d5c2089.png

优点:资源占用少,无需进程间通信

缺点:

  • 不稳定:一个模块的意外崩溃会引起整个浏览器的崩溃,早期浏览器需要借助于插件来实现诸如 Web 视频、Web 游戏等各种强大的功能,但是插件是最容易出问题的模块

  • 不流畅:所有页面的渲染模块、JavaScript 执行环境以及插件都是运行在同一个线程中的,这就意味着同一时刻只能有一个模块可以执行

  • 不安全:插件可以使用 C/C++ 等代码编写,通过插件可以获取到操作系统的任意资源,当你在页面运行一个插件时也就意味着这个插件能完全操作你的电脑

多进程浏览器

43b43a8dae1ffd16a0a9461aa58d5a3c.png

e81accc31717e9fdbbda703f3af241aa.png

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

稳定性:进程是相互隔离的,所以当一个页面或者插件崩溃时,影响到的仅仅是当前的页面进程或者插件进程

流畅度:JavaScript 也是运行在渲染进程中的,所以即使 JavaScript 阻塞了渲染进程,影响到的也只是当前的渲染页面,而并不会影响浏览器和其他页面

安全性:多进程架构的额外好处是可以使用安全沙箱进行进程隔离,Chrome 把插件进程和渲染进程锁在沙箱里面,这样即使在渲染进程或者插件进程里面执行了恶意程序,恶意程序也无法突破沙箱去获取系统权限

最新Chrome架构

d00ba4ab5378682777f1149e65ee0dda.png

最新的 Chrome 浏览器包括:1 个浏览器(Browser)主进程、1 个 GPU 进程、1 个网络(NetWork)进程、多个渲染进程和多个插件进程。他们主要负责的任务如下:

  • 浏览器进程:主要负责界面显示、用户交互、子进程管理,同时提供存储等功能。

  • 渲染进程:核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页,排版引擎 Blink 和 JavaScript 引擎 V8 都是运行在该进程中,默认情况下,Chrome 会为每个 Tab 标签创建一个渲染进程。出于安全考虑,渲染进程都是运行在沙箱模式下。

  • GPU 进程:其实,Chrome 刚开始发布的时候是没有 GPU 进程的。而 GPU 的使用初衷是为了实现 3D CSS 的效果,只是随后网页、Chrome 的 UI 界面都选择采用 GPU 来绘制,这使得 GPU 成为浏览器普遍的需求。最后,Chrome 在其多进程架构上也引入了 GPU 进程。

  • 网络进程:主要负责页面的网络资源加载,之前是作为一个模块运行在浏览器进程里面的,直至最近才独立出来,成为一个单独的进程。

  • 插件进程:主要是负责插件的运行,因插件易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响。

再来看看文章开头的截图,不难看出图中各个进程与架构设计中一一对应关系:

b7985af643caa45c1e04ec36002429df.png

注:这里还多了一个V8代理解析工具,是因为我开启了代理,Chrome支持使用JavaScript来写连接代理服务器脚本,又称为在线pac代理脚本。刚开始的时候Chrome是在浏览器进程里面解析pac代理脚本的,由于是JavaScript脚本,所以需要在浏览器进程里面引入V8,不过把V8运行在浏览器进程似乎不太好,于是Chrome团队后来就把这个功能独立出来一个进程了,这个进程就叫“Utility: V8 Proxy Resolver”

FireFox的多进程架构

FireFox通过Electrolysis(e10s)项目提供多进程支持

e10s 多进程功能的发展路线综述

098f407fc2168dd5e5f8c8a962cfdb25.png

输入URL到页面展示中间发生了什么?

b68bc253def1cc457a1177b8606b5ad2.png

上图分进程描述了输入URL到页面展示的主要过程,这里主要可以细分为5段流程:用户输入、URL请求、准备渲染、提交文档、页面渲染

1. 用户输入

当用户在地址栏输入后确认访问,浏览器会判断输入内容是关键字还是URL,进行处理访问

回车访问后,浏览器开始加载新页面,在加载之前会给当前页面执行beforeUnload事件的机会,如果当前页面不处理则直接开始新页面的加载

如图,浏览器开始导航,展示加载状态,但此时页面内容还是旧的页面

2.URL请求

接下来浏览器进程通过进程间通信(IPC)将URL请求信息发送给网络进程,网络进程发起资源请求,这里通过一张示意图来分析浏览器进行网络请求的大致流程

fd8b5e9de88b17f20f8a60ac2b1df957.png

请求之前网络进程会先判断缓存是否存在且可用,如下图所示,描述了浏览器主要的缓存流程和主要策略:

2f3b00dc544d7662aa72a8667b6b8a22.png

29f276037f6fb633985cbef34ba40b24.png

详细的缓存策略可以参考运维知识体系笔记

若缓存不可用,则会进行DNS解析(也会先寻找缓存)利用IP地址与服务器建立TCP连接

服务器接收到请求信息后,会根据请求信息生成响应数据;若服务器响应行的状态码包含了 301、302 一类的跳转信息,浏览器会跳转到新的地址继续导航;如果响应行是 200,那么表示浏览器可以继续处理该请求

浏览器收到服务器返回的数据,根据响应头中的Content-Type进行解析:如果是application/octet-stream(下载类型)那么该请求会被提交给浏览器的下载管理器,同时该 URL 请求的导航流程就此结束。但如果是 HTML,那么浏览器则会继续进行导航流程

3.准备渲染进程

新开页面时,浏览器会为标签页创建渲染进程,创建策略如下:

  • 通常情况下,打开新的页面都会使用单独的渲染进程

  • 如果从 A 页面打开 B 页面,且 A 和 B 都属于同一站点(根域名+协议 相同)的话,那么 B 页面复用 A 页面的渲染进程(前提条件:rel != "noopener noreferrer");如果是其他情况,浏览器进程则会为 B 创建一个新的渲染进程。

f11de3a45a479af9bd504095947a5d62.png

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

4.提交文档

提交文档是指网络进程将请求完成的HTML数据提交给渲染进程:

  • 网络进程读取到响应头后会通知浏览器进程,浏览器进程向渲染进程发送“提交文档”的消息

  • 渲染进程收到“提交文档”消息后与网络进程建立传输管道,等待接收文档

  • 文档传输完成后,渲染进程通知浏览器进程“确认提交”的消息

  • 浏览器进程在收到“确认提交”的消息后,会更新浏览器界面状态,包括了安全状态、地址栏的 URL、前进后退的历史状态,并更新 Web 页面

5b50edb345c69399625d2da2ab3205f8.png

这也是为什么在浏览器的地址栏里面输入了一个地址后,之前的页面没有立马消失,地址栏也不是最终值,而是要加载一会儿才会更新页面;但此时还未渲染,页面内容还是空白

5.渲染阶段

渲染进程接收到提交的文档之后,开始进行页面渲染,浏览器通过解析HTML、CSS和JavaScript来实现渲染:

436eafeffcddb9f3637873c3d63501bc.png

按照渲染的时间顺序,流水线具体可分为如下几个子阶段:构建 DOM 树、样式计算、布局阶段、分层、绘制、分块、光栅化和合成。每一个子阶段都是分为输入、处理、输出三个过程。

ae097a8b11305bd54c4031b106c5f46c.png

(1)构建 DOM 树

浏览器无法直接理解HTML,而是需要将HTML解析转换为DOM,即输入HTML,通过解析器处理,输出DOM:

b09133509f65e34839d1a56affc0226c.png

DOM保存在内存中,可以直接通过JavaScript进行修改

(2)样式计算

样式计算大致分为以下阶段:

  • 把 CSS 转换为浏览器能够理解的结构

DOM只表示页面的结构,还需要知道页面的样式,样式通过CSS实现,CSS主要有以下来源:

afa8209f3b22555b2f83e5499e6b7a93.png

和HTML一样,浏览器也无法直接理解CSS文本,而是将其处理为层叠样式表styleSheets,在控制台打印document.styleSheets:

d0c8359e85f8256e079a2a616fb833d6.png

styleSheets同时具备查询和修改功能,因此可以在JavaScript中修改更新

  • 标准化样式属性值

    样式属性值中有很多是渲染引擎无法直接理解的,如em、blue、bold等,需要进行标准化处理

    8968f831f4b9fc338f55d0f1c9cc1684.png

    计算每个DOM节点的样式值

    计算出标准化的属性值后,需要计算每个DOM节点上对应的样式值

    计算规则遵循CSS 的继承规则和层叠规则:

    继承是指每个 DOM 节点都包含有父节点的样式,如下图:

    层叠定义了如何合并来自多个源的属性值的算法,最终的计算结果会被保存到节点的ComputedStyle内:

    2730b6d4640432f5fd445cd73eaaa746.png

(3)布局阶段

当具备了页面的结构和样式之后,就具备了创建布局的条件,布局阶段需要完成两个任务:创建布局树和布局计算。

  • 创建布局树

    布局树即将DOM和ComputedStyle进行合并,主要处理为遍历DOM 树中的所有可见节点,并把这些节点加到布局树中,dispaly:none的元素不会被加进布局树。

    f13c0d85a1ee76cd9a92080df90a2fd5.png

  • 布局计算

    拥有布局树的结构的下一步是计算每个布局树节点的坐标,再将其保存回到布局树中

(4)分层

理解分层和合成

当布局树和每个节点的位置确定之后,还有一点需要处理,页面中有很多特殊的效果,如3D 变换、页面滚动,或者使用 z-indexing 做 z 轴排序等,需要引入图层的概念来实现,类似于PS中的图层。Chrome 的“开发者工具”,选择“Layers”标签,就可以可视化页面的分层情况:

b502bb074b06100bbd21652bb28ba1b1.png

如上图,渲染引擎将页面分为多层,最终层叠在一起形成最终的界面;但通常情况下,并不是布局树的每个节点都包含一个图层,如果一个节点没有对应的层,那么这个节点就从属于父节点的图层:

c3c9b5ba3ce0194426a9336253334ca8.png

元素是否会单独分层取决于以下条件:

第一点,拥有层叠上下文属性的元素会被提升为单独的一层,明确定位属性的元素、定义透明属性的元素、使用 CSS 滤镜的元素等,都拥有层叠上下文属性

第二点,需要剪裁(clip)的地方也会被创建为图层。例如我们限定div的大小,而 div 里面的内容比较多,显示的区域肯定会超出限定面积,这时候就产生了剪裁,渲染引擎会把裁剪内容的一部分用于显示在 div 区域,如果出现滚动条,滚动条也会被提升为单独的层。下图是运行时的执行结果:

20b9f2261ac16217283977782f7c5549.png

(5)图层绘制

分层完成后,渲染引擎可以开始进行绘制操作,而绘制是分层进行的

渲染引擎实现图层的绘制与绘画类似,会把一个图层的绘制拆分成很多小的绘制指令,然后再把这些指令按照顺序组成一个待绘制列表,如下图所示:

99184df22eeab2002ade924ceab432a0.png

可以打开“开发者工具”的“Layers”标签,选择“document”层,来实际体验下绘制列表

25986f65047e444956ddbbc8d2518b61.png

(6)栅格化(raster)操作

绘制列表只是用来记录绘制顺序和绘制指令的列表,而实际上绘制操作是由渲染引擎中的合成线程来完成的。当图层的绘制列表准备好之后,主线程会把该绘制列表提交(commit)给合成线程:

af77c5a2348af7a2590a586e26c587e2.png

这里先介绍视口的概念:通常一个页面可能很大,但是用户只能看到其中的一部分,我们把用户可以看到的这个部分叫做视口(viewport);有的图层可以很大,比如有的页面使用滚动条要滚动好久才能滚动到底部,但是通过视口,用户只能看到页面的很小一部分,所以在这种情况下,要绘制出所有图层内容就会产生太大的开销,而且也没有必要。基于这个原因,合成线程会将图层划分为图块(tile),这些图块的大小通常是 256x256 或者 512x512:

7e734fe3b34cda11a0d251bf90c7329f.png

所谓栅格化,就是指将图块转换为位图。合成线程会按照视口附近的图块来优先生成位图,实际生成位图的操作是由栅格化来执行的。而图块是栅格化执行的最小单位。渲染进程维护了一个栅格化的线程池,所有的图块栅格化都是在线程池内执行的,运行方式如下图所示:

8aebfad2b3f7c00cc331847ba493956d.png

栅格化过程都会使用 GPU 来加速生成,使用 GPU 生成位图的过程叫快速栅格化,或者 GPU 栅格化,生成的位图被保存在 GPU 内存中。GPU 操作是运行在 GPU 进程中,如果栅格化操作使用了 GPU,那么最终生成位图的操作是在 GPU 中完成的,这就涉及到了跨进程操作:

09305bfcd1203f911dc0c3009b1f8ad2.png

(7)合成和显示

合成的图层会被提交给浏览器进程,浏览器进程里会执行显示合成(Display Compositor),也就是将所有的图层合成为可以显示的页面图片。最终显示器显示的就是浏览器进程中合成的页面图片;

一旦所有图块都被光栅化,合成线程就会生成一个绘制图块的命令——“DrawQuad”,然后将该命令提交给浏览器进程。浏览器进程里面有一个叫 viz 的组件,用来接收合成线程发过来的 DrawQuad 命令,然后根据 DrawQuad 命令,将其页面内容绘制到内存中,最后再将内存显示在屏幕上。

最后通过下图总结所有渲染流程:

b8b579543990d7be1cbc9961c0c9a9e9.png

结合上图,一个完整的渲染流程大致可总结为如下:

  • 渲染进程将 HTML 内容转换为能够读懂的 DOM 树结构。

  • 渲染引擎将 CSS 样式表转化为浏览器可以理解的 styleSheets,计算出 DOM 节点的样式。

  • 创建布局树,并计算元素的布局信息。对布局树进行分层,并生成分层树。

  • 为每个图层生成绘制列表,并将其提交到合成线程。

  • 合成线程将图层分成图块,并在光栅化线程池中将图块转换成位图。

  • 合成线程发送绘制图块命令 DrawQuad 给浏览器进程。

  • 浏览器进程根据 DrawQuad 消息生成页面,并显示到显示器上。

最后看看三个和渲染流水线相关的概念——“重排”“重绘”和“合成”。

48c4229088ea35d0ed79f86983760152.png

重排需要更新完整的渲染流水线,所以开销也是最大的。

e5ef757332920a4643244addf822fedc.png

重绘省去了布局和分层阶段,所以执行效率会比重排操作要高一些。

3b4bd5f6158d75c0d74ff949dca730a6.png

避开重排和重绘阶段,直接在非主线程上执行合成动画操作。这样的效率是最高的,因为是在非主线程上合成,并没有占用主线程的资源,另外也避开了布局和绘制两个子阶段,所以相对于重绘和重排,合成能大大提升绘制效率。

备注

理解合成和显示

可以把一张网页想象成是由很多个图片叠加在一起的,每个图片就对应一个图层,Chrome 合成器最终将这些图层合成了用于显示页面的图片。如果你熟悉 PhotoShop 的话,就能很好地理解这个过程了,PhotoShop 中一个项目是由很多图层构成的,每个图层都可以是一张单独图片,可以设置透明度、边框阴影,可以旋转或者设置图层的上下位置,将这些图层叠加在一起后,就能呈现出最终的图片了。在这个过程中,将素材分解为多个图层的操作就称为分层,最后将这些图层合并到一起的操作就称为合成。所以,分层和合成通常是一起使用的。考虑到一个页面被划分为两个层,当进行到下一帧的渲染时,上面的一帧可能需要实现某些变换,如平移、旋转、缩放、阴影或者 Alpha 渐变,这时候合成器只需要将两个层进行相应的变化操作就可以了,显卡处理这些操作驾轻就熟,所以这个合成过程时间非常短。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在Android中,WebView是用于显示网页内容的组件,而且它还具有加载HTML页面的功能。通常情况下,WebView会将网页内容在应用内部显示,但是我们也可以通过设置WebView的方式,使其在外部浏览器中打开网页。 要在外部浏览器中打开网页,我们可以通过重写WebViewWebViewClient类中的shouldOverrideUrlLoading方法来实现。该方法会在WebView加载URL之前被调用,并返回一个boolean值,该值表示是否由WebView处理URL。 代码示例如下: webView.setWebViewClient(new WebViewClient(){ @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { // 使用外部浏览器打开网页 Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); startActivity(intent); return true; // 返回true表示由外部浏览器处理URL } }); 在上述代码中,我们首先创建了一个WebViewClient对象,并重写了其中的shouldOverrideUrlLoading方法。在该方法中,我们创建了一个Intent对象,使用ACTION_VIEW action,将URL转换为Uri,并通过该Intent启动外部浏览器来打开网页。然后我们返回true,表示由外部浏览器处理URL。 最后,我们将创建的WebViewClient对象设置给WebView,这样在加载网页时,WebView会先调用shouldOverrideUrlLoading方法来判断是否在外部浏览器中打开。若返回true,则会在外部浏览器中打开网页;若返回false,则会在WebView显示网页内容。 通过上述方法,我们可以实现在Android中使用WebView加载H5页面时,使用外部浏览器打开网页
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值