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

简介: 本文探讨了Android自定义View中`invalidate()`和`postInvalidate()`的区别。`invalidate()`在UI线程中刷新View,而`postInvalidate()`用于非UI线程,通过消息机制切换到UI线程执行`invalidate()`。源码分析显示,`postInvalidate()`最终调用`ViewRootImpl`的`dispatchInvalidateDelayed`,通过Handler发送消息到UI线程执行刷新。

本文首发于公众号“AntDream”,欢迎微信搜索“AntDream”关注,和我一起每天进步一点点

我们在自定义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方法

@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方法

//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类

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的调用过程分析

//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方法

@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中的方法

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方法。

//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

//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

欢迎关注我的公众号AntDream查看更多精彩文章!

目录
相关文章
|
18天前
|
消息中间件 存储 Java
Android面试高频知识点(2) 详解Android消息处理机制(Handler)
Android 消息处理机制估计都被写烂了,但是依然还是要写一下,因为Android应用程序是通过消息来驱动的,Android某种意义上也可以说成是一个以消息驱动的系统,UI、事件、生命周期都和消息处理机制息息相关,并且消息处理机制在整个Android知识体系中也是尤其重要,在太多的源码分析的文章讲得比较繁琐,很多人对整个消息处理机制依然是懵懵懂懂,这篇文章通过一些问答的模式结合Android主线程(UI线程)的工作原理来讲解,源码注释很全,还有结合流程图,如果你对Android 消息处理机制还不是很理解,我相信只要你静下心来耐心的看,肯定会有不少的收获的。
58 3
Android面试高频知识点(2) 详解Android消息处理机制(Handler)
|
11天前
|
SQL 安全 测试技术
[go 面试] 接口测试的方法与技巧
[go 面试] 接口测试的方法与技巧
|
13天前
|
机器学习/深度学习 算法 Python
【机器学习】面试问答:决策树如何进行剪枝?剪枝的方法有哪些?
文章讨论了决策树的剪枝技术,包括预剪枝和后剪枝的概念、方法以及各自的优缺点。
27 2
|
18天前
|
Android开发
Android面试高频知识点(1) 图解 Android 事件分发机制
在Android开发中,事件分发机制是一块Android比较重要的知识体系,了解并熟悉整套的分发机制有助于更好的分析各种点击滑动失效问题,更好去扩展控件的事件功能和开发自定义控件,同时事件分发机制也是Android面试必问考点之一,如果你能把下面的一些事件分发图当场画出来肯定加分不少。废话不多说,总结一句:事件分发机制很重要。
63 9
|
13天前
|
机器学习/深度学习
【机器学习】面试题:LSTM长短期记忆网络的理解?LSTM是怎么解决梯度消失的问题的?还有哪些其它的解决梯度消失或梯度爆炸的方法?
长短时记忆网络(LSTM)的基本概念、解决梯度消失问题的机制,以及介绍了包括梯度裁剪、改变激活函数、残差结构和Batch Normalization在内的其他方法来解决梯度消失或梯度爆炸问题。
27 2
|
13天前
|
存储 机器学习/深度学习 缓存
【数据挖掘】XGBoost面试题:与GBDT的区别?为什么使用泰勒二阶展开?为什么可以并行训练?为什么快?防止过拟合的方法?如何处理缺失值?
XGBoost与GBDT的区别、XGBoost使用泰勒二阶展开的原因、并行训练的原理、速度优势、防止过拟合的策略以及处理缺失值的方法,突出了XGBoost在提升模型性能和训练效率方面的一系列优化。
17 1
|
13天前
|
机器学习/深度学习
|
18天前
|
XML 前端开发 Android开发
Android面试高频知识点(3) 详解Android View的绘制流程
View的绘制和事件处理是两个重要的主题,上一篇《图解 Android事件分发机制》已经把事件的分发机制讲得比较详细了,这一篇是针对View的绘制,View的绘制如果你有所了解,基本分为measure、layout、draw 过程,其中比较难理解就是measure过程,所以本篇文章大幅笔地分析measure过程,相对讲得比较详细,文章也比较长,如果你对View的绘制还不是很懂,对measure过程掌握得不是很深刻,那么耐心点,看完这篇文章,相信你会有所收获的。
39 2
|
11天前
|
运维 监控 算法
[go 面试] 优化线上故障排查与性能问题的方法
[go 面试] 优化线上故障排查与性能问题的方法
|
11天前
|
Java 开发工具 Android开发
Android经典面试题之开发中常见的内存泄漏,以及如何避免和防范
本文介绍Android开发中内存泄漏的概念及其危害,并列举了四种常见泄漏原因:静态变量持有Context、非静态内部类、资源未释放及监听器未注销。提供了具体代码示例和防范措施,如使用ApplicationContext、弱引用、适时释放资源及利用工具检测泄漏。通过遵循这些建议,开发者可以有效提高应用稳定性和性能。
21 0