Android并发编程高级面试题汇总最全最细面试题讲解持续更新中👊👊
👀你想要的面试题这里都有👀
👇👇👇
自定义View执行invalidate()方法,为什么有时候不会回调onDraw()
详细讲解
享学课堂系列课:高级UI专题:view的绘制流程& WMS专题答疑课
这道题想考察什么?
- 是否了解自定义View执行invalidate()方法的流程,为什么有时候不会回调onDraw()。
考察的知识点
- 自定义View执行invalidate()方法的流程,它回调onDraw()的过程细节概念。
考生应该如何回答
首先我们分析一下invalidate()的执行流程,源码是如何从invalidate调用到onDraw()的。由于这部分代码相对较为复杂,那么请大家参考下面的时序图。
- invalidate软件绘制流程

从上面的流程不难发现:1)view的invalidate会逐层找parent一直找到DecorView,DecorView是顶层view,它有个虚拟父view为ViewRootImpl。ViewRootImpl不是一个view或者viewGroup,它的成员mView就是DecorView,然后再由ViewRootImpl将所有的操作从ViewRootImpl自上而下开始分发,最终分发给所有的View。2)view的invalidate不会导致ViewRootImpl的invalidate被调用,而是递归调用父view的invalidateChildInParent,直到ViewRootImpl的invalidateChildInParent,然后触发peformTraversals,会导致当前view被重绘,由于mLayoutRequested为false,不会导致onMeasure和onLayout被调用,而OnDraw会被调用。
在整个调度流程里面有几个重要的地方 需要拿出来和大家一起探讨一下:
在View.java 类里面代码如下,请大家关注代码中的注解
public void invalidate() {
invalidate(true);
}
public void invalidate(boolean invalidateCache) {
//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 (skipInvalidate()) {
return;
}
//PFLAG_DRAWN 表示此前该View已经绘制过 PFLAG_HAS_BOUNDS表示该View已经layout过,确定过坐标了
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) {
//默认true
mLastIsOpaque = isOpaque();
//清除绘制标记
mPrivateFlags &= ~PFLAG_DRAWN;
}
//需要绘制
mPrivateFlags |= PFLAG_DIRTY;
if (invalidateCache) {
//1、加上绘制失效标记
//2、清除绘制缓存有效标记
//这两标记在硬件加速绘制分支用到
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;
//记录需要重新绘制的区域 damge,该区域为该View尺寸
damage.set(l, t, r, b);
//p 为该View的父布局
//调用父布局的invalidateChild
p.invalidateChild(this, damage);
}
...
}
}
从上面代码可以知道,当前要刷新的View确定了刷新区域后即调用了父布局ViewGroup的invalidateChild()方法。
有一个函数很重要,skipInvalidate():如果当前view不可见而且也没有在执行动画的时候这个时候不能触发invalidate。
private boolean skipInvalidate() {
return (mViewFlags & VISIBILITY_MASK) != VISIBLE && mCurrentAnimation == null &&
(!(mParent instanceof ViewGroup) ||
!((ViewGroup) mParent).isViewTransitioning(this));
}
另外一个很重要的函数invalidateChild也需要给大家解析一下:
public final void invalidateChild(View child, final Rect dirty) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null && attachInfo.mHardwareAccelerated) {
//1、如果是支持硬件加速,则走该分支
onDescendantInvalidated(child, child);
return;
}
//2、软件绘制
ViewParent parent = this;
if (attachInfo != null) {
//动画相关,忽略
...
do {
View view = null;
if (parent instanceof View) {
view = (View) parent;
}
...
parent = parent.invalidateChildInParent(location, dirty);
//动画相关
} while (parent != null);
}
}
从上面的代码注解大家不难发现,该方法分为硬件加速绘制和 软件绘制。
我们先看看硬件,如果该Window支持硬件加速,则走下边流程,ViewGroup.java中
public void onDescendantInvalidated(@NonNull View child, @NonNull View target) {
mPrivateFlags |= (target.mPrivateFlags & PFLAG_DRAW_ANIMATION);
if ((target.mPrivateFlags & ~PFLAG_DIRTY_MASK) != 0) {
//此处都会走
mPrivateFlags = (mPrivateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DIRTY;
//清除绘制缓存有效标记
mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
}
if (mLayerType == LAYER_TYPE_SOFTWARE) {
//如果是开启了软件绘制,则加上绘制失效标记
mPrivateFlags |= PFLAG_INVALIDATED | PFLAG_DIRTY;
//更改target指向
target = this;
}
if (mParent != null) {
//调用父布局的onDescendantInvalidated
mParent.onDescendantInvalidated(this, target);
}
}
onDescendantInvalidated 方法的目的是不断向上寻找其父布局,并将父布局PFLAG_DRAWING_CACHE_VALID 标记清空,也就是绘制缓存清空。 而我们知道,根View的mParent指向ViewRootImpl对象,因此来看看它里面的onDescendantInvalidated()方法:
@Override
public void onDescendantInvalidated(@NonNull View child, @NonNull View descendant) {
// TODO: Re-enable after camera is fixed or consider targetSdk checking this
// checkThread();
if ((descendant.mPrivateFlags & PFLAG_DRAW_ANIMATION) != 0) {
mIsAnimating = true;
}
invalidate();
}
@UnsupportedAppUsage
void invalidate() {
//mDirty 为脏区域,也就是需要重绘的区域
//mWidth,mHeight 为root view的尺寸
mDirty.set(0, 0, mWidth, mHeight);
if (!mWillDrawSoon) {
//开启View 三大流程
scheduleTraversals();
}
}
invalidate() 对于支持硬件加速来说,会将整个root view区域内的大小都设置为mDirty区域,刷新的时候就会全面的将整个区域进行刷新。所以,当刷新失效的时候,我们往往可以设置硬件刷新来让整个区域都可以刷新,从而达到刷新的效果。
然后,再来分析软件刷新分支,如果该Window不支持硬件加速,那么走软件绘制分支:parent.invalidateChildInParent(location, dirty) 返回mParent,只要mParent不为空那么一直调用invalidateChildInParent(),实际上这也是遍历ViewTree过程,来看看关键invalidateChildInParent()
public ViewParent invalidateChildInParent(final int[] location, final Rect dirty) {
//dirty 为失效的区域,也就是需要重绘的区域
if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID)) != 0) {
//该View绘制过或者绘制缓存有效
if ((mGroupFlags & (FLAG_OPTIMIZE_INVALIDATE | FLAG_ANIMATION_DONE))
!= FLAG_OPTIMIZE_INVALIDATE) {
//修正重绘的区域
dirty.offset(location[CHILD_LEFT_INDEX] - mScrollX,
location[CHILD_TOP_INDEX] - mScrollY);
if ((mGroupFlags & FLAG_CLIP_CHILDREN) == 0) {
//如果允许子布局超过父布局区域展示
//则该dirty 区域需要扩大
dirty.union(0, 0, mRight - mLeft, mBottom - mTop);
}
final int left = mLeft;
final int top = mTop;
if ((mGroupFlags & FLAG_CLIP_CHILDREN) == FLAG_CLIP_CHILDREN) {
//默认会走这
//如果不允许子布局超过父布局区域展示,则取相交区域
if (!dirty.intersect(0, 0, mRight - left, mBottom - top)) {
dirty.setEmpty();
}
}
//记录偏移,用以不断修正重绘区域,使之相对计算出相对屏幕的坐标
location[CHILD_LEFT_INDEX] = left;
location[CHILD_TOP_INDEX] = top;
} else {
...
}
//标记缓存失效
mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
if (mLayerType != LAYER_TYPE_NONE) {
//如果设置了缓存类型,则标记该View需要重绘
mPrivateFlags |= PFLAG_INVALIDATED;
}
//返回父布局
return mParent;
}
return null;
}
通过上面的代码发现,最终调用ViewRootImpl 的invalidateChildInParent().
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
checkThread();
if (DEBUG_DRAW) Log.v(mTag, "Invalidate child: " + dirty);
if (dirty == null) {
//脏区域为空,则默认刷新整个窗口
invalidate();
return null;
} else if (dirty.isEmpty() && !mIsAnimating) {
return null;
}
...
invalidateRectOnScreen(dirty);
return null;
}
private void invalidateRectOnScreen(Rect dirty) {
final Rect localDirty = mDirty;
//合并脏区域,取并集
localDirty.union(dirty.left, dirty.top, dirty.right, dirty.bottom);
...
if (!mWillDrawSoon && (intersected || mIsAnimating)) {
//开启View的三大绘制流程
scheduleTraversals();
}
}
invalidate() 对于软件绘制来说,目的就是通过计算找到需要重绘的区域,确定了需要重绘的区域后,然后再调用scheduleTraversals对这个区域触发它的绘制。关于scheduleTraversals的解释,大家可以去看7.1,在7.1详细的解释了它的整个流程。
小结
以上,从硬件加速绘制与软件绘制全面分析了invalidate触发onDraw的整个流程。如果window不支持硬件加速绘制,那么view的invalidate将不会导致ViewRootImpl的invalidate被调用和执行,而是通过软件绘制的方式递归调用父view的invalidateChildInParent,直到ViewRootImpl的invalidateChildInParent,然后触发peformTraversals,会导致当前view被重绘,由于mLayoutRequested为false,不会导致onMeasure和onLayout被调用,而OnDraw会被调用(只绘制需要重绘的视图)。
大家在看代码的时候请注意:1)同一个draw时序内连续调用同一View的invalidate时,会被Flag阻挡,不再向下走。2)同一个draw时序内不同View调用invalidate时只会调动一个,不会重复执行。3) 在执行scheduleTraversals方法的时候最终会执行到peformTraversals,由于mLayoutRequested为false,不会导致onMeasure和onLayout被调用,而OnDraw会被调用。4)在ViewGroup中ondraw总是不执行,或者说不被调用,原因是 如果ViewGroup的background是空的,那么onDraw就一定不会执行,但是他的dispatchDraw会执行,所以可以重写dispatchDraw方法;5)自定义一个view时,重写onDraw。调用view.invalidate(),会触发onDraw和computeScroll(),前提是该view被附加在当前窗口,也就是说view必须是当前Window上面的。
由于面试题内容比较多,篇幅有限,资料已经被整理成了PDF文档,有需要2023年Android中高级最全面试真题答案 完整文档的可扫描下方卡片免费获取~

