【Android】 View事件分发机制源码分析

【Android】 View事件分发机制源码分析

文章目录

  • [【Android】 View事件分发机制源码分析](#【Android】 View事件分发机制源码分析)
    • 前言
    • [1. View的事件分发机制](#1. View的事件分发机制)
      • [1.1 MotionEvent](#1.1 MotionEvent)
      • [1.1.1 事件类型](#1.1.1 事件类型)
      • [1.1.2 坐标方法](#1.1.2 坐标方法)
    • [1.2 点击事件的传递规则](#1.2 点击事件的传递规则)
    • [1.3 事件分发机制结论](#1.3 事件分发机制结论)
    • [2. ViewGroup事件分发过程的源码分析](#2. ViewGroup事件分发过程的源码分析)
    • [3. View事件分发过程的源码分析](#3. View事件分发过程的源码分析)

前言

本篇文章基于API36源码,结合安卓开发艺术探索,将从源码角度介绍事件从顶层ViewGroup向下传递的过程,以及View对于事件的处理。

1. View的事件分发机制

1.1 MotionEvent

1.1.1 事件类型

MotionEvent是Android中表示触摸屏输入事件的类,封装了所有触摸操作的信息,包括坐标、压力、触点数量、事件类型等。这里主要介绍比较常见几种事件类型:

  • ACTION_DOWN:手指刚接触屏幕。

  • ACTION_MOVE:手指在屏幕上滑动。

  • ACTION_UP:手指从屏幕上松开的一瞬间。

  • ACTION_CANCEL:当前View失去了事件处理权时。(比如父容器中途拦截了事件,当前View被移除或不可见,打进电话等)

另外还有一个比较重要的概念,在View事件分发机制中比较重要,就是一系列触摸事件组成的有序序列,称之为事件序列

  • 事件序列:从手指触摸屏幕到离开屏幕的完整过程,包含一系列有序的MotionEvent。

三种典型的事件序列:

  • 简单点击:点击屏幕后很快松开,事件序列为ACTION_DOWN → ACTION_UP

  • 典型滑动:点击屏幕后滑动一会再松开,事件序列为:ACTION_DOWN → ACTION_MOVE → ACTION_MOVE → ... → ACTION_MOVE → ACTION_UP

  • 长按操作:点击屏幕后长时间不抬起触发长按,触发后可能会有MOVE事件,事件序列为ACTION_DOWN → (等待500ms) → ACTION_MOVE? → ACTION_UP

1.1.2 坐标方法

我们可以通过MotionEvent对象得到点击事件发生的x和y坐标,系统提供了以下两组方法:

java 复制代码
float getX()           // 相对于当前View的X坐标
float getY()           // 相对于当前View的Y坐标
float getRawX()        // 相对于屏幕的原始X坐标  
float getRawY()        // 相对于屏幕的原始Y坐标

区别很简单,getX()/getY()返回的是相对于当前View左上角的x和y坐标,而getRawX()/getRawY()返回的是相对于手机屏幕左上角的x和y坐标。

1.2 点击事件的传递规则

所谓点击事件的事件分发,即使就是对MotionEvent事件的分发过程,即当一个MotionEvent产生了以后,系统需要把这个事件传递给一个具体的View,而这个传递的过程就是分发过程。事件分发过程由以下三个很重要的方法完成:

  • public boolean dispatchTouchEvent(MotionEvent ev):用来进行事件的分发。如果事件能够传递给当前View,那么此方法一定会被调用,返回结果受当前View的onTouchEvent和下级View的dispatchTouchEvent方法的影响,表示是否消耗当前事件。
  • public boolean onInterceptTouchEvent(MotionEvent event):在dispatchTouchEvent方法内部调用,用来判断是否拦截某个事件,如果当前View拦截了某个事件,那么在同一个事件序列中,此方法不会被再次调用,返回结果表示是否拦截当前事件。
  • public boolean onTouchEvent(MotionEvent ev) :在dispatchTouchEvent方法中调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前View无法再次接收到事件。
View被点击后事件的传递过程:

View被点击后传递的顺序为Activity -> Window -> View,也就是说事件先传递给Activity,然后Activity再传递给Window,然后Window再传递给顶级View(一般是根ViewGroup),顶级View接收到事件后就会按事件分发机制去分发事件。

另外,如果一个View的onTouchEvent方法返回false,那么它父容器的onTouchEvent方法将会被调用,以此类推。如果所有元素都不处理这个事件,那么这个事件将最终传递给Activity处理,即Activity的onTouchEvent方法会被调用。如下图所示:

当View要处理点击事件时,如果它设置的有OnTouchListener 那么这时事件如何处理还要看OnTouchListener的onTouch方法的返回值:

  • onTouch返回true:onTouchEvent不会被调用。
  • onTouch返回false:当前View的onTouchEvent方法会被调用。

也就是说OnTouchListener的优先级要比onTouchEvent高。

1.3 事件分发机制结论

本节介绍View事件分发机制的主要规则,基于源码分析得出的结论,主要有:

条目 结论
1 同一个事件序列是指从手指接触屏幕的那一刻起,到手指离开屏幕的那一刻结束,在这个过程中所产生的一系列事件,这个事件序列以down事件开始,中间含有数量不定的move事件,最终以up事件结束。
2 正常情况下,一个事件序列只能被一个View拦截并消耗。因为一旦一个元素拦截了某次事件,那么同一个事件序列内的所有事件都会直接交给它处理,因此同一个事件序列中的事件不能分别由两个View同时处理,但是通过特殊手段可以作到,比如一个View将本该自己处理的事件通过onTouchEvent强行传递给其他View处理。
3 某个View一旦决定拦截,那么这一个事件序列都只能由它来处理(如果事件序列能传递给它的话),并且它的onInterceptTouchEvent不会再被调用。
4 某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件(onTouchEvent返回了false),那么同一事件序列的其他事件都不会再交给它来处理,并且事件将重新交给它的父元素去处理,即父元素的onTouchEvent会被调用。
5 如果View不消耗除ACTION_DOWN以外的其他事件,那么这个点击事件会消失,此时父元素的onTouchEvent并不会被调用,并且当前View可以持续收到后续的事件,最终这些消失的点击事件会传递给Activity处理。
6 ViewGroup默认不拦截任何事件。源码中ViewGroup的onInterceptTouchEvent默认返回false。
7 View没有onInterceptTouchEvent方法,一旦有点击事件传递给它,那么它的onTouchEvent方法就会被调用。
8 View的onTouchEvent默认都会消耗事件(返回true),除非它是不可点击的(clickable和longClickable同时为false)。View的longClickable属性默认都为false,clickable属性要分情况,比如Button的clickable属性默认为true,而TextView的clickable属性默认为false。
9 View的enable属性不影响onTouchEvent的默认返回值。哪怕一个View是disable状态的,只要它的clickable或者longClickable有一个为true,那么它的onTouchEvent就返回true
10 onClick会发生的前提是当前View是可点击的,并且它收到了down和up事件。
11 事件传递过程是由外向内的,即事件总是先传递给父元素,然后再由父元素分发给子View,通过requestDisallowInterceptTouchEvent方法可以在子元素中干预父元素的事件分发过程,但是ACTION_DOWN事件除外。

2. ViewGroup事件分发过程的源码分析

在上一节已经说过,事件传递的顺序是Activity -> Window -> View,Window类将点击事件经过一系列方法传递给DecorView,由于DecorView继承自FrameLayout,所以事件最终会传递给View。这个View也就是顶级View,也叫根View,一般是ViewGroup。

然后事件会从顶层的ViewGroup的dispatchTouchEvent()方法分发下去,这个方法很长,接下来会分段说明,先看看dispatchTouchEvent对于down事件分发的处理。

down事件作为一个起始事件,dispatchTouchEvent()方法会先重置一些必要的状态和变量:

java 复制代码
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;

// Handle an initial down.
if (actionMasked == MotionEvent.ACTION_DOWN) {
    cancelAndClearTouchTargets(ev); // 取消之前的触摸目标
    resetTouchState(); // 重置触摸状态
}

ViewGroup会在resetTouchState()方法中重置FLAG_DISALLOW_INTERCEPT这个标志位,它的作用是让ViewGroup不再拦截事件(前提是ViewGroup不拦截ACTION_DOWN事件),通过requestDisallowInterceptTouchEvent方法来设置,一般用于子View中。一旦设置该标记位,ViewGroup将无法拦截除了ACTION_DOWN以外的其他事件。因为如果是ACRTION_DOWN事件,那么就会重置该标记位,导致子View中设置的这个标记位无效,所以在面对ACTION_DOWN事件时,ViewGroup总是会调用自己的onInterceptTouchEvent方法来询问自己是否要拦截事件。换句话说,子View无法通过FLAG_DISALLOW_INTERCEPT标志位禁止ViewGroup拦截down事件

接下来看看当前View是否要拦截点击事件:

java 复制代码
// Check for interception.
final boolean intercepted;
ViewRootImpl viewRootImpl = getViewRootImpl();
// 判断什么时候需要询问拦截
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
    // 判断子View是否要求不拦截
    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
    final boolean isBackGestureInProgress = (viewRootImpl != null
                                             && viewRootImpl.getOnBackInvokedDispatcher().isBackGestureInProgress());
    // 判断是否允许进行拦截判断
    if (!disallowIntercept || isBackGestureInProgress) { 
        intercepted = onInterceptTouchEvent(ev); // 调用拦截方法
        ev.setAction(action); 
    } else {
        intercepted = false; // 子View要求不拦截
    }
} else {
    // 没有触摸目标且不是down事件,直接拦截
    intercepted = true;
}

由源码可知,两种情况下会进入拦截判断:

  • actionMasked == MotionEvent.ACTION_DOWN:说明新序列开始,这时必须进行拦截判断,决定由谁处理整个序列。
  • mFirstTouchTarget != null:有子View在处理序列时,因为可能会中途拦截,从子View中抢走事件。

关于mFirstTouchTarget ,它的作用是记录当前正在处理触摸事件的子View,是一个单向链表结构,支持多点触控(每个手指对应不同的子View),mFirstTouchTarget 为nul时表示没有子View在处理点击事件。

反之,如果不是down事件并且mFirstTouchTarget == null,不进入拦截判断,直接拦截当前事件。比如当down事件父容器自己消费了(也就是mFirstTouchTarget == null),那么后续的move事件就没有子View需要了,直接拦截。

从以上的源码分析中,可以得出结论:

  1. 当ViewGroup决定拦截事件后,那么后续的点击事件将会默认交给它并且不再调用它的onInterceptTouchEvent方法,也就证实了3结论:

    某个View一旦决定拦截,那么这一个事件序列都只能由它来处理(如果事件序列能传递给它的话),并且它的onInterceptTouchEvent不会再被调用。

  2. FLAG_DISALLOW_INTERCEPT的作用是让ViewGroup不再拦截事件,前提是ViewGroup不拦截ACTION_DOWN事件,证实了11结论:

    事件传递过程是由外向内的,即事件总是先传递给父元素,然后再由父元素分发给子View,通过requestDisallowInterceptTouchEvent方法可以在子元素中干预父元素的事件分发过程,但是ACTION_DOWN事件除外。

接着当ViewGroup不拦截事件的时候,事件会向下分发交由它的子View进行处理:

java 复制代码
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) { // 从后先前遍历,后添加的View显示在上面,优先接收事件
    final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
    // 获取子View实例
    final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);

    // ...

    // 检查子View是否能够接收点击事件:
    // 满足可见性、可点击性等基本条件
    // 点击事件的坐标是否落在子元素的区域内
    if (!child.canReceivePointerEvents()
        || !isTransformedTouchPointInView(x, y, child, null)) {
        ev.setTargetAccessibilityFocus(false);
        continue;
    }

    // 检查该子View是否已经是当前的触摸目标
    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;
    }

    // 重置子View的取消标志,准备进行事件分发
    resetCancelNextUpFlag(child);
    // 尝试将触摸事件分发给子View
    if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
        // 进入这里表示子View消费了事件
        mLastTouchDownTime = ev.getDownTime();
        if (preorderedList != null) {
            // 计算并记录子View在原始数组中的索引
            for (int j = 0; j < childrenCount; j++) {
                if (children[childIndex] == mChildren[j]) {
                    mLastTouchDownIndex = j;
                    break;
                }
            }
        } else {
            mLastTouchDownIndex = childIndex;
        }
        // 记录按下坐标
        mLastTouchDownX = x;
        mLastTouchDownY = y;
        // 创建新的触摸目标并添加到触摸目标链表
        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);
}

在for循环之前,还有一些判断,大概逻辑是如果事件没有被ViewGroup拦截,也没有取消,如果该事件是down事件,那么代码就会走进for循环中遍历子View判断其是否能够接收到点击事件,也就是if (!child.canReceivePointerEvents() || !isTransformedTouchPointInView(x, y, child, null))

  • !child.canReceivePointerEvents():检查子视图是否能接收触摸事件,如果返回false(即视图不能接收事件),可能的情况有视图不可见,视图被禁用(enabl == false)。
  • !isTransformedTouchPointInView(x, y, child, null):检查触摸点坐标(x,y)是否在子视图的区域内。

如果子元素满足这两个条件,那么事件就会传递给它来处理。

而ViewGroup的尝试分发事件是通过dispatchTransformedTouchEvent方法完成的,看看它的核心实现部分:

java 复制代码
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        // ...
            
        if (child == null) {
            // 其实是调用子元素的dispatchTouchEvent方法完成事件的分发
            handled = super.dispatchTouchEvent(event);
        } else {
            final float offsetX = mScrollX - child.mLeft;
            final float offsetY = mScrollY - child.mTop;
            event.offsetLocation(offsetX, offsetY);
 			
            // 同上
            handled = child.dispatchTouchEvent(event);

            event.offsetLocation(-offsetX, -offsetY);
        }
                    
        // ...
}

在上面的代码中child传递的值为null,所以会直接调用子View的dispatchTouchEvent方法来处理点击事件,这样事件就交由子View处理了,这样就完成了一次事件分发。

接着往下看,在addTouchTarget方法内部完成对mFirstTouchTarget的赋值,随后终止对子元素的遍历。如果子元素的dispatchTouchEvent返回false,ViewGroup就会把事件分发给下一个子元素(如果还有下一个子元素的话)。mFirstTouchTarget的赋值,将直接影响到ViewGroup对事件的拦截策略,如果mFirstTouchTarget为null,那么ViewGroup就默认拦截接下来同一序列中所有的点击事件。

java 复制代码
// 创建新的触摸目标并添加到触摸目标链表
newTouchTarget = addTouchTarget(child, idBitsToAssign);
// 标记该事件已经分发过,避免重复处理
alreadyDispatchedToNewTouchTarget = true;
break;

如果遍历所有的子元素后事件都没有被合适地处理,这包含两种情况:

  • ViewGroup没有子元素
  • 子元素处理了点击事件,但是dispatchTouchEvent方法返回了false,这一般是因为子元素在onTouchEvent中返回了false。

在这两种情况下,ViewGroup会自己处理点击事件,这就证实了4结论:

某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件(onTouchEvent返回了false),那么同一事件序列的其他事件都不会再交给它来处理,并且事件将重新交给它的父元素去处理,即父元素的onTouchEvent会被调用。

代码如下:

java 复制代码
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
    // 情况1:没有找到任何触摸目标(没有子View消费down事件)
    // 将当前ViewGroup当作普通View处理,自己消费事件
    handled = dispatchTransformedTouchEvent(ev, canceled, null,
                                            TouchTarget.ALL_POINTER_IDS);
} else {
    // 遍历触摸目标链表,分发给所有需要处理事件的子View
    // ...
    // 将事件分发给子View
    if (target.child != null && dispatchTransformedTouchEvent(ev, cancelChild,
                                                              target.child, target.pointerIdBits)) {
        handled = true;
    }
    // ... 
}

从前面的分析可知,dispatchTransformedTouchEvent内部会调用child的dispatchTouchEvent,很显然,这就转到了View的dispatchTouchEvent方法,即点击事件开始交由View来处理。到此,ViewGroup处理down事件就分析完毕,可以用下面的图来表示:

3. View事件分发过程的源码分析

View.dispatchTouchEvent()方法相比于ViewGroup就很简单了,因为View不像ViewGroup需要处理事件的分发,而且View也没有onInterceptTouchEvent()方法。

java 复制代码
public boolean dispatchTouchEvent(MotionEvent event) {
        // ...
        boolean result = false;
		
    	// ...

        final int actionMasked = event.getActionMasked();
    	// 获取动作类型并处理down事件 
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // 表示新的事件序列开始,停止嵌套滚动
            stopNestedScroll();
        }

        if (onFilterTouchEventForSecurity(event)) {
            // 执行触摸回调处理
            result = performOnTouchCallback(event);
        }

        // ... 

        // Clean up after nested scrolls if this is the end of a gesture;
        // also cancel it if we tried an ACTION_DOWN but we didn't want the rest
        // of the gesture.
    	// 手势结束的清理工作 
        if (actionMasked == MotionEvent.ACTION_UP || // 正常结束
                actionMasked == MotionEvent.ACTION_CANCEL || // 被取消
                (actionMasked == MotionEvent.ACTION_DOWN && !result)) { // DOWN未被消费
            stopNestedScroll(); // 停止嵌套滚动 
        }

        return result;
    }

很简单,核心就在于performOnTouchCallback方法,源码如下:

java 复制代码
// 触摸事件处理优先级:
// 1. OnTouchListener(最高优先级)
// 2. onTouchEvent(默认处理)
private boolean performOnTouchCallback(MotionEvent event) {
    // 优先调用设置的OnTouchListener
    if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
            && mOnTouchListener.onTouch(this, event)) {
        return true;  // 监听器消费了事件
    }
    
    // 监听器未消费,调用默认的onTouchEvent
    return onTouchEvent(event);
}

