Android知识点<9>View的工作原理

参考 任玉刚大神的 android开发艺术探索 ,在后面增加了一些常见的自定义View 的问题

1. 相关概念: 


ViewRoot:

ViewRoot对应于ViewRootImpl类,它是连接WindowManager和DecorView的纽带,View的三大流程都是在viewroot中完成。

View的绘制流程: 
View的绘制流程是从ViewRoot的performTraversals方法开始的,它经过measure,layout,draw三个过程才最终将一个View绘制出来,performTraversals会一次调用performMeasure,performLayout和performDraw三个方法,这三个方法分别会完成view的measure,layout,draw的流程,在measure方法中,又会调用onMeasure方法,在onMeasure方法中会对所有的子元素进行measure过程,这个时候measure流程就从父容器传递给子容器,这样就完成了一次测量接着子元素会重复父容器的measure的测量过程,如此反复的完成整个View树的过程,同理performLayout的执行原理和performDraw的执行原理和performMeasure的原理类似。

measure过程中决定了View的宽和高,Measure完成后,可以通过getMeasureWidth和getMeasureHeight方法来获取到View测量后的宽和高,在几乎所有情况下,它都等于最终的宽/高,但有一些特殊的情况。

Layout过程中决定了View四个顶点的坐标,和实际View的宽和高。完成后可以通过getTop,getBottom,getLeft,getRight来拿到四个顶点的位置,并且可以通过getWidth,和getHeight完成最终的宽和高。

Draw过程决定View的显示,只有Draw以后View才会显示在屏幕上。

DecorView作为顶级View,一般情况它内部包含一个竖直方向的Linearlayout,在这个LinearLayout里面有上下两个部分,上面是标题栏,下面是内容栏,在Activity中我们设置的setContentView所设置的View,实际上是被加载到了内容栏上了,而内容栏的id叫content,我们的布局确实又加载到了content上,因此叫做setContentView。,实际上DecorView是一个FrameLayout,View事件都是经过DecorView才传到我们的View。

理解MeasureSpec

MeasureSpec可以翻译为测量规格,在测量中,系统会将View的LayoutParams根据父容器所施加的规则转换成相应的MeasureSpec,然后再根据MeasureSpec测量出View的宽和高。MeasureSpec代表一个32位的int值,高2位代表SpecMode,低30位代表SpecSize,SpecMode是指测量模式,而SpecSize是指某种测量模式下的测量出来的大小,

MeasureSpec有三种模式:

UNSPECIFIED 
父容器不对View有任何限制,要多大就多大,这种情况一般用于系统内部,表示一种测量状态。

EXACTLY 
父容器已经测量出来View的精确大小,这个时候View的最终大小就是MeasureSpec测量的值,它对应于LayoutParams中的Match_parent和具体数值的两种情况。

AT_MOST 
父容器指定了一个可用大小即SpecSize,View的大小不能大于这个值,具体多少,要看View的具体情况,它对应于LayoutParams的wrap_content这种情况。

View的宽高受View自身的LayoutParams和父容器的约束所决定,转换成相应的MeasureSpec,一旦MeasureSpec确定,onMeasure中就可以View测量的宽和高。

对于普通的View来说,这里是指我们布局中的View,View的measure过程是由ViewGroup传递过来的,先看一下ViewGroup的measureChiled

2. View的工作流程:

View的工作流程是指measure,layout,draw这三大流程,即测量,布局和绘制,其中measure是测量view的宽和高,layout是确定View控件四个顶点的位置,而draw则将View绘制在屏幕上。

2.1.1  View的measure过程: 
View的measure过程由其measure方法来完成,measure方法是一个final类型,这意味着子类不能去重新这个方法,而measure方法中会调用onMeasure这个方法,因此我们去重写onMeasure这个方法。

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(),widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(),widthMeasureSpec));
    }
  • 1
  • 2
  • 3
  • 4
  • 5

setMeasureDimension会设置View宽和高的测量值,下面看一下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;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

