Android控件TextView的实现原理分析
作者:网络转载 发布时间:[ 2013/5/16 10:28:29 ] 推荐标签:
参数widthMeasureSpec和heightMeasureSpec分别用来描述宽度测量规范和高度测量规范。测量规范使用一个int值来表法,这个int值包含了两个分量。
第一个是mode分量,使用高2位来表示。测量模式有三种,分别是MeasureSpec.UNSPECIFIED(0)、MeasureSpec.EXACTLY(1)、和MeasureSpec.AT_MOST(2)。
第二个是size分量,使用低30位来表示。当mode分量等于MeasureSpec.EXACTLY时,size分量的值是父视图要求当前控件要设置的宽度或者高度;当mode分量等于MeasureSpec.AT_MOST时,size分量的值是父视图限定当前控件可以设置的大宽度或者高度;当mode分量等于MeasureSpec.UNSPECIFIED时,父视图不限定当前控件所设置的宽度或者高度,这时候当前控件一般按照实际需求来设置自己的宽度和高度。
TextView类的成员函数onMeasure根据上述规则计算好自己的宽度wdith和高度height之后,必须要调用从父类View继承下来的成员函数setMeasuredDimension来通知父视图它所要设置的宽度和高度,否则的话,该函数调用结束之后,会抛出一个类型为IllegalStateException的异常。
2. 布局
前面的测量工作实际上是确定了控件的大小,但是控件的位置还未确定。控件的位置是通过布局这个操作来完成的。
我们知道,控件是按照树形结构组织在一起的,其中,子控件的位置由父控件来设置,也是说,只有容器类控件才需要执行布局操作,这是通过重写父类View的成员函数onLayout来实现的。从Activity窗口的结构可以知道,它的顶层视图是一个DecorView,这是一个容器类控件。Activity窗口的布局操作是从其顶层视图开始执行的,每碰到一个容器类的子控件,调用它的成员函数onLayout来让它有机会对自己的子控件的位置进行设置,依次类推。
我们常见的FrameLayout、LinearLayout、RelativeLayout、TableLayout和AbsoluteLayout,都是属于容器类控件,因此,它们都需要重写父类View的成员函数onLayout。由于TextView控件不是容器类控件,因此,它可以不重写父类View的成员函数onLayout。
3. 绘制
有了前面两个操作之后,控件的位置的大小确定下来了,接下来可以对它们的UI进行绘制了。控件为了能够绘制自己的UI,必须要重写父类View的成员函数onDraw。
TextView类的成员函数onDraw的实现如下所示:
[java] view plaincopyprint?
public class TextView extends View implements ViewTreeObserver.OnPreDrawListener {
......
@Override
protected void onDraw(Canvas canvas) {
//在画布canvas上绘制UI
......
}
......
}
public class TextView extends View implements ViewTreeObserver.OnPreDrawListener {
......
@Override
protected void onDraw(Canvas canvas) {
//在画布canvas上绘制UI
......
}
......
}
这个函数定义在文件frameworks/base/core/java/android/widget/TextView.java中。
参数canvas描述的是一块画布,控件的UI是绘制在这块画布上面的。画布提供了丰富的接口来绘制UI,例如画线(drawLine)、画圆(drawCircle)和贴图(drawBitmap)等等。有了这些UI画图接口之后,可以随心所欲地绘制控件的UI了。
从前面Android应用程序窗口(Activity)的测量(Measure)、布局(Layout)和绘制(Draw)过程分析一文可以知道,Java层的Canvas实际上是封装了C++层的SkCanvas。C++层的SkCanvas内部有一块图形缓冲区,这块图形绘冲区是窗口的绘图表面(Surface)里面的那块图形缓冲区。
从前面Android应用程序与SurfaceFlinger服务的关系概述和学习计划一文可以知道,窗口的绘图表面里面的那块图形缓冲区实际上是一块匿名共享内存,它是SurfaceFlinger服务负责创建的。SurfaceFlinger服务创建完成这块匿名共享内存之后,会将其返回给窗口所运行在的进程。窗口所运行在的进程获得了这块匿名共享内存之后,会映射到自己的进程空间来,因此,窗口的控件可以在本进程内访问这块匿名共享内存了,实际上是往这块匿名共享内存填入UI数据。注意,这个过程执行完成之后,控件的UI还没有反映到屏幕上来,因为这时候将控件的UI数据填入到图形缓冲区而已。
从前面Android窗口管理服务WindowManagerService的简要介绍和学习计划一文可以知道,窗口的UI的显示是WindowManagerService服务来控制的。因此,当窗口的所有控件都绘制完成自己的UI之后,窗口会向WindowManagerService服务发送一个Binder进程间程通信请求。WindowManagerService服务接收到这个Binder进程间程通信请求之后,会请求SurfaceFlinger服务刷新相应的窗口的UI。SurfaceFlinger服务刷新窗口UI的过程可以参考前面Android系统Surface机制的SurfaceFlinger服务渲染应用程序UI的过程分析一文。
从上面的描述可以看出,控件的UI虽然是在一块简单的画布进行绘制,但是其中蕴含了丰富的知识点,并且需要应用程序进程、WindowManagerService服务和SurfaceFlinger服务三方紧密而有序的配合。
如果我们仔细阅读Android应用程序窗口(Activity)的测量(Measure)、布局(Layout)和绘制(Draw)过程分析一文,还可以得出以下两个结论:
(1). 一个窗口的所有控件的UI都是绘制在窗口的绘图表面上的,也是说,一个窗口的所有控件的UI数据都是填写在同一块图形缓冲区中;
(2). 一个窗口的所有控件的UI的绘制操作是在主线程中执行的,事实上,所有与UI相关的操作都是必须是要在主线程中执行,否则的话,会抛出一个类型为CalledFromWrongThreadException的异常来。
为什么要规定所有与UI相关的操作都必须在主线程中执行呢?我们知道,这些与UI相关的操作都涉及到大量的控件内部状态以及需要访问窗口的绘图表面,也是说,要大量地访问控件类的成员变量以及窗口绘图表面里面的图形缓冲区,因此,如果不将这些与UI相关的操作限定在同一个线程中执行的话,那么会涉及到线程同步问题。线程同步的开销是很大的,因此,要保证那些与UI相关的操作都在同一个线程中执行。这个负责执行UI相关操作的线程便是应用程序进程的主线程,因此我们也将应用程序进程的主线程称为UI线程。
我们知道,应用程序进程的主线程除了负责执行与UI相关的操作之外,还负责响应用户的输入,因此,我们要尽量地避免执行很耗时的UI操作,否则的话,系统会由于应用程序进程的主线程无法及时响应用户输入而弹出ANR对话框。
那么,有没有办法让某一个控件的UI享有独立的图形缓冲区呢?也是这个控件不将自己的UI数据填入到它的宿主窗口的绘图表面的图形绘冲区里面去。如果可以的话,那么我们可以在另外一个独立的线程中绘制该控件的UI。这样做的好处是显而易见——可以在这个独立的线程执行相对比较耗时的UI绘制操作而不会导致主线程无法及时响应用户输入。答案是肯定的,在接下来的一篇文章中,我们分析一个可以具有独立图形缓冲区的控件——SurfaceView。
相关推荐
更新发布
功能测试和接口测试的区别
2023/3/23 14:23:39如何写好测试用例文档
2023/3/22 16:17:39常用的选择回归测试的方式有哪些?
2022/6/14 16:14:27测试流程中需要重点把关几个过程?
2021/10/18 15:37:44性能测试的七种方法
2021/9/17 15:19:29全链路压测优化思路
2021/9/14 15:42:25性能测试流程浅谈
2021/5/28 17:25:47常见的APP性能测试指标
2021/5/8 17:01:11