Android 事件传递机制总结

Android View虽然不是四大组件,但是其重要程度堪比四大组件。初级工程师到中高级工程师,这些都是很重要的,因为事件分发机制面试也会经常被提及,如果我们能get到要领,并跟面试官深入的交流一下,那么一定会让面试官对我们印象深刻,发放offer。 就为了这个我们刨根问底深入学习一下事件传递机制,也是很值得的。

下面我们就从以下几个部分分析一下事件传递机制:

  1. Activity View 树的结构构成
  2. View 的事件传递过程分析
  3. ViewGroup的事件传递过程分析
  4. 事件传递机制流程图
  5. 总结
  6. 参考资料

前言

在我们研究事件传递机制之前,先了解一下MotionEvent。

因为我们对屏幕的点击,滑动,抬起等一系的动作都是由一个一个MotionEvent对象组成的。
根据不同动作,主要有以下三种事件类型:

  • 1.ACTION_DOWN:手指刚接触屏幕,按下去的那一瞬间产生该事件
  • 2.ACTION_MOVE:手指在屏幕上移动时候产生该事件
  • 3.ACTION_UP:手指从屏幕上松开的瞬间产生该事件

从 ACTION_DOWN 开始到 ACTION_UP 结束我们称为一个事件序列。

正常情况下,我们日常的各种操作, 最终呈现在MotionEvent上来讲无外乎下面两种。

1.点击后抬起,也就是单击操作:ACTION_DOWN -> ACTION_UP 当然也包括 ACTION_DOWN -> duration -> ACTION_UP longclick

2.点击后再滑动一段距离,再抬起:ACTION_DOWN -> ACTION_MOVE -> … -> ACTION_MOVE -> ACTION_UP

看一下日志,

1. Activity View 树的结构构成

说事件传递流程之前我们还需要了解一下,从手指触碰到屏幕起, 事件是如何从最外层的Activity 传递至了指定的View之上。当然这前提是对Activity , View树结构的充分了解。
放一张图,

我们可以看到最外层是Activity , 里边是window ,关联的是Window的唯一实现类PhoneWindow, PhoneView内部装载了DecorView, 而DecorView是继承自FrameLayout的,同时FrameLayout是继承自ViewGroup 继承自View 一路下来的 。 所以我们需要进一步了解一下结构是如何的。事件是为什么这样的。

下边我就粘一些主要的代码来简单说明一下上边的这个图和一段话 。

    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*/);//  在Activity 类的attach方法中 mWindow 赋值为PhoneWindowmWindow = new PhoneWindow(this, window, activityConfigCallback);mWindow.setWindowControllerCallback(this);mWindow.setCallback(this);mWindow.setOnWindowDismissedCallback(this);mWindow.getLayoutInflater().setPrivateFactory(this);

ContentView方法 通过layoutresid进行设置ContentView

  @Overridepublic void setContentView(int layoutResID) {// Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window// decor, when theme attributes and the like are crystalized. Do not check the feature// before this happens.if (mContentParent == null) {/// 在这里将DecorView和Activity关联在了一起installDecor();} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {mContentParent.removeAllViews();}if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,getContext());transitionTo(newScene);} else {mLayoutInflater.inflate(layoutResID, mContentParent);}mContentParent.requestApplyInsets();final Callback cb = getCallback();if (cb != null && !isDestroyed()) {cb.onContentChanged();}mContentParentExplicitlySet = true;}

装载DecorView

  private void installDecor() {mForceDecorInstall = false;if (mDecor == null) {mDecor = generateDecor(-1);mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);mDecor.setIsRootNamespace(true);if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);}} else {mDecor.setWindow(this);}if (mContentParent == null) {mContentParent = generateLayout(mDecor);}

generateLayout的源码就不再粘了, 主要作用就是根据不同的情况加载不同的布局给DecorView