首先会判断有没有设置OnTouchListener,如果OnTouchListener中的onTouch方法返回true,那么onTouchEvent就不会被调用,可见OnTouchListener的优先级高于onTouchEvent,这样做的好处是方便在外界处理点击事件。

接着来看看onTouchEvent的实现:

java 复制代码
// 判断View是否可点击
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE // 可点击
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) // 可长按
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE; // 可上下文点击
		
		// 处理禁用状态View的触摸事件 
        if ((viewFlags & ENABLED_MASK) == DISABLED
                && (mPrivateFlags4 & PFLAG4_ALLOW_CLICK_WHEN_DISABLED) == 0) {
            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;
        }

事件进入onTouchEvent时会去判断View的clickable状态,如果满足 CLICKABLE == true || LONG_CLICKABLE == true || CONTEXT_CLICKABLE == true ,clickable 就会为 true。其中 clickable 和 longClickable比较常见,contextClickable 用的很少,在触摸事件中不会用到它,所以可以忽略掉。

判断完View的click状态后,接着会判断 View 的 enable 状态。如果 View 是 disable 且 它的 PFLAG4_ALLOW_CLICK_WHEN_DISABLED 标志位被设置为 0,即不允许 View 在 disable 状态下被点击,那么 disable 状态下的 View 不会消耗事件。PFLAG4_ALLOW_CLICK_WHEN_DISABLED 标志位默认值为 true。

