渲染主循环(main loop)和requestAnimationFrame

优质
小牛编辑
129浏览
2023-12-01

曾经写过一段JavaScript代码,因为涉及到需要循环调用某个函数来实现动画的功能,很自然地,我想到了使用setInterval函数(或者setTimeout,大家是否有类似经历呢?),然后心满意足地很快的搞定。结束后,朋友帮忙阅读了一下代码,他提醒我是不是可以考虑使用requestAnimationFrame。之前一直知道这个函数,也知道一些它的一些优点,问题是为什么呢?本着追究到底的精神,决定还是去阅读一下WebKit相关代码和一些相关文档,了解它们背后的故事。好吧,本章我将和大家一起来学习和探讨这背后的故事…

背景

接触过JavaScript的读者应该有过了解或者使用setTimeout或者setInterval的经历,其功能是在每个时间间隔之后一次性或者重复多次执行一段JavaScript代码(称为回调函数),以完成特定的动画要求。但是,这里面有还有些疑问:

  1. 时间间隔应该设置为多少才合适呢?跟屏幕的分辨率有关系吗?
  2. 设置的时间间隔会按照预想的执行吗?动画会被平滑地显示出效果吗?
  3. 回调函数是复杂的好还是简单的好呢?应该如何编写才能效率高呢?
  4. 与平台和浏览器相关吗?如何适应不同平台呢?

这对setTimeout和setInterval来说很重要。如果对mainloop机制和渲染机制有一定了解的读者来说,上面这几条其实是非常难做到地,哪怕是较为接近理想的结果。

幸运地是,总是有聪明的人来帮助大家解决难题。对问题提出一个漂亮解决方案的是mozilla的Robert O’Callahan。他的灵感和依据来源于CSS。CSS是知道动画什么时候发生,所以能够较为准确的知道什么时候刷新UI。对于JavaScript来说,是不是也可以根据类似的机制呢?答案是肯定地。其做法是增加一个新的方法requestAnimationFrame, 该方法告诉浏览器JavaScript想发起一个动画帧,然后在动画帧绘制之前,需要做一些动作,这样浏览器可以根据需要来优化自己的mainloop机制和调用时间点,以达到较好地平衡效果。 好吧,下面来看看mainloop机制及其工作原理。

渲染mainloop

因为chromium是多进程的结构(参看Chromium多进程架构篇),所以,跟一般浏览器不一样的是,Browser进程UI用户界面的mainloop和Renderer进程的主线程的mainloop不是同一个,分别位于两个不同的进程,所以UI和渲染可以互相不影响,听起来这好像很不错,是的,但是问题依然存在,那就是Renderer进程的渲染工作和JavaScript的执行工作都在其主线程中,由mainloop来负责调度完成,所以竞争依然存在。

大致过程是一个大的循环加上一个事件队列,具体的过程,如下图所示。当队列中有事件时,从队列中取出第一个事件,设置相应的状态信息,处理该事件及其对应的处理函数,直到该函数处理完后,才重新检查队列中是否有事件。如果有,继续处理;如果没有,则继续等待。这其中可以看出,如果队列中事件多的时候,那么很多事件可能来不及处理,从而造成比较大的延时,因而事件的平均等待时间会比较长。同时,如果事件的处理函数需要的时间很长,就会造成后面的事件一直在等待,同样会增加事件的平均等待时间。而当队列比较空闲时或者事件的处理函数需要的时间比较短,则事件的平均等待时间会相对小很多。

enter image description here

WebKit和Chromium中的实现

理解了mainloop之后,下面来看一看setTimeout和setInterval的实现。

来看一下它们的实现:WebKit中setTimeout和setInterval的实现机制是类似的,区别在于后者是重复性的,见下图所示的类图关系。

WebKit会为DOM中的每个setTimeout和setInterval的调用创建一个DOMTimer,而后该对象会由存储TLS(thread localstorage)中的ThreadTimers负责管理,其内部其实是一个最小堆,每次取timeout时间最小的,同时,时间相同的Timer可以合并。

当Timer超时后,Chromium清除该Timer对象,同时调用相应的回调函数,回调函数通常会更新页面的样式和布局,这会触发relayout,从而触发立即重新绘制一个新帧。

enter image description here

