Android View点击事件分发原理,源码解读

View点击事件分发原理,源码解读

  • 前言
  • 1. 原理总结
    • 2.1 时序图总结
    • 2.2 流程图总结
  • 2. 源码解读
    • 2.1 Activity到ViewGroup
    • 2.2 ViewGroup
      • 事件中断
      • 逆序搜索
      • 自己处理点击事件
      • ViewGroup总结
    • 2.3 View
      • OnTouchListener
      • onTouchEvent
  • 3. 附录:时序图uml代码

前言

两年前我曾经写过一篇点击事件的原理博客,在今年重新翻看的时候发现文章的结构不好,且没有总结,让人不容易理解,所以重新整理了一下再写一次。

1. 原理总结

注意:正文中虽然说的都是点击事件,实际上他并不是指我们常用语境中的onClick或者onLongClick,而是任意类型的事件,只是用点击事件来形容比较让人容易理解,实际上视图的事件分发是包括按下,抬起,移动这三个部分的。

MotionEvent.ACTION_DOWN按下View(所有事件的开始)
MotionEvent.ACTION_UP抬起View(与DOWN对应)
MotionEvent.ACTION_MOVE移动View
MotionEvent.ACTION_CANCEL结束事件(非人为原因)

再我看完点击事件分发的原理之后,我会用三个词来形容点击事件的全部原理:

  1. View树:

    首先我们需要知道的是,在Android中我们所写的视图代码,无论是xml还是通过代码手动添加的,其数据结构展现出来的就是一个树,一个一对多的存储关系的集合。

    在代码中我们表现这个树状数据结构的方式:在ViewGroup中设置了一个子View的List,通过让最上层的父节点DevorView(他也是一个ViewGroup)—持有好几个ViewGroup,里面的ViewGroup中每个又持有多个ViewGroup,层层嵌套到最底层的View为止。

  2. 深度搜索优先dfs:

    在用户触发任意一个点击事件的时候,我们是通过深度搜索优先的方式去寻找可以消费该点击事件的视图,从树的最深层开始处理点击事件。

    这个代表了什么意思呢?如果一个ViewGroup和它的子View同时都设置了OnClickListener,那么我们在点击它们之间重合的部分时,只会触发子View的点击事件而不会触发父View的点击事件。

    在代码表现这个深度搜索的方式:处理分发事件的时候,会先用for循环把所有的子View遍历,尝试调用子View的方法来处理该点击事件,只有确认所有子View都不能处理该点击事件之后,才会调用自己的点击事件处理方法。

  3. 逆序遍历:

    触发点击事件搜索子View的时候,总有个搜索的顺序,这个顺序是逆序,也就是从最后一个添加的子View开始查找和处理点击事件。

    其原理和添加VIew是相关联的,我们知道,在一个ViewGroup的两个子View中,如果这两个子View有重合的部分,那么一般而言总是后添加的视图会覆盖掉前面添加的视图的部分。

    点击事件也是同理,当用户点击他们重合的部分时,一般而言用户总是希望点击到用户本身可以看见的那个视图。所以我们的点击事件分发就和添加视图的顺序相反,从最后添加的视图开始遍历。
    在这里插入图片描述

2.1 时序图总结

为了方便我们看完源码之后以后复习方便,我先将点击事件分发的全部流程放到这里,在看的过程中有需要可以翻回来看:
在这里插入图片描述

2.2 流程图总结

由于时序图一般而言没有办法很好的展示我们深度优先搜索的思想,所以我额外又补充了一张流程图,这个流程图也画出了点击事件分发的原理:
在这里插入图片描述

2. 源码解读

2.1 Activity到ViewGroup

任何的事件源头都是从我们底层的SurfaceFlinger进程来的,他直接管理着用户可以看到的窗口,但是我们这里不用去深究那么底层的原理。只要知道从底层来的点击事件第一个触发的是Activity的DispatchTouchEvent就足够了。

