View绘制流程第四部,终结曲~

324 阅读9分钟

5 View的invalidate和postInvalidate方法源码分析

你可能已经看见了,在上面分析View的三步绘制流程中最后都有调运一个叫invalidate的方法,这个方法是啥玩意?为何出现频率这么高?很简单,我们拿出来分析分析不就得了。

5-1 invalidate方法源码分析

来看一下View类中的一些invalidate方法(ViewGroup没有重写这些方法),如下:

/**
     * Mark the area defined by dirty as needing to be drawn. If the view is
     * visible, {@link #onDraw(android.graphics.Canvas)} will be called at some
     * point in the future.
     * <p>
     * This must be called from a UI thread. To call from a non-UI thread, call
     * {@link #postInvalidate()}.
     * <p>
     * <b>WARNING:</b> In API 19 and below, this method may be destructive to
     * {@code dirty}.
     *
     * @param dirty the rectangle representing the bounds of the dirty region
     */
     //看见上面注释没有?public,只能在UI Thread中使用,别的Thread用postInvalidate方法,View是可见的才有效,回调onDraw方法,针对局部View
    public void invalidate(Rect dirty) {
        final int scrollX = mScrollX;
        final int scrollY = mScrollY;
        //实质还是调运invalidateInternal方法
        invalidateInternal(dirty.left - scrollX, dirty.top - scrollY,
                dirty.right - scrollX, dirty.bottom - scrollY, true, false);
    }
    /**
     * Mark the area defined by the rect (l,t,r,b) as needing to be drawn. The
     * coordinates of the dirty rect are relative to the view. If the view is
     * visible, {@link #onDraw(android.graphics.Canvas)} will be called at some
     * point in the future.
     * <p>
     * This must be called from a UI thread. To call from a non-UI thread, call
     * {@link #postInvalidate()}.
     *
     * @param l the left position of the dirty region
     * @param t the top position of the dirty region
     * @param r the right position of the dirty region
     * @param b the bottom position of the dirty region
     */
     //看见上面注释没有?public,只能在UI Thread中使用,别的Thread用postInvalidate方法,View是可见的才有效,回调onDraw方法,针对局部View
    public void invalidate(int l, int t, int r, int b) {
        final int scrollX = mScrollX;
        final int scrollY = mScrollY;
        //实质还是调运invalidateInternal方法
        invalidateInternal(l - scrollX, t - scrollY, r - scrollX, b - scrollY, true, false);
    }
    /**
     * Invalidate the whole view. If the view is visible,
     * {@link #onDraw(android.graphics.Canvas)} will be called at some point in
     * the future.
     * <p>
     * This must be called from a UI thread. To call from a non-UI thread, call
     * {@link #postInvalidate()}.
     */
     //看见上面注释没有?public,只能在UI Thread中使用,别的Thread用postInvalidate方法,View是可见的才有效,回调onDraw方法,针对整个View
    public void invalidate() {
        //invalidate的实质还是调运invalidateInternal方法
        invalidate(true);
    }
    /**
     * This is where the invalidate() work actually happens. A full invalidate()
     * causes the drawing cache to be invalidated, but this function can be
     * called with invalidateCache set to false to skip that invalidation step
     * for cases that do not need it (for example, a component that remains at
     * the same dimensions with the same content).
     *
     * @param invalidateCache Whether the drawing cache for this view should be
     *            invalidated as well. This is usually true for a full
     *            invalidate, but may be set to false if the View's contents or
     *            dimensions have not changed.
     */
     //看见上面注释没有?default的权限,只能在UI Thread中使用,别的Thread用postInvalidate方法,View是可见的才有效,回调onDraw方法,针对整个View
    void invalidate(boolean invalidateCache) {
    //实质还是调运invalidateInternal方法
        invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
    }
    //!!!!!!看见没有,这是所有invalidate的终极调运方法!!!!!!
    void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
            boolean fullInvalidate) {
        ......
            // Propagate the damage rectangle to the parent view.
            final AttachInfo ai = mAttachInfo;
            final ViewParent p = mParent;
            if (p != null && ai != null && l < r && t < b) {
                final Rect damage = ai.mTmpInvalRect;
                //设置刷新区域
                damage.set(l, t, r, b);
                //传递调运Parent ViewGroup的invalidateChild方法
                p.invalidateChild(this, damage);
            }
            ......
    }