下边看一下生成DecorView

  protected DecorView generateDecor(int featureId) {// System process doesn't have application context and in that case we need to directly use// the context we have. Otherwise we want the application context, so we don't cling to the// activity.Context context;if (mUseDecorContext) {Context applicationContext = getContext().getApplicationContext();if (applicationContext == null) {context = getContext();} else {context = new DecorContext(applicationContext, getContext().getResources());if (mTheme != -1) {context.setTheme(mTheme);}}} else {context = getContext();}/// 在这里创建了DecorView return new DecorView(context, featureId, this, getAttributes());}

通过上边的源码分析大概了解Activity View树的一个结构和关联关系。 下边继续研究View和ViewGroup的事件传递

2. View 的事件传递过程分析

我们查看一下View中的事件分发代码, 看一下dispatchTouchEvent方法

    /*** Pass the touch screen motion event down to the target view, or this* view if it is the target.* MotionEvent事件从父View传递进来, 进入dispatch方法 进行分发。  返回的结果就是是否处理该事件。 * @param event The motion event to be dispatched.* @return True if the event was handled by the view, false otherwise.* 返回结果, * true 则该view 处理了这个事件, 否则返回false*/public boolean dispatchTouchEvent(MotionEvent event) {// If the event should be handled by accessibility focus first.if (event.isTargetAccessibilityFocus()) {// We don't have focus or no virtual descendant has it, do not handle the event.if (!isAccessibilityFocusedViewOrHost()) {return false;}// We have focus and got the event, then use normal event dispatch.event.setTargetAccessibilityFocus(false);}boolean result = false;if (mInputEventConsistencyVerifier != null) {mInputEventConsistencyVerifier.onTouchEvent(event, 0);}final int actionMasked = event.getActionMasked();if (actionMasked == MotionEvent.ACTION_DOWN) {// Defensive cleanup for new gesturestopNestedScroll();}//如果窗口没有被遮盖if (onFilterTouchEventForSecurity(event)) {if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {result = true;}//noinspection SimplifiableIfStatementListenerInfo li = mListenerInfo;//  这句很有意思 , if 的条件语句里有四个 条件 , 用& 符号连接。& 符号连接的if语句 是从左往右执行的, 一旦有一个false, 后边就不会再执行了。 //  所以, 当监听信息为null , 或者 onTouchListener 没有设置, 或者 view 状态的enable状态为disable, 还有 设置了onTouchListener,但其onTouch事件返回的是false时 都消耗不了该事件。  换句话说, 默认状态, 一个没有设置监听,或者设置了监听没有修改onTouch返回值的,都不可以消耗该事件。 还有onTouch事件处于if & 语句的最后一个条件语句里, 但是比onTouchEvent靠前, 所以 执行顺序也在onTouchEvent之前。 if (    li != null && li.mOnTouchListener != null&& (mViewFlags & ENABLED_MASK) == ENABLED&& li.mOnTouchListener.onTouch(this, event)) {result = true;}当前边没有消耗该事件,并且onTouchEvent执行的结果为true的时候才会处理该事件。 if (!result && onTouchEvent(event)) {result = true;}}if (!result && mInputEventConsistencyVerifier != null) {mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);}// 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)) {stopNestedScroll();}return result;}

下边我们瞜一眼onTouchEvent方法 , 代码比较长, 就不全粘了, 找到ACTION_UP 中的performClick ,

  public boolean onTouchEvent(MotionEvent event) {final float x = event.getX();final float y = event.getY();final int viewFlags = mViewFlags;final int action = event.getAction();//1.如果View是设置成不可用的(DISABLED)仍然会消费点击事件if ((viewFlags & ENABLED_MASK) == DISABLED) {if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {setPressed(false);}// A disabled view that is clickable still consumes the touch// events, it just doesn't respond to them.return (((viewFlags & CLICKABLE) == CLICKABLE|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);}...//2.CLICKABLE 和LONG_CLICKABLE只要有一个为true就消费这个事件if (((viewFlags & CLICKABLE) == CLICKABLE ||(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {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;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 checkremoveLongPressCallback();// Only perform take click actions if we were in the pressed stateif (!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();}}}...break;}...return true;}return false;}

继续看一下performClick方法

    public boolean performClick() {final boolean result;final ListenerInfo li = mListenerInfo;if (li != null && li.mOnClickListener != null) {/// 播放click 按下效果声音playSoundEffect(SoundEffectConstants.CLICK);/// 执行oNClick方法li.mOnClickListener.onClick(this);result = true;} else {当然 如果onClickListener 没有设置为null的话 , 返回值就是false , 也就是不消耗事件。 result = false;}/// 这是无障碍事件的相关代码sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);notifyEnterOrExitForAutoFillIfNeeded(true);return result;}

小结:

对于View而言,

  • 如果设置了onTouchListener,并且View是可用的, 那么OnTouchListener方法中的onTouch方法会被回调。
    如果onTouch方法返回true,则onTouchEvent方法不会被调用(onClick事件是在onTouchEvent中调用)
    所以三者优先级是onTouch->onTouchEvent->onClick

  • View 的onTouchEvent 方法默认都会消费掉事件(返回true)
    除非它是不可点击的(clickable和longClickable同时为false),View的longClickable默认为false,clickable需要区分情况,

如Button的clickable默认为true,而TextView, ImageView 的clickable默认为false。

3. ViewGroup的事件传递过程分析

下面我们把ViewGroup的dispatchTouchEvent 的主要逻辑粘在这里,提取的伪代码如下

public boolean dispatchTouchEvent(MotionEvent ev) {boolean consume = false;//事件是否被消费if (onInterceptTouchEvent(ev)){//调用onInterceptTouchEvent判断是否拦截事件consume = onTouchEvent(ev);//如果拦截则调用自身的onTouchEvent方法}else{consume = child.dispatchTouchEvent(ev);//不拦截调用子View的dispatchTouchEvent方法}return consume;//返回值表示事件是否被消费,true事件终止,false调用父View的onTouchEvent方法}

public boolean dispatchTouchEvent(MotionEvent event)

通过方法名我们不难猜测,它就是事件分发的重要方法。
那么很明显,如果一个MotionEvent传递给了View,那么dispatchTouchEvent方法一定会被调用!

返回值:表示是否消费了当前事件。可能是View本身的onTouchEvent方法消费,也可能是子View的dispatchTouchEvent方法中消费。

  • 返回true表示事件被消费,本次的事件终止。
  • 返回false表示View以及子View均没有消费事件,将调用父View的onTouchEvent方法

public boolean onInterceptTouchEvent(MotionEvent ev)
事件拦截,当一个ViewGroup在接到MotionEvent事件序列时候,首先会调用此方法判断是否需要拦截。

这是ViewGroup特有的方法,View并没有该方法 这里先
返回值:是否拦截事件传递。

  • 返回true表示拦截了事件,那么事件将不再向下分发而是调用View本身的onTouchEvent方法。
  • 返回false表示不做拦截,事件将向下分发到子View的dispatchTouchEvent方法。

public boolean onTouchEvent(MotionEvent ev)
真正对MotionEvent进行处理或者说消费的方法。在dispatchTouchEvent进行调用。
返回值:是否消费该事件。

  • 返回true表示事件被消费,本次的事件终止。
  • 返回false表示事件没有被消费,将调用父View的onTouchEvent方法

继续, 我们一点一点研究源码。

    // 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();}

子View可以通过requestDisallowInterceptTouchEvent方法干预父View的事件分发过程(ACTION_DOWN事件除外)

当ViewGroup决定拦截事件后,后续事件将默认交给它处理并且不会再调用onInterceptTouchEvent方法来判断是否拦截。
子View可以通过设置FLAG_DISALLOW_INTERCEPT标志位来不让ViewGroup拦截除ACTION_DOWN以外的事件。

2108行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;}

所以我们onInterceptTouchEvent并非每次都会被调用。
如果要处理所有的点击事件那么需要选择dispatchTouchEvent方法
而FLAG_DISALLOW_INTERCEPT标志位可以帮助我们去有效的处理滑动冲突

    public boolean onInterceptTouchEvent(MotionEvent ev) {/// 判断这么几种情况的时候, 是需要拦截该事件,并且交于当前ViewGroup处理该事件。 if (ev.isFromSource(InputDevice.SOURCE_MOUSE)&& ev.getAction() == MotionEvent.ACTION_DOWN&& ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)/// 当事件的坐标位置处于滚动条上的时候 && isOnScrollbarThumb(ev.getX(), ev.getY())) {return true;}return false;}
 //判断1,View可见并且没有播放动画。2,点击事件的坐标落在View的范围内//如果上述两个条件有一项不满足则continue继续循环下一个Viewif (!canViewReceivePointerEvents(child)|| !isTransformedTouchPointInView(x, y, child, null)) {ev.setTargetAccessibilityFocus(false);continue;}//如果有子View处理即newTouchTarget 不为null则跳出循环。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;}//dispatchTransformedTouchEvent第三个参数child这里不为null//实际调用的是child的dispatchTouchEvent方法  调用方法获取该childview是否消费该事件。 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 indexfor (int j = 0; j < childrenCount; j++) {if (children[childIndex] == mChildren[j]) {mLastTouchDownIndex = j;break;}}} else {mLastTouchDownIndex = childIndex;}mLastTouchDownX = ev.getX();mLastTouchDownY = ev.getY();//当child处理了点击事件,那么会设置mFirstTouchTarget 在addTouchTarget被赋值newTouchTarget = addTouchTarget(child, idBitsToAssign);alreadyDispatchedToNewTouchTarget = true;//子View处理了事件,然后就跳出了for循环break;}

分发过程首先需要遍历ViewGroup的所有子View,可以接收点击事件的View需要满足下面条件。

1.如果View可见并且没有播放动画canViewReceivePointerEvents方法判断

    /*** Returns true if a child view can receive pointer events.* @hide*/private static boolean canViewReceivePointerEvents(@NonNull View child) {return (child.mViewFlags & VISIBILITY_MASK) == VISIBLE|| child.getAnimation() != null;}

2.点击事件的坐标落在View的范围内isTransformedTouchPointInView方法判断

   /*** Returns true if a child view contains the specified point when transformed* into its coordinate space.* Child must not be null.* @hide*/protected boolean isTransformedTouchPointInView(float x, float y, View child,PointF outLocalPoint) {final float[] point = getTempPoint();point[0] = x;point[1] = y;transformPointToViewLocal(point, child);//调用View的pointInView方法进行判断坐标点是否在View内final boolean isInView = child.pointInView(point[0], point[1]);if (isInView && outLocalPoint != null) {outLocalPoint.set(point[0], point[1]);}return isInView;}

接着看后面的代码newTouchTarget = getTouchTarget(child);

   /*** Gets the touch target for specified child view.* Returns null if not found.*/private TouchTarget getTouchTarget(@NonNull View child) {for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {if (target.child == child) {return target;}}return null;}

如果mFirstTouchTarget ==null , 则传递null 给dispatchTransformedTouchEvent , 当child为null,handled = super.dispatchTouchEvent(event);
所以此时将调用View的dispatchTouchEvent方法,点击事件给了View,也就是不消耗该事件, 层层往上调用。

// Dispatch to touch targets.if (mFirstTouchTarget == null) {// No touch targets so treat this as an ordinary view.handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS);}/*** Transforms a motion event into the coordinate space of a particular child view,* filters out irrelevant pointer ids, and overrides its action if necessary.* If child is null, assumes the MotionEvent will be sent to this ViewGroup instead.* */private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,View child, int desiredPointerIdBits) {final boolean handled;// Canceling motions is a special case.  We don't need to perform any transformations// or filtering.  The important part is the action, not the contents.final int oldAction = event.getAction();if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {event.setAction(MotionEvent.ACTION_CANCEL);if (child == null) {//如果ViewGroup里面没有子控件就交给自己处理(就是一个纯粹的View)handled = super.dispatchTouchEvent(event);} else {handled = child.dispatchTouchEvent(event);}event.setAction(oldAction);return handled;}}

如果它返回为true,那么就break跳出循环,如果返回为false则继续遍历下一个子View。
dispatchTransformedTouchEvent方法可以看到这样的关键逻辑
这里child是我们遍历传入的子View此时不为null,则调用了child.dispatchTouchEvent(event);
我们子View的dispatchTouchEvent方法返回true,表示子View处理了事件,那么我们一直提到的,mFirstTouchTarget 会被赋值

ViewGroup并没有子View或者子View处理了事件,但是子View的dispatchTouchEvent返回了false(一般是子View的onTouchEvent方法返回false)

    /*** Adds a touch target for specified child to the beginning of the list.* Assumes the target child is not already present.* mFirstTouchTarget 就是在addTouchTarget中被赋值,target 添加到list的头部, 这是个链表 。 */private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);/ nexttarget.next = mFirstTouchTarget;mFirstTouchTarget = target;/ 这个是头return target;}

TouchTarget 是个 放在View 上的多指触控的ids , 范围是0~31

小结:
ViewGroup会遍历所有子View去寻找能够处理点击事件的子View(可见,没有播放动画,点击事件坐标落在子View内部)最终调用子View的dispatchTouchEvent方法处理事件

如果当子View处理了事件则mFirstTouchTarget 被赋值,并终止子View的遍历,

如果ViewGroup并没有子View或者子View处理了事件,但是子View的dispatchTouchEvent返回了false(一般是子View的onTouchEvent方法返回false)那么ViewGroup会去处理这个事件(本质调用View的dispatchTouchEvent去处理)

4. 事件传递机制流程图

5. 总结

ViewGroup是View的子类,也就是说ViewGroup本身就是一个View,但是它可以包含子View(当然子View也可能是一个ViewGroup),所以不难理解,上面所展示的伪代码表示的是ViewGroup 处理事件分发的流程。而View本身是不存在分发,所以也没有拦截方法(onInterceptTouchEvent),它只能在onTouchEvent方法中进行处理消费或者不消费。

6. 参考资料

  1. https://www.jianshu.com/p/238d1b753e65

  2. https://www.jianshu.com/p/fc0590afb1bf

  3. Android开发艺术探索 View事件传递机制

Android 事件传递机制总结相关推荐

  1. Android事件传递机制(转)

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

  2. android imageview 事件传递,Android 事件传递机制TextView,ImageView等没有默认clickable属性的View单独设置onTouch事件注意事项...

    本文讲解TextView,ImageView等没有默认clickable属性的View单独设置onTouch事件 Android 事件传递机制:Android 事件传递机制初涉 我们知道 Button ...

  3. android touch机制,细说Android事件传递机制(dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent)...

    本文背景:前些天用到了之前写的自定义图片文字复合控件,在给他设置监听时遇到了麻烦.虽然最后解决了问题,但发现在不重写LinearLayout的onInterceptTouchEvent时,子Image ...

  4. Android事件传递机制【Touch事件】

    Android中提供了ViewGroup.View.Activity三个等级的Touch事件处理.也就是说,这三个地方都有事件回调方法. 测试DEMO视图结构: <com .orgcent.ev ...

  5. Android事件传递机制详解

    总结 dispatchTouchEvent方法:分发点击事件 onInterceptTouchEvent方法:拦截事件(只存在于ViewGroup,View没有此方法), 在dispatchTouch ...

  6. android事件传递机制以及onInterceptTouchEvent()和onTouchEvent()详解二之小秘与领导的故事...

    总结的不是很好,自己也有点看不懂,正好现在用到了,研究了一个,再次总结,方便大家查看 总则: 1.onInterceptTouchEvent中有个Intercept,这是什么意思呢?她叫拦截,你大概知 ...

  7. android 事件传递机制

    有三个方法: dispatchTouchEvent onInterceptTouchEvent onTouchEvent 首先:A的dispatchTouchEvent-A的onInterceptTo ...

  8. android touch事件坐标原点,图解Android:Touch事件传递机制

    前言 Android事件管理机制是一名专业Android研发工程师必须要了解的核心知识之一,深入了解一下该机制无论对我们日常开发还是找工作,乃至于对我们的架构思想都有很大的帮助.Android中我们用 ...

  9. Android Touch事件传递机制 二:单纯的(伪生命周期) 这个清楚一点

    转载于:http://blog.csdn.net/yuanzeyao/article/details/38025165 在前一篇文章中,我主要讲解了Android源码中的Touch事件的传递过程,现在 ...

最新文章

  1. “笨方法”学习Python笔记(2)-VS Code作为文本编辑器以及配置Python调试环境
  2. PCA图像数据降维及重构误差分析实战并使用TSNE进行异常数据可视化分析
  3. python语音播报-用Python写一个语音播放软件
  4. HyperlinkButton——WP8控件学习
  5. java dbtype_java 动态操作数据库
  6. C++new和delete
  7. python文本提取序列信息_从fasta文件中通过头中的ID号提取序列
  8. Docker学习总结(60)——Docker-Compose 基础知识回顾总结
  9. QTextEdit设置最大可输入字符
  10. AI学习笔记(三)特征选择与提取、边缘提取
  11. 终结VC2005分发包版本问题
  12. 使用 SCTP 优化网络
  13. 滤波器原理及其作用计算机网络,滤波器的原理与作用
  14. 波士顿大学计算机科学与技术专业,波士顿大学计算机科学专业.pdf
  15. 不符合直接升级win11?教你怎么直接安装win11系统
  16. java 读取文件inputstream_使用Inputstream读取文件
  17. 了解资本与公司年报、财报
  18. 一只青蛙一次可以跳上1级台阶也可以跳上2级求该青蛙跳上一个n级的台阶总共有多少种跳法
  19. WORD如何互相复制样式?
  20. 投影仪怎么看电视节目?超简单几个步骤小白也能马上学会

热门文章

  1. 在线教育进入快车道,你的网校平台技术也该升级了
  2. 字节跳动教育业务怎么样_字节跳动:未来三年,教育业务不考虑盈利
  3. Colab Stable Diffusion使用教程
  4. 软文撰写的3个技巧,让软文发布效果更有效!
  5. Unity 车轮碰撞器的入门使用(二)
  6. 【新型幻灯片制作软件】Focusky教程 | 如何鉴别自己安装的Focusky是中文版还是英文版呢?
  7. Java制作圣诞树找规律_java 实现简单圣诞树的示例代码(圣诞节快乐)
  8. 图文 | 海岸TDM平台部署架构
  9. 分子筛(合成沸石)-硅铝酸盐多微孔晶体
  10. Elasticsearch:操作数据的时候PUT和POST的区别