public class Activity extends ContextThemeWrapper
        implements LayoutInflater.Factory2,
        Window.Callback, KeyEvent.Callback,
        OnCreateContextMenuListener, ComponentCallbacks2,
        Window.OnWindowDismissedCallback,
        ContentCaptureManager.ContentCaptureClient {

	/**
     * 调用以处理触摸屏事件。您可以重写此方法,在将所有触摸屏事件发送到窗口之前拦截它们。
     * 请确保为应该正常处理的触摸屏事件调用此实现。
     * 
     * @param ev 点击事件本体
     * @return boolean 如果事件被消费了会return true
     */
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        // 重点是这行
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }
}

点击事件就这样从Activity手上分出去了,接下来看看Window类是如何处理的,顺便一提,Window本身是一个抽象类,作为他承载的实体一般而言是PhoneWindow类。

public class PhoneWindow extends Window implements MenuBuilder.Callback {

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

事件就这样直接转到了DecorView。

public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {
	@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        final Window.Callback cb = mWindow.getCallback();
        return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
                ? cb.dispatchTouchEvent(ev) : /* 重点看这部分 */super.dispatchTouchEvent(ev);
    }
}

DecorView作为最上层的特殊View,他在处理事件的时候会有特殊的窗口判断,但是一般而言是不会触发的,我们不去理他,重点看super.dispatchTouchEvent(ev),这个就代表了点击事件的真正起点。

2.2 ViewGroup

接下来就进入到我们真正的主角,ViewGroup了,我会将他的源码切成好几段一点点的说明。

事件中断

在正式开始点击事件之前,ViewGroup会通过onInterceptTouchEvent这个方法对点击事件做一个中断判断,如果被中断了就不会处理后续的流程了

onInterceptTouchEvent这个方法一般而言都是会返回false,也就是不中断。如果你有业务上的需求需要中断的话,可以返回true。这样事件就不会往下面的View分发,只会由自己进行处理。

public abstract class ViewGroup extends View implements ViewParent, ViewManager {
	@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
    	boolean handled = false;

		// 检查事件是否被中断
        final boolean intercepted;
        if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            if (!disallowIntercept) {
            	// 关注这部分
                intercepted = onInterceptTouchEvent(ev);
                ev.setAction(action);
            } else {
                intercepted = false;
            }
        } else {
            intercepted = true;
        }
		
		// 如果被拦截,就会跳过分发的流程
		if (!canceled && !intercepted) {
			// ...正式分发点击事件
		}

		// 自己处理点击事件
		
		return handled;
    }


	public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
                && ev.getAction() == MotionEvent.ACTION_DOWN
                && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
                && isOnScrollbarThumb(ev.getXDispatchLocation(0), ev.getYDispatchLocation(0))) {
            return true;
        }
        return false;
    }
}

逆序搜索

开始正式处理点击事件,会先逆序遍历所有的子View,然后进行一些判断

  1. 该子View能否点击。canReceivePointerEvents
  2. 用户点击的位置是否和该View重合。isTransformedTouchPointInView

两个判断条件都符合后,就会尝试在该子View中处理点击事件
注意,子View也有可能是一个ViewGroup,所以调用子View的dispatchTouchEvent后,有可能会实际上调用的还是ViewGroup.dispatchTouchEvent。

public abstract class ViewGroup extends View implements ViewParent, ViewManager {
	@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
    	boolean handled = false;

		// 检查事件是否被中断
        final boolean intercepted;
		
		TouchTarget newTouchTarget = null;
		if (!canceled && !intercepted) {
			// ...正式处理点击事件

			final int childrenCount = mChildrenCount;
			for (int i = childrenCount - 1; i >= 0; i--) {
				// 不用关注他的原理,我们只需要他掏出了一个View即可。
                final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
                final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
				
				// 对View做合法性判断,如果合法就可以继续点击
				if (!child.canReceivePointerEvents()
						|| !isTransformedTouchPointInView(x, y, child, null)) {
                    ev.setTargetAccessibilityFocus(false);
                    continue;
                }

				newTouchTarget = getTouchTarget(child);
				// 转换为点击事件,注意child这个入参我们是有传值的
				if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
					//...处理了一些逻辑
					break; //然后直接跳出循环
				}
			}