看见没有,View的invalidate(invalidateInternal)方法实质是将要刷新区域直接传递给了父ViewGroup的invalidateChild方法,在invalidate中,调用父View的invalidateChild,这是一个从当前向上级父View回溯的过程,每一层的父View都将自己的显示区域与传入的刷新Rect做交集 。所以我们看下ViewGroup的invalidateChild方法,源码如下:

 public final void invalidateChild(View child, final Rect dirty) {
        ViewParent parent = this;
        final AttachInfo attachInfo = mAttachInfo;
        ......
        do {
            ......
            //循环层层上级调运,直到ViewRootImpl会返回null
            parent = parent.invalidateChildInParent(location, dirty);
            ......
        } while (parent != null);
    }

这个过程最后传递到ViewRootImpl的invalidateChildInParent方法结束,所以我们看下ViewRootImpl的invalidateChildInParent方法,如下:

 @Override
    public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
        ......
        //View调运invalidate最终层层上传到ViewRootImpl后最终触发了该方法
        scheduleTraversals();
        ......
        return null;
    }

看见没有?这个ViewRootImpl类的invalidateChildInParent方法直接返回了null,也就是上面ViewGroup中说的,层层上级传递到ViewRootImpl的invalidateChildInParent方法结束了那个do while循环。看见这里调运的scheduleTraversals这个方法吗?scheduleTraversals会通过Handler的Runnable发送一个异步消息,调运doTraversal方法,然后最终调用performTraversals()执行重绘。开头背景知识介绍说过的,performTraversals就是整个View数开始绘制的起始调运地方,所以说View调运invalidate方法的实质是层层上传到父级,直到传递到ViewRootImpl后触发了scheduleTraversals方法,然后整个View树开始重新按照上面分析的View绘制流程进行重绘任务。

到此View的invalidate方法原理就分析完成了。

5-2 postInvalidate方法源码分析

上面分析invalidate方法时注释中说该方法只能在UI Thread中执行,其他线程中需要使用postInvalidate方法,所以我们来分析分析postInvalidate这个方法源码。如下:

public void postInvalidate() {
        postInvalidateDelayed(0);
    }

继续看下他的调运方法postInvalidateDelayed,如下:

   public void postInvalidateDelayed(long delayMilliseconds) {
        // We try only with the AttachInfo because there's no point in invalidating
        // if we are not attached to our window
        final AttachInfo attachInfo = mAttachInfo;
        //核心,实质就是调运了ViewRootImpl.dispatchInvalidateDelayed方法
        if (attachInfo != null) {
            attachInfo.mViewRootImpl.dispatchInvalidateDelayed(this, delayMilliseconds);
        }
    }

我们继续看他调运的ViewRootImpl类的dispatchInvalidateDelayed方法,如下源码:

 public void dispatchInvalidateDelayed(View view, long delayMilliseconds) {
        Message msg = mHandler.obtainMessage(MSG_INVALIDATE, view);
        mHandler.sendMessageDelayed(msg, delayMilliseconds);
    }

看见没有,通过ViewRootImpl类的Handler发送了一条MSG_INVALIDATE消息,继续追踪这条消息的处理可以发现:

public void handleMessage(Message msg) {
    ......
    switch (msg.what) {
    case MSG_INVALIDATE:
        ((View) msg.obj).invalidate();
        break;
    ......
    }
    ......
}

看见没有,实质就是又在UI Thread中调运了View的invalidate();方法,那接下来View的invalidate();方法我们就不说了,上名已经分析过了。

到此整个View的postInvalidate方法就分析完成了。

5-3 invalidate与postInvalidate方法总结

依据上面对View的invalidate分析我总结绘制如下流程图:

依据上面对View的postInvalidate分析我总结绘制如下流程图:

关于这两个方法的具体流程和原理上面也分析过了,流程图也给出了,相信已经很明确了,没啥需要解释的了。所以我们对其做一个整体总结,归纳出重点如下:

invalidate系列方法请求重绘View树(也就是draw方法),如果View大小没有发生变化就不会调用layout过程,并且只绘制那些“需要重绘的”View,也就是哪个View(View只绘制该View,ViewGroup绘制整个ViewGroup)请求invalidate系列方法,就绘制该View。