如果测量模式是MeasureSpec.UNSPECIFIED 则默认值为size,是其它两种模式的话,就用测量的值,测量的值为specSize. 
下面看一下,getSuggestedMinimumWidth这个方法:

protected int getSuggestedMinimumWidth() {
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }
  • 1
  • 2
  • 3

返回值得大小取决于控件view有没有设置背景,有设置背景,那么view的宽或者高的大小为mMinWidth和背景的最小宽或者高,如果没有背景,那么view的宽/高的测量返回这就是mMinWidth这个值,这个值为0。

下面我们实际书写写关于onMeasure方法的代码:

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int mWidth = 0;//这是我们想要设置的宽度
        int mHeight = 0;//这是我们想要设置的高度
        int widthMode=MeasureSpec.getMode(widthMeasureSpec);
        int heightMode=MeasureSpec.getMode(heightMeasureSpec);
        int widthSize=MeasureSpec.getSize(widthMeasureSpec);
        int heightSize=MeasureSpec.getSize(heightMeasureSpec);

        if(widthMeasureSpec==MeasureSpec.AT_MOST && heightMeasureSpec==MeasureSpec.AT_MOST){
            setMeasuredDimension(mWidth, mHeight);
        }else if(widthMode==MeasureSpec.AT_MOST){
            setMeasuredDimension(mWidth, heightSize);
        }else if(heightMode==MeasureSpec.AT_MOST){
            setMeasuredDimension(widthSize, mHeight);
        }
    }

2.1.2 ViewGroup的测量过程: 
对于ViewGroup来说,除了完成自己的measure过程外,还要遍历去测量子View的measure方法,各个子元素在递归去执行这个过程,和View不同的是,ViewGroup是一个抽象类,因此它没有重写View的onMeasure方法,但它提供了一个叫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);
            }
        }
    }

从代码中可以看出来,在measureChildren方法中,会遍历所有子View,然后通过measureChild这个方法去测量每一个子View。 
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);
    }

最后一行表明,去调用view的measure方法,在最后调用onMeasure方法,这又回到了View的measure过程。

实际情况可以这么写:

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
    {
        /**
         * 显示的设置一个宽度
         */
        if (!once)
        {
            LinearLayout wrapper = (LinearLayout) getChildAt(0);
            menu = (ViewGroup) wrapper.getChildAt(0);
            ViewGroup content = (ViewGroup) wrapper.getChildAt(1);
            mMenuWidth = mScreenWidth - mMenuRightPadding;
            mHalfMenuWidth = mMenuWidth / 2;
            menu.getLayoutParams().width = mMenuWidth;
            content.getLayoutParams().width = mScreenWidth;
        }
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

在onMeasure完后可以通过getMeasureWidth和getMeasureHeight来得到测量view的宽和高,在某些极端情况下,系统可能需要多次测量,在这种情况下,我们通过getMeasureWidth和getMeasureHeight拿到的宽和高是不准确的,一个较好的习惯是在onLayout方法中获取View的测量宽或者高。

可能遇到的问题: 

View的onMeasure和Acitivity的生命周期是不同步的,

在onCreat,onStart,onResume中getMeasureWidth/getMeasureHeight获得宽和高,很可能是0,这里给出了4中解决方法。

1.Activity中的onWindowFocesChanged方法,通过上面的方法可以获得测量后的宽高。单这个方法会被调用多次。

@Override
    public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);
        if(hasFocus){
            int mWidth=view.getMeasuredWidth();
            int height=view.getMeasuredHeight();
        }
    }

2.view.post(runnable)方法: 通过post可以将一个runnable投递到消息队列的尾部,然后等待Looper调用此runnable的时候,View也初始化好了。

view.post(new Runnable() {

            @Override
            public void run() {
                int mWidth=view.getMeasuredWidth();
                int mHeight=view.getMeasuredHeight();

            }
        });

