这可能是2020大小厂问的最经典的Android面试题了——事件分发机制、View渲染过程

2021-01-07 17:55:54 浏览数 (1)

面试官1:请详细叙述Android事件分发机制

这道题是很多家面试公司会问到的一道经典面试题,但又经常被面试者忽略。

看了很多博客也看了很多代码,大部分都是长篇大论,不利于阅读固总结如下:

主线传递只有三步:Activity->ViewGroup->View

Activity和View只有两个方法控制事件传递:dispatchTouchEvent(),onTouchEvent ();

ViewGroup有三个方法控制传递:dispatchTouchEvent(),onInterceptTouchEvent(),onTouchEvent ();

接下来用一张图给大家叙述下具体是怎么一步一步分发的。

总结:

1.对于 dispatchTouchEvent,onTouchEvent,return true是终结事件传递。return false 是回溯到父View的onTouchEvent方法。

2.ViewGroup 想把自己分发给自己的onTouchEvent,需要拦截器onInterceptTouchEvent方法return true 把事件拦截下来。

3.ViewGroup 的拦截器onInterceptTouchEvent 默认是不拦截的,所以return super.onInterceptTouchEvent()=return false;

4.View 没有拦截器,为了让View可以把事件分发给自己的onTouchEvent,View的dispatchTouchEvent默认实现(super)就是把事件分发给自己的onTouchEvent。

ViewGroup和View 的dispatchTouchEvent 是做事件分发,那么这个事件可能分发出去的四个目标。

注:------> 后面代表事件目标需要怎么做。

1、 自己消费,终结传递。------->return true

2、 给自己的onTouchEvent处理-------> 调用super.dispatchTouchEvent()系统默认会去调用 onInterceptTouchEvent,在onInterceptTouchEvent return true就会去把事件分给自己的onTouchEvent处理。

3、 传给子View------>调用super.dispatchTouchEvent()默认实现会去调用 onInterceptTouchEvent 在onInterceptTouchEvent return false,就会把事件传给子类。

4、 不传给子View,事件终止往下传递,事件开始回溯,从父View的onTouchEvent开始事件从下到上回归执行每个控件的onTouchEvent------->return false

注: 由于View没有子View所以不需要onInterceptTouchEvent 来控件是否把事件传递给子View还是拦截,所以View的事件分发调用super.dispatchTouchEvent()的时候默认把事件传给自己的onTouchEvent处理(相当于拦截),对比ViewGroup的dispatchTouchEvent 事件分发,View的事件分发只有dispatchTouchEvent()和onTouchEvent()不需要onInterceptTouchEvent()参与。

到此事件分发总结完毕。

面试官2:View的渲染过程,或者叫View的绘制流程

这道题也是比较老的一道题了,但是无论BAT还是小创业公司中出现的频率相当高。

接下来就总结性的叙述一遍View绘制流程,避免长篇大论,接下来的描述一切从简。

希望各位读者耐心看完,相信你会有很大的收获!

View绘图流程是在ViewRoot.java类的performTraversals()函数中展开的。

绘制部分一共需要三步:

measure() -> layout() -> draw();

1. 判读是否重新计算视图大小(measure)

原理

从顶层父View像子View递归调用view.measure(),measure方法中回调onMeasure() MeasureSpec是View的测量内部类,测量规格为int型,值由高2位规格模式specMode和低30位的具体尺寸specSize组成。

specMode有三种值

MeasureSpec.UPSPECIFIED : 父容器对于子容器没有任何限制,子容器想要多大就多大。

MeasureSpec.EXACTLY: 父容器已经为子容器设置了尺寸,子容器应当服从这些边界,不论子容器想要多大的空间。

MeasureSpec.AT_MOST:子容器可以是声明大小内的任意大小

  • View的measure方法是final,不可以重载,只能重载inMeasure完成自己的测量逻辑
  • 顶层的DecorView的MeasureSpec是由ViewRootImpl中的getRootMeasureSpec方法确定(LayoutParams宽高参数均为MATCH_PARENT,specMode是EXACTLY,specSize为物理屏幕大小)。
  • ViewGroup类提供了measureChild,measureChild和measureChildWithMargins方法,简化了父子View的尺寸计算。
  • 只要是ViewGroup的子类就必须要求LayoutParams继承子MarginLayoutParams,否则无法使用layout_margin参数。
  • View的布局大小由父View和子View共同决定。
  • 使用View的getMeasuredWidth()和getMeasuredHeight()方法来获取View测量的宽高,必须保证这两个方法在onMeasure流程之后被调用才能返回有效值。
2. 是否重新分配视图的位置(layout)

原理:

