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

73 阅读8分钟

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上面的。

更多Android面试题 可以详细Vx关注公众号:Android老皮 解锁      《2023最新Android中高级面试题汇总+解析》

目录

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面试题解析