window.requestAnimationFrame()
后浏览器会在下一次重绘前回调,前端在回调中再次调用该函数,则实现了与屏幕帧率一致的回调。
但发现 CPU 占用较高,内存也会缓慢增加,有些怀疑底层实现有问题。
底层实现
从 WebKit LocalDOMWindow::requestAnimationFrame
入口查起。
- MonitorManager 管理了一批 Monitor;
- Monitor 管理了一批 displayID 相同的 MonitorClient;
- Scheduler 继承 MonitorClient;
iOS 中 Monitor 是 DisplayRefreshMonitorIOS:
ini
DisplayRefreshMonitorIOS::startNotificationMechanism
-m_handler = adoptNS([[WebDisplayLinkHandler alloc] initWithMonitor:this]);
@implementation WebDisplayLinkHandler
- (id)initWithMonitor:(DisplayRefreshMonitorIOS*)monitor {
if (self = [super init]) {
m_monitor = monitor;
// Note that CADisplayLink retains its target (self), so a call to -invalidate is needed on teardown.
m_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(handleDisplayLink:)];
[m_displayLink addToRunLoop:WebThreadNSRunLoop() forMode:NSDefaultRunLoopMode];
m_displayLink.preferredFramesPerSecond = DisplayLinkFramesPerSecond;
}
return self;
}
- (void)invalidate {
[m_displayLink invalidate];
m_displayLink = nullptr;
}
底层果然是 CADisplayLink 去实现重绘时机捕获,但这里 CADisplayLink 与 WebDisplayLinkHandler 形成了循环引用。继续排查:
scss
m_monitors.append(DisplayRefreshMonitorWrapper { WTFMove(monitor) });
struct DisplayRefreshMonitorWrapper {
~DisplayRefreshMonitorWrapper() {
if (monitor)
monitor->stop();
}
RefPtr<DisplayRefreshMonitor> monitor;
};
MonitorManager 添加 Monitor 时有个 Wrapper 类,而 Wrapper 在析构时会调用 Monitor 的 stop 函数,进而打破循环引用。
回调分发
分发起点:
ini
void DisplayRefreshMonitorIOS::displayLinkCallbackFired() {
displayLinkFired(m_currentUpdate);
m_currentUpdate = m_currentUpdate.nextUpdate();
}
constexpr WebCore::FramesPerSecond DisplayLinkFramesPerSecond = 60;
m_currentUpdate = { 0, DisplayLinkFramesPerSecond };
currentUpdate 是个当前帧数与帧率的结构体:
arduino
// Used to represent a given update. An value of { 3, 60 } indicates that this is the third update in a 1-second interval
// on a 60fps cadence. updateIndex will reset to zero every second, so { 59, 60 } is followed by { 0, 60 }.
struct DisplayUpdate {
unsigned updateIndex { 0 };
FramesPerSecond updatesPerSecond { 0 };
DisplayUpdate nextUpdate() const {
return { (updateIndex + 1) % updatesPerSecond, updatesPerSecond };
}
...
由此可见,requestAnimationFrame 的帧率上限最多 60。
后续的分发逻辑就没啥特殊的了。
结论
requestAnimationFrame 底层基于 CADisplayLink 实现,不存在内存泄露,帧率上限 60。
回调后涉及 JS 引擎对 Dom 树的刷新等,JS 是解释执行语言,每秒 60 次高频调用确实有些成本。而由于垃圾回收机制的滞后性,缓慢的内存增量可能仅是来不及回收。
JS 引擎层面的问题就不深究了,我们能做的就是尽量避免使用 requestAnimationFrame,若非要获取帧刷新回调,应该设计为按需触发循环,禁止时挂起。