标签:
Chromium的Render进程接收到Browser进程分发过来的输入事件之后,会在Compoistor线程中处理掉滑动和捏合手势这两种特殊的输入事件,其它类型的输入事件则交给Main线程处理。Main线程又会进一步将输入事件分发给WebKit处理。WebKit则根据输入事件发生的位置在网页中找到对应的HTML元素进行处理。本文接下来详细分析Chromium分发输入事件给WebKit处理的过程。
老罗的新浪微博:http://weibo.com/shengyangluo,欢迎关注!
以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类的成员变量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流程。
4. 对当前正在处理的Render Layer关联的Render Object的Background层进行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$ vi 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分发给它的OM 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的OM 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。
标签:
原文地址:http://blog.csdn.net/luoshengyang/article/details/51875844