3.ViewTreeObserver 使用OnGlobalLayoutListener这个接口,当View树的状态发生改变或者View树内部View的可见性发生改变的时候,onGlobalLayout方法会被回调,需要注意的是随着View树的改变,这个方法会被调用多次:

ViewTreeObserver observer=view.getViewTreeObserver();
                observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {

                    @Override
                    public void onGlobalLayout() {
                        view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                        int mWidth=view.getMeasuredWidth();
                        int mHeight=view.getMeasuredHeight();
                    }
                });

4.view.measure(int widthMeasureSpec,int heightMeasureSpec ) 比较复杂,这里不做解释了。

2.2  Layout过程: 
Layout的作用是ViewGroup用来确定子元素的位置,当ViewGroup的位置被确定后,它在onLayout方法中会遍历所有子元素,并调用layout方法。而layout方法中onLayout方法会被调用,所以我们重写这个方法。Layout方法和Measure相比要简单许多,layout是确定View本身的位置,而onLayout方法会确定所有子元素的位置,先看View的layout方法:

 public void layout(int l, int t, int r, int b) {
        if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
            onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        }
        int oldL = mLeft;
        int oldT = mTop;
        int oldB = mBottom;
        int oldR = mRight;
        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            onLayout(changed, l, t, r, b);
            mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnLayoutChangeListeners != null) {
                ArrayList<OnLayoutChangeListener> listenersCopy =
                        (ArrayList<OnLayoutChangeListener>)li.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 &= ~PFLAG_FORCE_LAYOUT;
        mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
    }

在第16行走onLayout方法,layout方法的流程是:首先通过setFrame方法来设定View四个顶点的位置,即初始化了,l,t,r,b这四个值,View的这四个顶点一确定,那么View在父容器中的位置也就确定了,接着会被调用onLayout方法,这个方法的用途是为了确定子View在父控件中的位置,和onMeasure类似,onLayout的实现和具体的布局有关,view和viewGroup军没有实现onLayout方法,因此我们随便找一个父控件看onLayout方法,例如LinearLayout中的onLayout方法:

 @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (mOrientation == VERTICAL) {
            layoutVertical(l, t, r, b);
        } else {
            layoutHorizontal(l, t, r, b);
        }
    }

2.3 Draw过程: 
draw过程的原里就简单了,它的作用是将View绘制到屏幕上,View的绘制过程遵循如下几步: 
1.绘制背景 2.绘制自己 3.绘制children 4.绘制装饰。 
draw方法的代码如下:

