当前位置: 首页 > news >正文

【Android】View的工作流程

View的工作流程

开始了解一下View的工作流程,就是measure、layout和draw。measure用来测量View的宽高,layout用来确定View的位置,draw则用来绘制View。这一讲我们来看看measure流程,measure流程分为View的measure流程和ViewGroup的measure流程,只不过ViewGroup的measure流程除了要完成自己的测量还要遍历去调用子元素的measure()方法。

以下是按照原文格式提取内容与代码,并在代码中添加注释:

View的工作流程入口

这篇是新版本的东西

https://blog.csdn.net/Tai_Monster/article/details/130915515

一、DecorView被加载到Window中

当 DecorView 创建完毕,要加载到 Window 中时,我们需要先了解一下 Activity 的创建过程。当我们调用 Activity 的 startActivity方法时,最终是调用 ActivityThread 的 handleLaunchActivity方法来创建 Activity的,代码如下所示:

  1. Activity的创建过程
private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent) {// 创建ActivityActivity a = performLaunchActivity(r, customIntent);//1if (a!= null) {// 配置Configurationr.createdConfig = new Configuration(mConfiguration);Bundle oldState = r.state;// 处理Activity的状态handleResumeActivity(r.token, false, r.isForward,!r.activity.mFinished &&!r.startsNotResumed);//2}
}

在上面代码注释1处调用 performLaunchActivity方法来创建 Activity,在这里面会调用到Activity的 onCreate 方法,从而完成DecorView的创建。接着在上面代码注释2处调用handleResumeActivity方法,代码如下所示:

  1. DecorView的创建与加载到Window中
final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward, boolean reallyResume) {unscheduleGcIdler();mSomeActivitiesChanged = true;ActivityClientRecord r = performResumeActivity(token, clearHide);//1if (r!= null) {final Activity a = r.activity;if (r.window == null &&!a.mFinished && willBeVisible) {// 获取Activity的Windowr.window = r.activity.getWindow();// 获取Window的DecorViewView decor = r.window.getDecorView();//2decor.setVisiblity(View.INVISIBLE);WindowManager wm = a.getWindowManager();//3WindowManager.LayoutParams l = r.window.getAttributes();a.mDecor = decor;l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;l.softInputMode |= forwardBit;if (a.mVisibleFromClient) {a.mWindowAdded = true;wm.addView(decor, l);//4//。。。}}}
}

在上面代码注释1处的 performResumeActivity 方法中会调用 Activity 的onResume 方法。接着往下看,注释2处得到了DecorView。注释3处得到了WindowManager,WindowManager是一个接口并且继承了接口 ViewManager。在注释4处调用 WindowManager 的 addView 方法,WindowManager 的实现类是 WindowManagerlmpl,所以实际调用的是 WindowManagerlmpl 的addView 方法。具体代码如下所示:

  1. WindowManager添加View
public final class WindowManagerImpl implements WindowManager {private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();@Overridepublic void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {// 应用默认TokenapplyDefaultToken(params);// 通过WindowManagerGlobal添加视图mGlobal.addView(view, params, mDisplay, mParentWindow);}
}

在 WindowManagerlmpl 的 addView 方法中,又调用了 WindowManagerGlobal 的 addView

public void addView(View view, ViewGroup.LayoutParams params,Display display, Window parentWindow) {...ViewRootImpl root;ViewPanelParent = null;synchronized (mLock) {...root = new ViewRootImpl(view.getContext(), display); //1view.setLayoutParams(wparams);mViews.add(view);mRoots.add(root);mParams.add(wparams);...}try {root.setView(view, wparams, panelParent); //2} catch (RuntimeException e) {synchronized (mLock) {final int index = findViewLocked(view, false);if (index >= 0) {removeViewLocked(index, true);}}throw e;}
}

在上面代码注释1处创建了ViewRootImpl实例,在注释2处调用了ViewRootImplsetView方法并将DecorView作为参数传进去,这样就把DecorView加载到了Window中。当然界面仍不会显示出什么来,因为View的工作流程还没有执行完,还需要经过measurelayout以及draw才会把View绘制出来。

onCreate方法完成了DecorView的创建。在onResume方法中将DecorView加载进入Window。

二、ViewRootImpl的PerformTraversals方法

前面讲到了将DecorView加载到Window中,是通过ViewRootImplsetView方法。ViewRootImpl还有一个方法PerformTraversals,这个方法使得ViewTree开始View的工作流程,代码如下所示:

在这里插入图片描述

  1. PerformTraversals方法代码
private void performTraversals() {if (!mStopped) {// 获取根视图的宽度测量规格int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);// 获取根视图的高度测量规格int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);// 执行测量操作performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);// 执行布局操作performLayout(lp, desiredWindowWidth, desiredWindowHeight);if (!mCancelDraw &&!newSurface) {if (!skipDraw || mReportNextDraw) {if (mPendingTransitions!= null && mPendingTransitions.size() > 0) {// 启动过渡动画for (int i = 0; i < mPendingTransitions.size(); ++i) {mPendingTransitions.get(i).startChangingAnimations();}// 清除过渡动画列表mPendingTransitions.clear();}// 执行绘制操作performDraw();}}}
}

这里面主要执行了3个方法,分别是performMeasure、performLayout和performDraw,在其方法的内部又会分别调用View的measure、layout和draw方法。需要注意的是,performMeasure方法中需要传入两个参数,分别是childWidthMeasureSpec和childHeightMeasureSpec。要了解这两个参数,需要了解MeasureSpec。

理解MeasureSpec

MeasureSpecView的内部类,封装了一个View的规格尺寸,包括View的宽和高的信息。它的作用是在Measure流程中,系统将ViewLayoutParams根据父容器所施加的规则转换成对应的MeasureSpec,然后在onMeasure方法中根据这个MeasureSpec来确定View的宽和高。

在这里插入图片描述

public static class MeasureSpec {private static final int MODE_SHIFT = 30;private static final int MODE_MASK = 0x3 << MODE_SHIFT;public static final int UNSPECIFIED = 0 << MODE_SHIFT;public static final int EXACTLY = 1 << MODE_SHIFT;public static final int AT_MOST = 2 << MODE_SHIFT;public static int makeMeasureSpec(int size, int mode) {if (sUseBrokenMakeMeasureSpec) {return size + mode;} else {return (size & ~MODE_MASK) | (mode & MODE_MASK);}}public static int getMode(int measureSpec) {return (measureSpec & MODE_MASK);}public static int getSize(int measureSpec) {return (measureSpec & ~MODE_MASK);}public static int adjust(int measureSpec, int delta) {final int mode = getMode(measureSpec);int size = getSize(measureSpec);size += delta;if (size < 0) {Log.e(VIEW_LOG_TAG, "MeasureSpec.adjust: new size would be negative! (" + size + ") spec: " + toString(measureSpec) + " delta: " + delta);size = 0;}return makeMeasureSpec(size, mode);}public static String toString(int measureSpec) {int mode = getMode(measureSpec);int size = getSize(measureSpec);StringBuilder sb = new StringBuilder("MeasureSpec: ");if (mode == UNSPECIFIED) {sb.append("UNSPECIFIED ");} else if (mode == EXACTLY) {sb.append("EXACTLY ");} else if (mode == AT_MOST) {sb.append("AT_MOST ");}sb.append("size=").append(size);return sb.toString();}
}
  • MeasureSpec是一个32位的int值,高2位为SpecMode(测量的模式),低30位为SpecSize(测量的大小)。
  • SpecMode有3种模式:
    • UNSPECIFIED:未指定模式,View想多大就多大,父容器不做限制,一般用于系统内部的测量。
    • AT_MOST:最大模式,对应于wrap_content属性,只要尺寸不超过父控件允许的最大尺寸就行。
    • EXACTLY:精确模式,对应于match_parent属性和具体的数值,父容器测量出View所需要的大小,也就是SpecSize的值。

对于每一个View,都有一个MeasureSpec,而这个MeasureSpec保存了该View的尺寸规格。在View的测量流程中,通过makeMeasureSpec来保存宽和高的信息。通过getModegetSize得到模式和宽、高。MeasureSpec是受自身LayoutParams和父容器的MeasureSpec共同影响的。

作为顶层 View的 DecorView来说,其并没有父容器,那么它的 MeasureSpec 是如何得来的呢?为了解决这个疑问,我们再回到 ViewRootlmpl的 PerformTraveals 方法,如下所示:

private void performTraversals() {if (!mStopped) {// 获取根视图的宽度测量规格int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);// 获取根视图的高度测量规格int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);// 执行测量操作performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);// 执行布局操作performLayout(lp, desiredWindowWidth, desiredWindowHeight);if (!mCancelDraw &&!newSurface) {if (!skipDraw || mReportNextDraw) {if (mPendingTransitions!= null && mPendingTransitions.size() > 0) {// 启动过渡动画for (int i = 0; i < mPendingTransitions.size(); ++i) {mPendingTransitions.get(i).startChangingAnimations();}// 清除过渡动画列表mPendingTransitions.clear();}// 执行绘制操作performDraw();}}}
}

在这里插入图片描述

  1. **getRootMeasureSpec**方法分析
    1. 该方法用于根据窗口大小和根布局参数来确定测量规格(MeasureSpec)。

    2. private static int getRootMeasureSpec(int windowSize, int rootDimension) {int measureSpec;switch (rootDimension) {case ViewGroup.LayoutParams.MATCH_PARENT:measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);break;case ViewGroup.LayoutParams.WRAP_CONTENT:measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);break;default:measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);break;}return measureSpec;
      }
      