			// 下面又开始处理其他逻辑
		}
		
		return handled;
    }

	/**
	 * 分发转换为点击事件
	 */ 
	private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        
        if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
            event.setAction(MotionEvent.ACTION_CANCEL);
            // 注意child这个入参,我们此时传入的child不为空,所以走下面
            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                handled = child.dispatchTouchEvent(event);
            }
            event.setAction(oldAction);
            return handled;
        }
    }
}

自己处理点击事件

在遍历了所有子View都没有处理掉该事件之后,ViewGroup会尝试自己来处理该事件。

public abstract class ViewGroup extends View implements ViewParent, ViewManager {
	@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
    	boolean handled = false;

        
		TouchTarget newTouchTarget = null;
		if (!canceled && !intercepted) {
			// ...正式处理点击事件
			TouchTarget newTouchTarget = null;

			for (int i = childrenCount - 1; i >= 0; i--) {
				// 刚才的for循环,用来表示代码的相对位置
			}

			// 如果点击事件之前被子View给处理了,
			// 那么代码到这里之后newTouchTarget就不为空,或者mFirstTouchTarget不为空
			if (newTouchTarget == null && mFirstTouchTarget != null) {
                newTouchTarget = mFirstTouchTarget;
                while (newTouchTarget.next != null) {
                    newTouchTarget = newTouchTarget.next;
                }
                newTouchTarget.pointerIdBits |= idBitsToAssign;
            }
		}

		// 这里和上面是连着的,如果mFirstTouchTarget== null其实就代表着点击事件没有被子View给处理
		if (mFirstTouchTarget == null) {
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
                    TouchTarget.ALL_POINTER_IDS);
        } else {
        	// 处理其他逻辑
        }
		
		return handled;
    }

	/**
	 * 分发转换为点击事件
	 */ 
	private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        
        if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
            event.setAction(MotionEvent.ACTION_CANCEL);
            // 注意child这个入参,我们此时传入的child为空,所以走上面
            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                handled = child.dispatchTouchEvent(event);
            }
            event.setAction(oldAction);
            return handled;
        }
    }
}

ViewGroup总结

要不然就是通过某个子View层层遍历,走到最深层的某个View的dispatchTouchEvent
要不然就是没有子View,调用自己的super,dispatchTouchEvent,还是View的dispatchTouchEvent

总而言之,代码就会通过这两种方式走到View这个类里面,
ViewGroup的dispatchTouchEvent这个方法的功能也很明显了:找一个可以处理该点击事件的View(可能是自己),将点击事件(TouchEvent)分发(Dispatch)给它(View)。

2.3 View

OnTouchListener

点击事件到View之后,入口还是DispatchTouchEvent,他会先检查是否有TouchListener,有的话先执行它。

这里有两个点,第一个点就是在View里面,OnClickListener和OnTouchListener是完全不同的东西。
第二个点就是OnTouchListener这个方法的return是有开发者自己控制的,换句话说,开发者可以自行控制事件是否要停在onTouch这里

public class View implements Drawable.Callback, KeyEvent.Callback,
        AccessibilityEventSource {
	public boolean dispatchTouchEvent(MotionEvent event) {
	
		boolean result = false;
		
		// 这个if一般而言都是通过的
		if (onFilterTouchEventForSecurity(event)) {
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    // 重点看这里
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }

			// 如果事件没有被onTouch处理掉,就会进入事件处理流程
            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }
		
		return result;
	}

	public interface OnTouchListener {
        boolean onTouch(View v, MotionEvent event);
    }
}

onTouchEvent

这里就是单个事件真正的处理方法,但是对于我们而言我们反而不需要太关注这个方法的处理逻辑。
第一是本文主要关注事件时如何分发到这里的。
第二是该方法无非就是对我们常用的一些逻辑,如focus,onClickListener,onLongClick等内容进行判断,有的话这个方法就会return true,没有的话就return false

