Android经典面试题之View的invalidate方法和postInvalidate方法有什么区别

2024-07-22 18:36:49 浏览数 (2)

我们在自定义View时免不了要使用invalidate方法,这个方法的作用大家也比较清楚,就是让我们的View进行刷新重新绘制的。但是postInvalidate方法可能就不是那么熟悉了,因为平时开发时invalidate方法相对而言会用得比较多。不过需要大家注意的是,面试官在问到View相关的问题时,就很有可能会问到postInvalidate方法,所以我们还是有必要来学习一下。

那invalidate方法和postInvalidate方法到底有什么区别呢?

invalidate方法和postInvalidate方法的区别

其实答案也很简单,就一句话:

invalidate方法和postInvalidate方法都是用于进行View的刷新,invalidate方法应用在UI线程中,而postInvalidate方法应用在非UI线程中,用于将线程切换到UI线程,postInvalidate方法最后调用的也是invalidate方法。

当然,空口无凭,我们还是来看看源码

invalidate方法和postInvalidate方法源码分析

我们先来看看View中的postInvalidate方法

代码语言:javascript复制
@UiThread
public class View implements Drawable.Callback, KeyEvent.Callback,AccessibilityEventSource {
    
    ...
    
    public void postInvalidate() {
        postInvalidateDelayed(0);
    }
    
    public void postInvalidate(int left, int top, int right, int bottom) {
        postInvalidateDelayed(0, left, top, right, bottom);
    }
    
    public void postInvalidateDelayed(long delayMilliseconds) {
        final AttachInfo attachInfo = mAttachInfo;
        if (attachInfo != null) {
            attachInfo.mViewRootImpl.dispatchInvalidateDelayed(this, delayMilliseconds);
        }
    }
    ...      
}

从源码中我们可以看到,postInvalidate方法最后调用了ViewRootImpl的dispatchInvalidateDelayed方法

代码语言:javascript复制
//ViewRootImpl.class

final ViewRootHandler mHandler = new ViewRootHandler();

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

ViewRootImpl中的dispatchInvalidateDelayed方法就是像ViewRootHandler发送了一个MSG_INVALIDATE消息,ViewRootHandler是ViewRootImpl中的一个内部Handler类

代码语言:javascript复制
final class ViewRootHandler extends Handler {
    @Override
    public String getMessageName(Message message) {
        switch (message.what) {
            case MSG_INVALIDATE:
                return "MSG_INVALIDATE";
            ...
        }
        return super.getMessageName(message);
    }

    ...

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

我们可以看到ViewRootHandler中对于MSG_INVALIDATE消息的处理就是调用的View的invalidate方法。

总结

综上源码我们可以得出结论:

(1)postInvalidate方法调用了ViewRootImpl中的dispatchInvalidateDelayed方法向ViewRootImpl中的ViewRootHandler发送一个消息,最后调用的还是View的invalidate方法。

(2)因为ViewRootImpl是在UI线程的,所以postInvalidate方法的作用就是将非UI线程的刷新操作切换到UI线程,以便在UI线程中调用invalidate方法刷新View。所以如果我们自定义的View本身就是在UI线程,没有用到多线程的话,直接用invalidate方法来刷新View就可以了。而我们平时自定义View基本上都没有开起其他线程,所以这就是我们很少接触postInvalidate方法的原因

(3)所以一句话总结postInvalidate方法的作用就是:实现了消息机制,可以使我们在非UI线程也能调用刷新View的方法。


invalidate方法刷新View的调用过程分析
代码语言:javascript复制
//View.class
@UiThread
public class View implements Drawable.Callback, KeyEvent.Callback,AccessibilityEventSource {

    ...
    
    public void invalidate() {
        invalidate(true);
    }
    
    //invalidateCache为true表示全部重绘
    void invalidate(boolean invalidateCache) {
        invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
    }

    void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
            boolean fullInvalidate) {
        ...

        if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)) == (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)
                || (invalidateCache && (mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID)
                || (mPrivateFlags & PFLAG_INVALIDATED) != PFLAG_INVALIDATED
                || (fullInvalidate && isOpaque() != mLastIsOpaque)) {
                
            if (fullInvalidate) {
                mLastIsOpaque = isOpaque();
                mPrivateFlags &= ~PFLAG_DRAWN;
            }

            mPrivateFlags |= PFLAG_DIRTY;

            if (invalidateCache) {
                mPrivateFlags |= PFLAG_INVALIDATED;
                mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
            }

            //重点就在这里!!!
            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);
                p.invalidateChild(this, damage);
            }

            ...
        }
    }
}

View的invalidate方法最后调用的是invalidateInternal方法,invalidateInternal方法中的重点逻辑在上面已经标记出来了。

  • 其中的damage变量表示的是需要进行重绘的区域,后面在一系列的调用过程中会不断根据父布局来调整这个绘制区域。
  • invalidateInternal方法中通过调用View的父布局invalidateChild方法来请求重绘。那View的父布局是谁呢?这里有2种情况:要么是ViewGroup,要么就是ViewRootImpl类了。

我们先来看看ViewGroup中的invalidateChild方法

代码语言:javascript复制
@UiThread
public abstract class ViewGroup extends View implements ViewParent, ViewManager {

