引言

  上一篇文章我们介绍了View的事件分发机制,今天我们从源码的角度来学习下事件分发机制。

Activity对点击事件的分发过程

  事件最先传递给当前Activity,由Activity的dispatchTouchEvent进行事件分发,具体的工作是有Activity内部的Window来完成的。Window会将事件传递给DecorView,DecorView一般就是当前界面的底层容器(即setContentView所设置的View的父容器)。下面我们来看一下Activity的dispatchTouchEvent方法的源码。源代码如下:

  1. /**
  2. * Called to process touch screen events. You can override this to
  3. * intercept all touch screen events before they are dispatched to the
  4. * window. Be sure to call this implementation for touch screen events
  5. * that should be handled normally.
  6. *
  7. * @param ev The touch screen event.
  8. *
  9. * @return boolean Return true if this event was consumed.
  10. */
  11. public boolean dispatchTouchEvent(MotionEvent ev) {
  12. if (ev.getAction() == MotionEvent.ACTION_DOWN) {
  13. onUserInteraction();
  14. }
  15. if (getWindow().superDispatchTouchEvent(ev)) {
  16. return true;
  17. }
  18. return onTouchEvent(ev);
  19. }

  我们分析上面的源代码,事件首先交给Activity所属的Window进行分发,如果返回true,那么说明事件被子View拦截并且处理了。如果返回false说明事件没人处理,所有的View的onTouchEvent都返回false,那么这时候只有Activity的onTouchEvent会被调用了(还记得上一篇文章写的那些结论吗?看看第5条)。

  那么Window是如何将事件传递给ViewGroup的呢?我们看源代码会知道,Window是一个抽象类,Window的superDispatchTouchEvent方法也是一个抽象方法,我们需要找到Window的实现类才行。Window的实现类是PhoneWindow类,我们来看PhoneWindow是如何处理点击事件的。代码如下:

  1. @Override
  2. public boolean superDispatchTouchEvent(MotionEvent event) {
  3. return mDecor.superDispatchTouchEvent(event);
  4. }

  看到这里,逻辑变得很清晰了,PhoneWindow将事件直接传递给DecorView,那DecorView又是什么呢?在上一篇文章中,我们聊到过,DecorView是顶层的View。我们接着方法往下看,我们看到如下代码:

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

  DecorView事件分发也是要依靠父类方法的,DecorView继承自FrameLayout,而后者继承自ViewGroup。很显然DecorView的事件分发过程调用的是ViewGroup里面的方法。