public class View implements Drawable.Callback, KeyEvent.Callback,
        AccessibilityEventSource {

	public boolean onTouchEvent(MotionEvent event) {
		final int action = event.getAction();
		if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
			 switch (action) {
			 	case MotionEvent.ACTION_UP:
			 		// 一堆判断之后会走到这里,我们就不看这些判断了
			 		performClickInternal();
			 		break;
			 	case MotionEvent.ACTION_DOWN:
			 		break;
			 	case MotionEvent.ACTION_CANCEL:
			 		break;
			 	case MotionEvent.ACTION_MOVE:
			 		break;
			 }

			return true;
		}
		return false;
	}

	private boolean performClickInternal() {
        notifyAutofillManagerOnClick();
        return performClick();
    }

	public boolean performClick() {
        notifyAutofillManagerOnClick();

        final boolean result;
        final ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            // 这里就是我们设置的点击事件了,OnClickListener
            li.mOnClickListener.onClick(this);
            result = true;
        } else {
            result = false;
        }

        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);

        notifyEnterOrExitForAutoFillIfNeeded(true);

        return result;
    }
}

3. 附录:时序图uml代码

@startuml

participant Activity
participant PhoneWindow
participant DecorView
participant ViewGroup as vg
participant View as v
participant TouchListener as tl
participant "子View\nViewGroup" as cvg

activate Activity
Activity -> PhoneWindow : dispatchTouchEvent
activate PhoneWindow
	PhoneWindow -> DecorView : dispatchTouchEvent
	activate DecorView
		DecorView -> vg : dispatchTouchEvent\n进入View树处理点击事件
		activate vg 
			vg -> vg : onInterceptTouchEvent\n判断事件是否被拦截
			activate vg 
				vg --> vg : return boolean
			deactivate vg

			alt true
				vg --> vg : return\n不进行任何点击事件的处理\n流程结束
			end
			
			loop 逆序遍历子View
				vg -> cvg : isTransformedTouchPointInView\n判断是否可以点击
				activate cvg
					cvg --> vg : return boolean\ntrue代表可以点击
				deactivate cvg

				alt 不能点击
					vg -> vg : continue\n搜索下一个子View	
				else 可以点击
					vg -> vg : dispatchTransformedTouchEvent\n将分发事件转变为点击事件
					activate vg
						vg -> cvg : dispatchTouchEvent\n重复该ViewGroup的行为,继续往下分发
						activate cvg
						break 点击事件被消费
								cvg --> vg : return boolean\n告知点击事件是否被消费
						end
						deactivate cvg
					deactivate vg
				end
			end

			alt 点击事件没被消费
				vg -> vg : dispatchTransformedTouchEvent\n将分发事件转变为点击事件
				activate vg
					vg -> v :dispatchTouchEvent
					activate v
						alt 该View有TouchListener
							v -> tl : onTouch
							activate tl
								tl --> v : return boolean\n告知是否继续往下处理
							deactivate tl
						end 

						alt return true
								v --> vg : return true\n告知点击事件已经被处理
						else return false
								v -> v :onTouched
								activate v
									v --> v : return boolean\n告知是否处理了点击事件
								deactivate v
								v --> vg : return boolean\n告知是否处理了点击事件
						end
						
					deactivate v
				deactivate vg
			end

		vg --> DecorView : return boolean\n告知是否处理了点击事件
		deactivate vg

		DecorView --> PhoneWindow : return boolean\n告知是否处理了点击事件
	deactivate DecorView

PhoneWindow --> Activity : return boolean\n告知是否处理了点击事件
deactivate PhoneWindow
deactivate Activity

@enduml

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/735895.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Windows Api如何创建一个快捷方式并且在开始菜单搜索到自己的应用

原文链接&#xff1a;http://cshelloworld.com/home/detail/1804473083243925504 当我们点击win10系统搜索框的时候&#xff0c;输入名称 &#xff0c;win10会帮助我们匹配到对应的应用。这里搜索框实际上就是windows系统的开始菜单。 接下来我们随便找一个应用&#xff0c;右…

