理解WebKit和Chromium: 渲染主循环(main loop)和requestAnimationFrame

本文探讨了Chromium渲染主循环的工作原理,并深入分析了requestAnimationFrame与setTimeout/setInterval的区别。通过对WebKit源码的研究,揭示了requestAnimationFrame如何提高动画性能。

转载请注明出处:http://blog.youkuaiyun.com/milado_nju/

# Chromium渲染主循环(mainloop)和requestAnimationFrame

## 概述

曾经写过一段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来负责调度完成,所以竞争依然存在。

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


##WebKit和Chromium中的实现

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

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

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

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


结合上面的描述,我们大致地总结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内部的绘制过程结合,从而达到比较好的性能。


为了实现更好的性能,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

By yongsheng@chromium.org


----- pid 16703 at 2025-09-29 14:48:41.883422888+0530 ----- Cmd line: com.netflix.mediaclient Build fingerprint: 'realme/RMX5110IN/RE6440L1:16/BP2A.250605.015/V.R4T2.4a8a007-2a9cb6f-2a9cb71:user/release-keys' ABI: 'arm64' Build type: optimized Debug Store: 3,16,62172991::ID:0,C:P,T:62153949,N:Finish,D:tname=main;tid=2;prid=5d86522||ID:85,C:E,T:62153949||ID:85,C:S,T:62153947,N:BcRcvReg,D:tname=main;tid=2;act=android.net.conn.CONNECTIVITY_CHANGE;cmp=null;pkg=null;prid=5d86522||ID:84,C:E,T:62153854||ID:84,C:S,T:62153854,N:SchRcvReg,D:tname=binder:16703_1;tid=16337||ID:0,C:P,T:62127227,N:Finish,D:tname=main;tid=2;prid=b38aa6||ID:83,C:E,T:62127226||ID:83,C:S,T:62127225,N:BcRcvReg,D:tname=main;tid=2;act=android.net.conn.CONNECTIVITY_CHANGE;cmp=null;pkg=null;prid=b38aa6||ID:82,C:E,T:62127036||ID:82,C:S,T:62127028,N:SchRcvReg,D:tname=binder:16703_E;tid=16500||ID:81,C:E,T:62070398||ID:81,C:S,T:62070397,N:SvcBind,D:rebind=false;act=null;cmp=ComponentInfo{com.netflix.mediaclient/androidx.work.impl.background.systemjob.SystemJobService};pkg=null||ID:80,C:E,T:62070395||ID:80,C:S,T:62070391,N:SvcCreate,D:name=androidx.work.impl.background.systemjob.SystemJobService;pkg=com.netflix.mediaclient||ID:0,C:P,T:62037866,N:Finish,D:tname=main;tid=2;prid=4b849fd||ID:79,C:E,T:62037866;; suspend all histogram: Sum: 64.261ms 99% C.I. 5.258us-10086.400us Avg: 338.215us Max: 45153us DALVIK THREADS (169): "main" prio=5 tid=1 Native | group="main" sCount=1 ucsCount=0 flags=1 obj=0x74af07a8 self=0xb4000074a6ed3000 | sysTid=16703 nice=-10 cgrp=top-app sched=1073741824/0 handle=0x7573b9d098 | state=t schedstat=( 17928253747 3512985566 275365 ) utm=1230 stm=562 core=5 HZ=100 | stack=0x7fc2d1e000-0x7fc2d20000 stackSize=8188KB | held mutexes= native: #00 pc 06407f50 /product/app/TrichromeLibrary64/TrichromeLibrary64.apk (offset 8dc000) (???) (BuildId: bf5aa6b5b05b13c4c78a2303dff04b981eb23d76) native: #01 pc 067d66d4 /product/app/TrichromeLibrary64/TrichromeLibrary64.apk (offset 8dc000) (???) (BuildId: bf5aa6b5b05b13c4c78a2303dff04b981eb23d76) native: #02 pc 06407cb8 /product/app/TrichromeLibrary64/TrichromeLibrary64.apk (offset 8dc000) (???) (BuildId: bf5aa6b5b05b13c4c78a2303dff04b981eb23d76) native: #03 pc 02156780 /product/app/TrichromeLibrary64/TrichromeLibrary64.apk (offset 8dc000) (???) (BuildId: bf5aa6b5b05b13c4c78a2303dff04b981eb23d76) native: #04 pc 020efd20 /product/app/TrichromeLibrary64/TrichromeLibrary64.apk (offset 8dc000) (???) (BuildId: bf5aa6b5b05b13c4c78a2303dff04b981eb23d76) native: #05 pc 067b1104 /product/app/TrichromeLibrary64/TrichromeLibrary64.apk (offset 8dc000) (???) (BuildId: bf5aa6b5b05b13c4c78a2303dff04b981eb23d76) native: #06 pc 053a11d0 /product/app/TrichromeLibrary64/TrichromeLibrary64.apk (offset 8dc000) (???) (BuildId: bf5aa6b5b05b13c4c78a2303dff04b981eb23d76) native: #07 pc 05302d3c /product/app/TrichromeLibrary64/TrichromeLibrary64.apk (offset 8dc000) (???) (BuildId: bf5aa6b5b05b13c4c78a2303dff04b981eb23d76) native: #08 pc 053b2944 /product/app/TrichromeLibrary64/TrichromeLibrary64.apk (offset 8dc000) (Java_J_N_IZ+296) (BuildId: bf5aa6b5b05b13c4c78a2303dff04b981eb23d76) at J.N.IZ(Native method) at org.chromium.content.browser.BrowserStartupControllerImpl.c(chromium-TrichromeWebViewGoogle6432.aab-stable-720417933:115) at org.chromium.android_webview.AwBrowserProcess.e(chromium-TrichromeWebViewGoogle6432.aab-stable-720417933:82) at WV.i80.run(chromium-TrichromeWebViewGoogle6432.aab-stable-720417933:188) at WV.j80.run(chromium-TrichromeWebViewGoogle6432.aab-stable-720417933:24) at WV.o80.b(chromium-TrichromeWebViewGoogle6432.aab-stable-720417933:7) at com.android.webview.chromium.O.e(chromium-TrichromeWebViewGoogle6432.aab-stable-720417933:108) at com.android.webview.chromium.O.h(chromium-TrichromeWebViewGoogle6432.aab-stable-720417933:85) at com.android.webview.chromium.O.g(chromium-TrichromeWebViewGoogle6432.aab-stable-720417933:1) at com.android.webview.chromium.WebViewChromiumFactoryProvider.j(chromium-TrichromeWebViewGoogle6432.aab-stable-720417933:9) at com.android.webview.chromium.WebViewChromium.init(chromium-TrichromeWebViewGoogle6432.aab-stable-720417933:104) at android.webkit.WebView.<init>(WebView.java:455) at android.webkit.WebView.<init>(WebView.java:370) at android.webkit.WebView.<init>(WebView.java:352) at android.webkit.WebView.<init>(WebView.java:339) at java.lang.reflect.Constructor.newInstance0(Native method) at java.lang.reflect.Constructor.newInstance(Constructor.java:343) at android.view.LayoutInflater.createView(LayoutInflater.java:767) at android.view.LayoutInflater.createView(LayoutInflater.java:689) at com.android.internal.policy.PhoneLayoutInflater.onCreateView(PhoneLayoutInflater.java:58) at android.view.LayoutInflater.onCreateView(LayoutInflater.java:843) at android.view.LayoutInflater.onCreateView(LayoutInflater.java:863) at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:917) at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:874) at android.view.LayoutInflater.rInflate(LayoutInflater.java:1036) at android.view.LayoutInflater.rInflateChildren(LayoutInflater.java:997) at android.view.LayoutInflater.rInflate(LayoutInflater.java:1039) at android.view.LayoutInflater.rInflateChildren(LayoutInflater.java:997) at android.view.LayoutInflater.rInflate(LayoutInflater.java:1039) at android.view.LayoutInflater.rInflateChildren(LayoutInflater.java:997) at android.view.LayoutInflater.inflate(LayoutInflater.java:593) - locked <@addr=0x3946cd0> (a java.lang.Object[]) at android.view.LayoutInflater.inflate(LayoutInflater.java:481) at android.view.LayoutInflater.inflate(LayoutInflater.java:424) at o.aL.d(:751) at o.aF.setContentView(:193) at o.dRD.setContentView(:935) at o.koj.onCreate(:88) at o.kfw.onCreate(:114) at o.kfx.onCreate(:65354) at android.app.Activity.performCreate(Activity.java:9391) at android.app.Activity.performCreate(Activity.java:9363) at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1541) at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:4598) at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:4821) at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:242) at android.app.servertransaction.TransactionExecutor.executeNonLifecycleItem(TransactionExecutor.java:138) at android.app.servertransaction.TransactionExecutor.executeTransactionItems(TransactionExecutor.java:108) at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:85) at android.app.ActivityThread$H.handleMessage(ActivityThread.java:3110) at android.os.Handler.dispatchMessage(Handler.java:115) at android.os.Looper.loopOnce(Looper.java:298) at android.os.Looper.loop(Looper.java:408) at android.app.ActivityThread.main(ActivityThread.java:9952) at java.lang.reflect.Method.invoke(Native method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:613) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1074) DumpLatencyMs: 1.76715
最新发布
10-14
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值