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

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

View的绘制原理

背景

对于Android开发,在面试的时候,经常会被问到,说一说View的绘制流程?我也经常问面试者,View的绘制流程.

对于3年以上的开发人员来说,就知道onMeasure/onLayout/onDraw基本,知道他们是干些什么的,这样就够了吗?

如果你来我们公司,我是你的面试官,可能我会考察你这三年都干了什么,对于View你都知道些什么,会问一些更细节的问题,比如LinearLayout的onMeasure,onLayout过程?他们都是什么时候被发起的,执行顺序是什么?

如果以上问题你都知道,可能你进来我们公司就差不多了,可能我会考察你draw的 canvas是哪里来的,他是怎么被创建显示到屏幕上呢?看看你的深度有多少?

对于现在的移动开发市场逐渐趋向成熟,趋向饱和,很多不缺人的公司,都需要高级程序员.在说大家也都知道,面试要造飞机大炮,进去后拧螺丝,对于一个3年或者5年以上Android开发不稍微了解一些Android深一点的东西,不是很好混.扯了这么多没用的东西,还是回到今天正题,Android的绘图原理浅析.

详细讲解

本问题的系统回答大家可以移步到高级UI课程&WMS课程中学习,有详细的视频讲解

这道题想考察什么?

  1. 是否了解View绘制原理的知识?

考察的知识点

  1. View的Framework相关知识
  2. View的measure、layout、draw

考生应该如何回答

注意: 本文中涉及ActivityThread、WindowManagerImpl、WindowManagerGlobal、ViewRootImpl知识,如果对上述概念不熟悉的同学,先学习对应享学课堂相关的知识章节。

1.View的Framework相关知识

先简单说下View的起源,有助于我们后续的分析理解。

1.1 从ActivityThread.java开始

下面只贴出源码中的关键代码,重点是**vm.addView(decor, l)**这句,那么有三个对象需要理解:

vm:a.getWindowManager()即通过Activity.java的getWindowManager方法得到的对象;继续跟踪源码,发现此对象由Window.java的getWindowManager方法获得,即是((WindowManagerImpl)wm).createLocalWindowManager(this)。

decor:通过r.activity.getWindow()可知r.window是PhoneWindow对象,在PhoneWindow中找到getDecorView方法,得知decor即DecorView对象。

l:通过下面源码得知,l通过r.window.getAttributes获取到,由于PhoneWindow中没有getAttributes方法,故从他的父类Window中获取,得知宽高均为LayoutParams.MATCH_PARENT

//ActivityThread.java

public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward, String reason)  {   
       ...
       r.window = r.activity.getWindow();
       View decor = r.window.getDecorView();
       decor.setVisibility(View.INVISIBLE);
       ViewManager wm = a.getWindowManager();
       WindowManager.LayoutParams l = r.window.getAttributes();
       a.mDecor = decor;
       l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
       l.softInputMode |= forwardBit;
       ...
       if (a.mVisibleFromClient) {
           if (!a.mWindowAdded) {
              a.mWindowAdded = true;
              wm.addView(decor, l); // 核心代码
           } else {
            ...
           }
      }
}

1.2 接上面wm.addView(decor, l),通过上面分析wm是WindowManagerImpl,则走进此类的addView中

  @Override
    public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        applyDefaultToken(params);
        mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
    }

addView 会将函数转交给WindowManagerGlobal 类中的addView,我们继续看下面的代码:

1.3 WindowManagerGlobal

public void addView(View view, ViewGroup.LayoutParams params,
        Display display, Window parentWindow) {
        ...
        ViewRootImpl root;
        ...
        root = new ViewRootImpl(view.getContext(), display);
        ...
        try {
            root.setView(view, wparams, panelParentView);
        } catch (RuntimeException e) {
            ...
        }
    }
}

在WindowManagerGlobal中的addView里面会先创建一个 ViewRootImpl对象,ViewRootImpl大家可以理解为管理viewTree的根布局的一个对象,甚至可以狭义的理解为viewTree根布局的管理者,具体的解析大家可以参考7.8章节关于ViewRootImpl的理解。从上面的代码我们看到WindowManagerGlobal中addView会调用viewRootImpl的setView函数。

1.4 由1.3步可知,最终走到了ViewRootImpl.java,曙光在前方

 public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
      ...
      requestLayout();
      ...
}

通过上面的代码我们发现,setView中调用了一个非常重要的代码,那就是requestLayout()函数,这个函数是view系统体系中非常重要的一个函数,可以说是viewTree 绘制管理的真正启动,具体的代码,我们看下面的调用流程:

 public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            ...
            scheduleTraversals();
        }
 }

上面的代码会调用 scheduleTraversals(),那么这个函数是干什么的呢?继续往下看:

  void scheduleTraversals() {
        if (!mTraversalScheduled) {
            ...
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            ...
        }
    }

通过scheduleTraversals(),我们发现它设置了一个回调函数mTraversalRunnable,回调函数里面又有什么呢?

 final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
            doTraversal();
        }
    }

