浅析 requestAnimationFrame
相信现在绝大多数人在 JavaScript 中绘制动画已经在使用 requestAnimationFrame 了,关于 requestAnimationFrame 的种种就不多说了,关于这个 API 的资料,详见 http://www.w3.org/TR/animation-timing/,https://developer.mozilla.org/en/docs/Web/API/window.requestAnimationFrame。
如果我们把时钟往前拨到引入 requestAnimationFrame 之前,如果在 JavaScript 中要实现动画效果,怎么办呢?无外乎使用 setTimeout 或 setInterval。那么问题就来了:
● 如何确定正确的时间间隔(浏览器、机器硬件的性能各不相同)?
● 毫秒的不精确性怎么解决?
● 如何避免过度渲染(渲染频率太高、tab 不可见等等)?
开发者可以用很多方式来减轻这些问题的症状,但是彻底解决,这个、基本、很难。
归根到底,问题的根源在于时机。对于前端开发者来说,setTimeout 和 setInterval 提供的是一个等长的定时器循环(timer loop),但是对于浏览器内核对渲染函数的响应以及何时能够发起下一个动画帧的时机,是完全不了解的。对于浏览器内核来讲,它能够了解发起下一个渲染帧的合适时机,但是对于任何 setTimeout 和 setInterval 传入的回调函数执行,都是一视同仁的,它很难知道哪个回调函数是用于动画渲染的,因此,优化的时机非常难以掌握。悖论就在于,写 JavaScript 的人了解一帧动画在哪行代码开始,哪行代码结束,却不了解应该何时开始,应该何时结束,而在内核引擎来说,事情却恰恰相反,所以二者很难完美配合,直到 requestAnimationFrame 出现。
requestAnimationFrame 是什么?做什么?执行过程?优势?
● 【是什么】HTML5 新增加的 API,类似于 setTimeout 定时器,浏览器专门为动画提供的 API,让 DOM 动画、Canvas 动画、SVG 动画、WebGL 动画等有一个统一的刷新机制。
● 【做什么】浏览器的渲染页面的标准帧率也为 60FPS(frames/ per second),requestAnimationFrame API 按帧对网页进行重绘。告诉浏览器下一次重绘之前调用回调函数来更新动画 ,回调函数的执行时机,由系统来决定。比如显示器屏幕刷新率为 60Hz,使用requestAnimationFrame API,那么回调函数就每1000ms / 60 ≈ 16.7ms执行一次;
● 【执行过程】
○ 首先判断document.hidden属性是否为true(页面是否可见),页面处于可见状态才会执行后面步骤
○ 浏览器清空上一轮的动画函数
○ requestAnimationFrame将回调函数追加到动画帧请求回调函数列表的末尾
○ 当浏览器再执行列表中的回调函数的时候,判断每个元组的 callback 的cancelled,如果为false,则执行 callback
● 【优势】
○ requestAnimationFrame采用系统时间间隔,保持最佳绘制效率。不会因为间隔时间过短,造成过度绘制,增加开销;也不会因为间隔时间过长,使动画卡顿
○ 从实现的功能和使用方法上,requestAnimationFrame与定时器setTimeout都相似,所以说其优势是同setTimeout实现的动画相比。
■ 提升性能,防止掉帧
■ 节约资源,节省电源
■ 函数节流
本人很喜欢 requestAnimationFrame 这个名字,因为起得非常直白 -- request animation frame,对于这个 API 最好的解释就是名字本身了。这样一个 API,你传入的 API 不是用来渲染一帧动画,你上街都不好意思跟人打招呼。
requestAnimationFrame 怎么实现的?
由于本人是个喜欢阅读代码的人,为了体现自己好学的态度,特意读了下 Chrome 的代码去了解它是怎么实现 requestAnimationFrame 的(代码基于 Android 4.4):
int Document::requestAnimationFrame(PassRefPtr<RequestAnimationFrameCallback> callback)
{
if (!m_scriptedAnimationController) {
m_scriptedAnimationController = ScriptedAnimationController::create(this);
// We need to make sure that we don't start up the animation controller on a background tab, for example.
if (!page())
m_scriptedAnimationController->suspend();
}
return m_scriptedAnimationController->registerCallback(callback);
}
仔细看看就觉得底层实现意外地简单,生成一个 ScriptedAnimationController 的实例,然后注册这个 callback。那我们就看看 ScriptAnimationController 里面做了些什么:
void ScriptedAnimationController::serviceScriptedAnimations(double monotonicTimeNow)
{
if (!m_callbacks.size() || m_suspendCount)
return;
double highResNowMs = 1000.0 * m_document->loader()->timing()->monotonicTimeToZeroBasedDocumentTime(monotonicTimeNow);
double legacyHighResNowMs = 1000.0 * m_document->loader()->timing()->monotonicTimeToPseudoWallTime(monotonicTimeNow);
// First, generate a list of callbacks to consider. Callbacks registered from this point
// on are considered only for the "next" frame, not this one.
CallbackList callbacks(m_callbacks);
// Invoking callbacks may detach elements from our document, which clears the document's
// reference to us, so take a defensive reference.
RefPtr<ScriptedAnimationController> protector(this);
for (size_t i = 0; i < callbacks.size(); ++i) {
RequestAnimationFrameCallback* callback = callbacks[i].get();
if (!callback->m_firedOrCancelled) {
callback->m_firedOrCancelled = true;
InspectorInstrumentationCookie cookie = InspectorInstrumentation::willFireAnimationFrame(m_document, callback->m_id);
if (callback->m_useLegacyTimeBase)
callback->handleEvent(legacyHighResNowMs);
else
callback->handleEvent(highResNowMs);
InspectorInstrumentation::didFireAnimationFrame(cookie);
}
}
// Remove any callbacks we fired from the list of pending callbacks.
for (size_t i = 0; i < m_callbacks.size();) {
if (m_callbacks[i]->m_firedOrCancelled)
m_callbacks.remove(i);
else
++i;
}
if (m_callbacks.size())
scheduleAnimation();
}
这个函数自然就是执行回调函数的地方了。那么动画是如何被触发的呢?我们需要快速地看一串函数(一个从下往上的 call stack):
void PageWidgetDelegate::animate(Page* page, double monotonicFrameBeginTime)
{
FrameView* view = mainFrameView(page);
if (!view)
return;
view->serviceScriptedAnimations(monotonicFrameBeginTime);
}
void WebViewImpl::animate(double monotonicFrameBeginTime)
{
TRACE_EVENT0("webkit", "WebViewImpl::animate");
if (!monotonicFrameBeginTime)
monotonicFrameBeginTime = monotonicallyIncreasingTime();
// Create synthetic wheel events as necessary for fling.
if (m_gestureAnimation) {
if (m_gestureAnimation->animate(monotonicFrameBeginTime))
scheduleAnimation();
else {
m_gestureAnimation.clear();
if (m_layerTreeView)
m_layerTreeView->didStopFlinging();
PlatformGestureEvent endScrollEvent(PlatformEvent::GestureScrollEnd,
m_positionOnFlingStart, m_globalPositionOnFlingStart, 0, 0, 0,
false, false, false, false);
mainFrameImpl()->frame()->eventHandler()->handleGestureScrollEnd(endScrollEvent);
}
}
if (!m_page)
return;
PageWidgetDelegate::animate(m_page.get(), monotonicFrameBeginTime);
if (m_continuousPaintingEnabled) {
ContinuousPainter::setNeedsDisplayRecursive(m_rootGraphicsLayer, m_pageOverlays.get());
m_client->scheduleAnimation();
}
}
void RenderWidget::AnimateIfNeeded() {
if (!animation_update_pending_)
return;
// Target 60FPS if vsync is on. Go as fast as we can if vsync is off.
base::TimeDelta animationInterval = IsRenderingVSynced() ? base::TimeDelta::FromMilliseconds(16) : base::TimeDelta();
base::Time now = base::Time::Now();
// animation_floor_time_ is the earliest time that we should animate when
// using the dead reckoning software scheduler. If we're using swapbuffers
// complete callbacks to rate limit, we can ignore this floor.
if (now >= animation_floor_time_ || num_swapbuffers_complete_pending_ > 0) {
TRACE_EVENT0("renderer", "RenderWidget::AnimateIfNeeded")
animation_floor_time_ = now + animationInterval;
// Set a timer to call us back after animationInterval before
// running animation callbacks so that if a callback requests another
// we'll be sure to run it at the proper time.
animation_timer_.Stop();
animation_timer_.Start(FROM_HERE, animationInterval, this, &RenderWidget::AnimationCallback);
animation_update_pending_ = false;
if (is_accelerated_compositing_active_ && compositor_) {
compositor_->Animate(base::TimeTicks::Now());
} else {
double frame_begin_time = (base::TimeTicks::Now() - base::TimeTicks()).InSecondsF();
webwidget_->animate(frame_begin_time);
}
return;
}
TRACE_EVENT0("renderer", "EarlyOut_AnimatedTooRecently");
if (!animation_timer_.IsRunning()) {
// This code uses base::Time::Now() to calculate the floor and next fire
// time because javascript's Date object uses base::Time::Now(). The
// message loop uses base::TimeTicks, which on windows can have a
// different granularity than base::Time.
// The upshot of all this is that this function might be called before
// base::Time::Now() has advanced past the animation_floor_time_. To
// avoid exposing this delay to javascript, we keep posting delayed
// tasks until base::Time::Now() has advanced far enough.
base::TimeDelta delay = animation_floor_time_ - now;
animation_timer_.Start(FROM_HERE, delay, this, &RenderWidget::AnimationCallback);
}
}
特别说明:RenderWidget 是在 ./content/renderer/render_widget.cc 中(content::RenderWidget)而非在 ./core/rendering/RenderWidget.cpp 中。笔者最早读 RenderWidget.cpp 还因为其中没有任何关于 animation 的代码而困惑了很久。
看到这里其实 requestAnimationFrame 的实现原理就很明显了:
● 注册回调函数
● 浏览器更新时触发 animate
● animate 会触发所有注册过的 callback
这里的工作机制可以理解为所有权的转移,把触发帧更新的时间所有权交给浏览器内核,与浏览器的更新保持同步。这样做既可以避免浏览器更新与动画帧更新的不同步,又可以给予浏览器足够大的优化空间。
在往上的调用入口就很多了,很多函数(RenderWidget::didInvalidateRect,RenderWidget::CompleteInit等)会触发动画检查,从而要求一次动画帧的更新。
这里一张图说明 requestAnimationFrame 的实现机制(来自官方):