Chromium的Render进程接收到Browser进程分发过来的输入事件之后,会在Compoistor线程中处理掉滑动和捏合手势这两种特殊的输入事件,其它类型的输入事件则交给Main线程处理。Main线程又会进一步将输入事件分发给WebKit处理。WebKit则根据输入事件发生的位置在网页中找到对应的HTML元素进行处理。本文接下来详细分析Chromium分发输入事件给WebKit处理的过程。
以Touch事件为例。WebKit接收到Chromium分发给它的Touch事件通知后,所做的第一件事情是做Hit Test,也就是检测Touch事件发生在哪一个HTML元素上,然后再将接收到的Touch事件分发给该它处理,如图1所示:
图1 WebKit处理Touch事件的过程
HTML元素对应于网页DOM Tree中的Node。要从DOM Tree中找到输入事件的目标Node,必须要知道所有Node的Z轴位置大小,然后按照从上到下的顺序进行Hit Test。从前面Chromium网页Graphics Layer Tree创建过程分析一文可以知道,DOM Tree中的Node在Z轴上的实际位置,除了跟它们本身的Z-Index属性值有关之外,还与它们所处理的Stacking Context的Z-Index值有关。网页的Render Layer Tree记录了所有的Stacking Context,因此,WebKit要从网页的Render Layer Tree入手,才能在DOM Tree中找到正确的目标Node处理当前发生的输入事件。
接下来,我们就从Compoistor线程将输入事件分发给Main线程开始,分析WebKit接收和处理输入事件的过程。从前面Chromium网页滑动和捏合手势处理过程分析一文可以知道,Compoistor线程是在InputEventFilter类的成员函数ForwardToHandler中将它不处理的输入事件发分给Main线程处理的,如下所示:
void InputEventFilter::ForwardToHandler(const IPC::Message& message) {
......
int routing_id = message.routing_id();
InputMsg_HandleInputEvent::Param params;
if (!InputMsg_HandleInputEvent::Read(&message, ¶ms))
return;
const WebInputEvent* event = params.a;
ui::LatencyInfo latency_info = params.b;
.......
InputEventAckState ack_state = handler_.Run(routing_id, event, &latency_info);
if (ack_state == INPUT_EVENT_ACK_STATE_NOT_CONSUMED) {
......
IPC::Message new_msg = InputMsg_HandleInputEvent(
routing_id, event, latency_info, is_keyboard_shortcut);
main_loop_->PostTask(
FROM_HERE,
base::Bind(&InputEventFilter::ForwardToMainListener,
this, new_msg));
return;
}
......
}
这个函数定义在文件external/chromium_org/content/renderer/input/input_event_filter.cc中。
InputEventFilter类的成员函数ForwardToHandler的详细分析可以参考前面Chromium网页滑动和捏合手势处理过程分析一文。对于Compositor线程不处理的输入事件,InputEventFilter类的成员函数ForwardToHandler会将它重新封装在一个InputMsg_HandleInputEvent消息中。这个InputMsg_HandleInputEvent消息又会进一步封装在一个Task中,并且发送给Main线程的消息队列。这个Task绑定了InputEventFilter类的成员函数ForwardToMainListener。
这意味着接下来InputEventFilter类的成员函数ForwardToMainListener就会在Main线程中被调用,用来处理Compositor线程分发过来的输入事件,如下所示:
void InputEventFilter::ForwardToMainListener(const IPC::Message& message) {
main_listener_->OnMessageReceived(message);
}
这个函数定义在文件external/chromium_org/content/renderer/input/input_event_filter.cc中。
从前面Chromium网页滑动和捏合手势处理过程分析一文可以知道,InputEventFilter类的成员变量main_listener_指向的是一个RenderThreadImpl对象。这个RenderThreadImpl对象描述的就是Render进程中的Render Thread,也就是Main Thread。InputEventFilter类的成员函数ForwardToMainListener调用这个RenderThreadImpl对象的成员函数OnMessageReceived处理参数message描述的输入事件。
RenderThreadImpl类的成员函数OnMessageReceived是从父类ChildThread继承下来的,它的实现如下所示:
bool ChildThread::OnMessageReceived(const IPC::Message& msg) {
.....
bool handled = true;
IPC_BEGIN_MESSAGE_MAP(ChildThread, msg)
......
IPC_MESSAGE_UNHANDLED(handled = false)
IPC_END_MESSAGE_MAP()
if (handled)
return true;
......
return router_.OnMessageReceived(msg);
}
这个函数定义在文件external/chromium_org/content/child/child_thread.cc 中。
ChildThread类的成员函数OnMessageReceived首先检查参数msg描述的消息是否要由自己处理。如果不处理,那么就再分发给注册在它里面的Route进行处理。
从前面Chromium网页Frame Tree创建过程分析一文可以知道,Render进程会为每一个网页创建一个RenderViewImpl对象,用来代表网页所加载在的控件。这个RenderViewImpl对象在初始化的过程中,会通过父类RenderWidget的成员函数DoInit将自己注册为Render Thread中的一个Route,如下所示:
bool RenderWidget::DoInit(int32 opener_id,
WebWidget* web_widget,
IPC::SyncMessage* create_widget_message) {
......
bool result = RenderThread::Get()->Send(create_widget_message);
if (result) {
RenderThread::Get()->AddRoute(routing_id_, this);
.......
return true;
}
......
}
这个函数定义在文件external/chromium_org/content/renderer/render_widget.cc中。
这意味着前面分析的RenderThreadImpl类的成员函数OnMessageReceived会将接收到的、自己又不处理的消息分发给RenderWidget类处理。RenderWidget类通过成员函数OnMessageReceived接收RenderThreadImpl类分发过来的消息,并且判断分发过来的消息是否与输入事件有关,也就是判断是否为一个类型为InputMsg_HandleInputEvent的消息。如果是的话,那么就会进行处理。如下所示:
bool RenderWidget::OnMessageReceived(const IPC::Message& message) {
bool handled = true;
IPC_BEGIN_MESSAGE_MAP(RenderWidget, message)
IPC_MESSAGE_HANDLER(InputMsg_HandleInputEvent, OnHandleInputEvent)
......
IPC_MESSAGE_UNHANDLED(handled = false)
IPC_END_MESSAGE_MAP()
return handled;
}
这个函数定义在文件external/chromium_org/content/renderer/render_widget.cc中。
RenderWidget类通过成员函数OnMessageReceived会将类型为InputMsg_HandleInputEvent的消息分发给另外一个成员函数OnHandleInputEvent处理,如下所示:
void RenderWidget::OnHandleInputEvent(const blink::WebInputEvent* input_event,
const ui::LatencyInfo& latency_info,
bool is_keyboard_shortcut) {
......
base::TimeTicks start_time;
if (base::TimeTicks::IsHighResNowFastAndReliable())
start_time = base::TimeTicks::HighResNow();
......
bool prevent_default = false;
if (WebInputEvent::isMouseEventType(input_event->type)) {
const WebMouseEvent& mouse_event =
*static_cast<const WebMouseEvent*>(input_event);
......
prevent_default = WillHandleMouseEvent(mouse_event);
}
if (WebInputEvent::isKeyboardEventType(input_event->type)) {
......
#if defined(OS_ANDROID)
// The DPAD_CENTER key on Android has a dual semantic: (1) in the general
// case it should behave like a select key (i.e. causing a click if a button
// is focused). However, if a text field is focused (2), its intended
// behavior is to just show the IME and don't propagate the key.
// A typical use case is a web form: the DPAD_CENTER should bring up the IME
// when clicked on an input text field and cause the form submit if clicked
// when the submit button is focused, but not vice-versa.
// The UI layer takes care of translating DPAD_CENTER into a RETURN key,
// but at this point we have to swallow the event for the scenario (2).
const WebKeyboardEvent& key_event =
*static_cast<const WebKeyboardEvent*>(input_event);
if (key_event.nativeKeyCode == AKEYCODE_DPAD_CENTER &&
GetTextInputType() != ui::TEXT_INPUT_TYPE_NONE) {
OnShowImeIfNeeded();
prevent_default = true;
}
#endif
}
if (WebInputEvent::isGestureEventType(input_event->type)) {
const WebGestureEvent& gesture_event =
*static_cast<const WebGestureEvent*>(input_event);
......
prevent_default = prevent_default || WillHandleGestureEvent(gesture_event);
}
bool processed = prevent_default;
if (input_event->type != WebInputEvent::Char || !suppress_next_char_events_) {
suppress_next_char_events_ = false;
if (!processed && webwidget_)
processed = webwidget_->handleInputEvent(*input_event);
}
// If this RawKeyDown event corresponds to a browser keyboard shortcut and
// it's not processed by webkit, then we need to suppress the upcoming Char
// events.
if (!processed && is_keyboard_shortcut)
suppress_next_char_events_ = true;
InputEventAckState ack_result = processed ?
INPUT_EVENT_ACK_STATE_CONSUMED : INPUT_EVENT_ACK_STATE_NOT_CONSUMED;
......
// dispatch compositor-handled scroll gestures.
bool event_type_can_be_rate_limited =
input_event->type == WebInputEvent::MouseMove ||
input_event->type == WebInputEvent::MouseWheel ||
(input_event->type == WebInputEvent::TouchMove &&
ack_result == INPUT_EVENT_ACK_STATE_CONSUMED);
bool frame_pending = compositor_ && compositor_->BeginMainFrameRequested();
// If we don't have a fast and accurate HighResNow, we assume the input
// handlers are heavy and rate limit them.
bool rate_limiting_wanted = true;
if (base::TimeTicks::IsHighResNowFastAndReliable()) {
base::TimeTicks end_time = base::TimeTicks::HighResNow();
total_input_handling_time_this_frame_ += (end_time - start_time);
rate_limiting_wanted =
total_input_handling_time_this_frame_.InMicroseconds() >
kInputHandlingTimeThrottlingThresholdMicroseconds;
}
// Note that we can't use handling_event_type_ here since it will be overriden
// by reentrant calls for events after the paused one.
bool no_ack = ignore_ack_for_mouse_move_from_debugger_ &&
input_event->type == WebInputEvent::MouseMove;
if (!WebInputEventTraits::IgnoresAckDisposition(*input_event) && !no_ack) {
InputHostMsg_HandleInputEvent_ACK_Params ack;
ack.type = input_event->type;
ack.state = ack_result;
ack.latency = swap_latency_info;
scoped_ptr<IPC::Message> response(
new InputHostMsg_HandleInputEvent_ACK(routing_id_, ack));
if (rate_limiting_wanted && event_type_can_be_rate_limited &&
frame_pending && !is_hidden_) {
// We want to rate limit the input events in this case, so we'll wait for
// painting to finish before ACKing this message.
......
if (pending_input_event_ack_) {
// As two different kinds of events could cause us to postpone an ack
// we send it now, if we have one pending. The Browser should never
// send us the same kind of event we are delaying the ack for.
Send(pending_input_event_ack_.release());
}
pending_input_event_ack_ = response.Pass();
......
} else {
Send(response.release());
}
}
......
}
这个函数定义在文件external/chromium_org/content/renderer/render_widget.cc中。
RenderWidget类的成员函数OnHandleInputEvent将参数input_event描述的输入事件分发给WebKit之前,会做以下三件事情:
1. 检查参数input_event描述的输入事件是否是一个鼠标事件。如果是的话,那么就会调用另外一个成员函数WillHandleMouseEvent询问RenderWidget类是否要对它进行处理。如果处理,那么本地变量prevent_default的值就会被设置为true。
2. 检查参数input_event描述的输入事件是否是一个DPAD键盘事件。如果是的话,并且当前获得焦点的是一个Input Text控件,那么就调用另外一个成员函数OnShowImeIfNeeded弹出输入法,以便用户可以输入文本给Input Text控件去。注意,这个检查是针对Android平台的,因为Android平台才会存在DPAD键。
3. 检查参数input_event描述的输入事件是否是一个手势操作。如果是的话,那么就会调用另外一个成员函数WillHandleGestureEvent询问RenderWidget类是否要对它进行处理。如果处理,那么本地变量prevent_default的值也会被设置为true。
RenderWidget类的成员函数WillHandleMouseEvent和WillHandleGestureEvent的返回值均为false,这表明RenderWidget类不会处理鼠标和手势操作这两种输入事件。
RenderWidget类的成员函数OnHandleInputEvent接下来继续判断参数input_event描述的输入事件是否为键盘字符输入事件。如果不是,并且RenderWidget类不对它进行处理,那么RenderWidget类的成员函数OnHandleInputEvent就会将它分发给WebKit处理。这表明在RenderWidget类不处理的情况下,非键盘字符输入事件一定会分发给WebKit处理。
另一方面,对于键盘字符输入事件,它也会分发给WebKit处理。不过,分发给WebKit之后,如果WebKit决定不对它进行处理,并且它被设置为浏览器的快捷键,即参数is_keyboard_shortcut的值等于true,那么下一个键盘字符输入事件将不会再分发给WebKit处理。因为下一个键盘字符输入事件将会作为浏览器快捷键的另外一部分。在这种情况下,RenderWidget类的成员变量suppress_next_char_events_的值会被设置为true。
将参数input_event描述的输入事件分发给WebKit之后,RenderWidget类的成员函数OnHandleInputEvent考虑是否需要给Browser进程发送一个ACK。从前面Chromium网页输入事件捕捉和手势检测过程分析一文可以知道,Browser进程收到Render进程对上一个输入事件的ACK之后,才会给它分发下一个输入事件。因此,Render进程是否给Browser进程发送ACK,可以用来控制Browser进程分发输入事件给Render进程的节奏。
有三种类型的输入事件是需要控制分发节奏的:鼠标移动、鼠标中键滚动和触摸事件。这三类输入事件都有一个特点,它们会连续大量地输入。每一次输入Render进程都需要对它们作出响应,也就是对网页进行处理。如果它们发生得太过频繁,那么就会造成Render进程的负担很重。因此,对于这三类输入事件,RenderWidget类的成员函数OnHandleInputEvent并不会都马上对它们进行ACK。这里有一点需要注意,对于触摸事件,只有WebKit没有对它进行处理,那么RenderWidget类的成员函数OnHandleInputEvent会马上对它进行ACK。这是因为这个触摸事件和接下来发生的触摸事件,可能会触发手势操作,例如滑动手势和捏合手势。这些手势操作必须要迅速进行处理,否则的话,用户就会觉得浏览器没有反应了。
什么情况下不会马上进行ACK呢?如果网页的当前帧请求执行了一次Commit操作,也就是请求了重新绘制网页的CC Layer Tree,但是网页的CC Layer Tree还没有绘制完成,并且也没有同步到CC Pending Layer Tree,那么就不会马上进行ACK。这个ACK会被缓存在RenderWidget类的成员变量pending_input_event_ack_中。等到网页的CC Layer Tree重新绘制完成并且同步到CC Pending Layer Tree之后,被缓存的ACK才会发送给Browser进程。之所以要这样做,是因为Main线程在重新绘制网页的CC Layer Tree的时候,任务是相当重的,这时候不宜再分发新的输入事件给它处理。
此外,如果平台实现了高精度时钟,那么RenderWidget类的成员函数OnHandleInputEvent也不一定要等到网页的CC Layer Tree重新绘制完成并且同步到CC Pending Layer Tree之后,才将缓存的ACK才会发送给Browser进程。如果已经过去了一段时间,网页的CC Layer Tree还没有绘制完成,也没有同步到CC Pending Layer Tree,那么RenderWidget类的成员函数OnHandleInputEvent就会马上给Browser进程发送一个ACK。这个时间被设置为kInputHandlingTimeThrottlingThresholdMicroseconds(4166)微秒,大约等于1/4帧时间(假设一帧时间为16毫秒)。
还有一种特殊情况,造成RenderWidget类的成员函数OnHandleInputEvent不会缓存输入事件的ACK,那就是网页当前不可见。对于不可见的网页,Main线程不需要对它们进行处理,因为处理了也是白处理(用户看不到)。因此,可以认为此时分发给网页的任何输入都在瞬间完成,于是就可以安全地将它们ACK给Browser进程,不用担心Main线程的负载问题。
最后,我们还看到,如果浏览器连上了Debugger,并且Debugger希望不要对鼠标移动事件进行ACK,那么鼠标移动事件在任何情况下,都不会进行ACK。Browser进程是通过类型为InputHostMsg_HandleInputEvent的消息向Render进程分发输入事件的。相应地,Render进程通过向Browser进程发送类型为InputHostMsg_HandleInputEvent_ACK的消息对输入事件进行ACK。
接下来,我们就重点分析Chromium分发输入事件给WebKit处理的过程。从前面Chromium网页Frame Tree创建过程分析一文可以知道, RenderWidget类的成员变量webwidget_指向的是一个WebViewImpl对象。RenderWidget类的成员函数OnHandleInputEvent就是通过调用这个WebViewImpl对象的成员函数handleInputEvent将参数input_event描述的输入事件分发给WebKit处理的。前面我们已经假设这是一个Touch事件。
WebViewImpl类的成员函数handleInputEvent的实现如下所示:
bool WebViewImpl::handleInputEvent(const WebInputEvent& inputEvent)
{
......
return PageWidgetDelegate::handleInputEvent(m_page.get(), *this, inputEvent);
}
这个函数定义在文件external/chromium_org/third_party/WebKit/Source/web/WebViewImpl.cpp中。
对于Touch事件,WebViewImpl类的成员函数handleInputEvent将会调用PageWidgetDelegate类的静态成员函数handleInputEvent对它进行处理,如下所示:
bool PageWidgetDelegate::handleInputEvent(Page* page, PageWidgetEventHandler& handler, const WebInputEvent& event)
{
LocalFrame* frame = page && page->mainFrame()->isLocalFrame() ? page->deprecatedLocalMainFrame() : 0;
switch (event.type) {
......
case WebInputEvent::TouchStart:
case WebInputEvent::TouchMove:
case WebInputEvent::TouchEnd:
case WebInputEvent::TouchCancel:
if (!frame || !frame->view())
return false;
return handler.handleTouchEvent(*frame, *static_cast<const WebTouchEvent*>(&event));
......
default:
return false;
}
}
这个函数定义在文件external/chromium_org/third_party/WebKit/Source/web/PageWidgetDelegate.cpp中。
从前面的调用过程可以知道,参数page是从WebViewImpl类的成员变量m_page传递过来的,它指向的是一个Page对象。PageWidgetDelegate类的静态成员函数handleInputEvent通过这个Page对象可以获得一个LocalFrame对象。这个LocalFrame对象用来在WebKit层描述在当前进程中加载的网页,它的创建过程可以参考前面Chromium网页Frame Tree创建过程分析一文。
另外一个参数handler描述的是一个WebViewImpl对象。当第三个参数event描述的是一个Touch相关的事件时,PageWidgetDelegate类的静态成员函数handleInputEvent就会调用参数handler描述的WebViewImpl对象的成员函数handleTouchEvent对它进行处理。
WebViewImpl类的成员函数handleTouchEvent是从父类PageWidgetEventHandler继承下来的,它的实现如下所示:
bool PageWidgetEventHandler::handleTouchEvent(LocalFrame& mainFrame, const WebTouchEvent& event)
{
return mainFrame.eventHandler().handleTouchEvent(PlatformTouchEventBuilder(mainFrame.view(), event));
}
这个函数定义在文件external/chromium_org/third_party/WebKit/Source/web/PageWidgetDelegate.cpp中。
PageWidgetEventHandler类的成员函数handleTouchEvent首先调用参数mainFrame描述的一个LocalFrame对象的成员函数eventHandler获得一个EventHandler对象,接着再调用这个EventHandler对象的成员函数handleTouchEvent将处参数event描述的Touch事件,如下所示:
bool EventHandler::handleTouchEvent(const PlatformTouchEvent& event)
{
......
const Vector<PlatformTouchPoint>& points = event.touchPoints();
unsigned i;
......
bool allTouchReleased = true;
for (i = 0; i < points.size(); ++i) {
const PlatformTouchPoint& point = points[i];
......
if (point.state() != PlatformTouchPoint::TouchReleased && point.state() != PlatformTouchPoint::TouchCancelled)
allTouchReleased = false;
}
......
// First do hit tests for any new touch points.
for (i = 0; i < points.size(); ++i) {
const PlatformTouchPoint& point = points[i];
......
if (point.state() == PlatformTouchPoint::TouchPressed) {
HitTestRequest::HitTestRequestType hitType = HitTestRequest::TouchEvent | HitTestRequest::ReadOnly | HitTestRequest::Active;
LayoutPoint pagePoint = roundedLayoutPoint(m_frame->view()->windowToContents(point.pos()));
HitTestResult result;
if (!m_touchSequenceDocument) {
result = hitTestResultAtPoint(pagePoint, hitType);
} else if (m_touchSequenceDocument->frame()) {
LayoutPoint framePoint = roundedLayoutPoint(m_touchSequenceDocument->frame()->view()->windowToContents(point.pos()));
result = hitTestResultInFrame(m_touchSequenceDocument->frame(), framePoint, hitType);
} else
continue;
Node* node = result.innerNode();
......
if (!m_touchSequenceDocument) {
// Keep track of which document should receive all touch events
// in the active sequence. This must be a single document to
// ensure we don't leak Nodes between documents.
m_touchSequenceDocument = &(result.innerNode()->document());
......
}
......
m_targetForTouchID.set(point.id(), node);
......
}
}
......
// Holds the complete set of touches on the screen.
RefPtrWillBeRawPtr<TouchList> touches = TouchList::create();
// A different view on the 'touches' list above, filtered and grouped by
// event target. Used for the 'targetTouches' list in the JS event.
typedef WillBeHeapHashMap<EventTarget*, RefPtrWillBeMember<TouchList> > TargetTouchesHeapMap;
TargetTouchesHeapMap touchesByTarget;
// Array of touches per state, used to assemble the 'changedTouches' list.
typedef WillBeHeapHashSet<RefPtrWillBeMember<EventTarget> > EventTargetSet;
struct {
// The touches corresponding to the particular change state this struct
// instance represents.
RefPtrWillBeMember<TouchList> m_touches;
// Set of targets involved in m_touches.
EventTargetSet m_targets;
} changedTouches[PlatformTouchPoint::TouchStateEnd];
for (i = 0; i < points.size(); ++i) {
const PlatformTouchPoint& point = points[i];
PlatformTouchPoint::State pointState = point.state();
RefPtrWillBeRawPtr<EventTarget> touchTarget = nullptr;
if (pointState == PlatformTouchPoint::TouchReleased || pointState == PlatformTouchPoint::TouchCancelled) {
// The target should be the original target for this touch, so get
// it from the hashmap. As it's a release or cancel we also remove
// it from the map.
touchTarget = m_targetForTouchID.take(point.id());
} else {
// No hittest is performed on move or stationary, since the target
// is not allowed to change anyway.
touchTarget = m_targetForTouchID.get(point.id());
}
LocalFrame* targetFrame = 0;
bool knownTarget = false;
if (touchTarget) {
Document& doc = touchTarget->toNode()->document();
// If the target node has moved to a new document while it was being touched,
// we can't send events to the new document because that could leak nodes
// from one document to another. See http://crbug.com/394339.
if (&doc == m_touchSequenceDocument.get()) {
targetFrame = doc.frame();
knownTarget = true;
}
}
......
RefPtrWillBeRawPtr<Touch> touch = Touch::create(
targetFrame, touchTarget.get(), point.id(), point.screenPos(), adjustedPagePoint, adjustedRadius, point.rotationAngle(), point.force());
......
// Ensure this target's touch list exists, even if it ends up empty, so
// it can always be passed to TouchEvent::Create below.
TargetTouchesHeapMap::iterator targetTouchesIterator = touchesByTarget.find(touchTarget.get());
if (targetTouchesIterator == touchesByTarget.end()) {
touchesByTarget.set(touchTarget.get(), TouchList::create());
targetTouchesIterator = touchesByTarget.find(touchTarget.get());
}
// touches and targetTouches should only contain information about
// touches still on the screen, so if this point is released or
// cancelled it will only appear in the changedTouches list.
if (pointState != PlatformTouchPoint::TouchReleased && pointState != PlatformTouchPoint::TouchCancelled) {
touches->append(touch);
targetTouchesIterator->value->append(touch);
}
// Now build up the correct list for changedTouches.
// Note that any touches that are in the TouchStationary state (e.g. if
// the user had several points touched but did not move them all) should
// never be in the changedTouches list so we do not handle them
// explicitly here. See https://bugs.webkit.org/show_bug.cgi?id=37609
// for further discussion about the TouchStationary state.
if (pointState != PlatformTouchPoint::TouchStationary && knownTarget) {
......
if (!changedTouches[pointState].m_touches)
changedTouches[pointState].m_touches = TouchList::create();
changedTouches[pointState].m_touches->append(touch);
changedTouches[pointState].m_targets.add(touchTarget);
}
}
if (allTouchReleased) {
m_touchSequenceDocument.clear();
......
}
......
// Now iterate the changedTouches list and m_targets within it, sending
// events to the targets as required.
bool swallowedEvent = false;
for (unsigned state = 0; state != PlatformTouchPoint::TouchStateEnd; ++state) {
......
const AtomicString& stateName(eventNameForTouchPointState(static_cast<PlatformTouchPoint::State>(state)));
const EventTargetSet& targetsForState = changedTouches[state].m_targets;
for (EventTargetSet::const_iterator it = targetsForState.begin(); it != targetsForState.end(); ++it) {
EventTarget* touchEventTarget = it->get();
RefPtrWillBeRawPtr<TouchEvent> touchEvent = TouchEvent::create(
touches.get(), touchesByTarget.get(touchEventTarget), changedTouches[state].m_touches.get(),
stateName, touchEventTarget->toNode()->document().domWindow(),
event.ctrlKey(), event.altKey(), event.shiftKey(), event.metaKey(), event.cancelable());
touchEventTarget->toNode()->dispatchTouchEvent(touchEvent.get());
swallowedEvent = swallowedEvent || touchEvent->defaultPrevented() || touchEvent->defaultHandled();
}
}
return swallowedEvent;
}
这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/page/EventHandler.cpp中。
EventHandler类的成员函数handleTouchEvent主要是做三件事情:
1. 对当前发生的Touch事件的每一个Touch Point进行Hit Test,分别找到它们的Target Node。
2. 对Touch Point进行分类。还与屏幕接触是一类,不再与屏幕接触是另一类。与屏幕接触的一类又划分为两个子类。一个子类是静止不动的,另一个子类是正在移动的。另外,目标Node相同的Touch Point也会被组织在同一个Touch List中。
3. 将Touch事件分发给Target Node处理。
接下来,我们就将EventHandler类的成员函数handleTouchEvent划分为三段进行分析,每一段对应于上述的一个事件。
第一段代码如下所示:
bool EventHandler::handleTouchEvent(const PlatformTouchEvent& event)
{
......
const Vector<PlatformTouchPoint>& points = event.touchPoints();
unsigned i;
......
bool allTouchReleased = true;
for (i = 0; i < points.size(); ++i) {
const PlatformTouchPoint& point = points[i];
......
if (point.state() != PlatformTouchPoint::TouchReleased && point.state() != PlatformTouchPoint::TouchCancelled)
allTouchReleased = false;
}
......
// First do hit tests for any new touch points.
for (i = 0; i < points.size(); ++i) {
const PlatformTouchPoint& point = points[i];
......
if (point.state() == PlatformTouchPoint::TouchPressed) {
HitTestRequest::HitTestRequestType hitType = HitTestRequest::TouchEvent | HitTestRequest::ReadOnly | HitTestRequest::Active;
LayoutPoint pagePoint = roundedLayoutPoint(m_frame->view()->windowToContents(point.pos()));
HitTestResult result;
if (!m_touchSequenceDocument) {
result = hitTestResultAtPoint(pagePoint, hitType);
} else if (m_touchSequenceDocument->frame()) {
LayoutPoint framePoint = roundedLayoutPoint(m_touchSequenceDocument->frame()->view()->windowToContents(point.pos()));
result = hitTestResultInFrame(m_touchSequenceDocument->frame(), framePoint, hitType);
} else
continue;
Node* node = result.innerNode();
......
if (!m_touchSequenceDocument) {
// Keep track of which document should receive all touch events
// in the active sequence. This must be a single document to
// ensure we don't leak Nodes between documents.
m_touchSequenceDocument = &(result.innerNode()->document());
......
}
......
m_targetForTouchID.set(point.id(), node);
......
}
}
这段代码首先获得参数event描述的Touch事件关联的所有Touch Point,保存在本地变量points描述的一个Vector中。
每一个Touch Point都用一个PlatformTouchPoint对象描述,它有五种状态,如下所示:
class PlatformTouchPoint {
public:
enum State {
TouchReleased,
TouchPressed,
TouchMoved,
TouchStationary,
TouchCancelled,
TouchStateEnd // Placeholder: must remain the last item.
};
......
};
这些状态定义在文件external/chromium_org/third_party/WebKit/Source/platform/PlatformTouchPoint.h中。
一个Touch Point的一般状态变化过程为:TouchPressed->TouchMoved/TouchStationary->TouchReleased/TouchCancelled。
回到上面第一段代码中,它主要做的事情就是对那些状态为TouchPressed的Touch Point进行Hit Test,目的是找到它们的Target Node,并且将这些Target Node保存在EventHandler类的成员变量m_targetForTouchID描述的一个Hash Map中,键值为Touch Point对应的ID。有了这个Hash Map,当一个Touch Point从TouchPressed状态变为其它状态时,就可以轻松地知道它的Target Node,避免做重复的Hit Test。
一系列连续的Touch Event只能发生在一个Document上。如果两个Touch Event有两个或者两个以上的Touch Point具有相同的ID,那么它们就是连续的Touch Event。它们所发生在的Document由第一个连续的Touch Event的第一个处于TouchPressed状态的Touch Point确定,也就是这个Touch Point的Target Node所在的Document。这个Document一旦确定,就会维护在EventHandler类的成员变量m_touchSequenceDocument中。
当一个Touch Event的所有Touch Point的状态都处于TouchReleased或者TouchCancelled时,它就结束一个连续的Touch Event系列。这时候EventHandler类的成员变量m_touchSequenceDocument就会设置为NULL,表示接下来发生的Touch Event属于另外一个连续的系列。
当一个Touch Event所在的Document所未确定时,EventHandler类的成员函数handleTouchEvent调用成员函数hitTestResultAtPoint对第一个Touch Point做Hit Test;当一个Touch Event所在的Document确定时,则调用另外一个成员函数hitTestResultInFrame做Hit Test。后者会将Hit Test的范围限制在指定的Document中。后面我们将以EventHandler类的成员函数hitTestResultAtPoint为例,分析Hit Test的执行过程。
第二段代码如下所示:
// Holds the complete set of touches on the screen.
RefPtrWillBeRawPtr<TouchList> touches = TouchList::create();
// A different view on the 'touches' list above, filtered and grouped by
// event target. Used for the 'targetTouches' list in the JS event.
typedef WillBeHeapHashMap<EventTarget*, RefPtrWillBeMember<TouchList> > TargetTouchesHeapMap;
TargetTouchesHeapMap touchesByTarget;
// Array of touches per state, used to assemble the 'changedTouches' list.
typedef WillBeHeapHashSet<RefPtrWillBeMember<EventTarget> > EventTargetSet;
struct {
// The touches corresponding to the particular change state this struct
// instance represents.
RefPtrWillBeMember<TouchList> m_touches;
// Set of targets involved in m_touches.
EventTargetSet m_targets;
} changedTouches[PlatformTouchPoint::TouchStateEnd];
for (i = 0; i < points.size(); ++i) {
const PlatformTouchPoint& point = points[i];
PlatformTouchPoint::State pointState = point.state();
RefPtrWillBeRawPtr<EventTarget> touchTarget = nullptr;
if (pointState == PlatformTouchPoint::TouchReleased || pointState == PlatformTouchPoint::TouchCancelled) {
// The target should be the original target for this touch, so get
// it from the hashmap. As it's a release or cancel we also remove
// it from the map.
touchTarget = m_targetForTouchID.take(point.id());
} else {
// No hittest is performed on move or stationary, since the target
// is not allowed to change anyway.
touchTarget = m_targetForTouchID.get(point.id());
}
LocalFrame* targetFrame = 0;
bool knownTarget = false;
if (touchTarget) {
Document& doc = touchTarget->toNode()->document();
// If the target node has moved to a new document while it was being touched,
// we can't send events to the new document because that could leak nodes
// from one document to another. See http://crbug.com/394339.
if (&doc == m_touchSequenceDocument.get()) {
targetFrame = doc.frame();
knownTarget = true;
}
}
......
RefPtrWillBeRawPtr<Touch> touch = Touch::create(
targetFrame, touchTarget.get(), point.id(), point.screenPos(), adjustedPagePoint, adjustedRadius, point.rotationAngle(), point.force());
......
// Ensure this target's touch list exists, even if it ends up empty, so
// it can always be passed to TouchEvent::Create below.
TargetTouchesHeapMap::iterator targetTouchesIterator = touchesByTarget.find(touchTarget.get());
if (targetTouchesIterator == touchesByTarget.end()) {
touchesByTarget.set(touchTarget.get(), TouchList::create());
targetTouchesIterator = touchesByTarget.find(touchTarget.get());
}
// touches and targetTouches should only contain information about
// touches still on the screen, so if this point is released or
// cancelled it will only appear in the changedTouches list.
if (pointState != PlatformTouchPoint::TouchReleased && pointState != PlatformTouchPoint::TouchCancelled) {
touches->append(touch);
targetTouchesIterator->value->append(touch);
}
// Now build up the correct list for changedTouches.
// Note that any touches that are in the TouchStationary state (e.g. if
// the user had several points touched but did not move them all) should
// never be in the changedTouches list so we do not handle them
// explicitly here. See https://bugs.webkit.org/show_bug.cgi?id=37609
// for further discussion about the TouchStationary state.
if (pointState != PlatformTouchPoint::TouchStationary && knownTarget) {
......
if (!changedTouches[pointState].m_touches)
changedTouches[pointState].m_touches = TouchList::create();
changedTouches[pointState].m_touches->append(touch);
changedTouches[pointState].m_targets.add(touchTarget);
}
}
if (allTouchReleased) {
m_touchSequenceDocument.clear();
......
}
这段代码主要是对Touch Point进行分门别类。
首先,那些还与屏幕有接触的Touch Point将会保存在本地变量touches描述的一个Touch List中。那些状态不等于TouchReleased和TouchCancelled的Touch Point即为还与屏幕有接触的Touch Point。
其次,具有相同Target Node的Touch Point又会保存在相同的Touch List中。这些Touch List它们关联的Target Node为键值,保存在本地变量touchesByTarget描述的一个Hash Map中。
第三,那些位置或者状态发生变化,并且有Target Node的Touch Point会按照状态保存在本地变量changedTouches描述的一个数组中。相同状态的Touch Point保存在同一个Touch List中,它们的Target Node也会保存在同一个Hash Set中。位置或者状态发生变化的Touch Point,即为那些状态不等于TouchStationary的Touch Point。另外,如果一个Touch Point的Target Node所在的Document与当前Touch Event所发生在的Document不一致,那么该Touch Point会被认为是没有Target Node。
这段代码还会做另外两件事情:
1. 如果一个Touch Point的状态变为TouchReleased或者TouchCancelled,那么它就会从EventHandler类的成员变量m_targetForTouchID描述的一个Hash Map中移除。结合前面对第一段代码的分析,我们就可以知道,一个连续的Touch Event系列,它关联的Touch Point是会动态增加和移除的。
2. 如果当前发生的Touch Event的所有Touch Point的状态均为TouchReleased或者TouchCancelled,那么当前连续的Touch Event系列就会结束。这时候EventHandler类的成员变量m_touchSequenceDocument将被设置为NULL。
第三段代码如下所示:
// Now iterate the changedTouches list and m_targets within it, sending
// events to the targets as required.
bool swallowedEvent = false;
for (unsigned state = 0; state != PlatformTouchPoint::TouchStateEnd; ++state) {
......
const AtomicString& stateName(eventNameForTouchPointState(static_cast<PlatformTouchPoint::State>(state)));
const EventTargetSet& targetsForState = changedTouches[state].m_targets;
for (EventTargetSet::const_iterator it = targetsForState.begin(); it != targetsForState.end(); ++it) {
EventTarget* touchEventTarget = it->get();
RefPtrWillBeRawPtr<TouchEvent> touchEvent = TouchEvent::create(
touches.get(), touchesByTarget.get(touchEventTarget), changedTouches[state].m_touches.get(),
stateName, touchEventTarget->toNode()->document().domWindow(),
event.ctrlKey(), event.altKey(), event.shiftKey(), event.metaKey(), event.cancelable());
touchEventTarget->toNode()->dispatchTouchEvent(touchEvent.get());
swallowedEvent = swallowedEvent || touchEvent->defaultPrevented() || touchEvent->defaultHandled();
}
}
return swallowedEvent;
}
这段代码将Touch Event分发给Target Node处理。注意,并不是所有的Target Node都会被分发Touch Event。只有那些Touch Point位置或者状态发生了变化的Target Node才会获得Touch Event。
这段代码按照Touch Point的状态分发Touch Event给Target Node处理,顺序为TouchReleased->TouchPressed->TouchMoved->TouchCancelled。如果具有相同状态的Touch Point关联了不同的Target Node,那么每一个Target Node都会获得一个Touch Event。
每一个Target Node获得的Touch Event是不同的TouchEvent对象,每一个TouchEvent对象包含了以下三种信息:
1. 所有与屏幕接触的Touch Point。这些Touch Point有的位于Target Node的范围内,有的可能位于Target Node的范围外。
2. 位于Target Node的范围内的Touch Point。
3. 具有相同状态的Touch Point。
注意,这些Touch Point限定在当前发生的Touch Event关联的Touch Point中,也就是限定在参数event描述的Touch Event关联的Touch Point中。
EventHandler类的成员函数是通过Target Node的成员函数handleTouchEvent给它们分发Touch Event,也就是通过调用Node类的成员函数handleTouchEvent将Touch Event分发给Target Node处理。
接下来,我们就继续分析EventHandler类的成员函数hitTestResultAtPoint和Node类的成员函数handleTouchEvent的实现,以便了解WebKit根据Touch Point找到Target Node和将Touch Event分发给Target Node处理的过程。
EventHandler类的成员函数hitTestResultAtPoint的实现如下所示:
HitTestResult EventHandler::hitTestResultAtPoint(const LayoutPoint& point, HitTestRequest::HitTestRequestType hitType, const LayoutSize& padding)
{
......
// We always send hitTestResultAtPoint to the main frame if we have one,
// otherwise we might hit areas that are obscured by higher frames.
if (Page* page = m_frame->page()) {
LocalFrame* mainFrame = page->mainFrame()->isLocalFrame() ? page->deprecatedLocalMainFrame() : 0;
if (mainFrame && m_frame != mainFrame) {
FrameView* frameView = m_frame->view();
FrameView* mainView = mainFrame->view();
if (frameView && mainView) {
IntPoint mainFramePoint = mainView->rootViewToContents(frameView->contentsToRootView(roundedIntPoint(point)));
return mainFrame->eventHandler().hitTestResultAtPoint(mainFramePoint, hitType, padding);
}
}
}
HitTestResult result(point, padding.height(), padding.width(), padding.height(), padding.width());
......
// hitTestResultAtPoint is specifically used to hitTest into all frames, thus it always allows child frame content.
HitTestRequest request(hitType | HitTestRequest::AllowChildFrameContent);
m_frame->contentRenderer()->hitTest(request, result);
......
return result;
}
这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/page/EventHandler.cpp中。
EventHandler类的成员函数hitTestResultAtPoint首先会检查当前正在处理的网页的Main Frame是否是一个Local Frame。如果是的话,那么就会在它上面做Hit Test。否则的话,就在当前正在处理的网页所加载在的Frame上做Hit Test,也就是EventHandler类的成员变量m_frame描述的Frame。这个Frame是一个Local Frame。
前面Chromium网页加载过程简要介绍和学习计划一文提到,在WebKit中,一个Frame有可能是Local的,也可能是Remote的。Local Frame就在当前进程加载一个网页,而Remote Frame在另外一个进程加载一个网页。Frame与Frame之间会形成一个Frame Tree。Frame Tree的根节点就是一个Main Frame。结合前面分析的EventHandler类的成员函数handleTouchEvent,我们就可以知道,如果当前正在处理的网页不是加载在一个Main Frame上,并且这个网页的Main Frame是一个Local Frame,那么发生在这个网页上的Touch Event将会在它的Main Frame上做Hit Test。在其余情况下,发生在当前正在处理的网页上的Touch Event,将会在该网页所加载在的Frame上做Hit Test。
一旦确定在哪个Frame上做Hit Test之后,EventHandler类的成员函数hitTestResultAtPoint就会调用这个Frame的成员函数contentRenderer获得一个RenderView对象。从前面Chromium网页Render Object Tree创建过程分析一文可以知道,这个RenderView对象描述的就是当前正在处理的网页的Render Object Tree的根节点。有了这个RenderView对象之后,EventHandler类的成员函数hitTestResultAtPoint就会调用它的成员函数hitTest对参数point描述的Touch Point进行Hit Test。
RenderView类的成员函数hitTest的实现如下所示:
bool RenderView::hitTest(const HitTestRequest& request, HitTestResult& result)
{
return hitTest(request, result.hitTestLocation(), result);
}
这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/rendering/RenderView.cpp中。
RenderView类的成员函数hitTest调用另外一个重载版本的成员函数hitTest对参数result描述的Touch Point进行Hit Test,如下所示:
bool RenderView::hitTest(const HitTestRequest& request, const HitTestLocation& location, HitTestResult& result)
{
......
return layer()->hitTest(request, location, result);
}
这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/rendering/RenderView.cpp中。
RenderView类重载版本的成员函数hitTest首先会调用另外一个成员函数layer获得一个RenderLayer对象。从前面Chromium网页Render Layer Tree创建过程分析一文可以知道,这个RenderLayer对象描述的就是网页的Render Layer Tree的根节点。有了这个RenderLayer对象之后,RenderView类重载版本的成员函数hitTest就会调用它的成员函数hitTest对象参数result描述的Touch Point进行Hit Test,如下所示:
bool RenderLayer::hitTest(const HitTestRequest& request, const HitTestLocation& hitTestLocation, HitTestResult& result)
{
......
LayoutRect hitTestArea = renderer()->view()->documentRect();
......
RenderLayer* insideLayer = hitTestLayer(this, 0, request, result, hitTestArea, hitTestLocation, false);
......
return insideLayer;
}
这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/rendering/RenderLayer.cpp中。
RenderLayer类的成员函数hitTest首先是获得网页的Document对象占据的区域hitTestArea,然后再调用另外一个成员函数hitTestLayer在该区域上对参数hitTestLocation描述的Touch Point进行Hit Test,如下所示:
RenderLayer* RenderLayer::hitTestLayer(RenderLayer* rootLayer, RenderLayer* containerLayer, const HitTestRequest& request, HitTestResult& result,
const LayoutRect& hitTestRect, const HitTestLocation& hitTestLocation, bool appliedTransform,
const HitTestingTransformState* transformState, double* zOffset)
{
......
// Ensure our lists and 3d status are up-to-date.
m_stackingNode->updateLayerListsIfNeeded();
update3DTransformedDescendantStatus();
......
// The following are used for keeping track of the z-depth of the hit point of 3d-transformed
// descendants.
double localZOffset = -numeric_limits<double>::infinity();
double* zOffsetForDescendantsPtr = 0;
double* zOffsetForContentsPtr = 0;
bool depthSortDescendants = false;
if (preserves3D()) {
depthSortDescendants = true;
// Our layers can depth-test with our container, so share the z depth pointer with the container, if it passed one down.
zOffsetForDescendantsPtr = zOffset ? zOffset : &localZOffset;
zOffsetForContentsPtr = zOffset ? zOffset : &localZOffset;
} else if (m_has3DTransformedDescendant) {
// Flattening layer with 3d children; use a local zOffset pointer to depth-test children and foreground.
depthSortDescendants = true;
zOffsetForDescendantsPtr = zOffset ? zOffset : &localZOffset;
zOffsetForContentsPtr = zOffset ? zOffset : &localZOffset;
} else if (zOffset) {
zOffsetForDescendantsPtr = 0;
// Container needs us to give back a z offset for the hit layer.
zOffsetForContentsPtr = zOffset;
}
......
// This variable tracks which layer the mouse ends up being inside.
RenderLayer* candidateLayer = 0;
// Begin by walking our list of positive layers from highest z-index down to the lowest z-index.
RenderLayer* hitLayer = hitTestChildren(PositiveZOrderChildren, rootLayer, request, result, hitTestRect, hitTestLocation,
localTransformState.get(), zOffsetForDescendantsPtr, zOffset, unflattenedTransformState.get(), depthSortDescendants);
if (hitLayer) {
if (!depthSortDescendants)
return hitLayer;
candidateLayer = hitLayer;
}
// Now check our overflow objects.
hitLayer = hitTestChildren(NormalFlowChildren, rootLayer, request, result, hitTestRect, hitTestLocation,
localTransformState.get(), zOffsetForDescendantsPtr, zOffset, unflattenedTransformState.get(), depthSortDescendants);
if (hitLayer) {
if (!depthSortDescendants)
return hitLayer;
candidateLayer = hitLayer;
}
......
// Next we want to see if the mouse pos is inside the child RenderObjects of the layer. Check
// every fragment in reverse order.
if (isSelfPaintingLayer()) {
// Hit test with a temporary HitTestResult, because we only want to commit to 'result' if we know we're frontmost.
HitTestResult tempResult(result.hitTestLocation());
bool insideFragmentForegroundRect = false;
if (hitTestContentsForFragments(layerFragments, request, tempResult, hitTestLocation, HitTestDescendants, insideFragmentForegroundRect)
&& isHitCandidate(this, false, zOffsetForContentsPtr, unflattenedTransformState.get())) {
......
if (!depthSortDescendants)
return this;
// Foreground can depth-sort with descendant layers, so keep this as a candidate.
candidateLayer = this;
}
}
// Now check our negative z-index children.
hitLayer = hitTestChildren(NegativeZOrderChildren, rootLayer, request, result, hitTestRect, hitTestLocation,
localTransformState.get(), zOffsetForDescendantsPtr, zOffset, unflattenedTransformState.get(), depthSortDescendants);
if (hitLayer) {
if (!depthSortDescendants)
return hitLayer;
candidateLayer = hitLayer;
}
......
// If we found a layer, return. Child layers, and foreground always render in front of background.
if (candidateLayer)
return candidateLayer;
if (isSelfPaintingLayer()) {
HitTestResult tempResult(result.hitTestLocation());
bool insideFragmentBackgroundRect = false;
if (hitTestContentsForFragments(layerFragments, request, tempResult, hitTestLocation, HitTestSelf, insideFragmentBackgroundRect)
&& isHitCandidate(this, false, zOffsetForContentsPtr, unflattenedTransformState.get())) {
......
return this;
}
......
}
return 0;
}
这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/rendering/RenderLayer.cpp中。
RenderLayer类的成员变量m_stackingNode指向的是一个RenderLayerStackingNode对象。这个RenderLayerStackingNode对象描述了当前正在处理的Render Layer所在的Stacking Context。RenderLayer类的成员函数hitTestLayer首先调用这个RenderLayerStackingNode对象的成员函数updateLayerListsIfNeeded更新它所描述的Stacking Context所包含的Render Layer的层次关系,以便接下来可以按照它们的Z轴位置进行Hit Test。
RenderLayer类的成员函数hitTestLayer同时还会调用另外一个成员函数update3DTransformedDescendantStatus检查当前正在处理的Render Layer的子Render Layer是否设置了3D。如果设置了,那么RenderLayer类的成员变量m_has3DTransformedDescendant就会被设置为true。3D变换使得Hit Test不能简单地按照原来Z-Index的大小进行Hit Test。
RenderLayer类的成员函数hitTestLayer接下来根据两种不同的情况,采取两种不同的Hit Test方法:
1. 当前正在处理的Render Layer将CSS属性tranform-type设置为"preserve-3d",或者它的子Render Layer设置了3D变换。在这种情况下,本地变量depthSortDescendants的值会被设置为true,并且另外两个本地变量zOffsetForDescendantsPtr和zOffsetForContentsPtr指向了一个类型为double的地址。这个地址包含的double值描述的是上一个被Hit的Render Layer在Touch Point处的Z轴位置。其中,本地变量zOffsetForContentsPtr描述的Z轴位置是给当前正在处理的Render Layer使用的,而本地变量zOffsetForDescendantsPtr描述的Z轴位置是给当前正在处理的Render Layer的子Render Layer使用的。在一个设置了3D变换的环境中,Z-Index值大的Render Layer不一定位于Z-Index值小的Render Layer的上面,需要进一步结合它们的3D变换情况进行判断。因此,就需要将上一个被Hit的Render Layer在Touch Point处的Z轴位置保存下来,用来与下一个也被Hit的Render Layer进行比较,以便得出正确的被Hit的Render Layer。
2. 当前正在处理的Render Layer没有将CSS属性tranform-type设置为"preserve-3d",以及它的子Render Layer也没有设置3D变换。在这种情况下,本地变量depthSortDescendants的值会被设置为false。另外两个本地变量zOffsetForDescendantsPtr和zOffsetForContentsPtr,前者被设置为NULL,后者设置为参数zOffset的值。将本地变量zOffsetForDescendantsPtr设置为NULL,是因为当前正在处理的Render Layer的子Render Layer在做Hit Test时,不需要与其它的子Render Layer在Touch Point处进行Z轴位置。将zOffsetForContentsPtr的值指定为参数zOffset的值,是因为调用者可能会指定一个Z轴位置,要求当前正在处理的Render Layer在Touch Point处与其进行比较。
对第二种情况的处理比较简单,流程如下所示:
1. 按照Z-Index从大到小的顺序对Z-Index值大于等于0的子Render Layer进行Hit Test。如果发生了Hit,那么停止Hit Test流程。
2. 对当前正在处理的Render Layer的Foreground层进行Hit Test。如果发生了Hit,那么停止Hit Test流程。
3. 按照Z-Index从大到小的顺序对Z-Index值小于0的子Render Layer进行Hit Test。如果发生了Hit,那么停止Hit Test流程。
对第一种情况的处理相对就会复杂一些,如下所示:
1. 对当前正在处理的Render Layer的所有子Render Layer,以及当前正在处理的Render Layer的Foreground层,都会一一进行Hit Test。在这个Hit Test过程中,所有被Hit的Render Layer,都会根据它们3D变换情况,检查它们在Touch Point处的Z轴位置。Z轴位置最大的Render Layer或者Render Object,将会选择用来接收Touch Event。
2. 如果所有子Render Layer和当前正在处理的Render Layer的Foreground都没有发生Hit,那么就会再对当前正在处理的Render Layer的Background层进行Hit Test。
注意,对于每一个子Render Layer,RenderLayer类的成员函数hitTestLayer就会调用另外一个成员函数hitTestChildren对分别对它们进行Hit Test。RenderLayer类的成员函数hitTestChildren又会调用hitTestLayer对每一个子Render Layer执行具体的Hit Test。
这意味着,RenderLayer类的成员函数hitTestLayer会被递归调用来对Render Layer Tree中的每一个Render Layer进行Hit Test。当前正在处理的Render Layer是否被Hit,RenderLayer类的成员函数hitTestLayer是通过调用两次成员函数hitTestContentsForFragments进行检查。第一次调用是确定当前正在处理的Render Layer的Foreground层是否发生了Hit Test。第二次调用是确定当前正在处理的Render Layer的Background层是否发生了Hit Test。
一旦检查当前正在处理的Render Layer发生了Hit,那么RenderLayer类的成员函数hitTestLayer还需要调用另外一个成员函数isHitCandidate将它在Touch Point处的Z轴位置与本地变量zOffsetForContentsPtr描述的Z轴位置(上一个被Hit的Render Layer在Touch Point的Z轴位置)进行比较。比较后如果发现当前正在处理的Render Layer在Touch Point处的Z轴位置较大,那么才会认为它是被Hit的Render Layer。
接下来我们就继续分析RenderLayer类的成员函数hitTestContentsForFragments的实现,以便可以了解一个Render Layer在什么情况会被认为是发生了Hit,如下所示:
bool RenderLayer::hitTestContentsForFragments(const LayerFragments& layerFragments, const HitTestRequest& request, HitTestResult& result,
const HitTestLocation& hitTestLocation, HitTestFilter hitTestFilter, bool& insideClipRect) const
{
......
for (int i = layerFragments.size() - 1; i >= 0; --i) {
const LayerFragment& fragment = layerFragments.at(i);
......
if (hitTestContents(request, result, fragment.layerBounds, hitTestLocation, hitTestFilter))
return true;
}
return false;
}
这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/rendering/RenderLayer.cpp中。
前面Chromium网页Layer Tree绘制过程分析一文提到,Render Layer是按Fragment进行划分的。因此,RenderLayer类的成员函数hitTestContentsForFragments分别调用另外一个成员函数hitTestContents对每一个Fragment进行Hit Test。只要其中一个Fragment发生了Hit,那么就会认为它所在的Render Layer发生了Hit。
RenderLayer类的成员函数hitTestContents的实现如下所示:
bool RenderLayer::hitTestContents(const HitTestRequest& request, HitTestResult& result, const LayoutRect& layerBounds, const HitTestLocation& hitTestLocation, HitTestFilter hitTestFilter) const
{
......
if (!renderer()->hitTest(request, result, hitTestLocation, toLayoutPoint(layerBounds.location() - renderBoxLocation()), hitTestFilter)) {
......
return false;
}
......
return true;
}
这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/rendering/RenderLayer.cpp中。
RenderLayer类的成员函数hitTestContents首先调用成员函数renderer获得一个Render Object。这个Render Object是当前正在处理的Render Layer的宿主Render Object。也就是说,我们在为网页创建Render Layer Tree时,为上述Render Object创建了一个Render Layer。该Render Object的子Render Object如果没有自己的Render Layer,那么就会与该Render Object共享同一个Render Layer。这一点可以参考前面Chromium网页Render Layer Tree创建过程分析一文。
获得了当前正在处理的Render Layer的宿主Render Object之后,RenderLayer类的成员函数hitTestContents就调用它的成员函数hitTest检查它在参数layerBounds描述的区域内是否发生了Hit。如果发生了Hit,那么RenderLayer类的成员函数hitTestContents就直接返回一个true值给调用者。否则的话,就会返回一个false值给调用者。
这一步执行完成之后,就从Render Layer Tree转移到Render Object Tree进行Hit Test,也就是调用RenderObject类的成员函数hitTest进行Hit Test,如下所示:
bool RenderObject::hitTest(const HitTestRequest& request, HitTestResult& result, const HitTestLocation& locationInContainer, const LayoutPoint& accumulatedOffset, HitTestFilter hitTestFilter)
{
bool inside = false;
if (hitTestFilter != HitTestSelf) {
// First test the foreground layer (lines and inlines).
inside = nodeAtPoint(request, result, locationInContainer, accumulatedOffset, HitTestForeground);
// Test floats next.
if (!inside)
inside = nodeAtPoint(request, result, locationInContainer, accumulatedOffset, HitTestFloat);
// Finally test to see if the mouse is in the background (within a child block's background).
if (!inside)
inside = nodeAtPoint(request, result, locationInContainer, accumulatedOffset, HitTestChildBlockBackgrounds);
}
// See if the mouse is inside us but not any of our descendants
if (hitTestFilter != HitTestDescendants && !inside)
inside = nodeAtPoint(request, result, locationInContainer, accumulatedOffset, HitTestBlockBackground);
return inside;
}
这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/rendering/RenderObject.cpp中。
从前面分析的RenderLayer类的成员函数hitTestLayer可以知道,一个Reder Layer关联的Render Object会进行两次Hit Test。第一次是针对该Render Object的Foreground进行Hit Test,这时候参数hitTestFilter的值等于HitTestDescendants。第二次是针对该Render Object的Background层进行Hit Test,这时候参数hitTestFilter的值等于HitTestSelf。
无论是Foreground层,还是Background层,RenderObject类的成员函数hitTest都是通过调用另外一个成员函数nodeAtPoint进行Hit Test的。RenderObject类的成员函数nodeAtPoint是从父类RenderBox继承下来的,它的实现如下所示:
bool RenderBox::nodeAtPoint(const HitTestRequest& request, HitTestResult& result, const HitTestLocation& locationInContainer, const LayoutPoint& accumulatedOffset, HitTestAction action)
{
LayoutPoint adjustedLocation = accumulatedOffset + location();
// Check kids first.
for (RenderObject* child = slowLastChild(); child; child = child->previousSibling()) {
if ((!child->hasLayer() || !toRenderLayerModelObject(child)->layer()->isSelfPaintingLayer()) && child->nodeAtPoint(request, result, locationInContainer, adjustedLocation, action)) {
updateHitTestResult(result, locationInContainer.point() - toLayoutSize(adjustedLocation));
return true;
}
}
// Check our bounds next. For this purpose always assume that we can only be hit in the
// foreground phase (which is true for replaced elements like images).
LayoutRect boundsRect = borderBoxRect();
boundsRect.moveBy(adjustedLocation);
if (visibleToHitTestRequest(request) && action == HitTestForeground && locationInContainer.intersects(boundsRect)) {
updateHitTestResult(result, locationInContainer.point() - toLayoutSize(adjustedLocation));
if (!result.addNodeToRectBasedTestResult(node(), request, locationInContainer, boundsRect))
return true;
}
return false;
}
这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/rendering/RenderBox.cpp中。
RenderBox类的成员函数nodeAtPoint首先对当前正在处理的Render Object的子Render Object进行Hit Test。这是通过递归调用RenderBox类的成员函数nodeAtPoint实现的。如果子Render Object没有被Hit,那么RenderBox类的成员函数才会判断当前正在处理的Render Object是否发生Hit,也就是判断参数locationInContainer描述的Touch Point是否落在当前正在处理的Render Object的区域boundsRect内。
这里有一点需要注意,当前正在处理的Render Object的每一个子Render Objec并不是都会被递归Hit Test。只有那些没有创建Render Layer的子Render Object才会进行递归Hit Test。这些没有创建自己的Render Layer的子Render Object将会与当前正在处理的Render Object共享同一个Render Layer。对于那些有自己的Render Layer的子Render Object,它们的Hit Test将由前面分析的RenderLayer类的成员函数hitTestLayer发起。
当一个Render Object发生Hit时,RenderBox类的成员函数nodeAtPoint就会调用另外一个成员函数updateHitTestResult将Hit信息记录在参数result描述的一个HitTestResult对象中。RenderBox类的成员函数updateHitTestResult由子类RenderObject实现,如下所示:
void RenderObject::updateHitTestResult(HitTestResult& result, const LayoutPoint& point)
{
......
Node* node = this->node();
// If we hit the anonymous renderers inside generated content we should
// actually hit the generated content so walk up to the PseudoElement.
if (!node && parent() && parent()->isBeforeOrAfterContent()) {
for (RenderObject* renderer = parent(); renderer && !node; renderer = renderer->parent())
node = renderer->node();
}
if (node) {
result.setInnerNode(node);
......
}
}
这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/rendering/RenderObject.cpp中。
RenderObject类的成员函数updateHitTestResult首先调用成员函数node获得与当前正在处理的Render Object关联的HTML元素,也就是位于网页的DOM Tree中的一个Node,作为当前发生的Touch Event的Target Node。
如果当前正在处理的Render Object没有关联一个HTML元素,那么就说明当前正在处理的Render Object是一个匿名的Render Object。这时候需要在网页的Render Object Tree中找到一个负责生成它的、非匿名的父Render Object,然后再获得与这个父Render Object关联的HTML元素,作为当前发生的Touch Event的Target Node。
一旦找到了Target Node,RenderObject类的成员函数updateHitTestResult就会将它保存在参数result描述的一个HitTestResult对象中。这是通过调用HitTestResult类的成员函数setInnerNode实现的,如下所示:
void HitTestResult::setInnerNode(Node* n)
{
......
m_innerNode = n;
}
这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/rendering/HitTestResult.cpp中。
HitTestResult类的成员函数setInnerNode主要是将参数n描述的一个HTML元素保存在成员变量m_innerNode中。这个HTML元素可以通过调用HitTestResult类的成员函数innerNode获得,如下所示:
class HitTestResult {
public:
......
Node* innerNode() const { return m_innerNode.get(); }
......
};
这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/rendering/HitTestResult.h中。
这一步执行完成后,WebKit就通过网页的Render Layer Tree和Render Object Tree,最终在DOM Tree中找到了Target Node。这个Target Node负责接收和处理当前发生的Touch Event。
回到前面分析的EventHandler类的成员函数handleTouchEvent中,接下来它就会将当前发生的Touch Event分发给前面找到的Target Node处理,这是通过调用它的成员函数dispatchTouchEvent实现的,如下所示:
bool Node::dispatchTouchEvent(PassRefPtrWillBeRawPtr<TouchEvent> event)
{
return EventDispatcher::dispatchEvent(this, TouchEventDispatchMediator::create(event));
}
这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/dom/Node.cpp中。
Node类的成员函数dispatchTouchEvent首先调用TouchEventDispatchMediator类的静态成员函数create将参数描述的Touch Event封装在一个TouchEventDispatchMediator对象中,然的再调用EventDispatcher类的静态成员函数dispatchEvent对该Touch Event进行处理,如下所示:
bool EventDispatcher::dispatchEvent(Node* node, PassRefPtrWillBeRawPtr<EventDispatchMediator> mediator)
{
......
EventDispatcher dispatcher(node, mediator->event());
return mediator->dispatchEvent(&dispatcher);
}
这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/events/EventDispatcher.cpp中。
EventDispatcher类的静态成员函数dispatchEvent首先从参数mediator描述的TouchEventDispatchMediator对象中取出它所封装的Touch Event,并且将该Touch Event封装在一个EventDispatcher对象中,最后调用参数mediator描述的TouchEventDispatchMediator对象的成员函数dispatchEvent对上述Touch Event进行分发处理,如下所示:
bool TouchEventDispatchMediator::dispatchEvent(EventDispatcher* dispatcher) const
{
event()->eventPath().adjustForTouchEvent(dispatcher->node(), *event());
return dispatcher->dispatch();
}
这个函数定义在文件/external/chromium_org/third_party/WebKit/Source/core/events/TouchEvent.cpp中。
TouchEventDispatchMediator类的成员函数dispatchEvent首先调用成员函数event获得一个TouchEvent对象。这个TouchEvent描述的就是当前发生的Touch Event。调用这个TouchEvent对象的成员函数eventPath可以获得一个EventPath对象。这个EventPath对象描述的是当前发生的Touch Event的分发路径。关于WebKit中的Event分发路径,下面我们再描述。有了这个EventPath对象之后,TouchEventDispatchMediator类的成员函数dispatchEvent调用它的成员函数adjustForTouchEvent调整Touch Event分发路径中的Shadow DOM的Touch List。关于Shadow DOM的更详细描述,可以参考What the Heck is Shadow DOM这篇文章。
TouchEventDispatchMediator类的成员函数dispatchEvent最后调用参数dispatcher描述的EventDispatcher对象的成员函数dispatch处理它所封装的Touch Event,如下所示:
bool EventDispatcher::dispatch()
{
.......
void* preDispatchEventHandlerResult;
if (dispatchEventPreProcess(preDispatchEventHandlerResult) == ContinueDispatching)
if (dispatchEventAtCapturing(windowEventContext) == ContinueDispatching)
if (dispatchEventAtTarget() == ContinueDispatching)
dispatchEventAtBubbling(windowEventContext);
dispatchEventPostProcess(preDispatchEventHandlerResult);
......
return !m_event->defaultPrevented();
}
这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/events/EventDispatcher.cpp中。
在网页发生的一个Event,分为五个阶段进行分发处理:
1. Pre Process
2. Capturing
3. Target
4. Bubbling
5. Post Process
我们通过一个例子说明这五个阶段的处理过程,如下所示:
<html>
<body>
<div id="div1">
<div id="div2">
<div id="div3">
</div>
</div>
</div>
</body>
</html>
假设在div3上发生了一个Touch Event。
在Pre Process阶段,WebKit会将Touch Event分发给div3的Pre Dispatch Event Handler处理,让div3有机会在DOM Event Handler处理Touch Event之前做一些事情,用来实现自己的行为。
在Capturing阶段,WebKit会将Touch Event依次分发给html -> body -> div1 -> div2的DOM Event Handler处理。
在Target阶段,WebKit会将Touch Event依次分发给div3的DOM Event Handler处理。。
在Bubbling阶段, WebKit会将Touch Event依次分发给div2 -> div1 -> body -> html的DOM Event Handler处理。
在Post Process阶段,WebKit会将Touch Event分发给div3的Post Dispatch Event Handler处理,让div3有机会在DOM Event Handler处理Touch Event之后做一些事情,与它的Pre Dispatch Event Handler相呼应。
此外,如果在前面4个阶段,Touch Event的preventDefault函数没有被调用,那么WebKit会将它依次分发给div3 -> div2 -> div1 -> body -> html的Default Event Handler处理。在这个过程中,如果某一个Node的Default Event Handler处理了该Touch Event,那么该Touch Event的分发过程就会中止。
其中,Pre Process和Post Process这两个阶段是一定会执行的。在Capturing、Target和Bubbling这三个阶段,如果某一个Node的DOM Event Handler调用了Touch Event的stopPropagation函数,那么它就会提前中止,后面的阶段也不会被执行。
Pre Dispatch Event Handler、Post Dispatch Event Handler和Default Event Handler是由WebKit实现的,DOM Event Handler可以通过JavaScript进行注册。在注册的时候,可以指定DOM Event Handler在Capturing阶段还是Bubbling阶段接收Event,但是不能同时在这两个阶段都接收。此外,注册Target Node上的DOM Event Handler没有Capturing阶段还是Bubbling阶段之分,如果Event在Capturing阶段没有被中止,那么它将在Target阶段接收。
明白了WebKit中的Event处理流程之后,接下来我们主要分析Target Node在Target阶段处理Touch Event的过程,也就是EventDispatcher类的成员函数dispatchEventAtTarget的实现,如下所示:
inline EventDispatchContinuation EventDispatcher::dispatchEventAtTarget()
{
m_event->setEventPhase(Event::AT_TARGET);
m_event->eventPath()[0].handleLocalEvents(m_event.get());
return m_event->propagationStopped() ? DoneDispatching : ContinueDispatching;
}
这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/events/EventDispatcher.cpp中。
EventDispatcher类的成员变量m_event描述的就是当前发生的Touch Event。这个Touch Event的Event Path是一个Node Event Context列表。列表中的第一个Node Event Context描述的就是当前发生的Touch Event的Target Node的上下文信息。有了这个Node Event Context之后,就可以调用它的成员函数handleLocalEvents将当前发生的Touch Event分发给Target Node的DOM Event Handler处理,如下所示:
void NodeEventContext::handleLocalEvents(Event* event) const
{
......
event->setTarget(target());
event->setCurrentTarget(m_currentTarget.get());
m_node->handleLocalEvents(event);
}
这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/events/NodeEventContext.cpp中。
这时候,NodeEventContext类的成员变量m_node描述的就是Touch Event的Target Node。NodeEventContext类的成员函数handleLocalEvents调用这个Target Node的成员函数handleLocalEvents,用来将当前发生的Touch Event分发给它的DOM Event Handler处理,如下所示:
void Node::handleLocalEvents(Event* event)
{
......
fireEventListeners(event);
}
这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/dom/Node.cpp中。
Node类的成员函数handleLocalEvents主要是调用另外一个成员函数fireEventListeners将当前发生的Touch Event分发给Target Node的DOM Event Handler处理。
Node类的成员函数fireEventListeners是从父类EventTarget继承下来的,它的实现如下所示:
bool EventTarget::fireEventListeners(Event* event)
{
......
EventTargetData* d = eventTargetData();
......
EventListenerVector* legacyListenersVector = 0;
AtomicString legacyTypeName = legacyType(event);
if (!legacyTypeName.isEmpty())
legacyListenersVector = d->eventListenerMap.find(legacyTypeName);
EventListenerVector* listenersVector = d->eventListenerMap.find(event->type());
......
if (listenersVector) {
fireEventListeners(event, d, *listenersVector);
} else if (legacyListenersVector) {
......
fireEventListeners(event, d, *legacyListenersVector);
......
}
......
return !event->defaultPrevented();
}
这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/events/EventTarget.cpp中。
Node类的成员函数fireEventListeners主要根据Touch Event的类型在Target Node注册的DOM Event Handler中找到对应的DOM Event Handler。例如,如果当前发生的是类型为MOVE的Touch Event,那么Node类的成员函数fireEventListeners就会找到注册在Target Node上的类型为TouchMove的DOM Event Handler。
找到了对应的DOM Event Handler之后,Node类的成员函数fireEventListeners再调用另外一个重载版本的成员函数fireEventListeners将当前发生的Touch Event分发给它们处理,如下所示:
void EventTarget::fireEventListeners(Event* event, EventTargetData* d, EventListenerVector& entry)
{
......
size_t i = 0;
size_t size = entry.size();
......
for ( ; i < size; ++i) {
RegisteredEventListener& registeredListener = entry[i];
if (event->eventPhase() == Event::CAPTURING_PHASE && !registeredListener.useCapture)
continue;
if (event->eventPhase() == Event::BUBBLING_PHASE && registeredListener.useCapture)
continue;
// If stopImmediatePropagation has been called, we just break out immediately, without
// handling any more events on this target.
if (event->immediatePropagationStopped())
break;
ExecutionContext* context = executionContext();
if (!context)
break;
......
registeredListener.listener->handleEvent(context, event);
......
}
......
}
这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/events/EventTarget.cpp中。
在Capturing和Bubbling阶段,WebKit也是通过Node类的成员函数fireEventListeners将当前发生的Event分发给Event Path上的Node处理。从这里我们就可以看到,在Capturing阶段,如果一个Node在注册了一个在Capturing阶段接收Event的DOM Event Handler,那么此时该DOM Event Handler就会获得当前发生的Event。同样,在Bubbling阶段,如果一个Node在注册了一个在Bubbling阶段接收Event的DOM Event Handler,那么此时该DOM Event Handler就会获得当前发生的Event。对于Target Node来说,它注册的DOM Event Handler则会在Target阶段获得当前发生的Event。
这些DOM Event Handler在注册的时候,会被JavaScript引擎V8封装在一个V8AbstractEventListener对象中。Node类的成员函数fireEventListeners通过调用这些V8AbstractEventListener对象的成员函数handleEvent将当前发生的Event分发给它们所封装的DOM Event Handler处理。这就会进入到JavaScript引擎V8里面去执行了。以后我们分析JavaScript引擎V8时,再回过头来看它对Event的处理流程。
至此,我们就分析完成Chromium分发输入事件给WebKit,以及WebKit进行处理的过程了。这个过程是在Render进程的Main线程中执行的。回忆前面Chromium网页滑动和捏合手势处理过程分析一文,滑动和捏合手势这两种特殊的输入事件,是在Render进程的Compositor线程中处理的。
WebKit在处理输入事件的过程中,需要通过Render Layer Tree和Render Object Tree,在DOM Tree中找到输入事件的Target Node(Hit Test)。找到了输入事件的Target Node之后,分为Pre Process、Capturing、Target、Bubbling和Post Process五个阶段对输入事件进行处理。其中,Capturing、Target和Bubbling这三个阶段是将输入事件分发给Target Node的DOM Event Handler处理,也就是我们通过JavaScript注册的Event Handler。DOM Event Handler最终是在JavaScript引擎V8执行的。
至此,Chromium的网页输入事件处理机制我们就全部分析完成了。重新学习可以参考前面Chromium网页输入事件处理机制简要介绍和学习计划这篇文章。更多的信息也可以关注老罗的新浪微博:http://weibo.com/shengyangluo。
Copyright© 2013-2020
All Rights Reserved 京ICP备2023019179号-8