常见的引起invalidate方法操作的原因主要有:

  • 直接调用invalidate方法.请求重新draw,但只会绘制调用者本身。

  • 触发setSelection方法。请求重新draw,但只会绘制调用者本身。

  • 触发setVisibility方法。 当View可视状态在INVISIBLE转换VISIBLE时会间接调用invalidate方法,继而绘制该View。当View的可视状态在INVISIBLE\VISIBLE 转换为GONE状态时会间接调用requestLayout和invalidate方法,同时由于View树大小发生了变化,所以会请求measure过程以及draw过程,同样只绘制需要“重新绘制”的视图。

  • 触发setEnabled方法。请求重新draw,但不会重新绘制任何View包括该调用者本身。

  • 触发requestFocus方法。请求View树的draw过程,只绘制“需要重绘”的View。

5-4 通过invalidate方法分析结果回过头去解决一个背景介绍中的疑惑

分析完invalidate后需要你回过头去想一个问题。还记不记得这篇文章的开头背景介绍,我们说整个View绘制流程的最初代码是在ViewRootImpl类的performTraversals()方法中开始的。上面当时只是告诉你了这个结论,至于这个ViewRootImpl类的performTraversals()方法为何会被触发没有说明原因。现在我们就来分析一下这个触发的源头。

让我们先把大脑思考暂时挪回到《Android应用setContentView与LayoutInflater加载解析机制源码分析》这篇博文的setContentView机制分析中(不清楚的请点击先看这篇文章再回过头来继续看)。我们先来看下那篇博文分析的PhoneWindow的setContentView方法源码,如下:

@Override
    public void setContentView(View view, ViewGroup.LayoutParams params) {
        ......
        //如果mContentParent为空进行一些初始化,实质mContentParent是通过findViewById(ID_ANDROID_CONTENT);获取的id为content的FrameLayout的布局(不清楚的请先看《Android应用setContentView与LayoutInflater加载解析机制源码分析》文章)
        if (mContentParent == null) {
            installDecor();
        } 
        ......
        //把我们的view追加到mContentParent
        mContentParent.addView(view, params);
        ......
    }

这个方法是Activity中setContentView的实现,我们继续看下这个方法里调运的addView方法,也就是ViewGroup的addView方法,如下:

  public void addView(View child) {
        addView(child, -1);
    }
    public void addView(View child, int index) {
        ......
        addView(child, index, params);
    }
    public void addView(View child, int index, LayoutParams params) {
        ......
        //该方法稍后后面会详细分析
        requestLayout();
        //重点关注!!!
        invalidate(true);
        ......
    }

看见addView调运invalidate方法没有?这不就真相大白了。当我们写一个Activity时,我们一定会通过setContentView方法将我们要展示的界面传入该方法,该方法会讲我们界面通过addView追加到id为content的一个FrameLayout(ViewGroup)中,然后addView方法中通过调运invalidate(true)去通知触发ViewRootImpl类的performTraversals()方法,至此递归绘制我们自定义的所有布局。


6 View的requestLayout方法源码分析

6-1 requestLayout方法分析

和invalidate类似,其实在上面分析View绘制流程时或多或少都调运到了这个方法,而且这个方法对于View来说也比较重要,所以我们接下来分析一下他。如下View的requestLayout源码:

public void requestLayout() {
        ......
        if (mParent != null && !mParent.isLayoutRequested()) {
            //由此向ViewParent请求布局
            //从这个View开始向上一直requestLayout,最终到达ViewRootImpl的requestLayout
            mParent.requestLayout();
        }
        ......
    }

看见没有,当我们触发View的requestLayout时其实质就是层层向上传递,直到ViewRootImpl为止,然后触发ViewRootImpl的requestLayout方法,如下就是ViewRootImpl的requestLayout方法:

 @Override
    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();
            mLayoutRequested = true;
            //View调运requestLayout最终层层上传到ViewRootImpl后最终触发了该方法
            scheduleTraversals();
        }
    }

看见没有,类似于上面分析的invalidate过程,只是设置的标记不同,导致对于View的绘制流程中触发的方法不同而已。

6-2 requestLayout方法总结

可以看见,这些方法都是大同小异。对于requestLayout方法来说总结如下:

requestLayout()方法会调用measure过程和layout过程,不会调用draw过程,也不会重新绘制任何View包括该调用者本身。

7 View绘制流程总结

至此整个关于Android应用程序开发中的View绘制机制及相关重要方法都已经分析完毕。关于各个方法的总结这里不再重复,直接通过该文章前面的目录索引到相应方法的总结小节进行查阅即可。




                                   

                                                                 关注『DvlpNews』

                                                                  把握前沿技术脉搏