在上一篇博文中分析了事件分发的流程及规则,本篇会从源码的角度更进一步理解事件分发机制的原理,如果对事件分发规则还不太清楚的童鞋,建议先看一下上一篇博文 《Android查缺补漏(View篇)--事件分发机制》 ,先来看一下本篇的分析思路,一会儿会按照事件传递的顺序,针对以下几点进行源码分析:

  • Activity对点击事件的分发过程
  • PhoneWindow是如何处理点击事件的
  • 顶级View对点击事件的分发过程
  • View对点击事件的处理过程

Activity对点击事件的分发过程

通过上一篇博文中我们就知道了当一个点击事件发生后,最先传递给当前的Activity,并有Activity的dispatchTouchEvent方法来分发,其具体的分发工作时由Activity内部的Window处理的。

Activity的dispatchTouchEvent方法源码如下:

public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction(); // 这是一个空方法,不用理会
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}

在dispatchTouchEvent内部我们看到通过getWindow获取了Window,然后将事件分发的处理操作交给了Window,通过上述源码我们即可看出从大的流程上Window将分发给顶级View,如果在getWindow().superDispatchTouchEvent(ev)这一步返回了true,那么事件循环就此结束,返回false意味着各界的View的onTouchEvent都返回了false,最终会去调用当前Activity的onTouchEvent。

PhoneWindow是如何处理点击事件的

接着详细看一下在getWindow().superDispatchTouchEvent(ev)中都干了啥,我们知道Window是一个抽象类,我们必须找出他们实现类才可以进行下一步分析,很简单,通过在Activity中搜索mWindow变量,很快我们就在Activity的attch方法中找到了它的赋值过程:

final void attach(Context context, ActivityThread aThread,
Instrumentation instr, IBinder token, int ident,
Application application, Intent intent, ActivityInfo info,
CharSequence title, Activity parent, String id,
NonConfigurationInstances lastNonConfigurationInstances,
Configuration config, String referrer, IVoiceInteractor voiceInteractor,
Window window, ActivityConfigCallback activityConfigCallback) {
attachBaseContext(context); mFragments.attachHost(null /*parent*/); mWindow = new PhoneWindow(this, window, activityConfigCallback);
...// 省略其他代码
}

没错,Window的实现类就是PhoneWindow,接下来直接去看PhoneWindow的superDispatchTouchEvent方法:

@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}

PhoneWindow什么都没做,直接把DecorView,DecorView就是Activity的顶级View,在Activity中可以通过getWindow().getDecorView()获取。

顶级View(DecorView)对点击事件的分发过程

DecorView的superDispatchTouchEvent源码如下:

public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}

其实DecorView继承了FrameLayout,当然也是一个ViewGroup,所以这里我们再跳转到ViewGroup的dispatchTouchEvent方法中进一步分析,在上一篇博文《Android查缺补漏(View篇)--事件分发机制》 中已经详细介绍了ViewGroup的dispatchTouchEvent对点击事件分发的流程,这里再通过源代码了解一下它的实现过程。

这个方法非常长,但我们一步步分析还是能找到一些头绪的,请着重看代码中注释。