public void draw(Canvas canvas) {
        if (mClipBounds != null) {
            canvas.clipRect(mClipBounds);
        }
        final int privateFlags = mPrivateFlags;
        final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
                (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
        mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
        /*
         * Draw traversal performs several drawing steps which must be executed
         * in the appropriate order:
         *
         *      1. Draw the background
         *      2. If necessary, save the canvas' layers to prepare for fading
         *      3. Draw view's content
         *      4. Draw children
         *      5. If necessary, draw the fading edges and restore layers
         *      6. Draw decorations (scrollbars for instance)
         */
        // Step 1, draw the background, if needed
        int saveCount;
        if (!dirtyOpaque) {
            final Drawable background = mBackground;
            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);
                }
            }
        }
        // skip step 2 & 5 if possible (common case)
        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);
            if (mOverlay != null && !mOverlay.isEmpty()) {
                mOverlay.getOverlayView().dispatchDraw(canvas);
            }
            // we're done...
            return;
        }
        /*
         * Here we do the full fledged routine...
         * (this is an uncommon case where speed matters less,
         * this is why we repeat some of the tests that have been
         * done above)
         */
        boolean drawTop = false;
        boolean drawBottom = false;
        boolean drawLeft = false;
        boolean drawRight = false;
        float topFadeStrength = 0.0f;
        float bottomFadeStrength = 0.0f;
        float leftFadeStrength = 0.0f;
        float rightFadeStrength = 0.0f;
        // Step 2, save the canvas' layers
        int paddingLeft = mPaddingLeft;
        final boolean offsetRequired = isPaddingOffsetRequired();
        if (offsetRequired) {
            paddingLeft += getLeftPaddingOffset();
        }
        int left = mScrollX + paddingLeft;
        int right = left + mRight - mLeft - mPaddingRight - paddingLeft;
        int top = mScrollY + getFadeTop(offsetRequired);
        int bottom = top + getFadeHeight(offsetRequired);
        if (offsetRequired) {
            right += getRightPaddingOffset();
            bottom += getBottomPaddingOffset();
        }
        final ScrollabilityCache scrollabilityCache = mScrollCache;
        final float fadeHeight = scrollabilityCache.fadingEdgeLength;
        int length = (int) fadeHeight;
        // clip the fade length if top and bottom fades overlap
        // overlapping fades produce odd-looking artifacts
        if (verticalEdges && (top + length > bottom - length)) {
            length = (bottom - top) / 2;
        }
        // also clip horizontal fades if necessary
        if (horizontalEdges && (left + length > right - length)) {
            length = (right - left) / 2;
        }
        if (verticalEdges) {
            topFadeStrength = Math.max(0.0f, Math.min(1.0f, getTopFadingEdgeStrength()));
            drawTop = topFadeStrength * fadeHeight > 1.0f;
            bottomFadeStrength = Math.max(0.0f, Math.min(1.0f, getBottomFadingEdgeStrength()));
            drawBottom = bottomFadeStrength * fadeHeight > 1.0f;
        }
        if (horizontalEdges) {
            leftFadeStrength = Math.max(0.0f, Math.min(1.0f, getLeftFadingEdgeStrength()));
            drawLeft = leftFadeStrength * fadeHeight > 1.0f;
            rightFadeStrength = Math.max(0.0f, Math.min(1.0f, getRightFadingEdgeStrength()));
            drawRight = rightFadeStrength * fadeHeight > 1.0f;
        }
        saveCount = canvas.getSaveCount();
        int solidColor = getSolidColor();
        if (solidColor == 0) {
            final int flags = Canvas.HAS_ALPHA_LAYER_SAVE_FLAG;
            if (drawTop) {
                canvas.saveLayer(left, top, right, top + length, null, flags);
            }
            if (drawBottom) {
                canvas.saveLayer(left, bottom - length, right, bottom, null, flags);
            }
            if (drawLeft) {
                canvas.saveLayer(left, top, left + length, bottom, null, flags);
            }
            if (drawRight) {
                canvas.saveLayer(right - length, top, right, bottom, null, flags);
            }
        } else {
            scrollabilityCache.setFadeColor(solidColor);
        }
        // Step 3, draw the content
        if (!dirtyOpaque) onDraw(canvas);
        // Step 4, draw the children
        dispatchDraw(canvas);
        // Step 5, draw the fade effect and restore layers
        final Paint p = scrollabilityCache.paint;
        final Matrix matrix = scrollabilityCache.matrix;
        final Shader fade = scrollabilityCache.shader;
        if (drawTop) {
            matrix.setScale(1, fadeHeight * topFadeStrength);
            matrix.postTranslate(left, top);
            fade.setLocalMatrix(matrix);
            canvas.drawRect(left, top, right, top + length, p);
        }
        if (drawBottom) {
            matrix.setScale(1, fadeHeight * bottomFadeStrength);
            matrix.postRotate(180);
            matrix.postTranslate(left, bottom);
            fade.setLocalMatrix(matrix);
            canvas.drawRect(left, bottom - length, right, bottom, p);
        }
        if (drawLeft) {
            matrix.setScale(1, fadeHeight * leftFadeStrength);
            matrix.postRotate(-90);
            matrix.postTranslate(left, top);
            fade.setLocalMatrix(matrix);
            canvas.drawRect(left, top, left + length, bottom, p);
        }
        if (drawRight) {
            matrix.setScale(1, fadeHeight * rightFadeStrength);
            matrix.postRotate(90);
            matrix.postTranslate(right, top);
            fade.setLocalMatrix(matrix);
            canvas.drawRect(right - length, top, right, bottom, p);
        }
        canvas.restoreToCount(saveCount);
        // Step 6, draw decorations (scrollbars)
        onDrawScrollBars(canvas);
        if (mOverlay != null && !mOverlay.isEmpty()) {
            mOverlay.getOverlayView().dispatchDraw(canvas);
        }
    }

