Android控件TextView的实现原理分析.docx
《Android控件TextView的实现原理分析.docx》由会员分享,可在线阅读,更多相关《Android控件TextView的实现原理分析.docx(30页珍藏版)》请在冰豆网上搜索。
Android控件TextView的实现原理分析
Android控件TextView的实现原理分析
在前面一个系列的文章中,我们以窗口为单位,分析了WindowManagerService服务的实现。
同时,在再前面一个系列的文章中,我们又分析了窗口的组成。
简单来说,窗口就是由一系列的视图按照一定的布局组织起来的。
实际上,每一个视图都是一个控件,这些控制可以将自己的UI绘制在窗口的绘图表面上,同时还可以与用户进行交互,即获得用户的键盘或者触摸屏输入。
在本文中,我们就详细分析窗口控件的上述实现原理。
由于Android系统提供的控件比较多,因此我们只能挑一个比较有代表的控件进行分析。
这个比较有代表性的控件便是TextView,其它的一些基础控件,例如Button、EditText和CheckBox等,都是直接或者间接地以它为父类的。
每一个控件的实现都是相当复杂的,不过基本上都是一些细节问题,而且不同的控件有不同的实现细节,因此,本文并不打算详细地分析TextView的具体实现,而是从所有控件为了实现自己的功能而需要的东西出发,去分析TextView的实现框架。
那么,控件为了实现自己的功能而需要的东西是什么呢?
有两个材料是必不可少的。
第一个材料是画布,第二个材料是用户输入。
有画布才能绘制UI,而有用户输入才能与用户进行交互。
因此,接下来我们主要分析TextView的绘制流程,以及它获得用户输入的过程。
用户输入主要包括键盘输入以及触摸屏输入,本文主要关注的是键盘输入。
触摸屏输入与键盘输入的获取过程是类似的,读者如果有兴趣的话,可以参照本文的内容来自己研究一下。
从前面这个系列的文章可以知道,应用程序窗口,即Activity窗口,是由一个PhoneWindow对象,一个DecorView对象,以及一个ViewRoot对象来描述的。
其中,PhoneWindow对象用来描述窗口对象,DecorView对象用来描述窗口的顶层视图,ViewRoot对象除了用来与WindowManagerService服务通信之外,还用来接收用户输入。
窗口控件本身也是一个视图,即一个View对象,它们是以树形结构组织在一起形成整个窗口的UI的。
为了简单起见,本文假设要分析的TextView控件是直接以窗口的顶层视图为父视图的,即以DecorView为父视图,如图1所示:
图1显示的是一个包含了TextView控件的Activity窗口的结构示意图以及DecorView、TextView的简单类关系图,从中可以看出:
1.用户输入首先是由ViewRoot接收,然后再分发给TextView处理;
2.DecorView是一个视图容器,因此,它是从ViewGroup继承下来,而ViewGroup本身又是从View继承下来的;
3.TextView是一个简单视图,因此,它是直接继承了View。
接下来,我们就以图1所示的Activity窗口为例,来分析TextView控件的UI绘制框架及其获得键盘输入的过程。
一.TextView控件的UI绘制框架
从前面一文可以知道,Activity窗口的UI绘制操作分为三步来走,分别是测量、布局和绘制。
1.测量
为了能告诉父视图自己的所占据的空间的大小,所有控件都必须要重写父类View的成员函数onMeasure。
TextView类的成员函数onMeasure的实现如下所示:
[java]viewplaincopy在CODE上查看代码片派生到我的代码片
publicclassTextViewextendsViewimplementsViewTreeObserver.OnPreDrawListener{
......
@Override
protectedvoidonMeasure(intwidthMeasureSpec,intheightMeasureSpec){
intwidthMode=MeasureSpec.getMode(widthMeasureSpec);
intheightMode=MeasureSpec.getMode(heightMeasureSpec);
intwidthSize=MeasureSpec.getSize(widthMeasureSpec);
intheightSize=MeasureSpec.getSize(heightMeasureSpec);
intwidth;
intheight;
//计算TextView控件的宽度和高度
......
setMeasuredDimension(width,height);
}
......
}
这个函数定义在文件frameworks/base/core/Java/android/widget/TextView.java中。
参数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]viewplaincopy在CODE上查看代码片派生到我的代码片
publicclassTextViewextendsViewimplementsViewTreeObserver.OnPreDrawListener{
......
@Override
protectedvoidonDraw(Canvascanvas){
//在画布canvas上绘制UI
......
}
......
}
这个函数定义在文件frameworks/base/core/java/android/widget/TextView.java中。
参数canvas描述的是一块画布,控件的UI就是绘制在这块画布上面的。
画布提供了丰富的接口来绘制UI,例如画线(drawLine)、画圆(drawCircle)和贴图(drawBitmap)等等。
有了这些UI画图接口之后,就可以随心所欲地绘制控件的UI了。
从前面一文可以知道,Java层的Canvas实际上是封装了C++层的SkCanvas。
C++层的SkCanvas内部有一块图形缓冲区,这块图形缓冲区就是窗口的绘图表面(Surface)里面的那块图形缓冲区。
从前面一文可以知道,窗口的绘图表面里面的那块图形缓冲区实际上是一块匿名共享内存,它是SurfaceFlinger服务负责创建的。
SurfaceFlinger服务创建完成这块匿名共享内存之后,就会将其返回给窗口所运行在的进程。
窗口所运行在的进程获得了这块匿名共享内存之后,就会映射到自己的进程空间来,因此,窗口的控件就可以在本进程内访问这块匿名共享内存了,实际上就是往这块匿名共享内存填入UI数据。
注意,这个过程执行完成之后,控件的UI还没有反映到屏幕上来,因为这时候将控件的UI数据填入到图形缓冲区而已。
从前面一文可以知道,窗口的UI的显示是WindowManagerService服务来控制的。
因此,当窗口的所有控件都绘制完成自己的UI之后,窗口就会向WindowManagerService服务发送一个Binder进程间程通信请求。
WindowManagerService服务接收到这个Binder进程间程通信请求之后,就会请求SurfaceFlinger服务刷新相应的窗口的UI。
SurfaceFlinger服务刷新窗口UI的过程可以参考前面一文。
从上面的描述就可以看出,控件的UI虽然是在一块简单的画布进行绘制,但是其中蕴含了丰富的知识点,并且需要应用程序进程、WindowManagerService服务和SurfaceFlinger服务三方紧密而有序的配合。
如果我们仔细阅读一文,还可以得出以下两个结论:
(1).一个窗口的所有控件的UI都是绘制在窗口的绘图表面上的,也就是说,一个窗口的所有控件的UI数据都是填写在同一块图形缓冲区中;
(2).一个窗口的所有控件的UI的绘制操作是在主线程中执行的,事实上,所有与UI相关的操作都是必须是要在主线程中执行,否则的话,就会抛出一个类型为CalledFromWrongThreadException的异常来。
为什么要规定所有与UI相关的操作都必须在主线程中执行呢?
我们知道,这些与UI相关的操作都涉及到大量的控件内部状态以及需要访问窗口的绘图表面,也就是说,要大量地访问控件类的成员变量以及窗口绘图表面里面的图形缓冲区,因此,如果不将这些与UI相关的操作限定在同一个线程中执行的话,那么就会涉及到线程同步问题。
线程同步的开销是很大的,因此,就要保证那些与UI相关的操作都在同一个线程中执行。
这个负责执行UI相关操作的线程便是应用程序进程的主线程,因此我们也将应用程序进程的主线程称为UI线程。
我们知道,应用程序进程的主线程除了负责执行与UI相关的操作之外,还负责响应用户的输入,因此,我们就要尽量地避免执行很耗时的UI操作,否则的话,系统就会由于应用程序进程的主线程无法及时响应用户输入而弹出ANR对话框。
那么,有没有办法让某一个控件的UI享有独立的图形缓冲区呢?
也就是这个控件不将自己的UI数据填入到它的宿主窗口的绘图表面的图形缓冲区里面去。
如果可以的话,那么我们就可以在另外一个独立的线程中绘制该控件的UI。
这样做的好处是显而易见——可以在这个独立的线程执行相对比较耗时的UI绘制操作而不会导致主线程无法及时响应用户输入。
答案是肯定的,在接下来的一篇文章中,我们就分析一个可以具有独立图形缓冲区的控件——SurfaceView。
二.TextView控件获取键盘输入的过程分析
从前面一文可以知道,每一个窗口的创建的时候,都会与系统的输入管理器建立一个用户输入接收通道。
输入管理器在启动两个线程,其中一个用来监控用户输入,即监控用户是否按下或者放开了键盘按键,或者是否触摸了屏幕,另外一个用来将监控到的用户输入事件分发给当前激活的窗口来处理,而这个分发过程就是通过前面建立的通道来进行的。
当前激活的窗口接收到输入管理器分发过来的用户输入事件之后,就会该事件封装成一个消息发送到当前激活的窗口所运行在的应用程序进程的主线程的消息队列中去。
等到这个消息被处理的时候,就会调用与当前激活的窗口所关联的一个ViewRoot对象的成员函数deliverKeyEvent或者deliverPointerEvent来将前面接收到的用户输入分发给合适的控件。
其中,ViewRoot类的成员函数deliverKeyEvent负责分发键盘输入事件,而ViewRoot类的成员函数deliverPointerEvent负责分发触摸屏输入事件。
接下来,我们就从ViewRoot类的成员函数deliverKeyEvent开始,分析一个TextView控件获得键盘输入的过程(获得触摸屏输入的过程是类似的),如图2所示:
这个过程可以分为14个步骤,接下来我们就详细分析每一个步骤。
Step1.ViewRoot.deliverKeyEvent
[java]viewplaincopy在CODE上查看代码片派生到我的代码片
publicfinalclassViewRootextendsHandlerimplementsViewParent,
View.AttachInfo.Callbacks{
......
privatevoiddeliverKeyEvent(KeyEventevent,booleansendDone){
//IfmViewisnull,wejustconsumethekeyeventbecauseitdoesn't
//makesensetodoanythingelsewithit.
booleanhandled=mView!
=null
?
mView.dispatchKeyEventPreIme(event):
true;
if(handled){
if(sendDone){
finishInputEvent();
}
return;
}
//Ifitispossibleforthiswindowtointeractwiththeinput
//methodwindow,thenwewanttofirstdispatchourkeyevents
//totheinputmethod.
if(mLastWasImTarget){
InputMethodManagerimm=InputMethodManager.peekInstance();
if(imm!
=null&&mView!
=null){
intseq=enqueuePendingEvent(event,sendDone);
......
imm.dispatchKeyEvent(mView.getContext(),seq,event,
mInputMethodCallback);
return;
}
}
deliverKeyEventToViewHierarchy(event,sendDone);
}
......
}
这个函数定义在文件frameworks/base/core/java/android/view/ViewRoot.java中。
参数event描述的是窗口接收到的键盘事件,另外一个参数sendDone表示该键盘事件处理完成后,是否需要向系统的输入管理器发送一个通知。
ViewRoot类的成员变量mView描述的是窗口的顶层视图,即它指向的是一个DecorView对象,ViewRoot类的成员函数deliverKeyEvent首先是调用它的成员函数dispatchKeyEventPreIme来让它优先于输入法处理参数event所描述的键盘事件。
如果这个DecorView对象的成员函数dispatchKeyEventPreIme的返回值handled等于true,那么就说明参数event所描述的键盘事件已经处理完毕,即ViewRoot类的成员函数deliverKeyEvent不用往下执行了。
在这种情况下,如果参数sendDone的值等于true,那么ViewRoot类的成员函数deliverKeyEvent在返回之前,还会调用成员函数finishInputEvent来通知系统的输入管理器,当前激活的窗口已经处理完成刚刚发生的键盘事件了。
在接下来的Step2到Step4中,我们再详细分析键盘事件优先于输入法分发给窗口处理的过程。
假设窗口不在输入法前面拦截参数event所描述的键盘事件,接下来ViewRoot类的成员函数deliverKeyEvent就会将该键盘事件分发给输入法处理,这个分发过程如下所示:
1.调用InputMethodManager类的静态成员函数peekInstance获得一个类型为InputMethodManager输入法管理器imm;
2.调用ViewRoot类的成员函数enqueuePendingEvent将参数event所描述的键盘事件缓存起来,等到输入法处理完成该键盘事件之后,再继续对它进行处理;
3.调用第1步获得的输入法管理器imm的成员函数dispatchKeyEvent来将参数event所描述的键盘事件分发给输入法处理。
这里有两个地方是需要注意的。
第一个地方是只有当前窗口正在显示输入法的情况下,ViewRoot类的成员函数deliverKeyEvent才会将参数event所描述的键盘事件分发给输入法处理,这是通过检查ViewRoot类的成员变量mLastWasImTarget的值是否等于true来确定的。
第二个地方是在将参数event所描述的键盘事件分发给输入法处理时,ViewRoot类的成员函数deliverKeyEvent会同时传递一个类型为InputMethodCallback的回调接口给输入法,以便输入法处理完成参数event所描述的键盘事件之后,可以调用这个回调接口的成员函数finishedEvent来向窗口发送一个键盘事件处理完成通知。
这个类型为InputMethodCallback的回调接口就保存在ViewRoot类的成员变量mInputMethodCallback中,当它的成员函数finishedEvent被调用的时候,它就会调用ViewRoot类的成员函数deliverKeyEventToViewHierarchy来继续将参数event所描述的键盘事件分发给窗口处理。
如果窗口当前不需要与输入法交互,即ViewRoot类的成员变量mLastWasImTarget的值等于false,那么ViewRoot类的成员函数deliverKeyEvent就会直接调用成员函数deliverKeyEventToViewHierarchy来将参数event所描述的键盘事件分发给窗口处理。
接下来,我们就先析窗口在输入法之前处理键盘输入的过程,接着再分析窗口在输入法之后处理键盘输入的过程。
从前面的分析可以知道,ViewRoot类的成员函数deliverKeyEvent是通过调用DecorView类的成员函数dispatchKeyEventPreIme来将获得的键盘输入优先于输入法分发给窗口处理的。
DecorView类的成员函数dispatchKeyEventPreIme是从父类ViewGroup继承下来的,因此,接下来我们就继续分析ViewGroup类的成员函数dispatchKeyEventPreIme的实现。
Step2.ViewGroup.dispatchKeyEventPreIme
[java]viewplaincopy在CODE上查看代码片派生到我的代码片
publicabstractclassViewGroupextendsViewimplementsViewParent,ViewManager{
......
//TheviewcontainedwithinthisViewGroupthathasorcontainsfocus.
privateViewmFocused;
......
@Override
publicbooleandispatchKeyEventPreIme(KeyEventevent){
if((mPrivateFlags&(FOCUSED|HAS_BOUNDS))==(FOCUSED|HAS_BOUNDS)){
returnsuper.dispatchKeyEventPreIme(event);
}elseif(mFocused!
=null&&(mFocused.mPrivateFlags&HAS_BOUNDS)==HAS_BOUNDS){
returnmFocused.dispatchKeyEventPreIme(event);
}
returnfalse;
}
......
}
这个函数定义在文件frameworks/base/core/java/android/view/ViewGroup.java中。
ViewGroup类的成员函数dispatchKeyEventPreIme首先是检查当前正在处理的视图容器是否能够获得焦点。
如果能够获得焦点的话,那么ViewGroup类的成员变量mPrivateFlags的FOCUSED位就会等于1。
在当前正在处理的视图容器能够获得焦点的情况下,还要检查正在处理的视图容器是否已经计算过大小了,即检查ViewGroup类的成员变量mPrivateFlags的HAS_BOUNDS位是否等于1。
只有在已经计算过大小并且能够获得焦点的情况下,那么正在处理的视图容器才有资格处理参数event所描述的键盘事件。
注意,正在处理的视图容器是通过调用其父类View的成员函数dispatchKeyEventPreIme来处理参数event所描述的键盘事件的。
如果当前正在处理的视图容器没有资格处理参数event所描述的键盘事件,但是它有一个能够获得焦点的子视图,并且这个子视图的大小也是已经计算好了的,那么ViewGroup类的成员函数dispatchKeyEventPreIme就会将参数event所描述的键盘事件分发给该子视图处理。
当前正在处理的视图容器能够获得焦点的子视图是通过ViewGroup类的成员变量mFocused来描述的,通过调用这个成员变量所描述的一个View对象