@Override
public boolean dispatchTouchEvent(MotionEvent ev) { ... boolean handled = false;
// 对事件进行安全过滤
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK; // 初始化事件流的状态,当有down类型的touch事件传来时说明是一个新的事件流,此时需要重置事件状态。 // Handle an initial down.
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Throw away all previous state when starting a new touch gesture.
// The framework may have dropped the up or cancel event for the previous gesture
// due to an app switch, ANR, or some other state change.
cancelAndClearTouchTargets(ev);
resetTouchState();
} // 检查是否拦截事件,当事件交由ViewGroup的子元素处理时,就会将mFirstTouchTarget赋值并指向子元素。
// 更详细来说就是,当ViewGroup拦截事件时,mFirstTouchTarget为null,这时候除了down以外的事件类型到来时将不再调用onInterceptTouchEvent,同一事件流中的其他时间类型都会讲给ViewGroup处理。当ViewGroup不拦截事件时,mFirstTouchTarget将会被赋值指向为子元素。 // Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
// 这里包含了一种特殊情况,那就是在子View中可以通过requesetDisallowInterceptTouchEvent方法来干预ViewGroup对除了down以外的事件的分发过程,
// 这里的FLAG_DISALLOW_INTERCEPT标记为就是通过在requestDisallowInterceptTouchEvent方法来设置的,FLAG_DISALLOW_INTERCEPT被设置后,ViewGroup就不在对事件拦截。
// 当down事件传来后,在前面的resetTouchState方法中会对FLAG_DISALLOW_INTERCEPT重置,所以说子View无法影响ViewGroup对down事件的处理。 final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
} // If intercepted, start normal event dispatch. Also if there is already
// a view that is handling the gesture, do normal event dispatch.
if (intercepted || mFirstTouchTarget != null) {
ev.setTargetAccessibilityFocus(false);
} // Check for cancelation.
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL; // Update list of touch targets for pointer down, if needed.
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
if (!canceled && !intercepted) { // If the event is targeting accessiiblity focus we give it to the
// view that has accessibility focus and if it does not handle it
// we clear the flag and dispatch the event to all children as usual.
// We are looking up the accessibility focused host to avoid keeping
// state since these events are very rare.
View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
? findChildWithAccessibilityFocus() : null; if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
final int actionIndex = ev.getActionIndex(); // always 0 for down
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
: TouchTarget.ALL_POINTER_IDS; // Clean up earlier touch targets for this pointer id in case they
// have become out of sync.
removePointersFromTouchTargets(idBitsToAssign); final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
// Find a child that can receive the event.
// Scan children from front to back.
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled(); // 当ViewGroup不拦截事件的情况下,事件就会交给子View处理
// 首先遍历ViewGroup的所有子View,判断子View是否能够接收到点击事件:
// 判断的依据是:子元素是否在播放动画和点击事件的坐标是否落在子元素的范围内。 final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex); // If there is a view that has accessibility focus we want it
// to get the event first and if not handled we will perform a
// normal dispatch. We may do a double iteration but this is
// safer given the timeframe.
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
} if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
} newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
// Child is already receiving touch within its bounds.
// Give it the new pointer in addition to the ones it is handling.
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
} resetCancelNextUpFlag(child); /*
这里的dispatchTransformedTouchEvent方法实际调用的就是子元素的dispatchTouchEvent方法。
这个方法内部有一段逻辑是这样:
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
由于这里传入的child非null,所以此时事件就交给了子view处理。 如果子View的dispatchTouchEvent方法返回了true,那么mFirstTouchTarget就会被赋值(在addTouchTarget()中被赋值),同时跳出for循环。
如果子View返回了false,那就继续循环,按同样的套路将调用下一个子View的dispatchTouchEvent方法。(例如:多个View重叠在一起时,这样在该区域就有多个满足接收点击事件的条件的控件) */ if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
// childIndex points into presorted list, find original index
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
} // The accessibility focus didn't handle the event, so clear
// the flag and do a normal dispatch to all children.
ev.setTargetAccessibilityFocus(false);
}
if (preorderedList != null) preorderedList.clear();
} if (newTouchTarget == null && mFirstTouchTarget != null) {
// Did not find a child to receive the event.
// Assign the pointer to the least recently added target.
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
}
} // Dispatch to touch targets.
if (mFirstTouchTarget == null) {
/*
mFirstTouchTarget为null时,ViewGroup就会拦截接下来的同一序列中的所有事件。
如果遍历了所有的子元素事件都没有被合适的处理(可能是因为该ViewGroup没有子View,或者在子View的dispatchTouchEvent方法返回了false),ViewGroup就会自己处理事件。 这里的第三个参数传入了null,在dispatchTransformedTouchEvent中通过以下逻辑即可知道会调用super.dispatchTouchEvent(event); if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
*/ // No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
...
}
...
}

上面代码是对顶级View(其实也是ViewGroup)事件分发的分析,接下来再看看当事件传递给View时,View对事件的处理代码。

View对点击事件的处理过程

从上面的分析我们可知,ViewGroup是通过调用View的dispatchTouchEvent方法将事件传递给View的,那么就来看一下View的dispatchTouchEvent方法源码:

public boolean dispatchTouchEvent(MotionEvent event) {

    ...

    if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
} if (!result && onTouchEvent(event)) {
result = true;
}
} ... return result;
}

在上面的源码中,首先会获取View的ListenerInfo判断有没有设置OnTouchListener,如果设置了并且在OnTouchListener中的onTouch方法返回了true,那么就不再调用View自身的onTouchEvent方法,至此我们也找到了OnTouchListener监听器中的onTouch方法优先于onTouchEvent方法的原因。

接着再看onTouchEvent方法的源码:

public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction(); final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE; // 从这段代码可知,即使不可用状态的控件也会消费事件。
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return clickable;
} // mTouchDelegate是View的代理,有代理的情况下回调用代理的onTouchEvent方法。 if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
} // 从上面代码为clickable赋值时我们可以知道,不管View是不是disable状态,只要它的CLICKABLE和LONG_CLICKABLE有一个为true时,它就会消耗这个事件。 if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
if ((viewFlags & TOOLTIP) == TOOLTIP) {
handleTooltipUp();
}
if (!clickable) {
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
break;
}
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
// take focus if we don't have it already and we should in
// touch mode.
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
} if (prepressed) {
// The button is being released before we actually
// showed it as pressed. Make it show the pressed
// state now (before scheduling the click) to ensure
// the user sees it.
setPressed(true, x, y);
} if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// This is a tap, so remove the longpress check
removeLongPressCallback(); // Only perform take click actions if we were in the pressed state
if (!focusTaken) {
// Use a Runnable and post this rather than calling
// performClick directly. This lets other visual state
// of the view update before click actions start.
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) { // action_up事件会触发performClick方法,在performClick方法中调用了View的onClickListener监听器的onClick方法。 performClick();
}
}
} if (mUnsetPressedState == null) {
mUnsetPressedState = new UnsetPressedState();
} if (prepressed) {
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} else if (!post(mUnsetPressedState)) {
// If the post failed, unpress right now
mUnsetPressedState.run();
} removeTapCallback();
}
mIgnoreNextUpEvent = false;
break; case MotionEvent.ACTION_DOWN:
if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
}
mHasPerformedLongPress = false; if (!clickable) {
checkForLongClick(0, x, y);
break;
} if (performButtonActionOnTouchDown(event)) {
break;
} // Walk up the hierarchy to determine if we're inside a scrolling container.
boolean isInScrollingContainer = isInScrollingContainer(); // For views inside a scrolling container, delay the pressed feedback for
// a short period in case this is a scroll.
if (isInScrollingContainer) {
mPrivateFlags |= PFLAG_PREPRESSED;
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPendingCheckForTap.x = event.getX();
mPendingCheckForTap.y = event.getY();
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
// Not inside a scrolling container, so show the feedback right away
setPressed(true, x, y);
checkForLongClick(0, x, y);
}
break; case MotionEvent.ACTION_CANCEL:
if (clickable) {
setPressed(false);
}
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
break; case MotionEvent.ACTION_MOVE:
if (clickable) {
drawableHotspotChanged(x, y);
} // Be lenient about moving outside of buttons
if (!pointInView(x, y, mTouchSlop)) {
// Outside button
// Remove any future long press/tap checks
removeTapCallback();
removeLongPressCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
}
break;
} return true;
} return false;
}

当action_up事件到来时会触发performClick方法,在performClick方法中调用了View的onClickListener监听器的onClick方法,所以至此我们应该知道onClick方法是在up事件发生后调用的,优先级最低。

public boolean performClick() {
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
} sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED); notifyEnterOrExitForAutoFillIfNeeded(true); return result;
}

小结

到这里整个事件分发的源码基本分析完毕,大体总结一下:

  1. 当点击事件产生后,会首先触发Activity的dispatchTouchEvent方法,在此方法中会将事件传递个PhoneWindow处理;
  2. 在PhoneWindow的dispatch方法中又将事件传递个了DecorView的dispatchTouchEvent;
  3. 接着DecorView又调用了父类ViewGroup的dispatchTouchEvent方法对事件进行了分发;
  4. 在ViewGroup的dispatchTouchEvent方法中,首先先判断了自身是否拦截事件,如果拦截那么事件不再向其子View传递,如果不拦截就会遍历其所有的子View找到可以适合接收事件的子View并调用子View的dispatchTouchEvent方法。
  5. 在子View的dispatchTouchEvent方法中对事件做相应处理。

最后想说的是,本系列文章为博主对Android知识进行再次梳理,查缺补漏的学习过程,一方面是对自己遗忘的东西加以复习重新掌握,另一方面相信在重新学习的过程中定会有巨大的新收获,如果你也有跟我同样的想法,不妨关注我一起学习,互相探讨,共同进步!

参考文献:

  • 《Android开发艺术探索》