mTraversalRunnable其实就是一个 runnable,所以,她的关键是看run函数中所调用的doTraversal()。

    void doTraversal() {
            ...
            performTraversals();
            ...
    }

在doTraversal()里面出现了一个非常重要的函数performTraversals,为什么说它非常重要呢?

重点来了看下面的代码调用

 private void performTraversals() {
            ...
            WindowManager.LayoutParams lp = mWindowAttributes;
            ...
            int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
            int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
            ...
            performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
            ...
            performLayout(lp, mWidth, mHeight);
            ...
            performDraw();
            ...
    }

在上面代码中,有三个非常重要的函数performMeasure、performLayout、performDraw,相信大家通过名字不难发现这几个函数就是执行onMeasure、onLayout,onDraw的关键入口。那么他们是怎么触发到具体view 的onMeasure,onLyout,onDraw上面的呢?我们下面以performMeasure 为例进行讲解。

  private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
        ...
        try {
            mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        } finally {
            ...
        }
    }

上面的mView就是DecorView,也就是根view,当调用measure的时候,会调用view 的 measure函数,measure函数

2.绘制流程

2.1 measure过程

接上面说到了performMeasure,即走到了DecorView的measure,而DecorView实际是FrameLayout,FrameLayout的父类是ViewGroup,而ViewGroup的父类是View,所以直接走 到了View的measure里面。

主角登场,接下来分析View的measure,measure是final的,也就是不能不能重写此方法,但是里面有一个onMeasure方法,到这里应该很熟悉了,我们自定义控件都会重写这个方法;

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
	...
	onMeasure(widthMeasureSpec, heightMeasureSpec);
	...
}

由于DecorView的父类的FrameLayout,那么我们来看FrameLayout的onMeasure方法;可以看到会测量所有的子View,最后测量自己。所以有两点:测量子View宽高,确定自己宽高。

 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int count = getChildCount();
    ...
    for (int i = 0; i < count; i++) {
        final View child = getChildAt(i);
        ...
        if (mMeasureAllChildren || child.getVisibility() != GONE) {
            measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);//1
            ...
        }
     }
     ...省略代码段
     setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
           resolveSizeAndState(maxHeight, heightMeasureSpec,
                        childState << MEASURED_HEIGHT_STATE_SHIFT));//2
        ...
     }
}

上面的代码有两处非常重要的代码,其中代码1处是度量孩子的宽高, 具体的代码我们可以看接下来的解释,但是在这个函数里面还有一处非常重要的代码,那就是代码2,代码2 就是确定当前onMeasure的view的宽高确定的,那么当前的view是怎么进行确定的呢?它就是各种layout 自身的布局算法了,比如 FrameLayout,LinearLayout,RelativeLayout,在它们上面摆放的子view以一个什么方式排列,排列完成后,需要多高多宽,这些就是文中标注了省略代码段的代码计算的过程(这个过程就是各个layout的核心),最终计算的结果就是得到当前View的宽高,然后调用setMeasuredDimension将得到的宽高保存起来。

​ 上面代码1处所调用的measureChildWithMargins请看下面的解析。

​ 我们先来关注测量子View即measureChildWithMargins,大概的意思是:先获到宽高的measureSpec,然后再基于这个measureSpec对view进行measure,从而可以将度量动作分发给当前这个child的孩子。

 protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                        + widthUsed, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                        + heightUsed, lp.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

​ 在上面代码中先获取到LayoutParams,然后通过getChildMeasureSpec计算出自定义view的宽高,里面涉及父view的padding与子view的margin,接着调用子View的measure方法传入计算出的宽高MeasureSpec,层层递归直到无子View为止。

注意:MeasureSpec 是什么?怎么得到?这个也是一个面试常问的点,大家感兴趣可以去查找书籍找到答案,或者可以去享学课堂找到对应的课程进行学习。

​ 分析完测量子View,接下来看测量自己,即上面源码中提到的setMeasuredDimension

 protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
        ...
        setMeasuredDimensionRaw(measuredWidth, measuredHeight);
    }
  private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
        mMeasuredWidth = measuredWidth;
        mMeasuredHeight = measuredHeight;
        ...
    }

最后为mMeasureWidth、mMeasureSpec赋值,测量完毕。

​ 我们来看下View类中的onMeasure(即若不覆写onMeasure的默认逻辑),同上调用了setMeasureSpec为测量结果赋值;这里有需要注意的地方,当我们自定义View覆写onMeasure时,最后一定要为测量结果赋值(setMeasuredDimension),否则会报错。

小结

在这里插入图片描述

​ 图7-1

view的度量过程就是按照上面的图7-1 viewTree的层次结构进行分发,先从viewRootImpl中执行performMeasure函数,然后再调用view的measure函数,此时的view是rootView属于一个ViewGroup,此时的ViewGroup会执行自己的onMeasure函数:度量孩子并确定自己的宽高;同时在度量孩子的时候,孩子view(viewGroup)就会执行进行同样的分发流程,从而遍历整棵树完成所有view的度量。