layout也是从顶层父View向子View的递归调用View.layout方法的过程,父View根据上一步measure子View得到的布局大小和布局参数,将子View放在合适的位置上。

  • View.layout方法可以被重载,ViewGroup.layout为final不可以被重载,ViewGroup.onLayout为abstract的子类必须重载实现自己的位置逻辑
  • measure结束后得到的是每个View经测量后的measuredWidth和measuredHeight,Layout操作完以后得到的是每个View进行位置分配后的mLeft,mTop、mRight、mBottom,这些值都是相对父View
  • 凡是layout_XXX的布局属性都是针对父级View的,如果View没有父级容器则layout_XXX属性是没有任何意义的
  • 使用View 的getWidth()和getHright()方法获取View测量的宽高必须保证这两个方法在在onLayout流程之后。
3. 是否重新绘制(draw)

原理:

draw过程也是在ViewRootImpl的performTraversals()内部调运的,其调用顺序在measure()和layout()之后,这里的mView对于Actiity来说就是PhoneWindow.DecorView,ViewRootImpl中的代码会创建一个Canvas对象,然后调用View的draw()方法来执行具体的绘制工。所以又回归到了ViewGroup与View的树状递归draw过程

  • 如果该View是一个ViewGroup,则需要递归绘制其所包含的所有子View。
  • View默认不绘制任何内容,真正的绘制都在自己的子类中实现
  • View的绘制是借助onDraw()方法传入的Canvas类来进行的
  • 区分View 动画和ViewGroup动画,前者是View自身的动画可以通过setAnimation添加,后者可以通过xml布局的layoutAnimation属性添加
  • 在获取画布剪切区(每个View的draw中传入的Canvas)时会自动处理掉padding,子View获取Canvas不用关注这些逻辑,只关心如何绘制即可
  • 默认情况下子View的ViewGroup.drawChild绘制顺序和子View被添加的顺序一致,但是你也可以重载ViewGroup.getChildDrawingOrder()以提供不同的顺序
4. invalidate()

原理:

invalidate方法请求重绘View树(也就是draw方法),如果View大小没有发生变化就不会调用layout过程,并且只绘制那些“需要重绘的”View,也就是哪个View(View只绘制该View,ViewGroup绘制整个ViewGroup)请求invalidate系列方法,就绘制该View。

  • 直接调用invalidate方法。请求重新draw,但只会绘制调用者本身。
  • 触发setSelection方法。请求重新draw,但只会绘制调用者本身。
  • 触发setVisibility方法。当View可视状态在INVISIBLE转换VISIBLE时会间接调用invalidate方法,继而绘制该View。当View的可视状态在INVISIBLEVISIBLE 转换为GONE状态时会间接调用requestLayout和invalidate方法,同时由于View树大小发生了变化,所以会请求measure过程以及draw过程,同样只绘制需要“重新绘制”的视图。
  • 触发setEnabled方法。请求重新draw,但不会重新绘制任何View包括该调用者本身。
  • 触发requestFocus方法。请求View树的draw过程,只绘制“需要重绘”的View。

例:当我们写一个Activity时,我们一定会通过setContentView方法将我们要展示的界面传入该方法,该方法会讲我们界面通过addView追加到id为content的一个FrameLayout(ViewGroup)中,然后addView方法中通过调运invalidate(true)去通知触发ViewRootImpl类的performTraversals()方法,至此递归绘制我们自定义的所有布局。

5.requestLayout()

原理: View的requestLayout时其实质就是层层向上传递,直到ViewRootImpl为止,然后触发ViewRootImpl的requestLayout方法 requestLayout()方法会调用measure过程和layout过程,不会调用draw过程,也不会重新绘制任何View包括该调用者本身。

以上为View渲染的整体过程,如有问题欢迎指正。

最后

作者目前在深圳,13年java转Android开发,在小厂待过,也去过华为,OPPO等,去年四月份进了阿里一直到现在。等大厂待过也面试过很多人。深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。

不知不觉自己已经做了几年开发了,由记得刚出来工作的时候感觉自己能牛逼,现在回想起来感觉好无知。懂的越多的时候你才会发现懂的越少。

如果你的知识是一个圆,当你的圆越大时,圆外面的世界也就越大。

不用多说,相信大家都有一个共识:无论什么行业,最牛逼的人肯定是站在金字塔端的人。所以,想做一个牛逼的程序员,那么就要让自己站的更高,成为技术大牛并不是一朝一夕的事情,需要时间的沉淀和技术的积累。

关于这一点,在我当时确立好Android方向时,就已经开始梳理自己的成长路线了,包括技术要怎么系统地去学习,都列得非常详细。

这里最后分享耗时一年多整理的一系列Android学习资源:Android源码解析、Android第三方库源码笔记、Android进阶架构师七大专题学习、历年BAT面试题解析包、Android大佬学习笔记等等。

这些内容均免费分享给大家,需要完整版的朋友,点击这里可以看到全部内容。

0 人点赞