ViewGroup对事件分发过程

  上面的分析已经讲明,事件到达顶层View后会调用ViewGroup的dispatchTouchEvent方法进行分发。下面的逻辑要分两种情况来说:

  第一种:如果ViewGroup的onInterceptTouchEvent方法返回true,表示ViewGroup需要拦截该事件,这个事件会由ViewGroup来进行处理。

  第二种:如果ViewGroup的onInterceptTouchEvent方法返回false,表示ViewGroup不需要拦截该事件,这时候这个事件会传递给它所在的点击事件链上的子View,这时候子View的dispatchTouchEvent会被调用,这时候事件就有顶级View传递到了下一级的View。接下来的传递过程和上面的过程类似,如此往复,直到整个事件的分发。

  下面我们来看下ViewGroup中事件分发代码的逻辑,先看第一段。

  1. // Check for interception.
  2. final boolean intercepted;
  3. if (actionMasked == MotionEvent.ACTION_DOWN
  4. || mFirstTouchTarget != null) {
  5. final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
  6. if (!disallowIntercept) {
  7. intercepted = onInterceptTouchEvent(ev);
  8. ev.setAction(action); // restore action in case it was changed
  9. } else {
  10. intercepted = false;
  11. }
  12. } else {
  13. // There are no touch targets and this action is not an initial down
  14. // so this view group continues to intercept touches.
  15. intercepted = true;
  16. }

  从上面的代码我们知道,ViewGroup在如下两种情况下会判断是否要拦截当前事件:事件类型为ACTION_DOWN或者mFirstTouchTarget!=null。那么什么情况下mFirstTouchTarget!=null这个条件成立呢?通过后面的代码逻辑我们知道,当ViewGroup不拦截事件,并将事件交给子View处理时,mFirstTouchTarget!=null这个条件就是成立的。反过来说,一旦事件由ViewGroup拦截并且自己来处理时,mFirstTouchTarget!=null就是不成立的,那么当ACTION_MOVE和ACTION_UP事件到来时,由于if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null)这个条件为false就导致ViewGroup的onInterceptTouchEvent方法不会被调用,并且同一事件序列中的其他事件都会默认交给他来处理。

  这里还有一种特殊情况,就是FLAG_DISALLOW_INTERCEPT标记位,这个标记位是通过requestDisallowInterceptTouchEvent这个方法来设置的,一般用于子View中。FLAG_DISALLOW_INTERCEPT设置后,ViewGroup将无法拦截除ACTION_DOWN以外的其他点击事件。在进行事件分发时,如果是ACTION_DOWN事件,会重置这个标记位,这导致子View中设置的这个标记位无效,因此,当面对ACTION_DOWN事件时,ViewGroup总会调用自己的dispatchTouchEvent方法来询问自己是否要拦截事件。代码如下:

  1. // Handle an initial down.
  2. if (actionMasked == MotionEvent.ACTION_DOWN) {
  3. // Throw away all previous state when starting a new touch gesture.
  4. // The framework may have dropped the up or cancel event for the previous gesture
  5. // due to an app switch, ANR, or some other state change.
  6. cancelAndClearTouchTargets(ev);
  7. resetTouchState();
  8. }

  注意这段代码是在上面一个代码段之前的哦,去源码看就知道了。

  下面我们来看ViewGroup不拦截事件的时候,事件会向下分发交给它子View进行处理,这段代码如下:

  1. final int childrenCount = mChildrenCount;
  2. if (newTouchTarget == null && childrenCount != 0) {
  3. final float x = ev.getX(actionIndex);
  4. final float y = ev.getY(actionIndex);
  5. // Find a child that can receive the event.
  6. // Scan children from front to back.
  7. final ArrayList<View> preorderedList = buildOrderedChildList();
  8. final boolean customOrder = preorderedList == null
  9. && isChildrenDrawingOrderEnabled();
  10. final View[] children = mChildren;
  11. for (int i = childrenCount - 1; i >= 0; i--) {
  12. final int childIndex = customOrder
  13. ? getChildDrawingOrder(childrenCount, i) : i;
  14. final View child = (preorderedList == null)
  15. ? children[childIndex] : preorderedList.get(childIndex);
  16.  
  17. // If there is a view that has accessibility focus we want it
  18. // to get the event first and if not handled we will perform a
  19. // normal dispatch. We may do a double iteration but this is
  20. // safer given the timeframe.
  21. if (childWithAccessibilityFocus != null) {
  22. if (childWithAccessibilityFocus != child) {
  23. continue;
  24. }
  25. childWithAccessibilityFocus = null;
  26. i = childrenCount - 1;
  27. }
  28.  
  29. if (!canViewReceivePointerEvents(child)
  30. || !isTransformedTouchPointInView(x, y, child, null)) {
  31. ev.setTargetAccessibilityFocus(false);
  32. continue;
  33. }
  34.  
  35. newTouchTarget = getTouchTarget(child);
  36. if (newTouchTarget != null) {
  37. // Child is already receiving touch within its bounds.
  38. // Give it the new pointer in addition to the ones it is handling.
  39. newTouchTarget.pointerIdBits |= idBitsToAssign;
  40. break;
  41. }
  42.  
  43. resetCancelNextUpFlag(child);
  44. if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
  45. // Child wants to receive touch within its bounds.
  46. mLastTouchDownTime = ev.getDownTime();
  47. if (preorderedList != null) {
  48. // childIndex points into presorted list, find original index
  49. for (int j = 0; j < childrenCount; j++) {
  50. if (children[childIndex] == mChildren[j]) {
  51. mLastTouchDownIndex = j;
  52. break;
  53. }
  54. }
  55. } else {
  56. mLastTouchDownIndex = childIndex;
  57. }
  58. mLastTouchDownX = ev.getX();
  59. mLastTouchDownY = ev.getY();
  60. newTouchTarget = addTouchTarget(child, idBitsToAssign);
  61. alreadyDispatchedToNewTouchTarget = true;
  62. break;
  63. }
  64.  
  65. // The accessibility focus didn't handle the event, so clear
  66. // the flag and do a normal dispatch to all children.
  67. ev.setTargetAccessibilityFocus(false);
  68. }
  69. if (preorderedList != null) preorderedList.clear();
  70. }

  这段代码逻辑是这样的,首先遍历ViewGroup的所有子元素,然后判断子元素时候能够接收到点击事件。能否接收到点击事件主要由两点来衡量:子元素是否在播放动画和点击事件的坐标是否落在子元素的区域内。如果某一个子元素满足这两个条件,那么事件就会传递给它。

  我们看代码第44行,dispatchTransformedTouchEvent实际上调用的就是子元素的dispatchTouchEvent方法。后面会介绍child这个参数时候为null带来的影响。我们来看这段代码:

  1. if (child == null) {
  2. handled = super.dispatchTouchEvent(event);
  3. } else {
  4. handled = child.dispatchTouchEvent(event);
  5. }

  从上面的代码我们看出来,如果child为null那么就直接调用View的dispatchTouchEvent方法,进行事件的处理。如果child!=null就调用child.dispatchTouchEvent方法进行下一轮的事件分发。如果子元素的dispatchTouchEvent方法返回true,那么mFirstTouchTarget会被赋值,并且跳出for循环(第60行代码),代码如下:

  1. newTouchTarget = addTouchTarget(child, idBitsToAssign);
  2. alreadyDispatchedToNewTouchTarget = true;
  3. break;

  如果子元素的dispatchTouchEvent返回false,ViewGroup会把事件分发给下一个子元素进行处理(结合前两段代码)。

  其实mFirstTouchTarget真正的赋值过程是在addTouchTarget方法内部完成的,mFirstTouchTarget是一种单链表结构,其是否被赋值,将直接影响到ViewGroup对事件的拦截策略,如果mFirstTouchTarget为null,那么ViewGroup就默认拦截接下来同一序列中的所有点击事件,这一点在前面已经介绍过。

  如果遍历ViewGroup后事件都没有被合适的处理,那么这包含两种情况,第一种是ViewGroup没有子元素;第二种是子元素处理了点击事件,但是在dispatchTouchEvent中返回false,这一般是onTouchEvent方法返回false,在这两种情况下,ViewGroup会自己处理点击事件。看下面代码:

  1. // Dispatch to touch targets.
  2. if (mFirstTouchTarget == null) {
  3. // No touch targets so treat this as an ordinary view.
  4. handled = dispatchTransformedTouchEvent(ev, canceled, null,
  5. TouchTarget.ALL_POINTER_IDS);
  6. }

  看到第三个参数传递的null了吗?上面我们分析过,会调用View的dispatchTouchEvent方法。这时候点击事件就交给View来处理了。