结合上面的描述,我们大致地总结setTimeout和setInterval主要不足就是:

  1. setTimeout和setInterval从不考虑浏览器内部发生了其他什么事,它只要求浏览器在某个时间之后调用它的回调函数,无论浏览器很繁忙或者页面被隐藏(虽然某些浏览器做了这方面的优化,例如chromium);
  2. setTimeout和setInterval只要求浏览器做什么,而不管浏览器能不能做到(例如mainloop有很多事件需要处理),这有点强人所难,而且会带来极大的资源浪费。举个例子,例如屏幕的刷新率是60HZ,但是设置的时间间隔是5ms,其实对用户来说根本看不到这些变化,但是额外需要消耗更多的CPU资源,太不环保了…
  3. setTimeout和setInterval可能是编程风格方面的考虑。如果每一帧可能在不同的代码出需要设置回调函数,一个方法是统一到一个地方,但是这有点勉为其难,另一个方法是分别用setInterval设置它们,这个方法的问题是,浏览器可能需要计算更多次,刷新更多次的屏幕,唉。

现在再来看看requestAnimationFrame的实现,看看其如何解决这些不足之处的。其原理就是其会申请绘制下一帧,至于什么时候不知道,由浏览器决定,只需要浏览器在绘制下一帧前执行其设置的回调函数,完成JavaScript对动画所做的设置和逻辑即可。基本过程是这样的:

  1. JavaScript调用requestAnimationFrame,因而相应的webkit和chromium会调度一个需要绘制下一证的事件,该事件会将requestAnimationFrame的调用上下文和回调函数记录下来;
  2. 上面的请求会触发Chromium更新页面内容的事件,该事件被mainloop调度处理后,会检查是否需要调用动画的相关处理,因为有动画需要处理,所以会依次调用那些回调函数,JavaScript引擎会更新相应的CSS属性或者DOM树修改;
  3. Chromium触发重新计算layout(参看layout章节),更新自己的Renderer树(参看webkit渲染基础章节),而后绘制,完成一帧的渲染。

下图是一个上述过程对应的状态转换图,来源于chromium的官方网站,看着的确比较饶人,可以先理解一下其中几个主要的概念:

  • Floortime:指的是绘制下一帧之前需要等待的事件间隔
  • Invalidation:触发重新绘制请求的操作;
  • scheduleAnimation:JavaScript调用requestAnimationFrame所引起的WebKit内部请求调度动画的操作;
  • 这些状态的转换倒是说明了,requestAnimationFrame可以很好地和Chromium内部的绘制过程结合,从而达到比较好的性能。

enter image description here

为了实现更好的性能,chromium中对requestAnimationFrame有三个设计原则 1. 当页面不可见时,其回调函数不会被调用,这可以减少CPU和GPU的使用率,更环保嘛; 2. 其最大调用频率不会超过60hz,无论屏幕的刷新率是多少,因而回调函数也不会每秒调用超过60次,这是因为60FPS已经能够满足UI流畅的要求了,更频繁的刷新效果不明显; 3. 只有当页面真正开始渲染时,回调函数才会被调用。 为了对比二者的性能上的差异,我测试了GuiMark中HTML5Charting Test benchmark,修改里面一些代码(其缺省使用的是setInterval,改为requestAnimationFrame作对比),从实际测试的效果上看,在Google Chrome中,两者相差不是特别大,使用了requestAnimationFrame的benchmark的FPS大概只好了1~2FPS,所以chrome对timer机制的优化做地应该相当不错。如果你遇到了其他差别比较大的例子,欢迎跟我和大家分享。 Google Chrome对其处理的比较好不代表其他浏览器也是,所以各位还是在编程时候多考虑考虑,多思考思考,为了更好的性能,为了环保…

设计机制带来的编程考虑

最后,结合mainloop和requestAnimationFrame的设计原理和机制,看一看它们带给我们在编写JavaScript代码时有哪些方面的思考和便利: 1. 回调函数不能太大,不能占用太长时间,否则会影响页面的响应和绘制的频率; 2. requestAnimationFrame不需要设置间隔时间,不同刷新率的间隔时间不一样,这完全由浏览器来控制,而不需要JavaScript程序员操心; 3. 回调函数无需合并,程序员可以在任意位置设置回调函数,它们可以被浏览器集中处理,而无需要一个统一的入口。

源文件目录

third_party/WebKit/Source/WebCore/page/
  支持requestAnimationFrame,setTimeout和setInterval的绝大多数基础设施都在这里,建议在该目录下搜索这些关键字即可
third_party/WebKit/Source/WebCore/platform
  Timer方面的一些支持

参考文献

  1. http://dev.chromium.org/developers/design-documents/requestAnimationFrame-implementation
  2. http://www.cnblogs.com/rubylouvre/archive/2011/08/22/2148793.html
  3. http://www.nczonline.net/blog/2011/05/03/better-javascript-animations-with-requestAnimationFrame/
  4. https://developer.mozilla.org/en-US/docs/DOM/window.requestAnimationFrame
  5. http://www.w3.org/TR/animation-timing/#requestAnimationFrame
  6. http://creativejs.com/resources/requestAnimationFrame/
  7. http://www.craftymind.com/factory/guimark2/HTML5ChartingTest.html