一、JsBridge
JsBridge调用过程
-
Native初始化webview,注册Handler;加载页面完成后,将WebViewJavascriptBridge.js文件注入页面。查询消息队列是否有信息需要被接收。
-
H5页面初始化,注册Handler,查询消息队列是否有信息需要别接收。
-
用户操作,H5调用本地功能:Js将消息内容放在
sendMessageQueue
中,并设置iframe的src为yy://__QUEUE_MESSAGE__/
-
Webview设置的WebViewClient拦截到约定url,调用Webview的刷新消息队列的方法
flushMessageQueue
,此方法就是加载了一个url:javascript:WebViewJavascriptBridge._fetchQueue();
,这也是Js中定义的方法,另外定义了一个回调;回调方法主要做了两件事:①判断Native是否为此返回数据保有响应回调操作,若有,则执行,若没有,则为判断callId,不为空时为这个callId初始化一个回调。②通过handlername判断是否为默认的Handler还是自定义的Handler,调用相应Handler的handler方法,入参为消息数据内容和第一步中定义的回调。【这段较为难消化,需要阅读代码来理解】 -
Js中
_fetchQueue
设置了iframe的src,内容为:yy://return/_fetchQueue/
+第二步中放入sendMessageQueue
中的消息内容。 -
WebViewClient拦截到url为
yy://return/
,调用WebView的handlerReturnData
方法;通过url中定义的方法名,找到第四个步骤中定义的回调,并调用。回调方法走完后,删除此回调方法。 -
如果Js在调用Handler的时候设置了回调方法,也就是在第四步骤中的含有callId,就会调用queueMessage的方法,然后往下就是走Native给Js发送消息的步骤。
Ps: Native给Js发送消息的步骤跟上述从第三步骤到第七步骤完全相同,只不过Native和Js对象调换位置即可。
JsBridge的核心
JsBridge之所以能实现Native与Js相互调用的功能,其核心实现其实就是:
-
拦截Url
-
load url("javascript:js_method()");
先说第二点,Native调用Js,通过加载以javascript:
开头的url即可实现调用Js的方法。这个很好理解,在web中也是通过这种方式调用Js的方法的。
然后细说下第一点的实现:
-
向body中添加一个不可见的iframe元素。通过拦截url的方法来执行相应的操作,但是页面本身不能跳转,所以改变一个不可见的iframe的src就可以让webview拦截到url,而用户是无感知的。
-
拦截url。通过
shouldOverrideUrlLoading
来拦截约定规则的Url,再做具体操作。
Ps: 添加iframe是H5自身可实现的,但是如果H5来实现的话,需要每个页面实现,且耦合较高;因此放在库里,通过加载完成注入的方式,则会降低耦合
二、浏览器渲染网页的具体流程
2.1 构建DOM树
当浏览器客户端从服务器那接受到HTML文档后,就会遍历文档节点然后生成DOM树,DOM树结构和HTML标签一一对应。需要注意记下几点:
-
DOM树在构建的过程中可能会被CSS和JS的加载而执行阻塞。(这在后面会详细介绍。)
-
display:none 的元素也会在DOM树中。
-
注释也会在DOM树中
-
script标签会在DOM树中
2.2 CSS解析
浏览器会解析CSS文件并生成CSS规则树,在过程中,每个CSS文件都会被分析成StyleSheet对象,每个对象都包括CSS规则,CSS规则对象包括对应的选择器和声明对象以及其他对象。
在这个过程需要注意的是:
-
CSS解析可以与DOM解析同进行。
-
CSS解析与script的执行互斥 。
-
在Webkit内核中进行了script执行优化,只有在JS访问CSS时才会发生互斥。
2.3 构建渲染树(Rendr tree construction)
通过DOM树和CSS规则树,浏览器就可以通过它两构建渲染树了。浏览器会先从DOM树的根节点开始遍历每个可见节点,让后对每个可见节点找到适配的CSS样式规则并应用。具体的规则有以下几点需要注意:
-
Render Tree和DOM Tree不完全对应。
-
display: none的元素不在Render Tree中
-
visibility: hidden的元素在Render Tree中
2.4 渲染树布局(layout of the render tree)
布局阶段会从渲染树的更节点开始遍历,由于渲染树的每个节点都是一个Render Object对象,包含宽高,位置,背景色等样式信息。所以浏览器就可以通过这些样式信息来确定每个节点对象在页面上的确切大小和位置,布局阶段的输出就是我们常说的盒子模型,它会精确地捕获每个元素在屏幕内的确切位置与大小。需要注意的是:
-
float元素,absoulte元素,fixed元素会发生位置偏移。
-
我们常说的脱离文档流,其实就是脱离Render Tree。
2.5 渲染树绘制(Painting the render tree)
在绘制阶段,浏览器会遍历渲染树,调用渲染器的paint()方法在屏幕上显示其内容。渲染树的绘制工作是由浏览器的UI后端组件完成的。
三、浏览器渲染网页的那些事儿
3.1 阻塞渲染
前面我们有提到这方面的问题,说到资源的阻塞我们清楚的是,现代浏览器总是并行加载自语言。例如当HTML解析器被脚本阻塞时,解析器虽然会停止构建DOM,但仍然会辨识该脚本后面的资源,并进行预加载。且由于以下两点。浏览器会延迟 JavaScript 的执行和 DOM 构建:
-
CSS 被默认被视为阻塞渲染的资源,因此浏览器将在 CSSOM 构建完毕前不会渲染任何已处理的内容。
-
JavaScript 不仅可以读取和修改 DOM 属性,还可以读取和修改 CSSOM 属性,因此CSS解析与script的执行互斥。
正是由于以上这些原因,script标签的位置很重要我们在实际开发中应该尽量坚持以下两个原则:
-
在引入顺序上,CSS 资源先于 JavaScript 资源。
-
JavaScript 应尽量少的去影响 DOM 的构建。
3.2 回流和重绘(reflow和repaint)
我们都知道HTML默认是流式布局的,但CSS和JS会打破这种布局,改变DOM的外观样式以及大小和位置。因此我们就需要知道两个概念:
-
reflow(回流):当浏览器发现某个部分发生了变化从而影响了布局,这个时候就需要倒回去重新渲染,大家称这个回退的过程叫 reflow。 常见的reflow是一些会影响页面布局的操作,诸如Tab,隐藏等。reflow 会从 html 这个 root frame 开始递归往下,依次计算所有的结点几何尺寸和位置,以确认是渲染树的一部分发生变化还是整个渲染树。reflow几乎是无法避免的,因为只要用户进行交互操作,就势必会发生页面的一部分的重新渲染,且通常我们也无法预估浏览器到底会reflow哪一部分的代码,因为他们会相互影响。
-
repaint(重绘): repaint则是当我们改变某个元素的背景色、文字颜色、边框颜色等等不影响它周围或内部布局的属性时,屏幕的一部分要重画,但是元素的几何尺寸和位置没有发生改变。
需要注意的是,display:none 会触发 reflow,而visibility: hidden属性则并不算是不可见属性,它的语义是隐藏元素,但元素仍然占据着布局空间,它会被渲染成一个空框,这在我们上面有提到过。所以visibility:hidden 只会触发 repaint,因为没有发生位置变化。
我们不能避免reflow,但还是能通过一些操作来减少回流:
-
用transform做形变和位移.
-
通过绝对位移来脱离当前层叠上下文,形成新的Render(渲染) Layer。
另外有些情况下,比如修改了元素的样式,浏览器并不会立刻reflow 或 repaint 一次,而是会把这样的操作积攒一批,然后做一次 reflow,这又叫异步 reflow 或增量异步 reflow。但是在有些情况下,比如resize 窗口,改变了页面默认的字体等。对于这些操作,浏览器会马上进行 reflow。
3.3 几条关于优化渲染效率的建议
结合上文和我看到的一些文章,有以下几点可以优化渲染效率
-
合法地去书写 HTML 和 CSS ,且不要忘了文档编码类型。
-
样式文件应当在 head 标签中,而脚本文件在 body 结束前,这样可以防止阻塞的方式。
-
简化并优化CSS选择器,尽量将嵌套层减少到最小。
-
尽量减少在 JavaScript 中进行DOM操作。
-
修改元素样式时,更改其class属性是性能最高的方法。
-
尽量用 transform 来做形变和位移