    3. getRootMeasureSpec方法的第一个参数windowSize指的是窗口的尺寸,所以对于DecorView来说,它的 MeasureSpec 由自身的 LayoutParams 和窗口的尺寸决定,这一点和普通 View 是不同的。接着往下看,就会看到根据自身的LayoutParams来得到不同的MeasureSpec。

    4. performMeasure方法中需要传入两个参数,即childWidthMeasureSpec和 childHcightMcasureSpcc,这代表什么我们也应该明白了。接着回到PcrformTraveals 方法,查看在注释2处的performMeasure方法内部做了什么,代码如下所示:

  2. **performMeasure**方法分析
    1. 该方法用于执行视图的测量操作。
    2. private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");try {mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);} finally {Trace.traceEnd(Trace.TRACE_TAG_VIEW);}
      }
      

getRootMeasureSpec方法用于根据根视图的布局参数和窗口大小确定根视图的测量规格,

performMeasure方法则是实际执行视图测量操作的方法。

  • 该方法首先开始跟踪视图测量操作,然后调用视图的measure方法进行测量,最后结束跟踪操作。

measure流程概述

measure流程分为Viewmeasure流程和ViewGroupmeasure流程。其中,ViewGroupmeasure流程除完成自身测量外,还需遍历调用子元素的measure()方法。

一、View的measure流程