也就是说,如果 View 是 disable 的,只要它的 clickable 和 longClickable 为 true,那么其 onTouchEvent() 方法就会返回 true,即即使 View 是不可用的,它只要可以被点击,也会消耗事件。

这就证实了9结论:

View的enable属性不影响onTouchEvent的默认返回值。哪怕一个View是disable状态的,只要它的clickable或者longClickable有一个为true,那么它的onTouchEvent就返回true

接着,如果View设置有代理,那么还会执行TouchDelegate的onTouchEvent方法,将剩下的逻辑交给touchDelegate,调用mTouchDelegate.onTouchEvent(event)

java 复制代码
if (mTouchDelegate != null) {
    if (mTouchDelegate.onTouchEvent(event)) {
        return true;
    }
}

下面看下onTouchEvent对点击事件的具体的处理过程,如下:

java 复制代码
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
    switch (action) {
        case MotionEvent.ACTION_UP:
            // ... 
            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;

                // ...
                // 处理点击操作(非长按且不忽略up事件)
                if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                    // 移除长按检查(因为这是点击,不是长按)
                    removeLongPressCallback();

                    // // 如果没有获取焦点,执行点击操作
                    if (!focusTaken) {
                        // 使用Runnable延迟执行点击,让视觉状态先更新
                        if (mPerformClick == null) {
                            mPerformClick = new PerformClick(); // 创建点击Runnable
                        }
                        if (!post(mPerformClick)) { // 投递到消息队列
                            performClickInternal(); // 立即执行点击
                        }
                    }
                } 
                // ...
            }
            break;
    }

    return true;
}

