Chromium的Render进程接收到Browser进程分发过来的输入事件之后,会在Compoistor线程中处理掉滑动和捏合手势这两种特殊的输入事件,其它类型的输入事件则交给Main线程处理。Main线程又会进一步将输入事件分发给WebKit处理。WebKit则根据输入事件发生的位置在网页中找到对应的HTML元素进行处理。本文接下来详细分析Chromium分发输入事件给WebKit处理的过程。

老罗的新浪微博:http://weibo.com/shengyangluo,欢迎关注!

《Android系统源代码情景分析》一书正在进击的程序员网(http://0xcc0xcd.com)中连载,点击进入!

以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, &params))  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);} elsecontinue;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);} elsecontinue;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流程。

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 descendantsif (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。

Chromium分发输入事件给WebKit处理的过程分析相关推荐

  1. Android输入事件从读取到分发三:InputDispatcherThread线程分发事件的过程

    分析完事件的读取后,东忙西忙,不知不觉已过去了快五个月了-也不是说没有时间把这部分源码分析完,而是实在是分析不下去,因此转战到其他地方去了.然而这一块始终是心头那不舍的惦记,经过一段时间的沉淀,又参考 ...

  2. Android输入事件从读取到分发四:InputDispatcherThread发送事件到View结构树的过程

    通过上一节的分析,我们发现InputDispatcherThread使用InputChannel的sendMessage方法发送了一条消息,但是我们不知道谁在接收这条消息.因此,这一节我们的目标很明确 ...

  3. 【Android 事件分发】事件分发源码分析 ( 驱动层通过中断传递事件 | WindowManagerService 向 View 层传递事件 )

    Android 事件分发 系列文章目录 [Android 事件分发]事件分发源码分析 ( 驱动层通过中断传递事件 | WindowManagerService 向 View 层传递事件 ) 文章目录 ...

  4. Android输入事件InputReader和InputDispatcher分析

    输入事件分发流程: 1.InputReader 线程通过 EventHub 监听 /dev/input 内的文件 2.InputReader 得到输入事件,封装后存入 InputDispatcher ...

  5. Android 输入事件一撸到底之View接盘侠(3)

    前言 系列文章 1.Android 输入事件一撸到底之源头活水(1) 2.Android 输入事件一撸到底之DecorView拦路虎(2) 3.Android 输入事件一撸到底之View接盘侠(3 前 ...

  6. 13.在Ubuntu20.04下,chromium无法输入中文以及如何卸载chromium浏览器

    因为chromium安装比较简单,所以在Ubuntu20.04下直接安装了chromium,但是最近发现,无法在chromium中输入中文了,经过一顿折腾,最终也没搞定,解决办法就是安装Chrome ...

  7. 【Android 事件分发】事件分发源码分析 ( ViewGroup 事件传递机制 七 )

    Android 事件分发 系列文章目录 [Android 事件分发]事件分发源码分析 ( 驱动层通过中断传递事件 | WindowManagerService 向 View 层传递事件 ) [Andr ...

  8. 【Android 事件分发】事件分发源码分析 ( ViewGroup 事件传递机制 六 )

    Android 事件分发 系列文章目录 [Android 事件分发]事件分发源码分析 ( 驱动层通过中断传递事件 | WindowManagerService 向 View 层传递事件 ) [Andr ...

  9. 【Android 事件分发】事件分发源码分析 ( ViewGroup 事件传递机制 五 )

    Android 事件分发 系列文章目录 [Android 事件分发]事件分发源码分析 ( 驱动层通过中断传递事件 | WindowManagerService 向 View 层传递事件 ) [Andr ...

最新文章

  1. 组合数函数-快速提取所有可能的组合数
  2. VirtWire 向客服发ticket
  3. 面试题整理19 矩阵Z字形扫描
  4. 运维一定要懂的100个网络安全小知识
  5. 【NLP】PET——文本分类的又一种妙解
  6. 与疫情赛跑|武汉火神山医院施工视频和现场画面
  7. 【前端5】vue:实例,插值表达式,v-,组件
  8. 关于在WinForm里用HttpWebRequest获得某个页面,并填写页面的textbox及点击button的方法...
  9. matlab2c使用c++实现matlab函数系列教程-save函数
  10. MacBooster 8 Pro for Mac(系统清理优化软件)
  11. html在线取色,JS实现的RGB网页颜色在线取色器完整实例
  12. 仿淘宝收货地址之三级联动
  13. python之类和对象
  14. less转换css及html里面JS的引入
  15. 齐治 堡垒机 mysql_齐治堡垒机:让数据库运维审计无死角
  16. 阿里聚石塔限制IP 过部署服务器详细流程
  17. Win10上的CPU内存的悬浮窗口
  18. 杜威分类法_设计机器人:从都会到休伊,杜威和路易
  19. IBM再次出手,蓝色巨人收购蓝色巨狼
  20. Multi class ovr or ovo

热门文章

  1. android跳转到qq页面显示,安卓仿QQ登录及登录成功界面跳转
  2. mulesoft Module 4 quiz解析
  3. 跨境电商joom蓝海选品方法
  4. ITSM实施必须经历的五个关键步骤
  5. 串联谐振和并联谐振有什么区别?分别有什么特点?
  6. 变送器选型需要提供哪些参数
  7. api-ms-win-crt-runtime-l1-1-0.dll丢失找不到解决方法
  8. 资本寒意渐浓:科技行业估值修正在所难免
  9. ios怎么更新测试软件,ios检测版本更新
  10. EGE入门小游戏:字母游戏