2.2 layout过程

​ layout的过程是基于度量的值,对viewTree上面的节点进行布局的过程,整体流程也是按照图7-1的结构,对7-1所指的树中的节点进行深度遍历,直到所有的树节点完成遍历为止,由于过程基本一致本文就不再赘述,感兴趣的朋友可以自行阅读源码或者通过享学课堂的课程进行学习。

2.3 draw 绘制流程

Draw 的分发过程也是和Measure的过程一样,入口是ViewRootImpl中的performTraversals(),然后再经过 performDraw()将draw的事件逐步通过函数调用分发到 View.java 中的draw(Canvas canvas)函数,所以我们接下来的分析就重点探讨View.java 中的draw函数。

代码中的draw方法注释和解析请大家认真理解

View.java
    public void draw(Canvas canvas) {
        ...

        /*
         * Draw traversal performs several drawing steps which must be executed
         * in the appropriate order:
         *
         *      1. Draw the background
         *      2. If necessary, save the canvas' layers to prepare for fading
         *      3. Draw view's content
         *      4. Draw children
         *      5. If necessary, draw the fading edges and restore layers
         *      6. Draw decorations (scrollbars for instance)
         */

        // Step 1, draw the background, if needed
        int saveCount;

        if (!dirtyOpaque) {
            ...
            drawBackground(canvas);
        }

        // skip step 2 & 5 if possible (common case)
        final int viewFlags = mViewFlags;
        boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
        boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
        if (!verticalEdges && !horizontalEdges) {
            // Step 3, draw the content
            if (!dirtyOpaque) onDraw(canvas);

            // Step 4, draw the children
            dispatchDraw(canvas);

            drawAutofilledHighlight(canvas);

            // Overlay is part of the content and draws beneath Foreground
            if (mOverlay != null && !mOverlay.isEmpty()) {
                mOverlay.getOverlayView().dispatchDraw(canvas);
            }

            // Step 6, draw decorations (foreground, scrollbars)
            onDrawForeground(canvas);

            // Step 7, draw the default focus highlight
            drawDefaultFocusHighlight(canvas);

            if (debugDraw()) {
                debugDrawFocus(canvas);
            }

            // we're done...
            return;
        }
    }

step1:绘制背景时,首先根据滚动值对canvas的坐标进行调整,然后再恢复坐标, 图中外面是一个任意ViewGroup的实例,内部包含一个TextView对象,粗实线区域代表该TextView 在 ViewGroup中的位置, TextView中的文字由于滚动,一部分已经超出了粗实线区域,从而不可见。此时,如果调用canvas.getClipBoundsO返回的矩形区域是指粗实线所示的区域,该矩形的坐标是相对其 父视图ViewGroup的左上角,并且如果调用canvas的 getHeight()和 getWidth()方法将返回父视图的高度 和宽度,此处分别为200dip和 320dip。如 果ViewGroup中包含多个子视图,那么每个子视图内部的onDraw()函数中参数canvas的大小都是相同的,为父视图的大小。唯一不同的是“剪切区”,这个剪切区正是父视图分配给子视图的显示区 域 。 canvas之所以被设计成这样正是为了 View树的绘制,对于任何一个View而言,绘制时都可以认为原点坐标就是该View本身的原点坐标,从而 对 于View而言,当用户滚动屏幕时,应用程序只需要 调 用View类 的 scrollBy()函数即可,而不需要在onDraw()函数中做任何额外的处理,View的 onDraw() 函数内部可以完全忽略滚动值。 由于背景本身针对的是可视区域的背景,而 不 是 整 个 V iew 内部的背景,因此,本步中先调用translateO将原点移动到粗实线的左上角,从而使得背景Drawable对象内部绘制的是粗实线的区域。当绘制完背景后,还需要重新调用transalte()将原点坐标再移回到TextView本 身 的 (0 ,0 )坐标。

step2:如果该程序员要求显示视图的渐变框,则需要先为该操作做一点准备,但是大多数情况下都不需要显示渐变框,因此,源码中针对这种情况进行快速处理,即略过该准备。

step3:绘制视图本身,实 际 上 回 调onDraw()函数即可, View 的设计者可以在onDraw()函数中调用canvas的各种绘制函数进行绘制。

step4:调 用 dispatchDraw()绘制子视图。如果该视图内部不包含子视图,则不需要重载该函数,而对所有 的ViewGroup实例而言,都必须重载该函数,否则它也就不是ViewGroup 了。

其他的步骤我们就不再介绍了,相信大家都能搞明白。本问题的系统回答大家可以移步到高级UI课程&WMS课程中学习,有详细的视频讲解

总结

View的绘制流程是面试最容易被问到的问题,而且这个问题面试者非常容易满足于一知半解。这道问题的正确的回答方式是从viewRootImpl开始,解析整个viewTree的构建分发流程。

由于面试题内容比较多,篇幅有限,资料已经被整理成了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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值