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查看更多精彩文章!

目录
相关文章
|
24天前
|
Android开发 UED 计算机视觉
Android自定义view之线条等待动画(灵感来源:金铲铲之战)
本文介绍了一款受游戏“金铲铲之战”启发的Android自定义View——线条等待动画的实现过程。通过将布局分为10份,利用`onSizeChanged`测量最小长度,并借助画笔绘制动态线条,实现渐变伸缩效果。动画逻辑通过四个变量控制线条的增长与回退,最终形成流畅的等待动画。代码中详细展示了画笔初始化、线条绘制及动画更新的核心步骤,并提供完整源码供参考。此动画适用于加载场景,提升用户体验。
252 5
Android自定义view之线条等待动画(灵感来源:金铲铲之战)
|
24天前
|
Android开发
Android自定义view之利用PathEffect实现动态效果
本文介绍如何在Android自定义View中利用`PathEffect`实现动态效果。通过改变偏移量,结合`PathEffect`的子类(如`CornerPathEffect`、`DashPathEffect`、`PathDashPathEffect`等)实现路径绘制的动态变化。文章详细解析了各子类的功能与参数,并通过案例代码展示了如何使用`ComposePathEffect`组合效果,以及通过修改偏移量实现动画。最终效果为一个菱形图案沿路径运动,源码附于文末供参考。
|
24天前
|
XML Java Android开发
Android自定义view之网易云推荐歌单界面
本文详细介绍了如何通过自定义View实现网易云音乐推荐歌单界面的效果。首先,作者自定义了一个圆角图片控件`MellowImageView`,用于绘制圆角矩形图片。接着,通过将布局放入`HorizontalScrollView`中,实现了左右滑动功能,并使用`ViewFlipper`添加图片切换动画效果。文章提供了完整的代码示例,包括XML布局、动画文件和Java代码,最终展示了实现效果。此教程适合想了解自定义View和动画效果的开发者。
135 65
Android自定义view之网易云推荐歌单界面
|
24天前
|
XML 前端开发 Android开发
一篇文章带你走近Android自定义view
这是一篇关于Android自定义View的全面教程,涵盖从基础到进阶的知识点。文章首先讲解了自定义View的必要性及简单实现(如通过三个构造函数解决焦点问题),接着深入探讨Canvas绘图、自定义属性设置、动画实现等内容。还提供了具体案例,如跑马灯、折线图、太极图等。此外,文章详细解析了View绘制流程(measure、layout、draw)和事件分发机制。最后延伸至SurfaceView、GLSurfaceView、SVG动画等高级主题,并附带GitHub案例供实践。适合希望深入理解Android自定义View的开发者学习参考。
381 84
|
21天前
|
人工智能 前端开发 Java
Java 面试资料中相关代码使用方法与组件封装方法解析
这是一份详尽的Java面试资料代码指南,涵盖使用方法与组件封装技巧。内容包括环境准备(JDK 8+、Maven/Gradle)、核心类示例(问题管理、学习进度跟踪)、Web应用部署(Spring Boot、前端框架)、单元测试及API封装。通过问题库管理、数据访问组件、学习进度服务和REST接口等模块化设计,帮助开发者高效组织与复用功能,同时支持扩展如用户认证、AI推荐等功能。适用于Java核心技术学习与面试备考,提升编程与设计能力。资源链接:[点此下载](https://pan.quark.cn/s/14fcf913bae6)。
53 6
Java 面试资料中相关代码使用方法与组件封装方法解析
|
24天前
|
前端开发 Android开发 UED
讲讲Android为自定义view提供的SurfaceView
本文详细介绍了Android中自定义View时使用SurfaceView的必要性和实现方式。首先分析了在复杂绘制逻辑和高频界面更新场景下,传统View可能引发卡顿的问题,进而引出SurfaceView作为解决方案。文章通过Android官方Demo展示了SurfaceView的基本用法,包括实现`SurfaceHolder.Callback2`接口、与Activity生命周期绑定、子线程中使用`lockCanvas()`和`unlockCanvasAndPost()`方法完成绘图操作。
|
10月前
|
存储 Java
【IO面试题 四】、介绍一下Java的序列化与反序列化
Java的序列化与反序列化允许对象通过实现Serializable接口转换成字节序列并存储或传输,之后可以通过ObjectInputStream和ObjectOutputStream的方法将这些字节序列恢复成对象。
|
7月前
|
存储 算法 Java
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
本文详解自旋锁的概念、优缺点、使用场景及Java实现。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
|
7月前
|
存储 缓存 算法
面试官:单核 CPU 支持 Java 多线程吗?为什么?被问懵了!
本文介绍了多线程环境下的几个关键概念,包括时间片、超线程、上下文切换及其影响因素,以及线程调度的两种方式——抢占式调度和协同式调度。文章还讨论了减少上下文切换次数以提高多线程程序效率的方法,如无锁并发编程、使用CAS算法等,并提出了合理的线程数量配置策略,以平衡CPU利用率和线程切换开销。
面试官:单核 CPU 支持 Java 多线程吗?为什么?被问懵了!
|
7月前
|
存储 缓存 Java
大厂面试必看!Java基本数据类型和包装类的那些坑
本文介绍了Java中的基本数据类型和包装类,包括整数类型、浮点数类型、字符类型和布尔类型。详细讲解了每种类型的特性和应用场景,并探讨了包装类的引入原因、装箱与拆箱机制以及缓存机制。最后总结了面试中常见的相关考点,帮助读者更好地理解和应对面试中的问题。
181 4