Android查缺补漏(View篇)--事件分发机制源码分析的更多相关文章

  1. Android事件分发机制源码分析

    Android事件分发机制源码分析 Android事件分发机制源码分析 Part1事件来源以及传递顺序 Activity分发事件源码 PhoneWindow分发事件源码 小结 Part2ViewGro ...

  2. Qt事件分发机制源码分析之QApplication对象构建过程

    我们在新建一个Qt GUI项目时,main函数里会生成类似下面的代码: int main(int argc, char *argv[]) { QApplication application(argc ...

  3. Android View事件分发-从源码分析

    View事件分发-从源码分析 学习自 <Android开发艺术探索> https://blog.csdn.net/qian520ao/article/details/78555397?lo ...

  4. Android View 事件分发机制 源码解析 (上)

    一直想写事件分发机制的文章,不管咋样,也得自己研究下事件分发的源码,写出心得~ 首先我们先写个简单的例子来测试View的事件转发的流程~ 1.案例 为了更好的研究View的事件转发,我们自定以一个My ...

  5. (转)spring boot实战(第三篇)事件监听源码分析

    原文:http://blog.csdn.net/liaokailin/article/details/48194777 监听源码分析 首先是我们自定义的main方法: package com.lkl. ...

  6. Android开发学习之路-Handler消息派发机制源码分析

    注:这里只是说一下sendmessage的一个过程,post就类似的 如果我们需要发送消息,会调用sendMessage方法 public final boolean sendMessage(Mess ...

  7. View的事件分发机制解析

    引言 Android事件构成 在Android中,事件主要包含点按.长按.拖拽.滑动等,点按又包含单击和双击,另外还包含单指操作和多指操作.全部这些都构成了Android中的事件响应.总的来说.全部的 ...

  8. Android查缺补漏(View篇)--自定义 View 的基本流程

    View是Android很重要的一部分,常用的View有Button.TextView.EditView.ListView.GridView.各种layout等等,开发者通过对这些View的各种组合以 ...

  9. Android查缺补漏(IPC篇)-- Bundle、文件共享、ContentProvider、Messenger四种进程间通讯介绍

    本文作者:CodingBlock 文章链接:http://www.cnblogs.com/codingblock/p/8387752.html 进程间通讯篇系列文章目录: Android查缺补漏(IP ...

随机推荐

  1. MicroPython可视化编程开发板—TurnipBit自制MP3教程实例

    转载请以链接形式注明文章来源(MicroPythonQQ技术交流群:157816561,公众号:MicroPython玩家汇) 当前我们都生活在一个有声有色的社会当中,欣赏美丽的景色,享受动人的音乐, ...

  2. 用AndroidStudio发布Libs到Bintray jCenter

    1 RootProject[根目录]build.gradle中添加如下插件引用 dependencies { ....... classpath 'com.jfrog.bintray.gradle:g ...

  3. 删除链表中间节点和a/b处的节点

    [题目]: 给定链表的头节点 head,实现删除链表的中间节点的函数. 例如: 步删除任何节点: 1->2,删除节点1: 1->2->3,删除节点2: 1->2->3-& ...

  4. javascript 执行环境细节分析、原理-12

    前言 前面几篇说了执行环境相关的概念,本篇在次回顾下 执行环境(Execution context,简称EC,也称执行上下文 ) 定义了变量或者函数有权访问的数据,决定了各自行为,每个执行环境都有一个 ...

  5. Django-Views模块详解

    http请求中产生的两个核心对象 http请求: HttpRequest http响应: HttpResponse 所在位置 django.http httpRequest属性: HttpReques ...

  6. 浅谈Android的广告欢迎界面(倒计时)

    前些时候就是别人问我他的android APP怎么做一个广告的欢迎界面,就是过几秒后自动跳转到主界面的实现. 也就是下面这种类似的效果.要插什么广告的话你就换张图吧. 那么我就思考了下,就用了andr ...

  7. Code Review 程序员的寄望与哀伤【转载】

    一个程序员,他写完了代码,在测试环境通过了测试,然后他把它发布到了线上生产环境,但很快就发现在生产环境上出了问题,有潜在的 bug. 事后分析,是生产环境的一些微妙差异,使得这种 bug 场景在线下测 ...

  8. time模块、装饰器、类的装饰器

    python  time模块 导入模块: import time #time模块是python中最基本的模块之一 输出时间戳:time.time() #可以用变量接收,要打印出来要用print(),如 ...

  9. 我们编写 React 组件的最佳实践

    刚接触 React 的时候,在一个又一个的教程上面看到很多种编写组件的方法,尽管那时候 React 框架已经相当成熟,但是并没有一个固定的规则去规范我们去写代码. 在过去的一年里,我们在不断的完善我们 ...

  10. bzoj:1230: [Usaco2008 Nov]lites 开关灯

    Description Farmer John尝试通过和奶牛们玩益智玩具来保持他的奶牛们思维敏捷. 其中一个大型玩具是牛栏中的灯. N (2 <= N <= 100,000) 头奶牛中的每 ...