(一)onMeasure()方法(测量尺寸)
// onMeasure方法是View测量过程的入口,它接收宽度和高度的测量规格参数,用于确定View最终的测量宽高
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {// 通过调用getDefaultSize方法,结合建议的最小宽度/高度以及传入的测量规格,来设置View的测量宽高// getSuggestedMinimumWidth和getSuggestedMinimumHeight方法分别用于获取建议的最小宽度和高度值// widthMeasureSpec和heightMeasureSpec是父容器传递过来的测量规格,包含了模式和大小信息setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

此方法通过调用setMeasuredDimension方法,并传入基于getDefaultSize以及相关获取建议最小宽度、高度方法得到的值,来开启View的测量过程。

(二)setMeasuredDimension()方法
// 此方法用于最终设置View的宽高,会考虑光学边界等因素进行调整后再设置
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {// 判断当前View与父容器的布局模式是否为光学模式(用于处理一些特殊的布局边界情况,比如在某些特定布局下考虑视图的视觉边界效果)boolean optical = isLayoutModeOptical(this);if (optical!= isLayoutModeOptical(mParent)) {// 获取光学边界的Insets信息(Insets包含了视图在上下左右方向上与其他元素的边界间隔信息,比如内边距等)Insets insets = getOpticalInsets();int opticalWidth = insets.left + insets.right;int opticalHeight = insets.top + insets.bottom;// 根据是否为光学模式,对测量的宽度和高度进行相应的调整(加上或减去光学边界宽度/高度)// 如果是光学模式,就加上光学宽度/高度,否则减去,以此来准确设置符合光学布局要求的宽高measuredWidth += optical? opticalWidth : -opticalWidth;measuredHeight += optical? opticalHeight : -opticalHeight;}// 调用底层方法设置最终的测量宽高,该方法一般是在系统内部进一步处理设置宽高的具体操作setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}

该方法主要用于设置View的宽高,会考虑当前View与父容器的光学布局模式情况,对宽高进行相应调整后,通过setMeasuredDimensionRaw方法完成最终设置。

(三)getDefaultSize()方法
// 根据测量规格和给定的默认大小,返回View最终的测量大小
// 参数size通常是默认的大小值,measureSpec则包含了父容器传递过来的测量规格信息(模式和大小)
public static int getDefaultSize(int size, int measureSpec) {int result = size;// 获取测量规格中的模式(UNSPECIFIED、AT_MOST、EXACTLY这三种模式,用于确定View尺寸的限制情况)int specMode = MeasureSpec.getMode(measureSpec);// 获取测量规格中的大小值(去除模式位后的数值,代表了父容器提供的一个尺寸参考值)int specSize = MeasureSpec.getSize(measureSpec);switch (specMode) {case MeasureSpec.UNSPECIFIED:// 如果是未指定模式,意味着父容器对View没有尺寸限制,View可以自行决定大小,所以直接返回传入的默认大小sizeresult = size;break;case MeasureSpec.AT_MOST:case MeasureSpec.EXACTLY:// 如果是最大模式或精确模式,说明父容器对View有一定的尺寸要求,此时返回测量规格中的大小值specSize// 对于AT_MOST模式,View的大小不能超过这个specSize;对于EXACTLY模式,View应按此大小进行布局result = specSize;break;}return result;
}

getDefaultSize方法依据传入的测量规格中的模式(specMode)来确定返回的View测量大小。在AT_MOSTEXACTLY模式下,返回测量规格中的大小值(specSize);在UNSPECIFIED模式下,则返回传入的第一个参数值。

(四)MeasureSpec类
// MeasureSpec类用于辅助测量View,它是一个32位的int值,通过位运算来分离和获取其中的测量模式以及测量大小信息
public static class MeasureSpec {private static final int MODE_SHIFT = 30;private static final int MODE_MASK = 0x3 << MODE_SHIFT;/*** 未指定模式,父容器对View没有尺寸限制,View可自行决定大小,常用于系统内部测量场景,比如某些自动调整大小的情况*/public static final int UNSPECIFIED = 0 << MODE_SHIFT;/*** 精确模式,对应match_parent属性或具体的数值,父容器确定了View的精确大小,View会按此大小进行布局,比如在固定布局中某个View占据固定的空间大小*/public static final int EXACTLY = 1 << MODE_SHIFT;/*** 最大模式,对应wrap_content属性,View的大小不能超过父容器允许的最大尺寸,常用于希望View根据自身内容自适应大小,但又不能超出父容器范围的情况*/public static final int AT_MOST = 2 << MODE_SHIFT;// 获取测量规格中的模式(通过与模式掩码进行按位与操作,提取出高两位表示的模式信息)public static int getMode(int measureSpec) {return (measureSpec & MODE_MASK);}// 获取测量规格中的大小值(通过与模式掩码取反后进行按位与操作,去除高两位的模式位,获取低30位表示的大小信息)public static int getSize(int measureSpec) {return (measureSpec & ~MODE_MASK);}
}

MeasureSpec类用于辅助测量View,它是一个32位的int值,高两位表示测量模式(分为UNSPECIFIEDEXACTLYAT_MOST三种),低30位表示测量大小。

  • UNSPECIFIED模式:父容器对View无尺寸限制,View可自行决定大小,常用于系统内部测量。
  • AT_MOST模式:对应wrap_content属性,View大小不能超过父控件允许的最大尺寸。
  • EXACTLY模式:对应match_parent属性或具体数值,父容器确定了View的精确大小。

SpecMode 是 View 的测量模式,而 SpecSize 是 View 的测量大小。

很显然,需要依据不同的 SpecMode 值来返回不同的 result 值,也就是 SpecSize。在 AT_MOST 和 EXACTLY 这两种模式下,都会返回 SpecSize 这个值,这意味着 View 在这两种模式下的测量宽和高直接取决于 SpecSize。由此可知,对于一个直接继承自 View 的自定义 View 来说,它的 wrap_content 和 match_parent 属性的效果是等同的。

所以,如果要实现自定义 View 的 wrap_content 功能,就需要重写 onMeasure 方法,并针对自定义 View 的 wrap_content 属性进行相应处理。

而在 UNSPECIFIED 模式下,返回的是 getDefaultSize 方法的第一个参数 size 的值。从 onMeasure 方法来看,size 的值是通过 getSuggestedMinimumWidth 方法或者 getSuggestedMinimumHeight 方法得到的。

接下来,我们查看一下 getSuggestedMinimumWidth 方法具体做了什么。实际上,弄懂了getSuggestedMinimumWidth 方法就可以了,因为 getSuggestedMinimumHeight 方法与其原理是一样的。

(五)getSuggestedMinimumWidth()方法(以宽度为例,高度同理)
// 该方法用于获取建议的最小宽度值,根据View是否设置背景等情况来确定返回值
// 这个值会作为getDefaultSize方法的一个参数参与到View宽度测量大小的计算中
protected int getSuggestedMinimumWidth() {// 如果View没有设置背景,返回mMinWidth(mMinWidth可通过属性或方法设置,默认值为0,代表View的最小宽度限制)return (mBackground == null)? mMinWidth : Math.max(mMinWidth, mBackground.getMinimumWidth());
}

此方法用于获取建议的最小宽度值。

View未设置背景,则取值为mMinWidth(可通过android:minWidth属性或setMinimumWidth方法设置,默认值为0);

若设置了背景,则取mMinWidth和背景(Drawable类型)最小宽度两者中的最大值。

// 用于设置View的最小宽度值,设置后会触发重新布局请求,以便系统根据新的最小宽度重新计算和调整布局
public void setMinimumWidth(int minWidth) {mMinWidth = minWidth;requestLayout();
}

setMinimumWidth方法用于设置View的最小宽度值,设置后会触发重新布局请求。

如果 View 设置了背景,则取值为 max(mMinWidth,mBackground.getMinimumWidth()),也就是取 mMinWidth 和 mBackground.getMinimumWidth()之间的最大值。

此前讲了 mMinWidth,下面看看mBackground.getMinimumWidth()。

这个mBackground是Drawable 类型的,Drawable类的 getMinimumWidth 方法如下所示:

// 获取Drawable类型的背景的最小宽度值,如果其固有宽度(比如图片资源本身的宽度等)大于0则返回固有宽度,否则返回0
// 这个方法主要用于在有背景的情况下,结合mMinWidth来确定View的建议最小宽度值
public int getMinimumWidth() {final int intrinsicWidth = getIntrinsicWidth();return intrinsicWidth > 0? intrinsicWidth : 0;
}

getMinimumWidth方法用于获取Drawable类型背景的最小宽度值,若其固有宽度大于0,则返回固有宽度,否则返回0。

getSuggestedMinimumWidth 方法就是:如果 View 没有设置背景,则返回 mMinWidth;如果设置了背景,就返回mMinWidth 和 Drawable 的最小宽度之间的最大值。

所以在我们结论就是:如果是UNSPECIFIED模式,那么view的大小就会被设置为有背景下的mMinWidth 和 Drawable 的最小宽度之间的最大值

其他情况就是:都返回specSize这个值,也就是View测量后的大小

二、ViewGroup的measure流程

讲完了 View的 measure 流程,接下来看看 ViewGroup的 measure 流程。对于 ViewGroup,它不只要测量自身,还要遍历地调用子元素的 measure()方法。ViewGroup中没有定义onMeasure()方法,但却定义了measureChildren()方法:

(一)measureChildren()方法(ViewGroup.java
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {final int size = mChildrenCount;final View[] children = mChildren;for (int i = 0; i < size; ++i) {final View child = children[i];if ((child.mViewFlags & VISIBILITY_MASK)!= GONE) {measureChild(child, widthMeasureSpec, heightMeasureSpec);}}
}

该方法会遍历ViewGroup的所有子元素,对非GONE状态的子元素调用measureChild方法进行测量。

(二)measureChild()方法
protected void measureChild(View child, int parentWidthMeasureSpec,int parentHeightMeasureSpec) {final LayoutParams lp = child.getLayoutParams();final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,mPaddingLeft + mPaddingRight, lp.width);final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,mPaddingTop + mPaddingBottom, lp.height);child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

此方法先获取子元素的布局参数,然后依据父容器的测量规格以及子元素自身布局参数,获取子元素的宽度和高度测量规格,最后调用子元素的measure方法进行测量。

调用 child.getLayoutParams()方法来获得子元素的LayoutParams 属性,获取子元素的MeasureSpec 并调用子元素的 measure()方法进行测量。getChildMeasureSpec()方法里写了什么呢?其代码如下:

(三)getChildMeasureSpec()方法
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {int specMode = MeasureSpec.getMode(spec);int specSize = MeasureSpec.getSize(spec);int size = Math.max(0, specSize - padding);int resultSize = 0;int resultMode = 0;switch (specMode) {case MeasureSpec.EXACTLY:if (childDimension >= 0) {resultSize = childDimension;resultMode = MeasureSpec.EXACTLY;} else if (childDimension == LayoutParams.MATCH_PARENT) {resultSize = size;resultMode = MeasureSpec.EXACTLY;} else if (childDimension == LayoutParams.WRAP_CONTENT) {resultSize = size;resultMode = MeasureSpec.AT_MOST;}break;case MeasureSpec.AT_MOST:if (childDimension >= 0) {resultSize = childDimension;resultMode = MeasureSpec.EXACTLY;} else if (childDimension == LayoutParams.MATCH_PARENT) {resultSize = size;resultMode = MeasureSpec.AT_MOST;} else if (childDimension == LayoutParams.WRAP_CONTENT) {resultSize = size;resultMode = MeasureSpec.AT_MOST;}break;case MeasureSpec.UNSPECIFIED:if (childDimension >= 0) {resultSize = childDimension;resultMode = MeasureSpec.EXACTLY;} else if (childDimension == LayoutParams.MATCH_PARENT) {resultSize = 0;resultMode = MeasureSpec.UNSPECIFIED;} else if (childDimension == LayoutParams.WRAP_CONTENT) {resultSize = 0;resultMode = MeasureSpec.UNSPECIFIED;}break;}return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

getChildMeasureSpec方法根据父容器的测量规格以及子元素的LayoutParams属性来确定子元素的测量规格。需注意,当父容器的测量规格属性为AT_MOST,子元素的LayoutParams属性为WRAP_CONTENT时,子元素的测量规格属性也为AT_MOST,其specSize值为父容器的specSize减去padding的值,这种情况下,为避免与设置MATCH_PARENT效果相同,可在LayoutParams属性为WRAP_CONTENT时指定默认宽高。

三、LinearLayout的measure流程

(一)onMeasure()方法(LinearLayout.java
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {if (mOrientation == VERTICAL) {measureVertical(widthMeasureSpec, heightMeasureSpec);} else {measureHorizontal(widthMeasureSpec, heightMeasureSpec);}
}

LinearLayout根据自身的方向(垂直或水平),调用相应的测量方法(measureVerticalmeasureHorizontal)来进行测量。

(二)measureVertical()方法(垂直方向测量部分源码)
v// 用于测量垂直方向布局相关尺寸的方法,接收宽度和高度的测量规格参数
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {// 初始化总长度为0,这个总长度会随着子元素的测量等操作不断更新,用于计算最终布局在垂直方向占用的空间大小mTotalLength = 0;// 遍历所有子元素(通过索引i),这里假设count表示子元素的总数for (int i = 0; i < count; ++i) {// 获取当前索引对应的虚拟子视图(具体获取逻辑由getVirtualChildAt方法决定)final View child = getVirtualChildAt(i);// 如果子视图为空,调用measureNullChild方法计算该空子视图对应的长度,并累加到总长度mTotalLength上,然后跳过当前循环后续部分,进入下一次循环if (child == null) {mTotalLength += measureNullChild(i);continue;}// 如果子视图的可见性为GONE,调用getChildrenSkipCount方法获取需要跳过的子元素数量,更新索引i,跳过这些不可见的子元素,进入下一次循环if (child.getVisibility() == View.GONE) {i += getChildrenSkipCount(child, i);continue;}// 如果在当前子元素之前存在分割线,将分割线的高度累加到总长度mTotalLength上if (hasDividerBeforeChildAt(i)) {mTotalLength += mDividerHeight;}// 获取当前子视图的布局参数,用于后续判断和尺寸计算等操作LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();// 累加当前子视图布局参数中的权重值(weight),用于后续根据权重计算尺寸等情况totalWeight += lp.weight;// 如果高度测量模式是EXACTLY(精确模式,即父容器已经明确指定了子视图的精确高度),并且当前子视图高度为0且权重大于0,// 进行以下操作:// 1. 记录当前总长度为totalLength。// 2. 更新总长度mTotalLength,取当前总长度和当前总长度加上子视图上下外边距后的较大值,目的可能是在权重布局下确保总长度满足一定要求。// 3. 设置skippedMeasure为true,表示可能有测量被跳过(具体含义根据上下文而定)if (heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0) {final int totalLength = mTotalLength;mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);skippedMeasure = true;} else {// 初始化一个变量oldHeight为Integer.MIN_VALUE,用于记录子视图原始高度(后续会根据情况更新和恢复这个值)int oldHeight = Integer.MIN_VALUE;// 如果子视图高度为0且权重大于0,将oldHeight设为0,并将子视图的高度设置为WRAP_CONTENT(包裹内容模式),// 可能是为了先按照包裹内容模式测量子视图尺寸if (lp.height == 0 && lp.weight > 0) {oldHeight = 0;lp.height = LayoutParams.WRAP_CONTENT;}// 在布局前测量子视图尺寸,传入宽度和高度的测量规格以及相关参数,// 如果totalWeight为0(可能表示没有权重布局相关情况),传入当前总长度mTotalLength作为垂直方向的偏移量,否则传入0measureChildBeforeLayout(child, i, widthMeasureSpec, 0, heightMeasureSpec,totalWeight == 0? mTotalLength : 0);// 如果之前记录的oldHeight不是初始值Integer.MIN_VALUE,说明之前对子视图高度进行了临时修改,这里将子视图高度恢复为原始值if (oldHeight!= Integer.MIN_VALUE) {lp.height = oldHeight;}// 获取子视图测量后的高度final int childHeight = child.getMeasuredHeight();// 记录当前总长度为totalLengthfinal int totalLength = mTotalLength;// 更新总长度mTotalLength,取当前总长度和当前总长度加上子视图高度、上下外边距以及获取下一个位置偏移量(getNextLocationOffset方法获取)后的较大值,// 目的是准确计算布局在垂直方向占用的总空间mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +lp.bottomMargin + getNextLocationOffset(child));}}// 如果使用最大子元素相关策略(useLargestChild为true),并且高度测量模式是AT_MOST(最大模式,子视图高度不能超过某个指定值)// 或者UNSPECIFIED(未指定模式,父容器对子视图高度没有限制,子视图可以根据自身内容决定高度),执行以下操作:if (useLargestChild &&(heightMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.UNSPECIFIED)) {// 重置总长度为0,准备重新计算mTotalLength = 0;// 再次遍历所有子元素for (int i = 0; i < count; ++i) {// 获取当前索引对应的虚拟子视图final View child = getVirtualChildAt(i);// 如果子视图为空,调用measureNullChild方法计算该空子视图对应的长度,并累加到总长度mTotalLength上,然后跳过当前循环后续部分,进入下一次循环if (child == null) {mTotalLength += measureNullChild(i);continue;}// 如果子视图的可见性为GONE,调用getChildrenSkipCount方法获取需要跳过的子元素数量,更新索引i,跳过这些不可见的子元素,进入下一次循环if (child.getVisibility() == GONE) {i += getChildrenSkipCount(child, i);continue;}// 获取当前子视图的布局参数final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams)child.getLayoutParams();// 记录当前总长度为totalLengthfinal int totalLength = mTotalLength;// 更新总长度mTotalLength,取当前总长度和当前总长度加上最大子元素高度(largestChildHeight,应该是之前记录好的某个最大子元素高度值)、// 子视图上下外边距以及获取下一个位置偏移量后的较大值,目的可能是基于最大子元素情况来确定布局在垂直方向占用的总空间mTotalLength = Math.max(totalLength, totalLength + largestChildHeight +lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));}}// 将顶部和底部内边距累加到总长度上,这样总长度就包含了内边距的空间占用情况mTotalLength += mPaddingTop + mPaddingBottom;// 将最终计算得到的总长度赋值给heightSize变量,这个变量可能用于后续向父容器汇报布局在垂直方向的尺寸情况等操作int heightSize = mTotalLength;
}

measureVertical方法中,首先定义mTotalLength用于存储LinearLayout在垂直方向的高度。然后遍历子元素,根据子元素的MeasureSpec模式分别计算每个子元素的高度。若子元素为wrap_content,则将每个子元素的高度、垂直方向的margin等相关值相加并赋值给mTotalLength,以此得出整个LinearLayout的高度;若布局高度设置为match_parent或具体数值,则与View的测量方法一样进行处理。

这是一段关于View布局流程的代码文档。以下是提取内容并整理格式后的结果:

View的layout布局流程

layout方法的作用是确定元素的位置。ViewGroup中的layout方法用来确定子元素的位置,View中的layout方法则用来确定自身的位置。首先我们看看View的layout方法:

// layout方法用于确定View自身在父容器中的位置
// 参数l、t、r、b分别代表View的左、上、右、下边界坐标
public void layout(int l, int t, int r, int b) {// 如果mPrivateFlags3的PFLAG_3_MEASURE_NEEDED_BEFORE_LAYOUT标志位被设置if ((mPrivateFlags3 & PFLAG_3_MEASURE_NEEDED_BEFORE_LAYOUT)!= 0) {// 调用onMeasure方法进行测量,传入旧的宽度和高度测量规格onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);// 清除PFLAG_3_MEASURE_NEEDED_BEFORE_LAYOUT标志位mPrivateFlags3 &= ~PFLAG_3_MEASURE_NEEDED_BEFORE_LAYOUT;}// 记录View的旧的左、上、右、下边界坐标int oldL = mLeft;int oldT = mTop;int oldB = mBottom;int oldR = mRight;// 判断是否基于光学边界布局(与父视图相关)boolean changed = isLayoutModeOptical(mParent);// 设置光学边界帧,内部会根据changed的值进行不同操作setOpticalFrame(l, t, r, b);// 如果布局发生了改变或者PFLAG_LAYOUT_REQUIRED标志位被设置if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {// 调用onLayout方法进行子视图布局(对于ViewGroup有实际操作,View中通常为空实现)onLayout(changed, l, t, r, b);// 清除PFLAG_LAYOUT_REQUIRED标志位mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;// 获取ListenerInfo对象,用于处理布局改变的监听器相关操作ListenerInfo li = mListenerInfo;// 如果监听器信息不为空且布局改变监听器列表不为空if (li!= null && li.mOnLayoutChangeListeners!= null) {// 克隆布局改变监听器列表ArrayList<OnLayoutChangeListener> listenersCopy =(ArrayList<OnLayoutChangeListener>) li.mOnLayoutChangeListeners.clone();// 获取监听器数量int numListeners = listenersCopy.size();// 遍历每个监听器并调用onLayoutChange方法通知布局改变for (int i = 0; i < numListeners; ++i) {listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);}}}// 计算View的旧宽度和旧高度int oldWidth = mRight - mLeft;int oldHeight = mBottom - mTop;// 计算View的新宽度和新高度int newWidth = right - left;int newHeight = bottom - top;// 判断View的大小是否发生改变boolean sizeChanged = (newWidth!= oldWidth) || (newHeight!= oldHeight);// 如果大小发生改变,设置RenderNode的边界if (sizeChanged) {mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);}// 返回布局是否发生改变return changed;
}

layout 方法的 4 个参数 l、t、r、b 分别是 View 从左、上、右、下相对于其父容器的距离。

传进来里面的四个参数分别是View的四个点的坐标,它的坐标不是相对屏幕的原点,而且相对于它的父布局来说的。

接着来查看 setFrame 方法里做了什么,代码如下所示:

protected boolean setFrame(int left, int top, int right, int bottom) {boolean changed = false;if (DBG) {Log.d("View", this + " View.setFrame(" + left + "," + top + "," + right + "," + bottom + ")");}if (mLeft!= left || mRight!= right || mTop!= top || mBottom!= bottom) {changed = true;int drawn = mPrivateFlags & PFLAG_DRAWN;int oldWidth = mRight - mLeft;int oldHeight = mBottom - mTop;int newWidth = right - left;int newHeight = bottom - top;boolean sizeChanged = (newWidth!= oldWidth) || (newHeight!= oldHeight);invalidate(sizeChanged);mLeft = left;mTop = top;mRight = right;mBottom = bottom;mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);}return changed;
}

setFrame方法用传进来的l、t、r、b这4个参数分别初始化mLeft、mTop、mRight、mBottom这4个值,这样就确定了该View在父容器中的位置。在调用setFrame方法后,会调用onLayout方法:

// onLayout方法在View中是一个空实现
// 它在布局过程中用于确定子视图的位置,不同的View或ViewGroup会有不同的实现
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}

onLayout方法是一个空方法,这和onMeasure方法类似。确定位置时根据不同的控件有不同的实现,所以在View和ViewGroup中均没有实现onLayout方法。既然这样,我们下面就来查看LinearLayout的onLayout方法:

// 在LinearLayout中重写了onLayout方法
// 根据方向(垂直或水平)调用不同的布局方法
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {// 如果方向是垂直的,调用layoutVertical方法进行布局if (mOrientation == VERTICAL) {layoutVertical(l, t, r, b);} else {// 如果方向是水平的,调用layoutHorizontal方法进行布局(此处未列出layoutHorizontal方法)layoutHorizontal(l, t, r, b);}
}

与onMeasure方法类似,根据方向来调用不同的方法。这里仍旧查看垂直方向的layoutVertical方法,如下所示:

// layoutVertical方法用于垂直方向上排列LinearLayout的子视图
void layoutVertical(int left, int top, int right, int bottom) {// 获取LinearLayout的虚拟子视图数量(可能包括GONE状态的子视图)final int count = getVirtualChildCount();// 遍历每个子视图for (int i = 0; i < count; ++i) {// 获取当前索引的子视图final View child = getVirtualChildAt(i);// 如果子视图为空if (child == null) {// 累加测量空子视图的高度到childTop(此处measureNullChild方法未列出)childTop += measureNullChild(i);} else if (child.getVisibility()!= GONE) {// 如果子视图可见// 获取子视图测量后的宽度和高度final int childWidth = child.getMeasuredWidth();final int childHeight = child.getMeasuredHeight();// 获取子视图的布局参数(LinearLayout.LayoutParams类型)final LinearLayout.LayoutParams lp =(LinearLayout.LayoutParams) child.getLayoutParams();// 如果在当前子视图之前有分割线,累加分割线高度到childTopif (hasDividerBeforeChildAt(i)) {childTop += mDividerHeight;}// 累加子视图的上外边距到childTopchildTop += lp.topMargin;// 设置子视图的位置和大小(调用setChildFrame方法)setChildFrame(child, childLeft, childTop, childWidth, childHeight);// 累加子视图的高度和下外边距到childTopchildTop += childHeight + lp.bottomMargin;// 根据子视图情况可能跳过一些索引(此处getChildrenSkipCount方法未列出)i += getChildrenSkipCount(child, i);}}
}

这个方法会遍历子元素并调用setChildFrame方法。其中childTop值是不断累加的,这样子元素才会依次按照垂直方向一个接一个排列下去而不会是重叠的。接着看setChildFrame方法:

// setChildFrame方法用于设置子视图的位置和大小
private void setChildFrame(View child, int left, int top, int width, int height) {// 调用子视图的layout方法设置其位置和大小child.layout(left, top, left + width, top + height);
}

以下是对上述代码的注释:

View的draw流程

View的draw流程很简单,下面先来看看View的draw方法。官方注释清楚地说明了每一步的做法,它们分别是:

  1. 如果需要,则绘制背景。
  2. 保存当前canvas层。
  3. 绘制View的内容。
  4. 绘制子View。
  5. 如果需要,则绘制View的褪色边缘,这类似于阴影效果。
  6. 绘制装饰,比如滚动条。

其中第2步和第5步可以跳过,所以这里不做分析,重点分析其他步骤。

步骤1:绘制背景(drawBackground)

// 绘制背景的方法
private void drawBackground(Canvas canvas) {// 获取View的背景Drawable对象final Drawable background = mBackground;// 如果背景为空,直接返回,不进行绘制if (background == null) {return;}// 设置背景的边界(这里可能涉及到一些布局相关的边界设置,具体方法未展示)setBackgroundBounds();// 获取View在X轴方向的滚动偏移量final int scrollX = mScrollX;// 获取View在Y轴方向的滚动偏移量final int scrollY = mScrollY;// 如果滚动偏移量在X和Y方向都为0,直接在当前canvas上绘制背景if ((scrollX | scrollY) == 0) {background.draw(canvas);} else {// 如果有滚动偏移量,先将canvas按照滚动偏移量进行平移canvas.translate(scrollX, scrollY);// 在平移后的canvas上绘制背景background.draw(canvas);// 绘制完成后,将canvas平移回原来的位置canvas.translate(-scrollX, -scrollY);}
}

从上面代码注释1处可看出绘制背景考虑了偏移参数scrollX和scrollY。如果有偏移值不为0,则会在偏移后的canvas绘制背景。

步骤3:绘制View的内容

// onDraw方法是一个空实现,用于在自定义View时重写来绘制View自身的内容
protected void onDraw(Canvas canvas) {
}

步骤4:绘制子View

调用了 dispatchDraw方法,这个方法也是一个空实现,如下所示:

protected void dispatchDraw(Canvas canvas){
}

ViewGroup重写了这个方法,紧接着看看ViewGroup的 dispatchDraw方法:

// dispatchDraw方法用于分发绘制操作给子View
protected void dispatchDraw(Canvas canvas) {// 这里可能是一些初始化或准备工作(省略号表示未展示的代码)...// 遍历子Viewfor (int i = 0; i < childrenCount; i++) {// 处理一些可能的临时子View情况(具体逻辑根据mTransientIndices和mTransients相关操作确定)while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) {final View transientChild = mTransients.get(transientIndex);// 如果子View可见或者有动画,调用drawChild方法绘制子Viewif ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||transientChild.getAnimation()!= null) {more |= drawChild(canvas, transientChild, drawingTime);}transientIndex++;}// 获取当前索引的子Viewfinal View child = mChildren.get(i);// 如果子View可见或者有动画,调用drawChild方法绘制子Viewif ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE ||child.getAnimation()!= null) {more |= drawChild(canvas, child, drawingTime);}}// 这里可能是一些后续处理或清理工作(省略号表示未展示的代码)...
}

源码****很长,这里截取了关键的部分,在dispatchDraw方法中对子类View进行遍历,并调用drawChild方法:

// drawChild方法用于绘制单个子View,它调用子View的draw方法
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {return child.draw(canvas, this, drawingTime);
}

这里主要调用了View的draw方法。代码如下所示:

// View的draw方法,用于实际绘制操作
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {// 这里可能是一些绘制相关的准备工作或判断(省略号表示未展示的代码)...// 如果不使用绘图缓存进行绘制if (!drawWithDrawingCache) {// 如果使用渲染节点进行绘制if (drawWithRenderNode) {// 清除一些标记mPrivateFlags &= ~PFLAG_DIRTY_MASK;// 使用DisplayListCanvas绘制渲染节点((DisplayListCanvas) canvas).drawRenderNode(renderNode);} else {// 清除一些标记mPrivateFlags &= ~PFLAG_DIRTY_MASK;// 调用dispatchDraw方法进行绘制(可能会递归调用到子View的绘制)dispatchDraw(canvas);}} else {// 清除一些脏标记mPrivateFlags &= ~PFLAG_DIRTY_MASK;// 直接调用draw方法进行绘制(可能会有不同的绘制逻辑,取决于是否使用缓存)draw(canvas);}// 这里可能是一些绘制完成后的处理工作(省略号表示未展示的代码)...
}

源码****很长,我们挑重点的看。在上面代码注释1处判断是否有缓存,如果没有则正常绘制,如果有则利用缓存显示。

步骤6:绘制装饰

// onDrawForeground方法用于绘制View的前景装饰
public void onDrawForeground(Canvas canvas) {// 绘制滚动指示器drawScrollIndicators(canvas);// 绘制滚动条drawScrollBars(canvas);// 获取前景装饰的Drawable对象final Drawable foregroundInfo = mForegroundInfo;// 如果前景装饰不为空,绘制前景装饰if (foregroundInfo!= null) {mForegroundInfo.draw(canvas);}
}
       // 清除一些标记mPrivateFlags &= ~PFLAG_DIRTY_MASK;// 使用DisplayListCanvas绘制渲染节点((DisplayListCanvas) canvas).drawRenderNode(renderNode);} else {// 清除一些标记mPrivateFlags &= ~PFLAG_DIRTY_MASK;// 调用dispatchDraw方法进行绘制(可能会递归调用到子View的绘制)dispatchDraw(canvas);}
} else {// 清除一些脏标记mPrivateFlags &= ~PFLAG_DIRTY_MASK;// 直接调用draw方法进行绘制(可能会有不同的绘制逻辑,取决于是否使用缓存)draw(canvas);
}
// 这里可能是一些绘制完成后的处理工作(省略号表示未展示的代码)


}


