Android高级UI面试题汇总(含详细解析 七)

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

Android并发编程高级面试题汇总最全最细面试题讲解持续更新中👊👊
👀你想要的面试题这里都有👀
👇👇👇

自定义View执行invalidate()方法,为什么有时候不会回调onDraw()

详细讲解

享学课堂系列课:高级UI专题:view的绘制流程& WMS专题答疑课

这道题想考察什么?

  1. 是否了解自定义View执行invalidate()方法的流程,为什么有时候不会回调onDraw()。

考察的知识点

  1. 自定义View执行invalidate()方法的流程,它回调onDraw()的过程细节概念。

考生应该如何回答

首先我们分析一下invalidate()的执行流程,源码是如何从invalidate调用到onDraw()的。由于这部分代码相对较为复杂,那么请大家参考下面的时序图。

  1. 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机器人小福利哦!!大家不要错过)

目录

img

第一章 Java方面

  • Java基础部分
  • Java集合
  • Java多线程
  • Java虚拟机

img

第二章 Android方面

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

img

第三章 音视频开发高频面试题

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

img

第四章 Flutter高频面试题

  • Dart部分
  • Flutter部分

img

第五章 算法高频面试题

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

img

第六章 Andrio Framework方面

  • 系统启动流程面试题解析
  • Binder面试题解析
  • Handler面试题解析
  • AMS面试题解析

img

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值