    @Override
    public final void invalidateChild(View child, final Rect dirty) {
        ViewParent parent = this;

        final AttachInfo attachInfo = mAttachInfo;
        if (attachInfo != null) {
            ...
            //这里省略了一些重新计算绘制区域的逻辑

            //这是一个从当前的布局View向上不断遍历当前布局View的父布局,最后遍历到ViewRootImpl的循环
            do {
                View view = null;
                if (parent instanceof View) {
                    view = (View) parent;
                }

                if (drawAnimation) {
                    if (view != null) {
                        view.mPrivateFlags |= PFLAG_DRAW_ANIMATION;
                    } else if (parent instanceof ViewRootImpl) {
                        ((ViewRootImpl) parent).mIsAnimating = true;
                    }
                }

                ...
                
                //这里调用的是父布局的invalidateChildInParent方法
                parent = parent.invalidateChildInParent(location, dirty);
                ...
            } while (parent != null);
        }
    }
    
    @Override
    public ViewParent invalidateChildInParent(final int[] location, final Rect dirty) {
        if ((mPrivateFlags & PFLAG_DRAWN) == PFLAG_DRAWN ||
                (mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID) {
            if ((mGroupFlags & (FLAG_OPTIMIZE_INVALIDATE | FLAG_ANIMATION_DONE)) !=
                        FLAG_OPTIMIZE_INVALIDATE) {
                ...
                //这里也是一些计算绘制区域的内容

                return mParent;

            } else {
                mPrivateFlags &= ~PFLAG_DRAWN & ~PFLAG_DRAWING_CACHE_VALID;

                ...
                //这里也是一些计算绘制区域的内容

                return mParent;
            }
        }

        return null;
    }
}

在ViewGroup的invalidateChild方法中有一个循环,循环里面会一直调用父布局的invalidateChildInParent方法,而View和ViewGroup的最终父布局都是ViewRootImpl

所以View中的invalidateInternal方法和ViewGroup中的invalidateChild方法最后殊途同归,都会调用到ViewRootImpl中的方法

代码语言:javascript复制
public final class ViewRootImpl implements ViewParent,View.AttachInfo.Callbacks, ThreadedRenderer.HardwareDrawCallbacks {
    
    //如果View没有父布局,那invalidateInternal方法就会调用这个方法
    @Override
    public void invalidateChild(View child, Rect dirty) {
        invalidateChildInParent(null, dirty);
    }

    //ViewGroup的invalidateChild方法最后会调用到这里
    @Override
    public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
        checkThread();

    //如果dirty为null就表示要重绘当前ViewRootImpl指示的整个区域
        if (dirty == null) {
            invalidate();
            return null;
        //如果dirty为empty则表示经过计算需要重绘的区域不需要绘制
        } else if (dirty.isEmpty() && !mIsAnimating) {
            return null;
        }

        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);
            }
        }

        invalidateRectOnScreen(dirty);

        return null;
    }   
    
    private void invalidateRectOnScreen(Rect dirty) {
        final Rect localDirty = mDirty;
        ...
        if (!mWillDrawSoon && (intersected || mIsAnimating)) {
            //调用scheduleTraversals方法进行绘制
            scheduleTraversals();
        }
    }
    
    //绘制整个ViewRootImpl区域
    void invalidate() {
        mDirty.set(0, 0, mWidth, mHeight);
        if (!mWillDrawSoon) {
            //调用scheduleTraversals方法进行绘制
            scheduleTraversals();
        }
    }
}

可以看到在ViewRootImpl中最后都会调用scheduleTraversals方法进行绘制。按照我们对于View的绘制过程的理解,View的绘制流程是从ViewRootImpl的performTraversals方法开始的,下面我们来看看ViewRootImpl中的scheduleTraversals方法。

代码语言:javascript复制
//ViewRootImpl.class
void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        //关键在这里!!!
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        if (!mUnbufferedInputDispatch) {
            scheduleConsumeBatchedInput();
        }
        notifyRendererOfFramePending();
        pokeDrawLockIfNeeded();
    }
}

居然没有我们要找的performTraversals方法。但是我们看到scheduleTraversals方法中调用了mChoreographer.postCallback方法,这里应该是关键

Choreoprapher类的作用是编排输入事件、动画事件和绘制事件的执行,它的postCallback方法的作用就是将要执行的事件放入内部的一个队列中,最后会执行传入的Runnable,这里也就是mTraversalRunnable。所以我们来看看mTraversalRunnable

代码语言:javascript复制
//ViewRootImpl.class
final class TraversalRunnable implements Runnable {
    @Override
    public void run() {
        doTraversal();
    }
}
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();

void doTraversal() {
        if (mTraversalScheduled) {
            mTraversalScheduled = false;
            mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

            if (mProfile) {
                Debug.startMethodTracing("ViewAncestor");
            }

            //找到我们的performTraversals方法来,这里就是开始绘制View的地方啦!
            performTraversals();

            if (mProfile) {
                Debug.stopMethodTracing();
                mProfile = false;
            }
        }
    }

当我们调用View的invalidate方法后,View会去不断向上调用父布局的绘制方法并在这个过程中计算需要重绘的区域,最终调用过程会走到ViewRootImpl中,调用的是ViewRootImpl的performTraversals执行重绘操作。

总结

  • 我们在自定义View时,当需要刷新View时,如果是在UI线程中,那就直接调用invalidate方法,如果是在非UI线程中,那就通过postInvalidate方法来刷新View
  • postInvalidate方法实现了消息机制,最终调用的也是invalidate方法来刷新View
  • invalidate方法最终调用的是ViewRootImpl中的performTraversals来重新绘制View

0 人点赞