View绘制过程的传递是通过dispatchDraw来实现的,dispatchDraw会遍历所有子元素的draw方法,如此draw事件就一层层的传递下去。


3. 自定义View所涉及到的一些有关问题:

1.如果有必要让你的View支持padding

这是因为直接继承View的控件,如果不在draw方法中处理paddding,那么padding属性无法起作用,另外直接继承自ViewGroup的控件需要在onMeasure和onLayout中考虑padding和子元素的margin对其造成的影响,不然将导致padding和子元素的margin失效。 
在onDraw中考虑padding的代码:

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int paddingLeft=getPaddingLeft();
        int paddingRight=getPaddingRight();
        int paddingTop=getPaddingTop();
        int paddingBottom=getPaddingBottom();

        int mWidth=getWidth()-paddingLeft-getPaddingRight();
        int mHeight=getHeight()-paddingTop-paddingBottom;

        canvas.drawCircle(mWidth/2, mHeight/2,50,paint);
    }

为了使用自定义属性,必须在布局文件中添加schemas声明:xmlns:app=”http:android.com/apl/res-auto”,在这个声明中,app是自定义的,可以换成任意名字,但是自定义属性xml使用的时候必须和这个一致,也有另外一种声明方式,如:schemas:xmlns:app=”http://schemas.android.com/apk/res/com.ryg.chapter_4”,这种方式是apk/res后面附加应用的包名,但是这种方式并没有本质的区别。

继承ViewGroop的onMeasure和onLayout方法:

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int mWidth=0;
        int mHeight=0;
        int mChildCount=getChildCount();
        measureChildren(widthMeasureSpec, heightMeasureSpec);
        int widthSpec=MeasureSpec.getSize(widthMeasureSpec);
        int heightSpec=MeasureSpec.getSize(heightMeasureSpec);
        int widthMode=MeasureSpec.getMode(widthMeasureSpec);
        int HeightMode=MeasureSpec.getMode(heightMeasureSpec);

        if(mChildCount==0){
            setMeasuredDimension(0, 0);
        }else{
            if(MeasureSpec.AT_MOST==widthMode && MeasureSpec.AT_MOST==HeightMode){
                setMeasuredDimension(mWidth, mHeight);
            }else if(MeasureSpec.AT_MOST==widthMode){
                setMeasuredDimension(mWidth, heightSpec);
            }else if(MeasureSpec.AT_MOST==HeightMode){
                setMeasuredDimension(widthMode, mHeight);
            }
        }


    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
        int childLeft=0;
        int childCount=getChildCount();
        for(int i=0;i<childCount;i++){
            View childView=getChildAt(i);
            if(childView.getVisibility() !=View.GONE){
                int childWidth=childView.getMeasuredWidth();
                childView.layout(childLeft, 0, childLeft+childWidth, childView.getMeasuredHeight());
                childLeft+=childLeft;
            }
        }
    }


一些其他问题 :

1.  自定义View如何考虑机型适配

这里要考虑的是屏幕的问题,屏幕的分辨率,大小,进而引申到密度

  • 合理使用warp_content,match_parent.
  • 尽可能的是使用RelativeLayout
  • 针对不同的机型,使用不同的布局文件放在对应的目录下,android会自动匹配。
  • 尽量使用点9图片。
  • 使用与密度无关的像素单位dp,sp
  • 引入android的百分比布局。
  • 切图的时候切大分辨率的图,应用到布局当中。在小分辨率的手机上也会有很好的显示效果。

2. View刷新机制