Adobe XD最新2023资源百度云盘下载(附教程)

如大家所了解的&#xff0c;Adobe XD是一种基于矢量的UI和UX设计工具&#xff0c;可用于设计从智能手表应用程序到成熟网站的任何内容&#xff0c;功能非常强大且操作便捷。目前最新已推出2023版本。 Adobe XD解决了Photoshop和其他图形应用程序无法解决的两个主要问题&#xf…

LSSS算法实现,基于eigen和pbc密码库【一文搞懂LSSS,原理+代码】

文章目录 一. LSSS简介1.1 概述1.2 线性秘密分享方案&#xff08;LSSS&#xff09;与 Shamir的秘密分享方案对比LSSS1.2.1 Shamir的秘密分享方案1.2.2 线性秘密分享方案&#xff08;LSSS&#xff09;1.2.3 主要区别 二. 基于矩阵的LSSS加解密原理分析2.1 LSSS矩阵构造2.1.1 定义…

Bytebase 对接本地部署的 llama3 开启ChatSQL功能

Bytebase 是为开发人员、测试、DBA和运维工程师构建的数据库 DevOps 领域的&#xff0c;类 GitLab/GitHub 平台。 这篇文章主要关注 Bytebase SQL 编辑器中的 AI 增强功能。使用此功能您可以使用自然语言在 Bytebase SQL 编辑器中查询数据库。同时还能给出针对查询的索引建议&…

WSL+Anconda(pytorch深度学习)环境配置

动机 最近在读point cloud相关论文&#xff0c;准备拉github上相应的code跑一下&#xff0c;但是之前没有深度学习的经验&#xff0c;在配置环境方面踩了超级多的坑&#xff0c;依次来记录一下。 一开始我直接将code拉到了windows本地来运行&#xff0c;遇到了数不清的问题&a…

骑马与砍杀-战团mod制作-基础篇-武器模型入骑砍(二)

骑马与砍杀战团mod制作-基础-武器模型入骑砍笔记&#xff08;二&#xff09; 资料来源 学习的资料来源&#xff1a; b站【三啸解说】手把手教你做【骑砍】MOD&#xff0c;基础篇&#xff0c;链接为&#xff1a; https://www.bilibili.com/video/BV19x411Q7No?p4&vd_sour…

【Python】已解决:安装python-Levenshtein包时遇到的subprocess-exited-with-error问题

文章目录 一、分析问题背景二、可能出错的原因三、错误代码示例四、正确代码示例及解决方案五、注意事项 已解决&#xff1a;安装python-Levenshtein包时遇到的subprocess-exited-with-error问题 一、分析问题背景 在安装python-Levenshtein这个Python包时&#xff0c;有时会…

Applied Spatial Statistics(七):Python 中的空间回归

Applied Spatial Statistics&#xff08;七&#xff09;&#xff1a;Python 中的空间回归 本笔记本演示了如何使用 pysal 的 spreg 库拟合空间滞后模型和空间误差模型。 OLS空间误差模型空间滞后模型三种模型的比较探索滞后模型中的直接和间接影响 import numpy as np impor…

【人工智能】—XGBoost算法在构建互联网防火墙异常行为识别模型应用案例

摘要&#xff1a; 近年来&#xff0c;各地党委、政府加快推进新型工业化&#xff0c;部署实施制造强市战略&#xff0c;提出工业企业“智改数转”是推动全市工业经济稳增长的重要引擎&#xff0c;更是稳增长、促发展的重要抓手。今天博主就以互联网防火墙异常行为识别为例给大家…

js实现canvas截图功能

