Android控件TextView的实现原理分析:
应用程序窗口,即Activity窗口,是由一个PhoneWindow对象,一个DecorView对象,以及一个ViewRoot对象来描述的。其中,PhoneWindow对象用来描述窗口对象,DecorView对象用来描述窗口的顶层视图,ViewRoot对象除了用来与WindowManagerService服务通信之外,还用来接收用户输入。窗口控件本身也是一个视图,即一个View对象,它们是以树形结构组织在一起形成整个窗口的UI的。
Activity窗口的UI绘制操作分为三步来走,分别是测量、布局和绘制。
Java层的Canvas实际上是封装了C++层的SkCanvas。C++层的SkCanvas内部有一块图形缓冲区,这块图形绘冲区就是窗口的绘图表面(Surface)里面的那块图形缓冲区。
窗口的绘图表面里面的那块图形缓冲区实际上是一块匿名共享内存,它是SurfaceFlinger服务负责创建的。SurfaceFlinger服务创建完成这块匿名共享内存之后,就会将其返回给窗口所运行在的进程。窗口所运行在的进程获得了这块匿名共享内存之后,就会映射到自己的进程空间来,因此,窗口的控件就可以在本进程内访问这块匿名共享内存了,实际上就是往这块匿名共享内存填入UI数据。注意,这个过程执行完成之后,控件的UI还没有反映到屏幕上来,因为这时候将控件的UI数据填入到图形缓冲区而已。
窗口的UI的显示是WindowManagerService服务来控制的。因此,当窗口的所有控件都绘制完成自己的UI之后,窗口就会向WindowManagerService服务发送一个Binder进程间程通信请求。WindowManagerService服务接收到这个Binder进程间程通信请求之后,就会请求SurfaceFlinger服务刷新相应的窗口的UI。
一个窗口的所有控件的UI都是绘制在窗口的绘图表面上的,也就是说,一个窗口的所有控件的UI数据都是填写在同一块图形缓冲区中;一个窗口的所有控件的UI的绘制操作是在主线程中执行的。
为什么要规定所有与UI相关的操作都必须在主线程中执行呢?我们知道,这些与UI相关的操作都涉及到大量的控件内部状态以及需要访问窗口的绘图表面,也就是说,要大量地访问控件类的成员变量以及窗口绘图表面里面的图形缓冲区,因此,如果不将这些与UI相关的操作限定在同一个线程中执行的话,那么就会涉及到线程同步问题。线程同步的开销是很大的,因此,就要保证那些与UI相关的操作都在同一个线程中执行。这个负责执行UI相关操作的线程便是应用程序进程的主线程,因此我们也将应用程序进程的主线程称为UI线程。
应用程序进程的主线程除了负责执行与UI相关的操作之外,还负责响应用户的输入。
那么,有没有办法让某一个控件的UI享有独立的图形缓冲区呢?也就是这个控件不将自己的UI数据填入到它的宿主窗口的绘图表面的图形绘冲区里面去。如果可以的话,那么我们就可以在另外一个独立的线程中绘制该控件的UI。这样做的好处是显而易见,可以在这个独立的线程执行相对比较耗时的UI绘制操作而不会导致主线程无法及时响应用户输入。答案是肯定的,在接下来的一篇文章中,我们就分析一个可以具有独立图形缓冲区的控件---SurfaceView。
每一个窗口的创建的时候,都会与系统的输入管理器建立一个用户输入接收通道。输入管理器在启动两个线程,其中一个用来监控用户输入,即监控用户是否按下或者放开了键盘按键,或者是否触摸了屏幕,另外一个用来将监控到的用户输入事件分发给当前激活的窗口来处理,而这个分发过程就是通过前面建立的通道来进行的。
当前激活的窗口接收到输入管理器分发过来的用户输入事件之后,就会把该事件封装成一个消息发送到当前激活的窗口所运行在的应用程序进程的主线程的消息队列中去。等到这个消息被处理的时候,就会调用与当前激活的窗口所关联的一个ViewRoot对象的成员函数deliverKeyEvent或者deliverPointerEvent来将前面接收到的用户输入分发给合适的控件。其中,ViewRoot类的成员函数deliverKeyEvent负责分发键盘输入事件,而ViewRoot类的成员函数deliverPointerEvent负责分发触摸屏输入事件。