Android的布局体系中,父View负责刷新、布局显示子View;而当子View需要刷新时,则是通知父View来完成:

1. 子View调用invalidate时,首先找到自己父View(View的成员变量mParent记录自己的父View),然后将AttachInfo中保存的信息告诉父View刷新自己。

2。invalidate中,调用父ViewinvalidateChild,向上回溯的过程,每一层的父View都将自己的显示区域与传入的刷新Rect做交集。

3.这个向上回溯的过程直到ViewRoot那里结束ViewRoot对这个最终的刷新区域做刷新

另外:

Invalidate()方法不能放在线程中,所以需要把Invalidate()方法放在Handler中。在MyThread中只需要在规定时间内发送一个Message给handler,当Handler接收到消息就调用Invalidate()方法。

postInvalidate()方法就可以放在线程中做处理,就不需要Handler。

Invalidate()方法在SDK中是这样描述的:Invalidatethe whole view. If the view is visible, onDraw(Canvas) will be called at somepoint in the future. This must be called from a UI thread. To call from anon-UI thread, call postInvalidate().  当Invalidate()被调用的时候,View的OnDraw()就会被调用,Invalidate()必须是在UI线程中被调用,如果在新线程中更新视图的就调用postInvalidate()。

3. 自定义View如何提供获取View属性的接口?

1、添加自定义View的属性文件
在res/values中新建attrs.xml文件(文件名可另取,不过推荐用attrs.xml,可以将自定义属性都放入其中),内容为

复制代码
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="myViewDefinedAttr"> 
<attr name="attr1" format="boolean"/> 
<attr name="attr2" format="integer"/>
</declare-styleable> 
</resources>
复制代码

定义名为myViewDefinedAttr的属性列表,这个name命名也可以用下划线形式。name会在下面第二步中使用。

<attr name="attr1" format="boolean"/> 其中name为属性名,format为属性类型,可以为boolean, string, integer, dimension, float, reference, color, fraction, enum, flag及其混合。

2、自定义View中获取属性值

 public MyView(Context context, AttributeSet attrs, int defStyle){
        super(context, attrs, defStyle);
        getAttrs(context, attrs);
    }
    
    /**
     * 得到属性值
     * 
     * @param context
     * @param attrs
     */
    private void getAttrs(Context context, AttributeSet attrs) {
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.myViewDefinedAttr);
        attr1 = ta.getBoolean(R.styleable.myViewDefinedAttr_attr1, true);
        attr2 = ta.getInt(R.styleable.myViewDefinedAttr_attr2, 0);
        ta.recycle();
    }

3.3、调用自定义View

 <com.trinea.android.common.view.MyView
        android:id="@+id/app_list"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:fastScrollEnabled="true"
        myViewXmlns:attr1="false"
        myViewXmlns:attr2="1"
        android:focusable="true" />

4. 渲染机制

1.知识储备

CPU: 中央处理器,它集成了运算,缓冲,控制等单元,包括绘图功能.CPU将对象处理为多维图形,纹理(Bitmaps、Drawables等都是一起打包到统一的纹理).

GPU:一个类似于CPU的专门用来处理Graphics的处理器, 作用用来帮助加快格栅化操作,当然,也有相应的缓存数据(例如缓存已经光栅化过的bitmap等)机制。

OpenGL ES是手持嵌入式设备的3DAPI,跨平台的、功能完善的2D和3D图形应用程序接口API,有一套固定渲染管线流程. 附相关OpenGL渲染流程资料

DisplayList 在Android把XML布局文件转换成GPU能够识别并绘制的对象。这个操作是在DisplayList的帮助下完成的。DisplayList持有所有将要交给GPU绘制到屏幕上的数据信息。

格栅化 是 将图片等矢量资源,转化为一格格像素点的像素图,显示到屏幕上,过程图如下.