PS:(文末还有使用ChatGPT机器人小福利哦!!大家不要错过)
目录

第一章 Java方面
- Java基础部分
- Java集合
- Java多线程
- Java虚拟机

第二章 Android方面
- Android四大组件相关
- Android异步任务和消息机制
- Android UI绘制相关
- Android性能调优相关
- Android中的IPC
- Android系统SDK相关
- 第三方框架分析
- 综合技术
- 数据结构方面
- 设计模式
- 计算机网络方面
- Kotlin方面

第三章 音视频开发高频面试题
- 为什么巨大的原始视频可以编码成很小的视频呢?这其中的技术是什么呢?
- 怎么做到直播秒开优化?
- 直方图在图像处理里面最重要的作用是什么?
- 数字图像滤波有哪些方法?
- 图像可以提取的特征有哪些?
- 衡量图像重建好坏的标准有哪些?怎样计算?

第四章 Flutter高频面试题
- Dart部分
- Flutter部分

第五章 算法高频面试题
- 如何高效寻找素数
- 如何运用二分查找算法
- 如何高效解决雨水问题
- 如何去除有序数组的重复元素
- 如何高效进行模幂运算
- 如何寻找最长回文子串

第六章 Andrio Framework方面
- 系统启动流程面试题解析
- Binder面试题解析
- Handler面试题解析
- AMS面试题解析

本文详细解析了Android自定义View中invalidate()方法如何触发onDraw(),涉及硬件加速和软件绘制的区别,以及invalidate流程中的关键函数。重点讨论了invalidate()的执行逻辑和可能导致onDraw()不回调的原因。
1369

被折叠的 条评论
为什么被折叠?