**源码****很长,我们挑重点的看。在上面代码注释1处判断是否有缓存,如果没有则正常绘制,如果有则利用缓存显示。****步骤6:绘制装饰**```Java
// onDrawForeground方法用于绘制View的前景装饰
public void onDrawForeground(Canvas canvas) {// 绘制滚动指示器drawScrollIndicators(canvas);// 绘制滚动条drawScrollBars(canvas);// 获取前景装饰的Drawable对象final Drawable foregroundInfo = mForegroundInfo;// 如果前景装饰不为空,绘制前景装饰if (foregroundInfo!= null) {mForegroundInfo.draw(canvas);}
}

这些代码详细描述了Android中View在绘制过程中的各个步骤和相关方法的实现逻辑,包括背景绘制、自身内容绘制、子View绘制和前景装饰绘制等操作。

相关文章:

【Android】View的工作流程

View的工作流程 开始了解一下View的工作流程&#xff0c;就是measure、layout和draw。measure用来测量View的宽高&#xff0c;layout用来确定View的位置&#xff0c;draw则用来绘制View。这一讲我们来看看measure流程&#xff0c;measure流程分为View的measure流程和ViewGroup…...

Linux基础指令

使用 tab 键补全 我们敲的所有的 Linux 命令 , 都可以使用 tab 键来尝试补全 , 加快效率 . 使用 ctrl c 重新输入 如果命令或者目录敲错了 , 可以 ctrl c 取消当前的命令 . ls &#xff1a;列出当前目录中的文件和子目录 语法 &#xff1a; ls [ 选项 ] [ 目录或文…...

Gemini 2.0 Flash重磅发布:多模态AI大模型,赋能实时交互与智能助手新体验

点击访问 chatTools 免费体验GPT最新模型&#xff0c;包括o1推理模型、GPT4o、Claude、Gemini等模型&#xff01; 在AI领域竞争日益激烈的今天&#xff0c;谷歌再次亮剑&#xff0c;推出了新一代至强AI大模型——Gemini 2.0 Flash。这款模型不仅具备强大的多模态输入输出能力&a…...

项目十二 杜甫作品问卷

【项目目标】 理解网格系统的原理。理解媒体查询的工作原理。【項目内容】 使用网格系统进行响应式网页设计。运用媒体查询对不同类型的设备应用不同的样式。【项目步骤】 Bootstrap 框架资源既可以直接从 CDN 服务商服务器中引入,也可以加入本地素材文件夹中给出的资…...

7_Sass Introspection 函数 --[CSS预处理]

Sass 的 Introspection 函数允许开发者检查和操作样式表的内部结构&#xff0c;包括选择器、属性、值等。这些函数提供了对编译过程中 Sass 文件内容的深入访问能力&#xff0c;使得更复杂的逻辑处理成为可能。以下是一些常用的 Sass Introspection 函数及其用法示例&#xff1…...

Qt:Q_GLOBAL_STATIC实现单例(附带单例使用和内存管理)

转载 https://blog.csdn.net/m0_71489826/article/details/142288179 前言 本文主要写Q_GLOBAL_STATIC实现单例以及单例的释放&#xff0c;网上很多教程只有单例的创建&#xff0c;但是并没有告诉我们单例的内存管理&#xff0c;这就很头疼。 正文 使用 Qt 的 Q_GLOBAL_STA…...

HTML/CSS总结

HTML 1.1 标题标签h 为了使网页更具有语义化&#xff0c;我们经常会在页面中用到标题标签&#xff0c;HTML提供了6个等级的标题&#xff0c;即 标题标签语义&#xff1a; 作为标题使用&#xff0c;并且依据重要性递减 其基本语法格式如下&#xff1a; <h1> 标题文本…...

字符串性能对比

效率(1) : String.indexOf与String.contains效率测试_string contains效率-CSDN博客 结论是前者效率高&#xff0c;源码里面conatins是使用indexof 在jdk8中contains直接调用的indexOf(其他版本没有验证),所以要说效率来说肯定是indexOf高,但contains也就多了一层方法栈,so 什…...

【漫话机器学习系列】005.神经网络的结构(architecture on the neural network)

神经网络&#xff08;Neural Network&#xff09;是一种模拟人脑神经系统的计算模型&#xff0c;由大量相互连接的神经元&#xff08;节点&#xff09;组成&#xff0c;广泛应用于深度学习和机器学习领域。以下是神经网络的基本结构及关键组成部分。 1. 神经网络的基本组成 一…...

关卡选择与布局器

unity布局管理器 使用unity布局管理器轻松对关卡选择进行布局。 实现过程 准备普通按钮button设置字体和对应的sprite设置父gameobject&#xff08;levelbase&#xff09; 再创建UI.image&#xff08;selectbackground&#xff09;布局背景和大小gameobject&#xff08;grid…...

数据分析实战—房价特征关系

1.实战内容 &#xff08;1&#xff09; 读取房价特征关系表&#xff08;house_price.npz&#xff09;绘制离地铁站的距离与单位面积的房价的散点图&#xff0c;并对其进行分析&#xff1b; import pandas as pd import numpy as np import warnings warnings.filterwarnings(&…...

@ResponseBody详解

ResponseBody 是 Spring Framework 中的一种注解&#xff0c;用于表示返回的内容应该直接写入 HTTP 响应体&#xff0c;而不是通过视图解析器来渲染一个视图&#xff08;如 JSP 页&#xff09;。当你在控制器的方法上使用 ResponseBody 时&#xff0c;Spring 会将方法的返回值直…...

Harmony Next开发通过bindSheet绑定半模态窗口

示例概述 Harmony Next开发通过bindSheet绑定半模态窗口 知识点 半模态窗口父子组件传值 组件 LoginComponent Component struct LoginComponent {// Prop 父子单项绑定值Prop message:string // Link 父子双向绑定值Link userName:stringLink password:stringLink isSh…...

Redis--高并发分布式结构

目录 一、引言 二、redis 1.什么是redis&#xff1f; 三、基础概念 1.什么是分布式&#xff1f; 2.应用服务和数据库服务分离 3.负载均衡 4.分库分表 5.微服务架构 四、总结 一、引言 本篇文章就简单介绍一下什么是redis&#xff0c;以及一些关于高并发和分布式结构的…...

Day38 动态规划part06

322. 零钱兑换 如果求组合数就是外层for循环遍历物品,内层for遍历背包。 如果求排列数就是外层for遍历背包,内层for循环遍历物品。 这句话结合本题 大家要好好理解。 视频讲解:动态规划之完全背包,装满背包最少的物品件数是多少?| LeetCode:322.零钱兑换_哔哩哔哩_bilib…...

1.1 类型(types)

源码 types.rs文件源码&#xff1a; use euclid::{Point2D, Vector2D};//引用外部泛型/// 绘图中所有事物所使用的笛卡尔坐标系。The cartesian coordinate system used by everything in a drawing. #[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd)] pub enu…...

PyTorch如何通过 torch.unbind 和torch.stack动态调整张量的维度顺序

笔者一篇博客PyTorch 的 torch.unbind 函数详解与进阶应用&#xff1a;中英双语中有一个例子如下&#xff1a; # 创建一个 3x2x2 的三维张量 x torch.tensor([[[1, 2], [3, 4]],[[5, 6], [7, 8]],[[9, 10], [11, 12]]])# 第一步&#xff1a;沿第 0 维分解为 3 个 2x2 张量 un…...

在 Node.js 中安装和使用 TensorFlow.js 的完整指南

在 Node.js 中安装和使用 TensorFlow.js 的完整指南 简介 TensorFlow.js 是一个开源的机器学习库&#xff0c;它允许在 JavaScript 环境中进行机器学习模型的开发和训练。本文将介绍如何在 Node.js 环境中安装和配置 TensorFlow.js。 环境准备 在开始之前&#xff0c;请确保…...

ubuntu 下如何查看用户的最近登录的时间

在 Ubuntu 系统下&#xff0c;可以通过以下几种方式查看用户最近的登录时间&#xff1a; 方法 1: 使用 last 命令 last 命令会显示系统上用户的登录记录&#xff0c;包括时间和来源。 last 用户名 例如&#xff0c;要查看用户 test 的最近登录记录&#xff1a; last test …...

Linux下调试工具:gdb

Windows和Linux下的调试有区别吗&#xff1f; 调试思路上一定是一样的&#xff1b;调试的操作方式有差别(Linux命令行调试&#xff0c;Windows窗口) 1.准备工作&#xff1a; 默认情况下&#xff0c;gdb无法进行对现在发布的程序进行调试(debug / release)。在Linux下用gcc编译…...

metagpt 多智能体系统

metagpt 多智能体系统 代码1. 动作及角色定义2. 主函数 代码解释1. 导入模块&#xff1a;2. 环境设置&#xff1a;3. 定义行动&#xff08;Action&#xff09;&#xff1a;4. 定义角色&#xff08;Role&#xff09;&#xff1a;5. 学生和老师的行为&#xff1a;6. 主函数&#…...

Python中opencv的一些函数及应用

Sobel 算子函数 功能&#xff1a; Sobel 算子用于计算图像的梯度&#xff08;变化率&#xff09;&#xff0c;常用于边缘检测。它通过对图像应用一个基于一阶导数的滤波器来强调图像中的边缘部分&#xff0c;特别是水平和垂直方向上的边缘。通过计算图像的梯度&#xff0c;可以…...

泷羽sec学习打卡-brupsuite8伪造IP和爬虫审计

声明 学习视频来自B站UP主 泷羽sec,如涉及侵权马上删除文章 笔记的只是方便各位师傅学习知识,以下网站只涉及学习内容,其他的都 与本人无关,切莫逾越法律红线,否则后果自负 关于brupsuite的那些事儿-Brup-FaskIP 伪造IP配置环境brupsuite导入配置1、扩展中先配置python环境2、安…...

麒麟信安推出支持信创PC的新一代云桌面方案,助力政务信创高效安全运维

12月11日&#xff0c;在第二届国家新一代自主安全计算系统产业集群融通生态大会上&#xff0c;麒麟信安发布了支持信创PC的新一代云桌面方案&#xff0c;该方案是基于国际TCI架构实现国产PC机云化纳管在国内的首次发布&#xff0c;并与银河麒麟桌面操作系统、长城国产PC整机实现…...

【我的开源】ESCurlGen 一款 ElasticSearch curl 命令生成器

由于经常编写复杂的 Elasticsearch 查询&#xff0c;并通过代码生成查询条件&#xff0c;我发现每次使用 curl 请求 Elasticsearch 时&#xff0c;手动复制配置信息并构建 curl 命令的过程非常繁琐&#xff0c;尤其是在管理多个环境的情况下更为不便。因此&#xff0c;我利用 A…...

基于ESP32的桌面小屏幕实战[4]:硬件设计之PCB Layout

1. PCB Layout 步骤 生成PCB 确定PCB layout规范 绘制板框尺寸 布局 布局规范&#xff1a; 按电气性能合理分区&#xff0c;一般分为&#xff1a;数字电路区&#xff08;即怕干扰、又产生干扰&#xff09;、模拟电路区(怕干扰)、功率驱动区&#xff08;干扰源&#xff09;&a…...

(css)element中el-select下拉框整体样式修改

(css)element中el-select下拉框整体样式修改 重点代码&#xff08;颜色可行修改&#xff09; // 修改input默认值颜色 兼容其它主流浏览器 /deep/ input::-webkit-input-placeholder {color: rgba(255, 255, 255, 0.50); } /deep/ input::-moz-input-placeholder {color: rgba…...

金融分析-Transformer模型(基础理论)

Transformer模型 1.基本原理 transformer的core是注意力机制&#xff0c;其本质就是编码器-解码器。他可以通过多个编码器进行编码&#xff0c;再把编码完的结果输出给解码器进行解码&#xff0c;然后得到最终的output。 1.1编码器 数据在编码器中会经过一个self-attention的…...

Parcel 常用插件:增强功能与性能的最佳选择

前言 Parcel 是一个现代化的零配置应用打包工具&#xff0c;旨在简化开发流程并提高效率。通过其智能的默认配置和丰富的插件生态系统&#xff0c;Parcel 使得开发者能够轻松应对各种构建需求。虽然 Parcel 在大多数情况下可以开箱即用&#xff0c;但为了满足特定的项目需求&a…...

Vite 与 Webpack 的区别

在前端开发中&#xff0c;构建工具是不可或缺的&#xff0c;Webpack 和 Vite 是当前最流行的选择之一。尽管它们的目标相似&#xff0c;但在实现方式和开发体验上却有显著差异。本文将探讨 Vite 和 Webpack 的主要区别&#xff0c;以便于根据项目需求选择合适的工具。 1. 构建…...

服务器ubuntu重装系统后将原来的用户进行关联

服务器ubuntu重装系统后将原来的用户数据进行关联 关联用户到已存在目录 sudo useradd user_name -m -s /bin/bash -d /home***/name添加sudo权限 vim /etc/sudoers# 文件末尾添加 user_name ALL(ALL:ALL) ALL更改拥有者 sudo chown -R user_name:user_name /home***/na…...

Python模块导入:import与from...import的深度解析

Python模块导入&#xff1a;import与from…import的深度解析 在Python编程中&#xff0c;模块的导入是组织和复用代码的关键环节。Python提供了import和from...import两种常见的模块导入方式&#xff0c;它们在使用方式、命名空间管理、可读性、内存使用等方面各有特点&#x…...

ROS2-humble中指定OpenCV版本进行开发

本地是Ubuntu22.04系统&#xff0c;安装了ROS2-humble&#xff0c;看了下humble自带的OpenCV版本4.5.4&#xff0c;由于DNN模块读取.onnx格式的模型要用OpenCV4.7及以上的版本&#xff0c;于是编译了4.10.0的OpenCV&#xff0c;但开发ROS2节点时&#xff0c;虽然CMake中已经指定…...

如何在 Ubuntu 22.04 上使用 vnStat 监控网络流量

简介 vnStat是一个免费的、开源的、基于控制台的Linux操作系统网络流量监控工具。通过vnStat&#xff0c;你可以在不同的时间段监控网络统计数据。它简单、轻量级&#xff0c;并且消耗的系统资源很小。vnStat允许你按小时、日、月、周和日生成网络流量数据。本教程将向你展示如…...

为什么要使用数据仓库?

现状和需求 大量的企业经营性数据&#xff08;订单&#xff0c;库存&#xff0c;原料&#xff0c;付款等&#xff09;在企业的业务运营系统以及其后台的(事务型)数据库中产生的。 企业的决策者需要及时地对这些数据进行归类分析&#xff0c;从中获得企业运营的各种业务特征&a…...

《宇宙机器人》提示错误弹窗“找不到d3dx9_43.dll”是什么原因?“d3dx9_43.dll缺失”怎么解决?

电脑游戏运行时常见问题解析&#xff1a;《宇宙机器人》提示“找不到d3dx9_43.dll”的解决之道 TGA2024落幕&#xff0c;年度最佳游戏——《宇宙机器人》&#xff0c;作为一名在软件开发领域深耕多年的从业者&#xff0c;我深知电脑游戏在运行过程中可能会遇到的各种挑战&…...

DAC数据手册中专有名词TERMINOLOGY 讲解

DAC数据手册中TERMINOLOGY专有名词 讲解 Relative Accuracy or Integral Nonlinearity (INL)&#xff1a;相对精度 或 积分非线性LSB&#xff08;Least Significant Bit&#xff09;&#xff1a;最小有效位 Differential Nonlinearity (DNL)&#xff1a;差分非线性单调性DNL很重…...

Java中基于TCP的Socket编程

一、概述 Socket&#xff08;套接字&#xff09;是网络通信的一种机制&#xff0c;允许不同主机之间的进程进行通信。在Java中&#xff0c;Socket支持TCP&#xff08;传输控制协议&#xff09;和UDP&#xff08;用户数据报协议&#xff09;。 1、TCP协议介绍 TCP协议在通信之…...

详解二叉树

一、树的概念和结构 树是⼀种非线性的数据结构&#xff0c;它是由 n&#xff08;n>0&#xff09; 个有限结点组成⼀个具有层次关系的集合。把它叫做 树是因为它看起来像⼀棵倒挂的树&#xff0c;也就是说它是根朝上&#xff0c;而叶朝下的。 • 有⼀个特殊的结点&#xff0…...

Kafka Connect

根据您提供的错误信息和日志分析&#xff0c;这里是一些针对 Kafka Connect、协调者不可用、网络客户端连接问题、消费者组偏移量提交失败以及消费者组协调者发现问题的具体解决建议&#xff1a; 检查 Kafka 集群状态&#xff1a; 使用 kafka-broker-api-versions.sh 检查每个 …...

Mac charles报错 invalid keystore format

1.问题说明 打开charles会有一个 invalid keystore format的提示&#xff0c;更隐藏的影响&#xff0c;是安卓设备安装了凭证&#xff0c;但是charles仍然抓不到包&#xff0c;会展示unknow&#xff0c;即使是charles配置好了ssl proxy setting&#xff0c;并且mac信任了char…...

Leetcode 409. Longest Palindrome

Problem Given a string s which consists of lowercase or uppercase letters, return the length of the longest palindrome that can be built with those letters. Letters are case sensitive, for example, “Aa” is not considered a palindrome. Algorithm Count …...

事件代理详解

一、基本概念 事件代理&#xff08;Event Delegation&#xff09;&#xff0c;也称为事件委托&#xff0c;是一种在 JavaScript 中处理事件的技术。它基于 DOM&#xff08;文档对象模型&#xff09;事件流的原理&#xff0c;利用事件冒泡机制&#xff0c;将一个元素&#xff0…...

代码随想录算法训练营第三天 | 链表理论基础 | 203.移除链表元素

感觉上是可以轻松完成的&#xff0c;因为对链接的结构&#xff0c;元素的删除过程心里明镜似的 实际上四处跑气 结构体的初始化好像完全忘掉了&#xff0c;用malloc折腾半天&#xff0c;忘记了用new&#xff0c;真想扇自己嘴巴子到飞起删除后写一个函数&#xff0c;把链表打印…...

专业140+总分410+浙江大学842信号系统与数字电路考研经验浙大电子信息与通信工程,真题,大纲,参考书。

考研落幕&#xff0c;本人本中游211&#xff0c;如愿以偿考入浙江大学&#xff0c;专业课842信号系统与数字电路140&#xff0c;总分410&#xff0c;和考前多次模考预期差距不大&#xff08;建议大家平时做好定期模考测试&#xff0c;直接从实战分数中&#xff0c;找到复习的脉…...

Python-pptx库简介

目录 一、Python-pptx 库概述 二、安装 Python-pptx 库 三、创建演示文稿 四、添加文本内容 五、添加形状 六、添加图片 七、添加图表 八、保存演示文稿 九、示例演示文稿 十、总结 在Python编程中&#xff0c;处理演示文稿是一项常见的任务。Python-pptx库为我们提供…...

电子应用设计方案-52:智能电子相框系统方案设计

智能电子相框系统方案设计 一、引言 智能电子相框作为一种能够展示数字照片和多媒体内容的设备&#xff0c;为用户提供了便捷、个性化的照片展示方式。本方案旨在设计一款功能丰富、用户体验良好的智能电子相框系统。 二、系统概述 1. 系统目标 - 高质量显示照片和视频&#…...

mac 安装CosyVoice (cpu版本)

CosyVoice 介绍 CosyVoice 是阿里研发的一个tts大模型 官方项目地址&#xff1a;https://github.com/FunAudioLLM/CosyVoice.git 下载项目&#xff08;非官方&#xff09; git clone --recursive https://github.com/v3ucn/CosyVoice_for_MacOs.git 进入项目 cd CosyVoic…...

mysql命令行界面(黑框)的登录

文章目录 开启关闭服务报错登录mysql退出mysql数据据database在电脑中的存放位置删除数据库语句 drop注意 cmd用管理员打开 开启关闭服务 报错 我有这个报错&#xff0c;但是使用没什么影响 登录mysql root替换成自己的用户名 退出mysql exit 数据据database在电脑中的…...

Git 快速入门

Git 是什么&#xff1f; Git 是一个分布式版本控制系统四大区域&#xff1a; 工作区&#xff1a;项目文件的当前状态&#xff0c;即本地目录。暂存区&#xff1a;保存将要提交的文件快照&#xff0c;是一个中间层&#xff0c;使用git add将文件添加到暂存区。本地仓库&#xf…...