View对点击事件的处理

  View对点击事件的处理稍微简单一些,注意这里的View不包括ViewGroup。我们来看它的dispatchTouchEvent方法源代码:

  1. /**
  2. * Pass the touch screen motion event down to the target view, or this
  3. * view if it is the target.
  4. *
  5. * @param event The motion event to be dispatched.
  6. * @return True if the event was handled by the view, false otherwise.
  7. */
  8. public boolean dispatchTouchEvent(MotionEvent event) {
  9. // If the event should be handled by accessibility focus first.
  10. if (event.isTargetAccessibilityFocus()) {
  11. // We don't have focus or no virtual descendant has it, do not handle the event.
  12. if (!isAccessibilityFocusedViewOrHost()) {
  13. return false;
  14. }
  15. // We have focus and got the event, then use normal event dispatch.
  16. event.setTargetAccessibilityFocus(false);
  17. }
  18.  
  19. boolean result = false;
  20.  
  21. if (mInputEventConsistencyVerifier != null) {
  22. mInputEventConsistencyVerifier.onTouchEvent(event, 0);
  23. }
  24.  
  25. final int actionMasked = event.getActionMasked();
  26. if (actionMasked == MotionEvent.ACTION_DOWN) {
  27. // Defensive cleanup for new gesture
  28. stopNestedScroll();
  29. }
  30.  
  31. if (onFilterTouchEventForSecurity(event)) {
  32. //noinspection SimplifiableIfStatement
  33. ListenerInfo li = mListenerInfo;
  34. if (li != null && li.mOnTouchListener != null
  35. && (mViewFlags & ENABLED_MASK) == ENABLED
  36. && li.mOnTouchListener.onTouch(this, event)) {
  37. result = true;
  38. }
  39.  
  40. if (!result && onTouchEvent(event)) {
  41. result = true;
  42. }
  43. }
  44.  
  45. if (!result && mInputEventConsistencyVerifier != null) {
  46. mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
  47. }
  48.  
  49. // Clean up after nested scrolls if this is the end of a gesture;
  50. // also cancel it if we tried an ACTION_DOWN but we didn't want the rest
  51. // of the gesture.
  52. if (actionMasked == MotionEvent.ACTION_UP ||
  53. actionMasked == MotionEvent.ACTION_CANCEL ||
  54. (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
  55. stopNestedScroll();
  56. }
  57.  
  58. return result;
  59. }

  因为View(这里不包括ViewGroup)是一个单独的元素,它没有子元素因此无法向下传递事件,所以他只能自己处理事件。从上面的代码中,我们可以看出View对点击事件的处理过程,首先会判断有没有设置OnTouchListener。如果OnTouchListener中的onTouch方法返回true,那么onTouchEvent就不会被调用,可见OnTouchListener优先级是高于onTouchEvent的。

  接下来我们来分析onTouchEvent的实现。我们分段来介绍,部分实现如下:

  1. if ((viewFlags & ENABLED_MASK) == DISABLED) {
  2. if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
  3. setPressed(false);
  4. }
  5. // A disabled view that is clickable still consumes the touch
  6. // events, it just doesn't respond to them.
  7. return (((viewFlags & CLICKABLE) == CLICKABLE
  8. || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
  9. || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
  10. }

  当View处于不可用状态下时View照样会消耗点击事件。如果View设置有代理,那么还会执行TouchDelagate的onTouchEvent方法,这个onTouchEvent的工作机制应该和OnTouchListener类似。代码如下:

  1. if (mTouchDelegate != null) {
  2. if (mTouchDelegate.onTouchEvent(event)) {
  3. return true;
  4. }
  5. }

  下面我们再来看一下onTouchEvent方法对点击事件的具体处理,代码如下:

  1. if (((viewFlags & CLICKABLE) == CLICKABLE ||
  2. (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
  3. (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
  4. switch (action) {
  5. case MotionEvent.ACTION_UP:
  6. boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
  7. if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
  8. // take focus if we don't have it already and we should in
  9. // touch mode.
  10. boolean focusTaken = false;
  11. if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
  12. focusTaken = requestFocus();
  13. }
  14.  
  15. if (prepressed) {
  16. // The button is being released before we actually
  17. // showed it as pressed. Make it show the pressed
  18. // state now (before scheduling the click) to ensure
  19. // the user sees it.
  20. setPressed(true, x, y);
  21. }
  22.  
  23. if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
  24. // This is a tap, so remove the longpress check
  25. removeLongPressCallback();
  26.  
  27. // Only perform take click actions if we were in the pressed state
  28. if (!focusTaken) {
  29. // Use a Runnable and post this rather than calling
  30. // performClick directly. This lets other visual state
  31. // of the view update before click actions start.
  32. if (mPerformClick == null) {
  33. mPerformClick = new PerformClick();
  34. }
  35. if (!post(mPerformClick)) {
  36. performClick();
  37. }
  38. }
  39. }
  40.  
  41. if (mUnsetPressedState == null) {
  42. mUnsetPressedState = new UnsetPressedState();
  43. }
  44.  
  45. if (prepressed) {
  46. postDelayed(mUnsetPressedState,
  47. ViewConfiguration.getPressedStateDuration());
  48. } else if (!post(mUnsetPressedState)) {
  49. // If the post failed, unpress right now
  50. mUnsetPressedState.run();
  51. }
  52.  
  53. removeTapCallback();
  54. }
  55. mIgnoreNextUpEvent = false;
  56. break;
  57.  
  58. case MotionEvent.ACTION_DOWN:
  59. mHasPerformedLongPress = false;
  60.  
  61. if (performButtonActionOnTouchDown(event)) {
  62. break;
  63. }
  64.  
  65. // Walk up the hierarchy to determine if we're inside a scrolling container.
  66. boolean isInScrollingContainer = isInScrollingContainer();
  67.  
  68. // For views inside a scrolling container, delay the pressed feedback for
  69. // a short period in case this is a scroll.
  70. if (isInScrollingContainer) {
  71. mPrivateFlags |= PFLAG_PREPRESSED;
  72. if (mPendingCheckForTap == null) {
  73. mPendingCheckForTap = new CheckForTap();
  74. }
  75. mPendingCheckForTap.x = event.getX();
  76. mPendingCheckForTap.y = event.getY();
  77. postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
  78. } else {
  79. // Not inside a scrolling container, so show the feedback right away
  80. setPressed(true, x, y);
  81. checkForLongClick(0);
  82. }
  83. break;
  84.  
  85. case MotionEvent.ACTION_CANCEL:
  86. setPressed(false);
  87. removeTapCallback();
  88. removeLongPressCallback();
  89. mInContextButtonPress = false;
  90. mHasPerformedLongPress = false;
  91. mIgnoreNextUpEvent = false;
  92. break;
  93.  
  94. case MotionEvent.ACTION_MOVE:
  95. drawableHotspotChanged(x, y);
  96.  
  97. // Be lenient about moving outside of buttons
  98. if (!pointInView(x, y, mTouchSlop)) {
  99. // Outside button
  100. removeTapCallback();
  101. if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
  102. // Remove any future long press/tap checks
  103. removeLongPressCallback();
  104.  
  105. setPressed(false);
  106. }
  107. }
  108. break;
  109. }
  110.  
  111. return true;
  112. }

  我们从上面的代码中看到,只要View的CLICKABLE和LONG_CLICKABLE其中有一个true,就会消耗掉事件。即onTouchEvent方法true,不管是不是DISABLE状态。然后就是当ACTION_UP事件发生时,会触发performClick方法。如果View设置了OnClickListener,那么performClick方法内部会调用它的onClick方法。代码如下:

  1. /**
  2. * Call this view's OnClickListener, if it is defined. Performs all normal
  3. * actions associated with clicking: reporting accessibility event, playing
  4. * a sound, etc.
  5. *
  6. * @return True there was an assigned OnClickListener that was called, false
  7. * otherwise is returned.
  8. */
  9. public boolean performClick() {
  10. final boolean result;
  11. final ListenerInfo li = mListenerInfo;
  12. if (li != null && li.mOnClickListener != null) {
  13. playSoundEffect(SoundEffectConstants.CLICK);
  14. li.mOnClickListener.onClick(this);
  15. result = true;
  16. } else {
  17. result = false;
  18. }
  19.  
  20. sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
  21. return result;
  22. }

  最后再说一句,调用setOnClickListener和setOnLongClickListener可以改变View的CLICKABLE和LONG_CLICKABLE属性。

  最后再说一句,下一篇文章通过一个简单的例子来介绍滑动冲突。(*^__^*)

Android View事件分发源码分析的更多相关文章

  1. ViewGroup事件分发源码分析

    1.AndroidStudio源码调试方式 AndroidStudio默认是支持一部分源码调试的,但是build.gradle(app) 中的sdk版本要保持一致, 最好是编译版本.运行版本以及手机的 ...

  2. android事件分发源码分析—笔记

    昨天晚上从源码角度复习了一下android的事件分发机制,今天将笔记整理下放在网上.其实说复习,也是按着<android开发艺术探索>这本书作者的思路做的笔记. 目录 事件是如何从Acti ...

  3. view事件分发源码理解

    有些困难无法逃避,没办法,那就只有去解决它.view事件分发对我而言是一块很难啃的骨头,看了<安卓开发艺术探索>关于这个知识点的讲解,看了好几遍,始终不懂,最终通过调试分析结果,看博客,再 ...

  4. Qt中事件分发源码剖析

    Qt中事件分发源码剖析 Qt中事件传递顺序: 在一个应该程序中,会进入一个事件循环,接受系统产生的事件,而且进行分发,这些都是在exec中进行的. 以下举例说明: 1)首先看看以下一段演示样例代码: ...

  5. 深入理解Spring系列之十:DispatcherServlet请求分发源码分析

    转载 https://mp.weixin.qq.com/s/-kEjAeQFBYIGb0zRpST4UQ DispatcherServlet是SpringMVC的核心分发器,它实现了请求分发,是处理请 ...

  6. Touch事件分发源码解析

    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 以下源码基于Gingerbread 2.3.7 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1.先看ViewGroup的di ...

  7. Android笔记--View绘制流程源码分析(二)

    Android笔记--View绘制流程源码分析二 通过上一篇View绘制流程源码分析一可以知晓整个绘制流程之前,在activity启动过程中: Window的建立(activit.attach生成), ...

  8. Android笔记--View绘制流程源码分析(一)

    Android笔记--View绘制流程源码分析 View绘制之前框架流程分析 View绘制的分析始终是离不开Activity及其内部的Window的.在Activity的源码启动流程中,一并包含 着A ...

  9. ApplicationEvent事件机制源码分析

    <spring扩展点之三:Spring 的监听事件 ApplicationListener 和 ApplicationEvent 用法,在spring启动后做些事情> <服务网关zu ...