暂不看switch语句里面有什么,可以看到的是,只要代码进了if语句,最后一定会返回true,也就是View.onTouchEvent()方法返回true,不管它是不是DISABLE状态的,也就是验证了8,9,10结论:

  1. View的onTouchEvent默认都会消耗事件(返回true),除非它是不可点击的(clickable和longClickable同时为false)。View的longClickable属性默认都为false,clickable属性要分情况,比如Button的clickable属性默认为true,而TextView的clickable属性默认为false。

  2. View的enable属性不影响onTouchEvent的默认返回值。哪怕一个View是disable状态的,只要它的clickable或者longClickable有一个为true,那么它的onTouchEvent就返回true

  3. onClick会发生的前提是当前View是可点击的,并且它收到了down和up事件。

当up事件发生时,会触发PerformClick()方法,如果View设置了OnClickListener,那么PerformClick方法内部会调用它的onClick()方法,如下:

java 复制代码
private final class PerformClick implements Runnable {
    @Override
    public void run() {
        recordGestureClassification(TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__SINGLE_TAP); //  最终会调用onClickListener.onClick() 方法
        performClickInternal();
    }
}

接着通过 post() 方法,将 PerformClick() 放到事件队列中等待调用,延时调用 onClickListener.onClick()。

到这里,View.onTouchEvent()就分析完了,图示如下:

内容参考:

安卓开发艺术探索,任玉刚

安卓基础知识之View篇(三):源码分析 View 事件分发机制安卓基础知识系列旨在简明扼要地提供面试或工作中常用的基础 - 掘金

相关推荐
北京地铁1号线2 小时前
数据结构:堆
java·数据结构·算法
百***86462 小时前
Spring Boot应用关闭分析
java·spring boot·后端
花落归零2 小时前
Android 小组件AppWidgetProvider的使用
android
tanxiaomi2 小时前
Spring、Spring MVC 和 Spring Boot ,mybatis 相关面试题
java·开发语言·mybatis
弥巷2 小时前
【Android】常见滑动冲突场景及解决方案
android·java
间彧2 小时前
GraalVM 深度解析:下一代 Java 技术平台
java
angushine3 小时前
解决MySQL慢日志输出问题
android·数据库·mysql
合作小小程序员小小店3 小时前
网页开发,在线%旧版本旅游管理%系统,基于eclipse,html,css,jquery,servlet,jsp,mysql数据库
java·数据库·servlet·eclipse·jdk·旅游·jsp
fouryears_234173 小时前
Android 与 Flutter 通信最佳实践 - 以分享功能为例
android·flutter·客户端·dart