关键代码 使用canvas的导出功能和drawImage函数 class CropShape{cropShape(shape){let {x,y,w,h} shapeconsole.log(x,y,w,h)const roiCanvas document.createElement(canvas);document.getElementById(app).append(roiCanvas)const roiCtx roiCanvas.getContext(2d);roi…

CTO的职责是什么?

看《架构思维》作者是这样讲的&#xff1a; CTO 到底是做什么的&#xff1f; 我当下的答案是&#xff1a;“CTO 就是一个从技术视角出发&#xff0c;为公司或者所在的部门做正确决策的 CEO。”怎么理解这句话呢&#xff1f;作为一个 CTO&#xff0c;其长期目标和决策优先级与…

vscode用vue框架2,续写登陆页面逻辑,以及首页框架的搭建

目录 前言&#xff1a; 一、实现登录页信息验证逻辑 1.实现登录数据双向绑定 2.验证用户输入数据是否和默认数据相同 补充知识1&#xff1a; 知识点补充2&#xff1a; 二、首页和登录页之间的逻辑(1) 1. 修改路由&#xff0c;使得程序被访问先访问首页 知识点补充3&am…

经典机器学习方法(7)—— 卷积神经网络CNN

参考&#xff1a;《动手学深度学习》第六章 卷积神经网络&#xff08;convolutional neural network&#xff0c;CNN&#xff09;是一类针对图像数据设计的神经网络&#xff0c;它充分利用了图像数据的特点&#xff0c;具有适合图像特征提取的归纳偏置&#xff0c;因而在图像相…

信息安全基础知识(完整)

信息安全基础知识 安全策略表达模型是一种对安全需求与安全策略的抽象概念表达&#xff0c;一般分为自主访问控制模型&#xff08;HRU&#xff09;和强制访问控制模型&#xff08;BLP、Biba&#xff09;IDS基本原理是通过分析网络行为&#xff08;访问方式、访问量、与历史访问…

程序猿大战Python——面向对象——继承进阶

方法重写 目标&#xff1a;掌握方法的重写。 当父类的同名方法达不到子类的要求&#xff0c;则可以在子类中对方法进行重写。语法&#xff1a; class 父类名(object):def 方法A(self):代码... class 子类名(父类名):def 方法A(self):代码... 例如&#xff0c;一起来完成&…

Ubuntu下安装docker

一、docker安装说明 解决官方源无法下载的问题 二、使用步骤 1.更新软件包索引 sudo apt-get update2.安装必要的软件包&#xff0c;以允许apt通过HTTPS使用仓库 sudo apt-get install apt-transport-https ca-certificates curl software-properties-common3.添加Docker的…

数据结构:冒泡排序,选择排序,插入排序,希尔排序的实现分析

✨✨小新课堂开课了&#xff0c;欢迎欢迎~✨✨ &#x1f388;&#x1f388;养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; 所属专栏&#xff1a;数据结构与算法 小新的主页&#xff1a;编程版小新-CSDN博客 1.冒泡排序 1.1算法思想 冒泡排序的基本思想就是&a…

LLM端侧部署系列 | 如何将阿里千问大模型Qwen部署到手机上?环境安装及其配置(上篇)

引言 下载待部署模型 安装minconda 安装tvm和mlc-llm 安装 JDK 安装 Android SDK 下载mlc-llm仓库 设置环境变量 安装Rust 1. 引言 梨花风起正清明&#xff0c;游子寻春半出城。 小伙伴们好&#xff0c;我是公众号《小窗幽记机器学习》的小编&#xff1a;卖青团的小…

38. 外观数列

题目 「外观数列」是一个数位字符串序列&#xff0c;由递归公式定义&#xff1a; countAndSay(1) "1" countAndSay(n) 是 countAndSay(n-1) 的行程长度编码。 行程长度编码&#xff08;RLE&#xff09;是一种字符串压缩方法&#xff0c;其工作原理是通过将连续相…

刷代码随想录有感(114):动态规划——最少数量的零钱换整

题干&#xff1a; 代码&#xff1a; class Solution { public:int coinChange(vector<int>& coins, int amount) {vector<int>dp(amount 1, INT_MAX);dp[0] 0;for(int i 0; i < coins.size(); i){for(int j coins[i]; j < amount; j){if(dp[j - coi…