垂直同步VSYNC:让显卡的运算和显示器刷新率一致以稳定输出的画面质量。它告知GPU在载入新帧之前,要等待屏幕绘制完成前一帧。下面的三张图分别是GPU和硬件同步所发生的情况,Refresh Rate:屏幕一秒内刷新屏幕的次数,由硬件决定,例如60Hz.而Frame Rate:GPU一秒绘制操作的帧数,单位是30fps,正常情况过程图如下.


2. 渲染流程线

UI对象—->CPU处理为多维图形,纹理 —–通过OpeGL ES接口调用GPU—-> GPU对图进行光栅化(Frame Rate ) —->硬件时钟(Refresh Rate)—-垂直同步—->投射到屏幕

图片名称

渲染时间线

Android系统每隔16ms发出VSYNC信号(1000ms/60=16.66ms),触发对UI进行渲染, 如果每次渲染都成功,这样就能够达到流畅的画面所需要的60fps,为了能够实现60fps,这意味着计算渲染的大多数操作都必须在16ms内完成。

正常情况

这里写图片描述

渲染超时,计算渲染时间超过16ms

当这一帧画面渲染时间超过16ms的时候,垂直同步机制会让显示器硬件 等待GPU完成栅格化渲染操作, 
这样会让这一帧画面,多停留了16ms,甚至更多.这样就这造成了 用户看起来 画面停顿

当GPU渲染速度过慢,就会导致如下情况,某些帧显示的画面内容就会与上一帧的画面相同

GPU超时情况


3.渲染时会出现的问题

GPU过度绘制

GPU的绘制过程,就跟刷墙一样,一层层的进行,16ms刷一次.这样就会造成,图层覆盖的现象,即无用的图层还被绘制在底层,造成不必要的浪费.

这里写图片描述

过度绘制查看工具

在手机端的开发者选项里,有OverDraw监测工具,调试GPU过度绘制工具
其中颜色代表渲染的图层情况,分别代表1层,2层,3层,4层覆盖.

这里写图片描述

手机的Monitor GPU Rendering

这里写图片描述

计算渲染的耗时

任何时候View中的绘制内容发生变化时,都会重新执行创建DisplayList,渲染DisplayList,更新到屏幕上等一 系列操作。这个流程的表现性能取决于你的View的复杂程度,View的状态变化以及渲染管道的执行性能。

举个例子,当View的大小发生改变,DisplayList就会重新创建,然后再渲染,而当View发生位移,则DisplayList不会重新创建,而是执行重新渲染的操作.

当你的View过于复杂,操作又过于复杂,就会计算渲染时间超过16ms,产生卡顿问题

渲染耗时呈现工具

工具中,不同手机呈现方式可能会有差别.分别关于StatusBar,NavBar,激活的程序Activity区域的GPU Rending信息。激活的程序Activity区域的GPU Rending信息。

界面上会滚动显示垂直的柱状图来表示每帧画面所需要渲染的时间,柱状图越高表示花费的渲染时间越长。

中间有一根绿色的横线,代表16ms,我们需要确保每一帧花费的总时间都低于这条横线,这样才能够避免出现卡顿的问题。

这里写图片描述

每一条柱状线都包含三部分, 
蓝色代表测量绘制Display List的时间, 
红色代表OpenGL渲染Display List所需要的时间, 
黄色代表CPU等待GPU处理的时间。


4. 如何优化

有人会说这些小地方,不值得优化.但是当你用的是低配机器,内存到饱和,CPU运算到达饱和,就像一个界面要做很多交互,绘制,加载图片,请求网络.后,一个小问题就会导致页面卡顿(就像我手机的淘宝客户端…),OOM,项目崩溃.

是的,这就是力量~

Android系统已经对它优化

在Android里面那些由主题所提供的资源,例如Bitmaps,Drawables都是一起打包到统一的Texture纹理当中,然后再传递到 GPU里面,这意味着每次你需要使用这些资源的时候,都是直接从纹理里面进行获取渲染的。

我们要做的优化

扁平化处理,防止过度绘制OverDraw

1.每一个layout的最外层父容器 是否需要?

