转载请标明出处:http://blog.csdn.net/sk719887916/article/details/48443429,作者:skay

   

通过学习了AndroidUI之绘图机基础 知道view 的两个重要的方法:invalidate()和requestLayout(); 在那篇文章中我没有做原理性的细说,大家知道前者是请求重绘,后者是请求布局的,那么ta们之前有何联系和区别呢,比如:invalidate()会进行测量吗,会进行重新布局吗,requestLayout()只做请求布局吗,关于这些问题,今天我们来看下view的重绘过程。

今天的知识对view的树结构布局抽象思维相对要求高,不了解View在上层绘制过程的请阅读下AndroidUI之绘图机基础 的介绍。也需要有一定的自定义view的基础,在学习今天的知识是之前我们先了解下VIewRoot, ViewGroup,ViewManager,ViewParent的基本概念,所有和view的增加,移除,绘制,刷新,都是跟他们息息相关的。

 一 ViewGroup

Android 中View充当具体可见的最小的基本单元,视图组建,填充到父容器中,ViewGruop是一组view的集合,用于存放和管理View的大小和具体位置功能,它是为View的子类,其可以理解为Activity和Fragmengt的关系,其两者生命周期非常类似。安卓的五大布局都是ViewGroup的子类,一些常用控件都是View的子类。

那么 我们看下viewGroup是和今天知识有联系的的关键源码。通过他 希望能找到一些不知道的“秘密”。

EP1:

public abstract class ViewGroup extends View implements ViewParent, ViewManager {public ViewGroup(Context context) {super(context);initViewGroup();}public ViewGroup(Context context, AttributeSet attrs) {super(context, attrs);initViewGroup();initFromAttributes(context, attrs);}public ViewGroup(Context context, AttributeSet attrs, int defStyle) {super(context, attrs, defStyle);initViewGroup();initFromAttributes(context, attrs);}
}

以上类实现(EP1)我们可以知道其也是View的子类,实现了 ViewManager, ViewParent,这样我们知道后两者为接口,具体里面结构我们还无法得知,因此还需知道ViewManager,ViewParent是的内部实现,在讲他们之前,我们还是继续看ViewGroup。

EP2:

/*** 添加子视图.如果子视图没有设置布局参数,则使用视图组的布局参数为该视图布局.** @param child 添加的子视图.** @see #generateDefaultLayoutParams()*/public void addView(View child) {addView(child, -1);}/*** 添加子视图.如果子视图没有设置布局参数,则使用视图组的布局参数为该视图布局.** @param child 添加的子视图.* @param index 子视图加入的位置索引.** @see #generateDefaultLayoutParams()*/public void addView(View child, int index) {LayoutParams params = child.getLayoutParams();if (params == null) {params = generateDefaultLayoutParams();if (params == null) {throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null");}}addView(child, index, params);}/*** 以指定的宽度和高度,以及视图组的默认布局参数添加子视图.** @param child 添加的子视图.*/public void addView(View child, int width, int height) {final LayoutParams params = generateDefaultLayoutParams();params.width = width;params.height = height;addView(child, -1, params);}

看了上面的代码(EP2),你是不是感觉非常熟悉,对,我们平时在代码动态使用的addview,就是 VIewGroup提供的方法,当然里面也提供了有 romoveView(). updateViewLayout()方法 也是他的内部方法,看了增加子控件的代码,或许不用我来说,你也能联想到其移除和刷新的内部实现了。通过以上代码,我们还是不知道它和invalidate()有啥联系,别着急,上面代码我们发现不管是哪一个add的多参方法,最后进了   addView(child, index, params);   好的,我们继续跟进代码,发现

/*** 用指定的布局参数添加一个子视图.** @param child 添加的子视图.* @param index 添加的子视图的索引.* @param params 为子视图指定得布局参数.*/public void addView(View child, int index, LayoutParams params) {if (DBG) {System.out.println(this + " addView");}// addViewInner() will call child.requestLayout() when setting the new LayoutParams// therefore, we call requestLayout() on ourselves before, so that the child's request// will be blocked at our levelrequestLayout();invalidate();addViewInner(child, index, params, false);}

仔细看,卧槽,亮瞎了,是不是感觉突然明了,到目前你知道了view的增,删,刷新,都是会先进行一遍requestLayout,再进行invalidate(),那么回到了我们今天的课题,但是我还是不能带你进入此内容,我们还要继续看,

EP4:

   /*** 传入本视图必要的宽度和高度及其内边距,要求子视图调整自身设置。* 主要操作都是在 getChildMeasureSpec 函数中完成的。** @param child 需要调整的子视图* @param parentWidthMeasureSpec 本视图必要的宽度* @param parentHeightMeasureSpec 本视图必要的高度*/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);}

看了以上代码(EP4),我们可以再次想到,ViewGroup中的LayoutChid()方法,验证了大家了解的ViewGroup内部都是已树层级执行的原理,因此layoutChild()我不再过多解释,但以下方法不得不说,虽然代码比较长,但我们看重要部分(Ep5)。

EP5:

<pre name="code" class="java">/*** {@inheritDoc}*/@Overrideprotected void dispatchDraw(Canvas canvas) {final int count = mChildrenCount;final View[] children = mChildren;int flags = mGroupFlags;if ((flags & FLAG_RUN_ANIMATION) != 0 && canAnimate()) {final boolean cache = (mGroupFlags & FLAG_ANIMATION_CACHE) == FLAG_ANIMATION_CACHE;for (int i = 0; i < count; i++) {final View child = children[i];if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {final LayoutParams params = child.getLayoutParams();attachLayoutAnimationParameters(child, params, i, count);bindLayoutAnimation(child);if (cache) {child.setDrawingCacheEnabled(true);child.buildDrawingCache(true);}}}final LayoutAnimationController controller = mLayoutAnimationController;if (controller.willOverlap()) {mGroupFlags |= FLAG_OPTIMIZE_INVALIDATE;}controller.start();mGroupFlags &= ~FLAG_RUN_ANIMATION;mGroupFlags &= ~FLAG_ANIMATION_DONE;if (cache) {mGroupFlags |= FLAG_CHILDREN_DRAWN_WITH_CACHE;}if (mAnimationListener != null) {mAnimationListener.onAnimationStart(controller.getAnimation());}}int saveCount = 0;final boolean clipToPadding = (flags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK;if (clipToPadding) {saveCount = canvas.save();canvas.clipRect(mScrollX + mPaddingLeft, mScrollY + mPaddingTop,mScrollX + mRight - mLeft - mPaddingRight,mScrollY + mBottom - mTop - mPaddingBottom);}// We will draw our child's animation, let's reset the flagmPrivateFlags &= ~DRAW_ANIMATION;mGroupFlags &= ~FLAG_INVALIDATE_REQUIRED;boolean more = false;final long drawingTime = getDrawingTime();if ((flags & FLAG_USE_CHILD_DRAWING_ORDER) == 0) {for (int i = 0; i < count; i++) {final View child = children[i];if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {more |= drawChild(canvas, child, drawingTime);}}} else {for (int i = 0; i < count; i++) {final View child = children[getChildDrawingOrder(count, i)];if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {more |= drawChild(canvas, child, drawingTime);}}}// Draw any disappearing views that have animationsif (mDisappearingChildren != null) {final ArrayList<View> disappearingChildren = mDisappearingChildren;final int disappearingCount = disappearingChildren.size() - 1;// Go backwards -- we may delete as animations finishfor (int i = disappearingCount; i >= 0; i--) {final View child = disappearingChildren.get(i);more |= drawChild(canvas, child, drawingTime);}}if (clipToPadding) {canvas.restoreToCount(saveCount);}// mGroupFlags might have been updated by drawChild()flags = mGroupFlags;if ((flags & FLAG_INVALIDATE_REQUIRED) == FLAG_INVALIDATE_REQUIRED) {invalidate();}if ((flags & FLAG_ANIMATION_DONE) == 0 && (flags & FLAG_NOTIFY_ANIMATION_LISTENER) == 0 &&mLayoutAnimationController.isDone() && !more) {// We want to erase the drawing cache and notify the listener after the// next frame is drawn because one extra invalidate() is caused by// drawChild() after the animation is overmGroupFlags |= FLAG_NOTIFY_ANIMATION_LISTENER;final Runnable end = new Runnable() {public void run() {notifyAnimationListener();}};post(end);}}

以上代码 ViewGroup重载了view的方法,当view绘制时就会调此方法,从代码大致看出,绘制的时候,先看是否有绘制过的缓存,接着检测是否有动画,接着为我们的view创建矩形框,紧接着判断透明度,是否需要重绘等步骤,最后再进行invadate(), 总之当我们在执行某个View的onDraw()方法时,view不一定会重绘,由父控件检测当前view的状态是否有动画,透明度,以及view所在window的矩形框是否改变了,如果发现有变化,那么才会进行重绘,可以发现,关于view的增加,删除,更新,设置动画,隐藏,展现,都会触发重绘动作。

ViewManager

      ViewManaer ,字面理解view的具体管理者,提供了view的增,删,改。具体源码如下 ,主要充当视图改变的接口类,它和ViewParent 都是辅助接口。其具体由viewGroup实现,在这里不用过多的解释。源码:EP6
    EP6:

/*** 允许向活动中添加和移除子视图的接口. 通过调用* {@link android.content.Context#getSystemService(java.lang.String)* Context.getSystemService()}来取得该类的实例。*/
public interface ViewManager
{public void addView(View view, ViewGroup.LayoutParams params);public void updateViewLayout(View view, ViewGroup.LayoutParams params);public void removeView(View view);
}

ViewParent

    
     定义了作为父视图应有功能的类. 是视图与父视图交互的接口。主要控制View的一些基础功能,例如焦点,图层变化,参数变化,请求重绘,它和我们的viewManager主要把视图和控制器分离开来,我们的谷歌工程师把mvc玩的琉璃精致,源码如下:EP7
  EP7:

** Copyright (C) 2006 The Android Open Source Project** Licensed under the Apache License, Version 2.0 (the "License");* you may not use this file except in compliance with the License.* You may obtain a copy of the License at**      http://www.apache.org/licenses/LICENSE-2.0** Unless required by applicable law or agreed to in writing, software* distributed under the License is distributed on an "AS IS" BASIS,* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.* See the License for the specific language governing permissions and* limitations under the License.*/package android.view;import android.graphics.Rect;/*** 定义了作为父视图应有功能的类. 是视图与父视图交互的接口。*/
public interface ViewParent {/*** 当某些变更导致该父视图的子视图的布局失效时调用该方法.该方法按照视图树的顺序调用.*/public void requestLayout();/*** 指出该父视图是否请求了布局操作。** @return 如果请求了布局,返回真;否则返回假。*/public boolean isLayoutRequested();/*** 当子视图需要收集视图层次中透明区域并报告给窗口排版组件时调用。* 需要在视图层次中“打洞”的视图,比如SurfaceView可以利用该API* 来提高系统性能。当视图层次中没有这样的视图时,不需要该优化,* 使用它会稍微降低一些视图层次的性能。* * @param child 请求计算透明区域的视图。* */public void requestTransparentRegion(View child);/*** 需要重绘子视图的部分或全部区域。* * @param child 需要重绘的视图。* @param r 子视图需要重绘的区域。*/public void invalidateChild(View child, Rect r);/*** 需要重绘子视图的部分或全部区域。** 位置数组中分别保存了待绘制子视图的左上位置的整型数组。** 如果指定的区域的父视图被设为无效,则返回父视图;如果指定矩形不会导致父视图无效,* 或者不存在父视图,该方法返回空。** 当返回非空值时,必须将位置数组中的值更新为本ViewParent的左上坐标。** @param location 包含设置无效区域的子视图左上坐标的双元素整型数组。* @param r 子视图中设为无效的区域。** @return 该ViewParent的父视图或空。*/public ViewParent invalidateChildInParent(int[] location, Rect r);/*** 如果存在父视图,返回真,否则返回假。** @return 父视图或者在没有父视图时返回空。*/public ViewParent getParent();/*** 当父视图的子视图请求获得焦点时,调用此方法。* * @param child 请求获得焦点的子视图。此视图将包含具有焦点视图,但其本身不一定具有焦点。* @param focused 事实上拥有焦点的子视图,他可能是 child 的下层视图。*/public void requestChildFocus(View child, View focused);/*** 告诉视图层次,全局视图属性需要重新评价。* * @param child 属性变更的视图。*/public void recomputeViewAttributes(View child);/*** 当该视图的子视图需要放弃焦点时调用。* * @param child 放弃焦点的视图。*/public void clearChildFocus(View child);public boolean getChildVisibleRect(View child, Rect r, android.graphics.Point offset);/*** 在指定的方向找到最近的可以获得焦点的视图。* * @param v 当前具有焦点的视图。* @param direction FOCUS_UP、FOCUS_DOWN、FOCUS_LEFT、FOCUS_RIGHT之一。*/public View focusSearch(View v, int direction);/*** 改变子视图的前后顺序,将其移动到所有视图的最前面。* * @param child*/public void bringChildToFront(View child);/*** 告诉父视图,一个新的可得焦点视图可用了。该方法用于处理,* 从没有的可焦点的视图,到出现第一个可得焦点视图时的转变。* * @param v 新的可得焦点视图。*/public void focusableViewAvailable(View v);/*** 为指定的视图或者其父类显示上下文菜单.* <p>* 大部分情况下,子类不需要重写该方法.但是,如果直接将子类添加到窗口管理器(例如:使用* {@link ViewManager#addView(View, android.view.ViewGroup.LayoutParams)}* 函数),此时就需要重写来显示上下文菜单.* * @param originalView 首先显示的上下文菜单的原始视图.* @return 如果显示了上下文菜单返回真.*/public boolean showContextMenuForChild(View originalView);/*** 通知父类,如果有必要可以向指定的上下文菜单中添加菜单项* (递归通知其父类)。* * @param menu 被填充的菜单。*/public void createContextMenu(ContextMenu menu);/*** 当子视图的可绘制对象状态发生改变时调用该方法。** @param child 可绘制对象发生改变的子视图。*/public void childDrawableStateChanged(View child);/*** 当子视图不希望他的父类及其祖先使用* {@link ViewGroup#onInterceptTouchEvent(MotionEvent)}* 打断触控事件时调用。* <p>* 父视图应该调用其父类的该方法。父类必须在触控事件期间遵守该请求,* 就是说,父类只有在收到抬起事件或取消事件时才可以清楚该标志。* * @param disallowIntercept 如果子视图不希望父类打断触控事件,设为真。*/public void requestDisallowInterceptTouchEvent(boolean disallowIntercept);/*** 当视图组里的某个子视图需要定位到屏幕上的特定矩形区域时,调用此方法.* {@link ViewGroup} 重写此方法时可以认为:* <ul>*   <li>child 是该视图的直接子视图.</li>*   <li>rectangle 使用子视图的坐标系.</li>* </ul>** <p>{@link ViewGroup}重写此方法时应该遵守如下约定:</p>* <ul>*   <li>如果矩形可见,不做任何变更.</li>*   <li>视窗滚动到矩形可见即可.</li>* <ul>** @param child 发出请求的直接子视图.* @param rectangle 子视图希望显示在屏幕上的、基于子视图坐标系的矩形.* @param immediate 设为真时,禁止动画形式或延迟的滚动;设为假时不禁止.* @return 该方法是否滚动了屏幕.*/public boolean requestChildRectangleOnScreen(View child, Rect rectangle,boolean immediate);
}

我们平时指定绘制的invalidate()就是其内部抽象接口,

    ViewRoot
        好了 该到我们最重要的ViewRoot大神了上场了 迫不及待,他是实现handler的子类,也实现了VIewParent,,主要负责view的通信和基础功能,其具体实现类为ViewRootImpl, 两者所谓安卓视图的核心成员,其博大精深我暂时无法解释,所以就只看关于invalidate()有关的部分 ,看invalidateChild()方法(EP8),
   EP8

 /*** {@inheritDoc}*/public void requestLayout() {checkThread();mLayoutRequested = true;scheduleTraversals();}/*** {@inheritDoc}*/public boolean isLayoutRequested() {return mLayoutRequested;}public void invalidateChild(View child, Rect dirty) {checkThread();if (DEBUG_DRAW) Log.v(TAG, "Invalidate child: " + dirty);if (mCurScrollY != 0 || mTranslator != null) {mTempRect.set(dirty);dirty = mTempRect;if (mCurScrollY != 0) {dirty.offset(0, -mCurScrollY);}if (mTranslator != null) {mTranslator.translateRectInAppWindowToScreen(dirty);}if (mAttachInfo.mScalingRequired) {dirty.inset(-1, -1);}}mDirty.union(dirty);if (!mWillDrawSoon) {scheduleTraversals();}}public ViewParent getParent() {return null;}public ViewParent invalidateChildInParent(final int[] location, final Rect dirty) {invalidateChild(null, dirty);return null;}

       从上面代码可以看出,viewRoot是我们的view重绘的具体执行者,大家也可以看到了熟悉的requestLayout()方法,从invalidateChild(View child, Rect dirty)看出每个view绘制都要根据当前检测缓存的一些参数,透明度,所在矩阵位置变化,但是最后执行了scheduleTraversals()方法,很好奇的进去看了一下,(EP 9)
  EP 9:

public void scheduleTraversals() {if (!mTraversalScheduled) {mTraversalScheduled = true;sendEmptyMessage(DO_TRAVERSAL);}}

     
    果然不出所料,他将要传进来的view 记录,然后发出了一条Messge,告诉父控件当前的这个child需要绘制,那么具体在哪里绘制的呢,我们接着看sendEmptyMessage()方法内部(EP 10)
   EP 10:
 @Overridepublic void handleMessage(Message msg) {switch (msg.what) {case View.AttachInfo.INVALIDATE_MSG:((View) msg.obj).invalidate();break;case View.AttachInfo.INVALIDATE_RECT_MSG:final View.AttachInfo.InvalidateInfo info = (View.AttachInfo.InvalidateInfo) msg.obj;info.target.invalidate(info.left, info.top, info.right, info.bottom);info.release();break;case DO_TRAVERSAL:if (mProfile) {Debug.startMethodTracing("ViewRoot");}performTraversals();if (mProfile) {Debug.stopMethodTracing();mProfile = false;}break;

以上只是部分代码,因为此类不仅控制view的视图改变,也控制事件的改变。发现最终是由我们的handler去调用  performTraversals()来,接着我们继续往下看

    performTraversals()方法 (EP 11)  
  EP 11:

private void performTraversals() {// cache mView since it is used so much below...final View host = mView;if (DBG) {System.out.println("======================================");System.out.println("performTraversals");host.debug();}if (host == null || !mAdded)return;mTraversalScheduled = false;mWillDrawSoon = true;boolean windowResizesToFitContent = false;boolean fullRedrawNeeded = mFullRedrawNeeded;boolean newSurface = false;boolean surfaceChanged = false;WindowManager.LayoutParams lp = mWindowAttributes;int desiredWindowWidth;int desiredWindowHeight;int childWidthMeasureSpec;int childHeightMeasureSpec;final View.AttachInfo attachInfo = mAttachInfo;final int viewVisibility = getHostVisibility();boolean viewVisibilityChanged = mViewVisibility != viewVisibility|| mNewSurfaceNeeded;float appScale = mAttachInfo.mApplicationScale;WindowManager.LayoutParams params = null;if (mWindowAttributesChanged) {mWindowAttributesChanged = false;surfaceChanged = true;params = lp;}Rect frame = mWinFrame;if (mFirst) {fullRedrawNeeded = true;mLayoutRequested = true;DisplayMetrics packageMetrics =mView.getContext().getResources().getDisplayMetrics();desiredWindowWidth = packageMetrics.widthPixels;desiredWindowHeight = packageMetrics.heightPixels;// For the very first time, tell the view hierarchy that it// is attached to the window.  Note that at this point the surface// object is not initialized to its backing store, but soon it// will be (assuming the window is visible).attachInfo.mSurface = mSurface;attachInfo.mUse32BitDrawingCache = PixelFormat.formatHasAlpha(lp.format) ||lp.format == PixelFormat.RGBX_8888;attachInfo.mHasWindowFocus = false;attachInfo.mWindowVisibility = viewVisibility;attachInfo.mRecomputeGlobalAttributes = false;attachInfo.mKeepScreenOn = false;viewVisibilityChanged = false;mLastConfiguration.setTo(host.getResources().getConfiguration());host.dispatchAttachedToWindow(attachInfo, 0);//Log.i(TAG, "Screen on initialized: " + attachInfo.mKeepScreenOn);} else {desiredWindowWidth = frame.width();desiredWindowHeight = frame.height();if (desiredWindowWidth != mWidth || desiredWindowHeight != mHeight) {if (DEBUG_ORIENTATION) Log.v(TAG,"View " + host + " resized to: " + frame);fullRedrawNeeded = true;mLayoutRequested = true;windowResizesToFitContent = true;}}if (viewVisibilityChanged) {attachInfo.mWindowVisibility = viewVisibility;host.dispatchWindowVisibilityChanged(viewVisibility);if (viewVisibility != View.VISIBLE || mNewSurfaceNeeded) {if (mUseGL) {destroyGL();}}if (viewVisibility == View.GONE) {// After making a window gone, we will count it as being// shown for the first time the next time it gets focus.mHasHadWindowFocus = false;}}boolean insetsChanged = false;if (mLayoutRequested) {// Execute enqueued actions on every layout in case a view that was detached// enqueued an action after being detachedgetRunQueue().executeActions(attachInfo.mHandler);if (mFirst) {host.fitSystemWindows(mAttachInfo.mContentInsets);// make sure touch mode code executes by setting cached value// to opposite of the added touch mode.mAttachInfo.mInTouchMode = !mAddedTouchMode;ensureTouchModeLocally(mAddedTouchMode);} else {if (!mAttachInfo.mContentInsets.equals(mPendingContentInsets)) {mAttachInfo.mContentInsets.set(mPendingContentInsets);host.fitSystemWindows(mAttachInfo.mContentInsets);insetsChanged = true;if (DEBUG_LAYOUT) Log.v(TAG, "Content insets changing to: "+ mAttachInfo.mContentInsets);}if (!mAttachInfo.mVisibleInsets.equals(mPendingVisibleInsets)) {mAttachInfo.mVisibleInsets.set(mPendingVisibleInsets);if (DEBUG_LAYOUT) Log.v(TAG, "Visible insets changing to: "+ mAttachInfo.mVisibleInsets);}if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT|| lp.height == ViewGroup.LayoutParams.WRAP_CONTENT) {windowResizesToFitContent = true;DisplayMetrics packageMetrics =mView.getContext().getResources().getDisplayMetrics();desiredWindowWidth = packageMetrics.widthPixels;desiredWindowHeight = packageMetrics.heightPixels;}}childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);// Ask host how big it wants to beif (DEBUG_ORIENTATION || DEBUG_LAYOUT) Log.v(TAG,"Measuring " + host + " in display " + desiredWindowWidth+ "x" + desiredWindowHeight + "...");host.measure(childWidthMeasureSpec, childHeightMeasureSpec);if (DBG) {System.out.println("======================================");System.out.println("performTraversals -- after measure");host.debug();}}if (attachInfo.mRecomputeGlobalAttributes) {//Log.i(TAG, "Computing screen on!");attachInfo.mRecomputeGlobalAttributes = false;boolean oldVal = attachInfo.mKeepScreenOn;attachInfo.mKeepScreenOn = false;host.dispatchCollectViewAttributes(0);if (attachInfo.mKeepScreenOn != oldVal) {params = lp;//Log.i(TAG, "Keep screen on changed: " + attachInfo.mKeepScreenOn);}}if (mFirst || attachInfo.mViewVisibilityChanged) {attachInfo.mViewVisibilityChanged = false;int resizeMode = mSoftInputMode &WindowManager.LayoutParams.SOFT_INPUT_MASK_ADJUST;// If we are in auto resize mode, then we need to determine// what mode to use now.if (resizeMode == WindowManager.LayoutParams.SOFT_INPUT_ADJUST_UNSPECIFIED) {final int N = attachInfo.mScrollContainers.size();for (int i=0; i<N; i++) {if (attachInfo.mScrollContainers.get(i).isShown()) {resizeMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;}}if (resizeMode == 0) {resizeMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN;}if ((lp.softInputMode &WindowManager.LayoutParams.SOFT_INPUT_MASK_ADJUST) != resizeMode) {lp.softInputMode = (lp.softInputMode &~WindowManager.LayoutParams.SOFT_INPUT_MASK_ADJUST) |resizeMode;params = lp;}}}if (params != null && (host.mPrivateFlags & View.REQUEST_TRANSPARENT_REGIONS) != 0) {if (!PixelFormat.formatHasAlpha(params.format)) {params.format = PixelFormat.TRANSLUCENT;}}boolean windowShouldResize = mLayoutRequested && windowResizesToFitContent&& ((mWidth != host.mMeasuredWidth || mHeight != host.mMeasuredHeight)|| (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT &&frame.width() < desiredWindowWidth && frame.width() != mWidth)|| (lp.height == ViewGroup.LayoutParams.WRAP_CONTENT &&frame.height() < desiredWindowHeight && frame.height() != mHeight));final boolean computesInternalInsets =attachInfo.mTreeObserver.hasComputeInternalInsetsListeners();boolean insetsPending = false;int relayoutResult = 0;if (mFirst || windowShouldResize || insetsChanged|| viewVisibilityChanged || params != null) {if (viewVisibility == View.VISIBLE) {// If this window is giving internal insets to the window// manager, and it is being added or changing its visibility,// then we want to first give the window manager "fake"// insets to cause it to effectively ignore the content of// the window during layout.  This avoids it briefly causing// other windows to resize/move based on the raw frame of the// window, waiting until we can finish laying out this window// and get back to the window manager with the ultimately// computed insets.insetsPending = computesInternalInsets&& (mFirst || viewVisibilityChanged);if (mWindowAttributes.memoryType == WindowManager.LayoutParams.MEMORY_TYPE_GPU) {if (params == null) {params = mWindowAttributes;}mGlWanted = true;}}if (mSurfaceHolder != null) {mSurfaceHolder.mSurfaceLock.lock();mDrawingAllowed = true;}boolean initialized = false;boolean contentInsetsChanged = false;boolean visibleInsetsChanged;boolean hadSurface = mSurface.isValid();try {int fl = 0;if (params != null) {fl = params.flags;if (attachInfo.mKeepScreenOn) {params.flags |= WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;}}if (DEBUG_LAYOUT) {Log.i(TAG, "host=w:" + host.mMeasuredWidth + ", h:" +host.mMeasuredHeight + ", params=" + params);}relayoutResult = relayoutWindow(params, viewVisibility, insetsPending);if (params != null) {params.flags = fl;}if (DEBUG_LAYOUT) Log.v(TAG, "relayout: frame=" + frame.toShortString()+ " content=" + mPendingContentInsets.toShortString()+ " visible=" + mPendingVisibleInsets.toShortString()+ " surface=" + mSurface);if (mPendingConfiguration.seq != 0) {if (DEBUG_CONFIGURATION) Log.v(TAG, "Visible with new config: "+ mPendingConfiguration);updateConfiguration(mPendingConfiguration, !mFirst);mPendingConfiguration.seq = 0;}contentInsetsChanged = !mPendingContentInsets.equals(mAttachInfo.mContentInsets);visibleInsetsChanged = !mPendingVisibleInsets.equals(mAttachInfo.mVisibleInsets);if (contentInsetsChanged) {mAttachInfo.mContentInsets.set(mPendingContentInsets);host.fitSystemWindows(mAttachInfo.mContentInsets);if (DEBUG_LAYOUT) Log.v(TAG, "Content insets changing to: "+ mAttachInfo.mContentInsets);}if (visibleInsetsChanged) {mAttachInfo.mVisibleInsets.set(mPendingVisibleInsets);if (DEBUG_LAYOUT) Log.v(TAG, "Visible insets changing to: "+ mAttachInfo.mVisibleInsets);}if (!hadSurface) {if (mSurface.isValid()) {// If we are creating a new surface, then we need to// completely redraw it.  Also, when we get to the// point of drawing it we will hold off and schedule// a new traversal instead.  This is so we can tell the// window manager about all of the windows being displayed// before actually drawing them, so it can display then// all at once.newSurface = true;fullRedrawNeeded = true;mPreviousTransparentRegion.setEmpty();if (mGlWanted && !mUseGL) {initializeGL();initialized = mGlCanvas != null;}}} else if (!mSurface.isValid()) {// If the surface has been removed, then reset the scroll// positions.mLastScrolledFocus = null;mScrollY = mCurScrollY = 0;if (mScroller != null) {mScroller.abortAnimation();}}} catch (RemoteException e) {}if (DEBUG_ORIENTATION) Log.v(TAG, "Relayout returned: frame=" + frame + ", surface=" + mSurface);attachInfo.mWindowLeft = frame.left;attachInfo.mWindowTop = frame.top;// !!FIXME!! This next section handles the case where we did not get the// window size we asked for. We should avoid this by getting a maximum size from// the window session beforehand.mWidth = frame.width();mHeight = frame.height();if (mSurfaceHolder != null) {// The app owns the surface; tell it about what is going on.if (mSurface.isValid()) {// XXX .copyFrom() doesn't work!//mSurfaceHolder.mSurface.copyFrom(mSurface);mSurfaceHolder.mSurface = mSurface;}mSurfaceHolder.mSurfaceLock.unlock();if (mSurface.isValid()) {if (!hadSurface) {mSurfaceHolder.ungetCallbacks();mIsCreating = true;mSurfaceHolderCallback.surfaceCreated(mSurfaceHolder);SurfaceHolder.Callback callbacks[] = mSurfaceHolder.getCallbacks();if (callbacks != null) {for (SurfaceHolder.Callback c : callbacks) {c.surfaceCreated(mSurfaceHolder);}}surfaceChanged = true;}if (surfaceChanged) {mSurfaceHolderCallback.surfaceChanged(mSurfaceHolder,lp.format, mWidth, mHeight);SurfaceHolder.Callback callbacks[] = mSurfaceHolder.getCallbacks();if (callbacks != null) {for (SurfaceHolder.Callback c : callbacks) {c.surfaceChanged(mSurfaceHolder, lp.format,mWidth, mHeight);}}}mIsCreating = false;} else if (hadSurface) {mSurfaceHolder.ungetCallbacks();SurfaceHolder.Callback callbacks[] = mSurfaceHolder.getCallbacks();mSurfaceHolderCallback.surfaceDestroyed(mSurfaceHolder);if (callbacks != null) {for (SurfaceHolder.Callback c : callbacks) {c.surfaceDestroyed(mSurfaceHolder);}}mSurfaceHolder.mSurfaceLock.lock();// Make surface invalid.//mSurfaceHolder.mSurface.copyFrom(mSurface);mSurfaceHolder.mSurface = new Surface();mSurfaceHolder.mSurfaceLock.unlock();}}if (initialized) {mGlCanvas.setViewport((int) (mWidth * appScale + 0.5f),(int) (mHeight * appScale + 0.5f));}boolean focusChangedDueToTouchMode = ensureTouchModeLocally((relayoutResult&WindowManagerImpl.RELAYOUT_IN_TOUCH_MODE) != 0);if (focusChangedDueToTouchMode || mWidth != host.mMeasuredWidth|| mHeight != host.mMeasuredHeight || contentInsetsChanged) {childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);if (DEBUG_LAYOUT) Log.v(TAG, "Ooops, something changed!  mWidth="+ mWidth + " measuredWidth=" + host.mMeasuredWidth+ " mHeight=" + mHeight+ " measuredHeight" + host.mMeasuredHeight+ " coveredInsetsChanged=" + contentInsetsChanged);// Ask host how big it wants to behost.measure(childWidthMeasureSpec, childHeightMeasureSpec);// Implementation of weights from WindowManager.LayoutParams// We just grow the dimensions as needed and re-measure if// needs beint width = host.mMeasuredWidth;int height = host.mMeasuredHeight;boolean measureAgain = false;if (lp.horizontalWeight > 0.0f) {width += (int) ((mWidth - width) * lp.horizontalWeight);childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width,MeasureSpec.EXACTLY);measureAgain = true;}if (lp.verticalWeight > 0.0f) {height += (int) ((mHeight - height) * lp.verticalWeight);childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height,MeasureSpec.EXACTLY);measureAgain = true;}if (measureAgain) {if (DEBUG_LAYOUT) Log.v(TAG,"And hey let's measure once more: width=" + width+ " height=" + height);host.measure(childWidthMeasureSpec, childHeightMeasureSpec);}mLayoutRequested = true;}}final boolean didLayout = mLayoutRequested;boolean triggerGlobalLayoutListener = didLayout|| attachInfo.mRecomputeGlobalAttributes;if (didLayout) {mLayoutRequested = false;mScrollMayChange = true;if (DEBUG_ORIENTATION || DEBUG_LAYOUT) Log.v(TAG, "Laying out " + host + " to (" +host.mMeasuredWidth + ", " + host.mMeasuredHeight + ")");long startTime = 0L;if (Config.DEBUG && ViewDebug.profileLayout) {startTime = SystemClock.elapsedRealtime();}host.layout(0, 0, host.mMeasuredWidth, host.mMeasuredHeight);if (Config.DEBUG && ViewDebug.consistencyCheckEnabled) {if (!host.dispatchConsistencyCheck(ViewDebug.CONSISTENCY_LAYOUT)) {throw new IllegalStateException("The view hierarchy is an inconsistent state,"+ "please refer to the logs with the tag "+ ViewDebug.CONSISTENCY_LOG_TAG + " for more infomation.");}}if (Config.DEBUG && ViewDebug.profileLayout) {EventLog.writeEvent(60001, SystemClock.elapsedRealtime() - startTime);}// By this point all views have been sized and positionned// We can compute the transparent areaif ((host.mPrivateFlags & View.REQUEST_TRANSPARENT_REGIONS) != 0) {// start out transparent// TODO: AVOID THAT CALL BY CACHING THE RESULT?host.getLocationInWindow(mTmpLocation);mTransparentRegion.set(mTmpLocation[0], mTmpLocation[1],mTmpLocation[0] + host.mRight - host.mLeft,mTmpLocation[1] + host.mBottom - host.mTop);host.gatherTransparentRegion(mTransparentRegion);if (mTranslator != null) {mTranslator.translateRegionInWindowToScreen(mTransparentRegion);}if (!mTransparentRegion.equals(mPreviousTransparentRegion)) {mPreviousTransparentRegion.set(mTransparentRegion);// reconfigure window managertry {sWindowSession.setTransparentRegion(mWindow, mTransparentRegion);} catch (RemoteException e) {}}}if (DBG) {System.out.println("======================================");System.out.println("performTraversals -- after setFrame");host.debug();}}if (triggerGlobalLayoutListener) {attachInfo.mRecomputeGlobalAttributes = false;attachInfo.mTreeObserver.dispatchOnGlobalLayout();}if (computesInternalInsets) {ViewTreeObserver.InternalInsetsInfo insets = attachInfo.mGivenInternalInsets;final Rect givenContent = attachInfo.mGivenInternalInsets.contentInsets;final Rect givenVisible = attachInfo.mGivenInternalInsets.visibleInsets;givenContent.left = givenContent.top = givenContent.right= givenContent.bottom = givenVisible.left = givenVisible.top= givenVisible.right = givenVisible.bottom = 0;attachInfo.mTreeObserver.dispatchOnComputeInternalInsets(insets);Rect contentInsets = insets.contentInsets;Rect visibleInsets = insets.visibleInsets;if (mTranslator != null) {contentInsets = mTranslator.getTranslatedContentInsets(contentInsets);visibleInsets = mTranslator.getTranslatedVisbileInsets(visibleInsets);}if (insetsPending || !mLastGivenInsets.equals(insets)) {mLastGivenInsets.set(insets);try {sWindowSession.setInsets(mWindow, insets.mTouchableInsets,contentInsets, visibleInsets);} catch (RemoteException e) {}}}if (mFirst) {// handle first focus requestif (DEBUG_INPUT_RESIZE) Log.v(TAG, "First: mView.hasFocus()="+ mView.hasFocus());if (mView != null) {if (!mView.hasFocus()) {mView.requestFocus(View.FOCUS_FORWARD);mFocusedView = mRealFocusedView = mView.findFocus();if (DEBUG_INPUT_RESIZE) Log.v(TAG, "First: requested focused view="+ mFocusedView);} else {mRealFocusedView = mView.findFocus();if (DEBUG_INPUT_RESIZE) Log.v(TAG, "First: existing focused view="+ mRealFocusedView);}}}mFirst = false;mWillDrawSoon = false;mNewSurfaceNeeded = false;mViewVisibility = viewVisibility;if (mAttachInfo.mHasWindowFocus) {final boolean imTarget = WindowManager.LayoutParams.mayUseInputMethod(mWindowAttributes.flags);if (imTarget != mLastWasImTarget) {mLastWasImTarget = imTarget;InputMethodManager imm = InputMethodManager.peekInstance();if (imm != null && imTarget) {imm.startGettingWindowFocus(mView);imm.onWindowFocus(mView, mView.findFocus(),mWindowAttributes.softInputMode,!mHasHadWindowFocus, mWindowAttributes.flags);}}}boolean cancelDraw = attachInfo.mTreeObserver.dispatchOnPreDraw();if (!cancelDraw && !newSurface) {mFullRedrawNeeded = false;draw(fullRedrawNeeded);if ((relayoutResult&WindowManagerImpl.RELAYOUT_FIRST_TIME) != 0|| mReportNextDraw) {if (LOCAL_LOGV) {Log.v(TAG, "FINISHED DRAWING: " + mWindowAttributes.getTitle());}mReportNextDraw = false;if (mSurfaceHolder != null && mSurface.isValid()) {mSurfaceHolderCallback.surfaceRedrawNeeded(mSurfaceHolder);SurfaceHolder.Callback callbacks[] = mSurfaceHolder.getCallbacks();if (callbacks != null) {for (SurfaceHolder.Callback c : callbacks) {if (c instanceof SurfaceHolder.Callback2) {((SurfaceHolder.Callback2)c).surfaceRedrawNeeded(mSurfaceHolder);}}}}try {sWindowSession.finishDrawing(mWindow);} catch (RemoteException e) {}}} else {// We were supposed to report when we are done drawing. Since we canceled the// draw, remember it here.if ((relayoutResult&WindowManagerImpl.RELAYOUT_FIRST_TIME) != 0) {mReportNextDraw = true;}if (fullRedrawNeeded) {mFullRedrawNeeded = true;}// Try againscheduleTraversals();}}

       当我看到这里时我忍不住惨叫,尼玛这是啥,所以发现谷歌工程师也会写这么多行的方法啊,我忍不住对自己公司的代码规范吐槽一下,一个方法行数不超过180行的规定。 代码很多,但不用吓到,我们仔细一琢磨。发现此方法里面包含一些基本常量 ,列如需要绘制view的宽高,透明度,AttachInfo,是否立马绘制,是否可见,是否需要测量 ,等等都是关于view的一些基础属性,接着一番根据屏幕密度像素,检测是否首次加载属性参数,如果是第一次绘制就进行一系列计算然后加载缓存中AttachInfo,接着检测是否需要重新LayoutRequest的,如果需要就重新测量,等等,最后执行 draw(fullRedrawNeeded);进行绘制,大家可能想问上面代码里的draw里面是啥,因为大家很熟悉,自定义view的时候都会重写ondraw()方法,但是里面做了什么大家不得而知。其实就是在这里回调你所重载的draw()方法,是一个递归过程,源码不再贴了。
     看了viewRoot的draw(fullRedrawNeeded)源码我分析得里,里面根据host(也就是当前view的矩阵进行判断,加上view的是否全屏,是否bivislity.如果改变了就重新绘制,最后直接调用VIew用Draw() 进行绘制的,尼玛 我突然要哭了,回来回去 还是回到了你重载view写的draw()中,因此invaldate()也渐渐明晰起来,
   这里也给大家说明一下,在view进行nvaldate ()的时候,viewRoot是需要检测checkThrad()得,如果当前线程是非UI线程的,是无法进行重绘的(焦点,属性,位置等变化),大家熟悉的子类无法更改父类UI的异常也在这里抛出的 ,当然,这仅仅也就是在viewRootIMPL实现的以后才会去检测,当然还没被完全实例化的时候,子线程是可以更新UI的。

  EP 12:
 void checkThread() {if (mThread != Thread.currentThread()) {throw new CalledFromWrongThreadException("Only the original thread that created a view hierarchy can touch its views.");}}

当前的viewThread(mThread)是在我的viewRoot构造是创建。代码如下(EP 13),
Ep 13:
 public ViewRoot(Context context) {super();if (MEASURE_LATENCY && lt == null) {lt = new LatencyTimer(100, 1000);}// For debug only//++sInstanceCount;// Initialize the statics when this class is first instantiated. This is// done here instead of in the static block because Zygote does not// allow the spawning of threads.getWindowSession(context.getMainLooper());mThread = Thread.currentThread();mLocation = new WindowLeaked(null);mLocation.fillInStackTrace();mWidth = -1;mHeight = -1;mDirty = new Rect();mTempRect = new Rect();mVisRect = new Rect();mWinFrame = new Rect();mWindow = new W(this, context);mInputMethodCallback = new InputMethodCallback(this);mViewVisibility = View.GONE;mTransparentRegion = new Region();mPreviousTransparentRegion = new Region();mFirst = true; // true for the first time the view is addedmAdded = false;mAttachInfo = new View.AttachInfo(sWindowSession, mWindow, this, this);mViewConfiguration = ViewConfiguration.get(context);mDensity = context.getResources().getDisplayMetrics().densityDpi;}

       通过学习了以上的四个只要的view相关的四个类,你是否对以前的疑问释然了,当然仅从上面的代码我们还无法清晰的理解我的重绘逻辑,下面总结其流程

inValidate过程

1 子view进行invalidate()

2 父View调用自己的invalidate()
 3 View 所在的父控件(ViewGroup)调用 mParent.invalidateChild();
 4 VIewGroup 调用invalidateChildInParent();
 5 VIewRoot执行scheduleTraversals()发送消息给实现类viewRootImpl
 6 接着viewRootImpl收到消息处理  performTraversals();
 7 接着方法内 调用 draw(fullRedrawNeeded); 进行一些列的判断 ,判断是否绘制,
 8 如果需要重绘 则调用 子View的Draw()。

       当view调用invalidate()时,ViewGroup的invalidateChildInParent()返回了它的parent, 也就是说,当前view调用invalidate, 计算出了脏矩形,然后告诉了父view( mViewParent.invalidateChild(dirty_child_view, dirty_rect) ), 父view计算了它的脏矩形,父view又告诉了它的父view (invalidateChildInParent), 它又计算了它的脏矩形,一直到Activity的根DecorView, 而DecorView的父节点为ViewRootImpl,ViewRootImpl最终调用performTraversals来进行处理。
      
     通过上面的代码,我们知道了view整个状态的变化,都要先经过invalidae(),其经过invalidate()的一些列判断,筛选后,最终执行需要绘制的view的draw()方法,是不是觉得很啃爹,是不是觉得很无语。安卓绘制view只会绘制状,态属性发生改变了的那个具体的view,也就说只有当你的view真正发生了状态和事件变化后才会被重新绘制,其他View都会存cahe中读取 不会被重绘,这不仅给提高了加载view的效率问题,也见证了谁改变 绘制谁的低耦合开发设计思想,view是视图层和控制层  由两个个独立的接口控制实现的。
   好了 该回答以上问题了,view绘制时,会进行参数的重新加载,会进行重新测量(如果已存在缓存数据,就不会进行测量),布局,和绘制的, view的任何一种改变也会触发自己的draw()方法,invalidate()会触发view Layout(),和draw(),但是它不会进行测量。  requestLayout()方法最终也会执行draw()的,当然今天我对它没做介绍,但是明确的是:view改变,必定会引起重绘的道理,一个view的改变不会影响其他view的重绘。理解了今天的内容以后我们再来《Android View 中requestLayout() 你了解多少?》分析requestLayout()过程,。
  谢谢阅读 ,转载请标明出处:http://blog.csdn.net/sk719887916/article/details/48443429,作者:skay
    

Android ViewManger解析 从ViewRoot 源码分析invalidate相关推荐

  1. Android shortcut的使用及源码分析

    Android shortcut的使用及源码分析 最近遇到了一个切换国家码后部分应用的shortcut未更新的问题,就学习了shortcut的相关知识,在这里分享一下我了解的知识,希望能对大家有帮助. ...

  2. 【Android 事件分发】ItemTouchHelper 源码分析 ( OnItemTouchListener 事件监听器源码分析 二 )

    Android 事件分发 系列文章目录 [Android 事件分发]事件分发源码分析 ( 驱动层通过中断传递事件 | WindowManagerService 向 View 层传递事件 ) [Andr ...

  3. 【Android 事件分发】ItemTouchHelper 源码分析 ( OnItemTouchListener 事件监听器源码分析 )

    Android 事件分发 系列文章目录 [Android 事件分发]事件分发源码分析 ( 驱动层通过中断传递事件 | WindowManagerService 向 View 层传递事件 ) [Andr ...

  4. Android Q 10.1 KeyMaster源码分析(二) - 各家方案的实现

    写在之前 这两篇文章是我2021年3月初看KeyMaster的笔记,本来打算等分析完KeyMaster和KeyStore以后再一起做成一系列贴出来,后来KeyStore的分析中断了,这一系列的文章就变 ...

  5. Android之vold进程启动源码分析

    1.Vold (Volume Daemon)介绍 vold进程接收来自内核的外部设备消息,用于管理和控制Android平台外部存储设备,包括SD插拨.挂载.卸载.格式化等:当外部设备发生变化时,内核通 ...

  6. scroller类的用法完全解析以及带源码分析

    上一篇:scrollTo与scrollBy用法以及TouchSlop与VelocityTracker解析 通过上一篇内容对scrollTo与scrollBy用法以及TouchSlop与Velocity ...

  7. Android之rild进程启动源码分析

    Android 电话系统框架介绍 在android系统中rild运行在AP上,AP上的应用通过rild发送AT指令给BP,BP接收到信息后又通过rild传送给AP.AP与BP之间有两种通信方式: 1. ...

  8. 【Android 插件化】VirtualApp 源码分析 ( 启动应用源码分析 | HomePresenterImpl 启动应用方法 | VirtualCore 启动插件应用最终方法 )

    文章目录 一.启动应用源码分析 1.HomeActivity 启动应用点击方法 2.HomePresenterImpl 启动应用方法 3.VirtualCore 启动插件应用最终方法 一.启动应用源码 ...

  9. 【Android 插件化】VirtualApp 源码分析 ( 添加应用源码分析 | LaunchpadAdapter 适配器 | 适配器添加元素 | PackageAppData 元素 )

    文章目录 一.添加应用源码分析 1.LaunchpadAdapter 适配器 2.适配器添加元素 3.PackageAppData 元素 一.添加应用源码分析 1.LaunchpadAdapter 适 ...

最新文章

  1. Eclipse for Tricore 的安装方法
  2. MySQL 用 limit 为什么会影响性能?
  3. Visual C++——《可视化编程技术》课程考核
  4. ❀❀ selenium 学习网站 ★★★★★
  5. Asp.NET大文件上传组件开发总结(二)---提取文件内容
  6. wxWidgets之wxGrid控件
  7. 专升本c语言网课听谁的好_都说塑钢泥比玻璃胶好,填缝永不变黑,师傅却说不好用,听谁的?...
  8. Python使用Condition对象实现多线程同步
  9. logback实现日志按天和大小切分
  10. javaweb文件压缩下载
  11. visual studio 2019语言中文和英文的切换
  12. PDF格式分析(六十五) Text 文字——字体数据结构
  13. 标签打印软件如何制作医疗废物标签
  14. 割线法的C语言程序,割线法实验报告.doc
  15. 14种鼻型图解_十种鼻型分类图解
  16. windows ubuntu 双系统 蓝屏Technical information: ***stop:0x0000007B(0x80786B58,0xC0000034,0x00000000,0x00
  17. 教务系统自动评教_新版正方教务管理系统自动评教脚本
  18. wordpress 调用指定页面内容详解2 get_children()
  19. Arduino驱动LM35温度传感器自制温度计
  20. 蓝桥杯python省赛冲刺篇2——常用算法的详细解析及对应蓝桥杯真题:打表模拟法、递推递归法、枚举法、贪心算法、差分与前缀和

热门文章

  1. MySQL 查询、子查询及连接查询
  2. 站内搜索 代码(Baidu,Google,Yahoo)
  3. 昨天睡眠质量记录84分
  4. 微信安卓input file 上传onchange不能触发问题
  5. 吐槽一下 XCode 开发工具,可以吗?
  6. 文献阅读(3):Near-Memory Computing
  7. Affordance Detection of Tool Parts from Geometric Features
  8. 跟我学Springboot开发后端管理系统8:AOP+logback+MDC日志输出
  9. ip加速器的原理是什么?
  10. unity全栈开发是什么意思_前端所谓的全栈和大前端有什么区别?