Android视图绘制流程完全解析(二)
转载:http://blog.csdn.net/guolin_blog/article/details/16330267
https://segmentfault.com/a/1190000004622988
任何一个视图都不可能凭空突然出现在屏幕上,它们都是要经过非常科学的绘制流程后才能显示出来的。每一个视图的绘制过程都必须经历三个最主要的阶段,即onMeasure()、onLayout()和onDraw(),下面我们逐个对这三个阶段展开进行探讨。
onMeasure()
measure是测量的意思,那么onMeasure()方法顾名思义就是用于测量视图的大小的。
View系统的绘制流程会从ViewRoot的[1.1]performTraversals()方法中开始,在其内部调用View的[1.4]measure()方法。
[1.4]measure()方法接收两个参数,widthMeasureSpec和heightMeasureSpec,这两个值分别用于确定视图的宽度和高度的规格和大小。
一、MeasureSpec的值由specSize和specMode共同组成的,其中specSize记录的是大小,specMode记录的是规格。
specMode一共有三种类型,如下所示:
1、 EXACTLY
父容器已经检测出View所需要的精确大小,这个时候View的最终大小就是SpecSize所指定的值。它对应于LayoutParams中的match_parent和具体的数值这两种模式。
2、AT_MOST
父容器指定了一个可用大小即SpecSize,View的大小不能大于这个值,具体是什么值要看不同View的具体实现。它对应于LayoutParams中的wrap_content。
3、UNSPECIFIED
父容器不对View有任何限制,要多大给多大,这种情况一般用于系统内部,表示一种测量状态。
二、widthMeasureSpec和heightMeasureSpec,通常情况下,这两个值都是由父视图经过计算后传递给子视图的,说明父视图会在一定程度上决定子视图的大小。
但是最外层的根视图的widthMeasureSpec和heightMeasureSpec是使用[1.2]getRootMeasureSpec()得到的。
这就需要去分析ViewRoot中的源码了,观察[1.1]performTraversals()方法可以发现如下代码:
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
这里调用了[1.2]getRootMeasureSpec()方法去获取widthMeasureSpec和heightMeasureSpec的值,注意方法中传入的参数,其中lp.width和lp.height在创建ViewGroup实例的时候就被赋值了,它们都等于MATCH_PARENT。
然后看下[1.2]getRootMeasureSpec()方法中的代码,如下所示:
private 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;
}
这里使用了[1.3]MeasureSpec.makeMeasureSpec()方法来组装一个MeasureSpec,
- 当rootDimension等于MATCH_PARENT的时候,MeasureSpec的specMode就等于EXACTLY,
- 当rootDimension等于WRAP_CONTENT的时候,MeasureSpec的specMode就等于AT_MOST。
并且MATCH_PARENT和WRAP_CONTENT时的specSize都是等于windowSize的,也就意味着根视图总是会充满全屏的。
三、接下来我们看下View的[1.4]measure()方法里面的代码吧,如下所示:
public final void measure(int widthMeasureSpec, int heightMeasureSpec) { if ((mPrivateFlags & FORCE_LAYOUT) == FORCE_LAYOUT || widthMeasureSpec != mOldWidthMeasureSpec || heightMeasureSpec != mOldHeightMeasureSpec) { mPrivateFlags &= ~MEASURED_DIMENSION_SET; if (ViewDebug.TRACE_HIERARCHY) { ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_MEASURE); } onMeasure(widthMeasureSpec, heightMeasureSpec); if ((mPrivateFlags & MEASURED_DIMENSION_SET) != MEASURED_DIMENSION_SET) { throw new IllegalStateException("onMeasure() did not set the" + " measured dimension by calling" + " setMeasuredDimension()"); } mPrivateFlags |= LAYOUT_REQUIRED; } mOldWidthMeasureSpec = widthMeasureSpec; mOldHeightMeasureSpec = heightMeasureSpec;
}
注意观察,[1.4]measure()这个方法是final的,因此我们无法在子类中去重写这个方法,说明Android是不允许我们改变View的measure框架的。
然后在第9行调用了[1.5]onMeasure()方法,这里才是真正去测量并设置View大小的地方。
[1.5]onMeasure()默认会调用[1.6]getDefaultSize()方法来获取视图的大小,如下所示:
public static int getDefaultSize(int size, int measureSpec) { int result = size; int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec); switch (specMode) { case MeasureSpec.UNSPECIFIED: result = size; break; case MeasureSpec.AT_MOST: case MeasureSpec.EXACTLY: result = specSize; break; } return result;
}
这里传入的measureSpec是一直从[1.4]measure()方法中传递过来的。
调用MeasureSpec.getMode()方法可以解析出specMode,
调用MeasureSpec.getSize()方法可以解析出specSize。判断specMode,如果等于AT_MOST或EXACTLY就返回specSize,这也是系统默认的行为。
在[1.5]onMeasure()方法中调用[1.7]setMeasuredDimension()方法来设定测量出的大小,这样一次measure过程就结束了。
测量子布局
当然,一个界面的展示可能会涉及到很多次的measure,因为一个布局中一般都会包含多个子视图,每个视图都需要经历一次measure过程。
ViewGroup中定义了一个[1.8]measureChildren()方法来去测量子视图的大小,如下所示:
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); } }
}
- 遍历当前布局下的所有子视图
- 逐个调用[1.9]measureChild()方法来测量相应子视图的大小
[1.9]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);
}
- 在第4行和第6行分别调用了[1.10]getChildMeasureSpec()方法来去计算子视图的MeasureSpec,计算的依据就是布局文件中定义的MATCH_PARENT、WRAP_CONTENT等值,这个方法的内部细节就不再贴出。
- 在第8行调用子视图的[1.4]measure()方法,并把计算出的MeasureSpec传递进去,之后的流程就和前面所介绍的一样了。
重写测量
[1.5]onMeasure()方法是可以重写的,也就是说,如果你不想使用系统默认的测量方式,可以按照自己的意愿进行定制,比如:
public class MyView extends View { @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(200, 200); }
}
这样的话就把View默认的测量流程覆盖掉了,不管在布局文件中定义MyView这个视图的大小是多少,最终在界面上显示的大小都将会是200*200。
需要注意的是,在[1.7]setMeasuredDimension()方法调用之后,我们才能使用getMeasuredWidth()和getMeasuredHeight()来获取视图测量出的宽高,以此之前调用这两个方法得到的值都会是0。
由此可见,视图大小的控制是由父视图、布局文件、以及视图本身共同完成的,
- 父视图会提供给子视图参考的大小,
- 开发人员可以在XML文件中指定视图的大小,
- 视图本身会对最终的大小进行排版。
到此为止,我们就把视图绘制流程的第一阶段分析完了。
onLayout()
measure过程结束后,视图的大小就已经测量好了,接下来就是layout的过程了。正如其名字所描述的一样,这个方法是用于给视图进行布局的,也就是确定视图的位置。
ViewRoot的[1.1]performTraversals()方法会在measure结束后继续执行,并调用View的[2.1]layout()方法来执行此过程,如下所示:
host.layout(0, 0, host.mMeasuredWidth, host.mMeasuredHeight);
[2.1]layout()方法接收四个参数,分别代表着左、上、右、下的坐标,当然这个坐标是相对于当前视图的父视图而言的。
这里还把刚才测量出的宽度和高度传到了[2.1]layout()方法中。
那么我们来看下[2.1]layout()方法中的代码:
public void layout(int l, int t, int r, int b) { int oldL = mLeft; int oldT = mTop; int oldB = mBottom; int oldR = mRight; boolean changed = setFrame(l, t, r, b); if (changed || (mPrivateFlags & LAYOUT_REQUIRED) == LAYOUT_REQUIRED) { if (ViewDebug.TRACE_HIERARCHY) { ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_LAYOUT); } onLayout(changed, l, t, r, b); mPrivateFlags &= ~LAYOUT_REQUIRED; if (mOnLayoutChangeListeners != null) { ArrayList<OnLayoutChangeListener> listenersCopy = (ArrayList<OnLayoutChangeListener>) mOnLayoutChangeListeners.clone(); int numListeners = listenersCopy.size(); for (int i = 0; i < numListeners; ++i) { listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB); } } } mPrivateFlags &= ~FORCE_LAYOUT;
}
在[2.1]layout()方法中,
- 首先会调用[2.1.1]setFrame()方法
- 判断视图的大小是否发生过变化,以确定有没有必要对当前的视图进行重绘
- 同时还会在这里把传递过来的四个参数分别赋值给mLeft、mTop、mRight和mBottom这几个变量。
- 接下来会在第11行调用[2.2]onLayout()方法,正如onMeasure()方法中的默认行为一样,
也许你已经迫不及待地想知道onLayout()方法中的默认行为是什么样的了。进入[2.2]onLayout()方法,咦?怎么这是个空方法,一行代码都没有?!
没错,View中的[2.2]onLayout()方法就是一个空方法,因为[2.2]onLayout()过程是为了确定视图在布局中所在的位置,而这个操作应该是由布局来完成的,即父视图决定子视图的显示位置。
既然如此,我们来看下ViewGroup中的[2.2]onLayout()方法是怎么写的吧,代码如下:
protected abstract void onLayout(boolean changed, int l, int t, int r, int b);
可以看到,ViewGroup中的[2.2]onLayout()方法竟然是一个抽象方法,这就意味着所有ViewGroup的子类都必须重写这个方法。
没错,像LinearLayout、RelativeLayout等布局,都是重写了这个方法,然后在内部按照各自的规则对子视图进行布局的。
由于LinearLayout和RelativeLayout的布局规则都比较复杂,就不单独拿出来进行分析了。
示例
这里我们尝试自定义一个布局,借此来更深刻地理解[2.2]onLayout()的过程。
public class SimpleLayout extends ViewGroup { public SimpleLayout(Context context, AttributeSet attrs) { super(context, attrs); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); if (getChildCount() > 0) { View childView = getChildAt(0); measureChild(childView, widthMeasureSpec, heightMeasureSpec); } } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { if (getChildCount() > 0) { View childView = getChildAt(0); childView.layout(0, 0, childView.getMeasuredWidth(), childView.getMeasuredHeight()); } } }
你已经知道,[1.5]onMeasure()方法会在[2.2]onLayout()方法之前调用,
- 因此这里在[1.5]onMeasure()方法中判断SimpleLayout中是否有包含一个子视图,如果有的话就调用[1.8]measureChild()方法来测量出子视图的大小。
- 在[2.2]onLayout()方法中同样判断SimpleLayout是否有包含一个子视图
- 然后调用这个子视图的[2.1]layout()方法来确定它在SimpleLayout布局中的位置。
- 这里传入的四个参数依次是0、0、childView.getMeasuredWidth()和childView.getMeasuredHeight(),分别代表着子视图在SimpleLayout中左上右下四个点的坐标。
其中,调用childView.getMeasuredWidth()和childView.getMeasuredHeight()方法得到的值就是在onMeasure()方法中测量出的宽和高。
这样就已经把SimpleLayout这个布局定义好了,下面就是在XML文件中使用它了,如下所示:
<com.example.viewtest.SimpleLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" > <ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/ic_launcher" /> </com.example.viewtest.SimpleLayout>
我们能够像使用普通的布局文件一样使用SimpleLayout,只是注意它只能包含一个子视图,多余的子视图会被舍弃掉。这里SimpleLayout中包含了一个ImageView,并且ImageView的宽高都是wrap_content。现在运行一下程序,结果如下图所示:
OK!ImageView成功已经显示出来了,并且显示的位置也正是我们所期望的。
如果你想改变ImageView显示的位置,只需要改变childView.[2.1]layout()方法的四个参数就行了。
在onLayout()过程结束后,我们就可以调用getWidth()方法和getHeight()方法来获取视图的宽高了。
getWidth()方法和getMeasureWidth()方法到底有什么区别呢?它们的值好像永远都是相同的。其实它们的值之所以会相同基本都是因为布局设计者的编码习惯非常好,实际上它们之间的差别还是挺大的。
- getMeasureWidth()方法在[1.4]measure()过程结束后就可以获取到了,而getWidth()方法要在[2.1]layout()过程结束后才能获取到。
- getMeasureWidth()方法中的值是通过[1.7]setMeasuredDimension()方法来进行设置的,而getWidth()方法中的值则是通过视图右边的坐标减去左边的坐标计算出来的。
观察SimpleLayout中[2.2]onLayout()方法的代码,这里给子视图的[2.1]layout()方法传入的四个参数分别是0、0、childView.getMeasuredWidth()和childView.getMeasuredHeight(),因此getWidth()方法得到的值就是childView.getMeasuredWidth() - 0 = childView.getMeasuredWidth() ,所以此时getWidth()方法和getMeasuredWidth() 得到的值就是相同。
如果你将[2.2]onLayout()方法中的代码进行如下修改:
protected void onLayout(boolean changed, int l, int t, int r, int b) { if (getChildCount() > 0) { View childView = getChildAt(0); childView.layout(0, 0, 200, 200); }
}
这样getWidth()方法得到的值就是200 - 0 = 200,不会再和getMeasuredWidth()的值相同了
onDraw()
measure和layout的过程都结束后,接下来就进入到draw的过程了。同样,根据名字你就能够判断出,在这里才真正地开始对视图进行绘制。
- ViewRoot中的代码会继续执行并创建出一个Canvas对象
- 调用View的[3.1]draw()方法来执行具体的绘制工作。
[3.1]draw()方法内部的绘制过程总共可以分为六步,其中第二步和第五步在一般情况下很少用到,因此这里我们只分析简化后的绘制过程。代码如下所示:
public void draw(Canvas canvas) { if (ViewDebug.TRACE_HIERARCHY) { ViewDebug.trace(this, ViewDebug.HierarchyTraceType.DRAW); } final int privateFlags = mPrivateFlags; final boolean dirtyOpaque = (privateFlags & DIRTY_MASK) == DIRTY_OPAQUE && (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState); mPrivateFlags = (privateFlags & ~DIRTY_MASK) | DRAWN; // Step 1, draw the background, if needed int saveCount; if (!dirtyOpaque) { final Drawable background = mBGDrawable; if (background != null) { final int scrollX = mScrollX; final int scrollY = mScrollY; if (mBackgroundSizeChanged) { background.setBounds(0, 0, mRight - mLeft, mBottom - mTop); mBackgroundSizeChanged = false; } if ((scrollX | scrollY) == 0) { background.draw(canvas); } else { canvas.translate(scrollX, scrollY); background.draw(canvas); canvas.translate(-scrollX, -scrollY); } } } final int viewFlags = mViewFlags; boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0; boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0; if (!verticalEdges && !horizontalEdges) { // Step 3, draw the content if (!dirtyOpaque) onDraw(canvas); // Step 4, draw the children dispatchDraw(canvas); // Step 6, draw decorations (scrollbars) onDrawScrollBars(canvas); // we're done... return; }
}
第一步
从第9行代码开始的,这一步的作用是对视图的背景进行绘制。
- 这里会先得到一个mBGDrawable对象。
- 然后根据layout过程确定的视图位置来设置背景的绘制区域,
- 再调用[3.2]Drawable的draw()方法来完成背景的绘制工作。
这个mBGDrawable对象是从哪里来的呢?
其实就是在XML中通过android:background属性设置的图片或颜色。当然你也可以在代码中通过setBackgroundColor()、setBackgroundResource()等方法进行赋值。
第三步
在第34行执行的,这一步的作用是对视图的内容进行绘制。
- 这里去调用了一下[3.3]onDraw()方法
那么onDraw()方法里又写了什么代码呢?进去一看又是个空方法啊。
其实也可以理解,因为每个视图的内容部分肯定都是各不相同的,这部分的功能交给子类来去实现也是理所当然的。
第四步
这一步的作用是对当前视图的所有子视图进行绘制。
- 但如果当前的视图没有子视图,那么也就不需要进行绘制了。
- 发现[3.4]View中的dispatchDraw()方法又是一个空方法,而[3.5]ViewGroup的dispatchDraw()方法中就会有具体的绘制代码。
第六步
这一步的作用是对视图的滚动条进行绘制
你可能会奇怪,当前的视图又不一定是ListView或者ScrollView,为什么要绘制滚动条呢?
其实不管是Button也好,TextView也好,任何一个视图都是有滚动条的,只是一般情况下我们都没有让它显示出来而已。绘制滚动条的代码逻辑也比较复杂,这里就不再贴出来了,因为我们的重点是第三步过程。
通过以上流程分析,相信大家已经知道,View是不会帮我们绘制内容部分的,因此需要每个视图根据想要展示的内容来自行绘制。
如果你去观察TextView、ImageView等类的源码,你会发现它们都有重写[3.3]onDraw()这个方法,并且在里面执行了相当不少的绘制逻辑。
绘制的方式主要是借助Canvas这个类,它会作为参数传入到[3.3]onDraw()方法中,供给每个视图使用。Canvas这个类的用法非常丰富,基本可以把它当成一块画布,在上面绘制任意的东西,那么我们就来尝试一下吧。
示例
创建一个非常简单的视图,并且用Canvas随便绘制了一点东西,代码如下所示:
public class MyView extends View { private Paint mPaint; public MyView(Context context, AttributeSet attrs) { super(context, attrs); mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); } @Override protected void onDraw(Canvas canvas) { mPaint.setColor(Color.YELLOW); canvas.drawRect(0, 0, getWidth(), getHeight(), mPaint); mPaint.setColor(Color.BLUE); mPaint.setTextSize(20); String text = "Hello View"; canvas.drawText(text, 0, getHeight() / 2, mPaint); }
}
我们创建了一个自定义的MyView继承自View,并在MyView的构造函数中创建了一个Paint对象。
Paint就像是一个画笔一样,配合着Canvas就可以进行绘制了。
- 在[3.3]onDraw()方法中先是把画笔设置成黄色,
- 调用Canvas的drawRect()方法绘制一个矩形。
- 在把画笔设置成蓝色,并调整了一下文字的大小,
- 调用drawText()方法绘制了一段文字。
就这么简单,一个自定义的视图就已经写好了,现在可以在XML中加入这个视图,如下所示:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" > <com.example.viewtest.MyView android:layout_width="200dp" android:layout_height="100dp" /> </LinearLayout>
将MyView的宽度设置成200dp,高度设置成100dp,然后运行一下程序,结果如下图所示:
图中显示的内容也正是MyView这个视图的内容部分了。由于我们没给MyView设置背景,因此这里看不出来View自动绘制的背景效果。
当然了Canvas的用法还有很多很多,这里我不可能把Canvas的所有用法都列举出来,剩下的就要靠大家自行去研究和学习了。
关注我的公众号,轻松了解和学习更多技术
Android视图绘制流程完全解析(二)相关推荐
- Android视图绘制流程完全解析,带你一步步深入了解
Android LayoutInflater原理分析,带你一步步深入了解View(一) Android视图绘制流程完全解析,带你一步步深入了解View(二) Android视图状态及重绘流程分析,带你 ...
- Android视图绘制流程完全解析,带你一步步深入了解View(二)
在上一篇文章中,我带着大家一起剖析了一下LayoutInflater的工作原理,可以算是对View进行深入了解的第一步吧.那么本篇文章中,我们将继续对View进行深入探究,看一看它的绘制流程到底是什么 ...
- Android UI绘制流程分析(三)measure
源码版本Android 6.0 请参阅:http://androidxref.com/6.0.1_r10 本文目的是分析从Activity启动到走完绘制流程并显示在界面上的过程,在源码展示阶段为了使跟 ...
- android 绘图流程,Android View绘制流程
前言 不知道大家有没有想过一个问题,当启动一个Activity的时候,相应的XML布局文件中的View是如何显示到屏幕上的?有些同学会说是通过onMeasure().onLayout().onDraw ...
- Android View 绘制流程
前面讲到 Android View 加载流程,使用 LayoutInflater 将 xml 文件转变成 View ,但是还需要将 View 绘制出来,才能被用户看到,这一过程为绘制流程.由于 And ...
- Android RecyclerView 绘制流程及Recycler缓存
前言 RecyclerView源码一万多行,想全部读懂学会挺麻烦的,感兴趣的可以自己去瞅瞅,这篇文章重点来看下 RecyclerView是如何一步步将每一个 ItemView 显示到屏幕上,然后再分析 ...
- Android RecyclerView 绘制流程及Recycler缓存,Android开发者必看避坑指南
- mState.mIsMeasuring = false; if (mState.mLayoutStep == State.STEP_START) { dispatchLayoutStep1(); ...
- Android视图绘制逻辑与思想(一):attachToRoot到底有什么作用?
attachToRoot到底有什么作用? 前言(唠嗑) 前两天在路上瞎逛的时候,一只黄色的小狗突然跑到我身边,绕着我转圈圈.我一瞬间就被这只小狗狗给征服了,脑袋以超光速开始运转,甚至连给小狗狗名字都取 ...
- 【Android 应用开发】Android 图表绘制 achartengine 示例解析
作者 : 韩曙亮 转载请注明出处 : http://blog.csdn.net/shulianghan/article/details/38420197 一. AChartEngine 简介 1. 项 ...
最新文章
- ceph monitor----paxos算法1
- 内核层CS段描述符信息
- 32.ExtJS简单的动画效果
- HTML5画布如何设置线的样式?
- python相关linux_Python实现获取Linux系统基本信息
- GridView的 使用
- 洛谷 P1008 [NOIP1998 普及组] 三连击
- 数据结构之栈与递归的应用(汉诺塔递归解法)
- mysql事务最好别用_理解完这些基本上能解决面试中MySql的事务问题
- 推荐最近使用的一个APP
- Asp.net使用HttpResponse.Filter 实现简繁/繁简转换
- 好莱坞法则_人工智能去好莱坞
- nokia n9 android 4.4,再现新神机 诺基亚N9运行Android4.1系统
- 抖音上很火的3D立体动态相册实现代码!
- 软件开发过程反思——从需求分析到最后开发出来的软件
- Mandriva 2009 Spring PWP中3D桌面的使用
- 【Linux】ab命令实现网站性能压力测试
- redis源码学习-03_动态字符串SDS
- 【个人网页设计】简单大方
- 工作流系统理解(1)
热门文章
- 被繁杂的数据搞到头大?让 Google Cloud 大数据平台帮你实现快准狠!
- ar vr mr 计算机技术,VR技术是什么?AR、MR又是什么?
- PMP项目管理学习总结,建议收藏
- java面试题:编写java程序,随机生成n个m以内的加减法算式,形如a±b=c,其中n,m∈N+,a,b,c∈[0,m].请结合自身请款勾选作答的题目,然后在下面写出代(伪)码.
- 多卡并行训练遇到的问题
- 获取颜色值 抓取颜色值 获取颜色代码RGB
- property属性的使用
- Hexo博客中插入图片,在网页中无法显示:采用图床外链的方法
- Word全英文件怎么翻译
- python opencv 三维重建_三维重建 3D Reconstruction