这里写图片描述

2.布局层级优化

进行检测时,可能会让多种检测工具冲突,用Android Device Monitor的时候,最好关闭相关手机上的开发者检测工具开关. 
查看自己的布局,深的层级,是否可以做优化. 
渲染比较耗时(颜色就能看出来),想办法能否减少层级以及优化每一个View的渲染时间.

Hierarchy Viewer工具

他是查看耗时情况,和布局树的深度的工具.

这里写图片描述

3.图片选择

Android的界面能用png最好是用png了,因为32位的png颜色过渡平滑且支持透明。jpg是像素化压缩过的图片,质量已经下降了,再拿来做9path的按钮和平铺拉伸的控件必然惨不忍睹,要尽量避免。

对于颜色繁杂的,照片墙纸之类的图片(应用的启动画面喜欢搞这种),那用jpg是最好不过了,这种图片压缩前压缩后肉眼分辨几乎不计,如果保存成png体积将是jpg的几倍甚至几十倍,严重浪费体积。

4.清理不必要的背景

这里写图片描述

5.当背景无法避免,尽量用Color.TRANSPARENT

因为透明色Color.TRANSPARENT是不会被渲染的,他是透明的.



//优化前
//优化前: 当图片不为空,ImageView加载图片,然后统一设置背景
Bean bean=list.get(i);
 if (bean.img == 0) { Picasso.with(getContext()).load(bean.img).into(holder.imageView); } chat_author_avatar.setBackgroundColor(bean.backPic); 

 

//优化后
//优化后:当图片不为空,ImageView加载图片,并设置背景为TRANSPARENT;
//当图片为空,ImageView加载TRANSPARENT,然后设置背景为无照片背景
Bean bean=list.get(i);
 if (bean.img == 0) { Picasso.with(getContext()).load(android.R.color.transparent).into(holder.imageView); holder.imageView.setBackgroundColor(bean.backPic); } else { Picasso.with(getContext()).load(bean.img).into(holder.imageView); holder.imageView.setBackgroundColor(Color.TRANSPARENT); } 

 

————-对比结果——————– 
这里写图片描述

6.优化自定义View的计算

View中的方法OnMeasure,OnLayout,OnDraw.在我们自定义View起到了决定作用,我们要学会研究其中的优化方法.

学会裁剪掉View的覆盖部分,增加cpu的计算量,来优化GPU的渲染


  /**
     * Intersect the current clip with the specified rectangle, which is
     * expressed in local coordinates.
     *
     * @param left   The left side of the rectangle to intersect with the
     *               current clip
     * @param top    The top of the rectangle to intersect with the current clip
     * @param right The right side of the rectangle to intersect with the * current clip * @param bottom The bottom of the rectangle to intersect with the current * clip * @return true if the resulting clip is non-empty */ public boolean clipRect(float left, float top, float right, float bottom) { return native_clipRect(mNativeCanvasWrapper, left, top, right, bottom, Region.Op.INTERSECT.nativeInt); } 

 


5.总结

性能优化其实不仅仅是一种技术,而是一种思想,你只听过它的高大上,却不知道它其实就是各个细节处的深入研究和处理.

当然,有的时候也需要自己进行权衡效果和性能,根据需求进行选择.

还有,Android Device Monitor 是个好东西~简直就是性能优化大本营,性能优化的工具基本都在其中.

所以在平时的开发过程中,养成良好的思考习惯,是第一步~

写代码的时候要想: 
1.你的代码是不是多余? 
2.你的对象有没有必要在循环中创建? 
3.你的计算方法是不是最优?

画界面的时候要想: 
1.布局是否有背景? 
2.是否可以删掉多余的布局? 
3.自定义View是否进行了裁剪处理? 
4.布局是否扁平化,移除非必需的UI组?

最后,Android Device Monitor 是个好东西~ 性能优化的工具基本都在其中.

参考 : https://www.cnblogs.com/ldq2016/p/6668148.html



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值