随机推荐

  1. 免费的Web服务

    这个网站包括和很多免费的Web服务,比如传说中的天气预报.手机号归属地.IP地址归属地.列车时刻表.邮箱验证.验证码图片生成.还有什么股票,基金 http://www.webxml.com.cn/zh ...

  2. 如何验证 Email 地址:SMTP 协议入门教程

    http://www.ruanyifeng.com/blog/2017/06/smtp-protocol.html 作者: 阮一峰 日期: 2017年6月25日   Email 是最常用的用户识别手段 ...

  3. Sencha Touch 实战开发培训 视频教程 第二期 第六节

    2014.4.18 晚上8:20左右开课. 本节课耗时没有超出一个小时. 本期培训一共八节,前两节免费,后面的课程需要付费才可以观看. 本节内容: 图片展示 利用list展示图片: 扩展Carouse ...

  4. hdu3507 Print Article[斜率优化dp入门题]

    Print Article Time Limit: 9000/3000 MS (Java/Others)    Memory Limit: 131072/65536 K (Java/Others)To ...

  5. Visual Studio 2013附加进程调试IE加载的ActiveX Control无效解决方法

    默认Attach to选择了Automatically determine the type of code to debug,显示Native Code.但附加进程到iexplore.exe断点无法 ...

  6. 新浪的动态策略灰度发布系统:ABTestingGateway

    原文链接:http://www.open-open.com/lib/view/open1439889185239.html ABTesingGateway 是一个可以动态设置分流策略的灰度发布系统,工 ...

  7. Des加密(js+java结果一致)【原创】

    des加密算法,javascript版本和java版本 目录: 1.资源文件下载 2.JavaScript文件(des.js) 3.html文件(des.html) 4.java文件(des.java ...

  8. php无限极分类递归与普通

    1. 递归 public function getInfo(){$data=$this->select();$arr=$this->noLimit($data,$f_id=0,$level ...

  9. Jenkins构建报错(Jenkins is reserved for jobs with matching label expression)解决办法

    Jenkins构建报错Jenkins is reserved for jobs with matching label expression 原因节点配置导致 修改配置

  10. Java编程思想第四版随书源码官方下载方法

    见不少人在找net.mindview.util.Print,CSDN上有下载,收积分,以下是官网的下载方法,免费: 官网链接:http://mindview.net/ 电子书下载地址:http://w ...