基于Android操作系统的框架层和应用层,介绍了View的绘制、触摸事件的传递流程,分析了View与用户交互时被回调的相关框架层代码和应用层代码,研究了Android应用中事件处理的相关重要机制。通过具体代码详细剖析了在Android系统下用户和View交互过程中折射出的回调机制,回调方法在系统框架的详细执行过程,以及基于回调机制的经典事件处理模型。
1 引言
Android是一种基于Linux的自由及开放源代码的操作系统,目前基于Android平台的应用日趋广泛。Android应用程序大多基于图形用户界面,因此,需要大量处理界面元素的显示和对相应控件的点击事件。在Android中,界面元素的组成控件都是继承自View类,View的点击事件常常连接着软件功能的各个模块,也最能体现用户在使用Android系统时的人机交互。但是,安卓应用开发者往往只站在操作系统的应用层进行开发,这种开发是浅层次的,没有从系统或引擎的框架源码进行分析,因此研究用户与View交互过程中的回调和具体过程有利于加深对Android系统框架层的认识,通过这个视角打破原有的思维方式,更快地解决Bug或者编写出质量更高的软件。在Android系统中存在很多工作机制,本文研究的View的点击事件实际上体现了Android中触摸事件的传递机制和回调机制。而对于核心的回调,首先讨论回调的概念、从理论上分析回调在Android中的基本模型,然后从Android系统框架层的源码上分析触摸事件具体如何被分发,具体回调方法是如何被调用的,最后回归到应用层,构建物理世界和虚拟世界的联系,由下而上总结和归纳View与用户在交互过程中的经典事件处理模型。
2 回调函数和Android系统中的回调机制
软件中相互联系的模块之间往往会产生调用,从调用方式上可分为三类:同步调用、异步调用和回调。同步调用和异步调用都是一种单向调用模式,而回调是一种双向调用模式,被调用方在方法被调用时也会调用对方的方法。回调函数就是一个通过函数指针调用的函数。回调函数并不是由该函数的实现方法直接去调用,而是把它的函数指针作为参数传递给另一个函数,在特定的事件或条件发生时由另外一方通过这个指针来调用,用于对该事件或条件进行响应。
2.1 Java中的回调函数
Java中不允许直接操作指针,而是通过接口或者内部类来实现的。Java方法回调是分离功能定义和功能实现的一种手段,这不仅是一种松耦合的设计思想,也是一种接口和对象的组合。开发者可以根据自己不同的需求实现接口,而方法的具体调用则由操作系统来完成。在Java中实现回调也有固定的模式,步骤如下:
a.定义接口Callback ,包含回调方法 callback();
b. 在一个类Caller 中声明一个Callback接口对象 mCallback;
c. 在程序中赋予 Caller类的接口成员(mCallback)一个具体实现了Callback接口的内部类对象。 interface Callback(){
void callback() ;
}
ClassCaller{
CallbackmCallback;
PublicCaller(Callback cb){ // 传入实例化之后的内部类对象cb
mCallback=cb;
}
if(根据实际情况回调){
mCallback.callback();
}
}
系统通过Caller类的mCallback成员回调callback()方法完成业务需要的逻辑。Callback并没有通过继承实际类用实际类对象来完成回调,而是通过实现接口用接口成员来完成,这是因为在Java语言中接口本身的设计和抽象类相似,可以对不同的业务需求形成多样的接口实现,有利于程序形成较好的多态性,同时对于开发者来说实现接口比继承实际类完成回调更加便捷并且更符合逻辑性,因此在Java程序中的回调常常是按照上述模式,通过传递已经实现了抽象接口的对象引用来完成方法回调。
2.2: Android系统应用层中的回调机制
在Android系统应用层中,回调机制是非常普遍和重要的。在操作系统层面,回调机制在通信、消息传递、事件处理等方面也有着较多的应用,以下以Button按钮的onClick方法为例介绍View被点击时的框架模型和相关方法的回调。
a.定义接口,接口内部就是系统要回调的方法,参数是即将发生回调的Button,这个接口常被叫做事件监听器,它相当于是View的一个属性。
public interface OnClickListener{
public void onClick(Buttonb);
}
b.定义Button类,这里代表View类
public class Button{
//申明Button类的接口(监听器)成员
OnClickListener listener;
public void click(){
listener.onClick(this);
}
//set函数用于接收外部传入的被实例化了的监听器实体对象,其实是一种面向对象的语言多态性的体现
public void setOnClickListener(OnClickListenerlistener) {
this.listener = listener;
}}
c.定义测试类,将上述监听器实体对象以匿名内部类的形式传入Button类
public class Activity{
public Activity(){
}
public static void main(String[]args) {
Button b = new Button();//实例化Button类
b.setOnClickListener(new OnClickListener(){
@Override
public void onClick(Buttonb) {//onClick中的内容是对接口的不同实现
System.out.println("clicked");
}
});
b.click(); //在主函数中进行回调的模拟测试
}
}
Android事件监听器是包含在View类的一个接口,包含一个单独的回调方法onClick(button b),如上,接口被具体实现并表现为一种内部类的形式,onClick方法中的具体内容将在按钮被点击时由Android系统框架进行回调。因此,若需对点击事件做处理,应该首先定义一个OnClickListener 接口(事件监听器),然后通过set()或者构造方法将已经具体实现了该接口的实际对象或者匿名对象赋予Button类的接口成员,最后操作系统通过该接口成员在合适的情形下回调onClick()的具体实现内容。在main()函数中,setOnClicklistener()就是这样一个set()函数用来接收已经实例化监听器接口的匿名类对象并将之赋予之前申明的接口成员;而另外一个click()方法,其实是一个模拟用户点击时状态环境的一个方法,表示用户点击这个按钮时的环境。此方法在主程序中被主动调用,但该方法实际上应该是屏幕捕捉到一个用户点击的动作之后转换成某一坐标A,如果这个坐标A处于该View所在的范围,那么才调用click方法,也就是说系统先是获取点击信号,如果在View的范围就会触发click事件,进而回调Button接口成员(事件监听器)的onClick方法,而这也完全符合实际中Android应用程序与用户交互时View的回调。
从回调本身来看,onClick()被调用后最终会在外部执行实际的接口(监听器)实现代码,而onClick()回调却在Android系统内部实现,这很好地体现了Android系统本身设计的层次性,同样体现了接口和对象的组合在实际开发中的作用。在实际的Android操作系统中,接口是系统框架提供的,接口的实现是上层的开发者来实现的,通过回调这种方式就可以达到接口统一而实现不同。系统在不同的状态“回调”实现类中的具体方法,以此达到接口和实现的分离,对象和接口的组合。
在Android系统中,回调不仅仅体现在View与用户交互的时候,在其他场景(消息传递、事件处理等)中也有广泛的应用,比如Activity生命周期中的onCreate()、onStop()等方法,其中的具体实现也需要开发者根据实际情况来书写,这些方法被封装在一个类似的接口中,同样在系统运行时系统会根据某一状态值回调相应的生命周期中的方法。Android中很多回调在设计上都是相似的,都是通过在主类里面封装一个接口然后在上层实现这个接口,接着系统底层在合适的时候回调接口里面已经实现的方法,这样上层和底层就被很好的分开。
3 Android系统框架层中回调函数的具体调用流程
图1:View树图
Android系统中回调机制的概念和在应用层中模拟的回调过程在前面被讨论,下面将从源代码的角度深入探寻在Android系统框架层中,View点击时触发的方法是如何在框架源码里面具体被回调的。源码来源于:Copyright(C) 2006 the Android Open Source Project,系统架构内核编号是Android 2.3
3.1 触摸事件的传递机制
在Android系统中,每个View或者View的子类都具有下面三个和触摸事件处理密切相关的方法:dispatchTouchEvent方法用来分发TouchEvent;onTouchEvent方法用来处理TouchEvent;如果这个View同时也是ViewGroup类型的话,那么还拥有一个onInterceptTouchEvent方法,它主要用来拦截TouchEvent。
如图1所示(图一 View树图)一个Android应用程序的界面可以看成是一颗View树,所有界面的视觉呈现实际上源于View树的自上而下的绘制。ViewGroup类也是继承自View,View和ViewGroup之间相互组合和嵌套形成View树。当TouchEvent发生时,首先系统将用户的触摸信号传递给最顶层的View,也就是最外层的ViewGroup(实际中可以是Relativelayout等容器),接着TouchEvent从上层逐渐传入到各分支:
图二:触摸流程图
a 对照(图二 触摸流程图),当用户的手指触摸到手机屏幕,会触发最顶层View的dispatchTouchEvent()。最顶层的View首先检查该函数的返回结果:
(1)如果返回true ,则交给这个View自己的onTouchEvent方法处理,不会向下传递。
(2)如果返回false,则交给这个View的 interceptTouchEvent 方法来决定是否要拦截这个事件。
b 接着到 onInterceptTouchEvent执行:
(1)如果返回 true ,即拦截掉了,则还是交给此View的onTouchEvent处理。
(2)如果返回 false,则传递给它的子View ,由子 View的dispatchTouchEvent 再来开始这个事件的分发。
c 以此类推,通常TouchEvent会传递到某一层子View 的 onTouchEvent 内,而接下来研究的回调的具体执行过程就是基于这一触摸事件的传递机制进行展开并在onTouchEvent源码内进行研究。
3.2 Android系统框架层下View内部回调函数的详细回调过程
还是以Button作为View的一个例子,源码位于系统结构路径的位置是 /frameworks/base/core/java/android/view/View.java。
首先是系统框架层下对OnClickListener接口的定义:
public interface OnClickListener {
void onClick(Viewv)
}
}
然后是对接口成员实例化的set函数,需要传入一个实现了OnClickListener接口的实际类对象if()语句表示Button的属性一定是clickable(可点击)的,这也解释了为什么在Button的xml里面不需要申明这个属性,而TextView需要这个属性,因为系统会自动对Button设置可点击这个属性值。
public void setOnClickListener(OnClickListenerl) {
if (!isClickable()) {
setClickable(true);
getListenerInfo().mOnClickListener = l;
}
getListenerInfo()方法 是返回所有自定义监听器的一个静态内部类ListenerInfo的对象,通过它寻找想要注册到Button上的某种监听器(这里使用的是OnClickListener),该内部类在View.java的前半部分被申明:
static class ListenerInfo {
…………
private OnKeyListener mOnKeyListener;
private OnTouchListener mOnTouchListner;
private OnHoverListener mOnHoverListener;
private OnGenericMotionListener mOnGenericMotionListener;
}
………
ListenerInfo getListenerInfo() {
ListenerInfo mListenerInfo;
if (mListenerInfo != null) {
return mListenerInfo;
}
mListenerInfo = new ListenerInfo();
return mListenerInfo;
}
根据3.1中介绍的触摸事件的传递机制,TouchEvent最终会被传递到View树最底层的View,交给该View的onTouchEvent进行处理,在这个方法里面大体要处理三种情况:当手指按下,手指移动和手指松开,系统会在这三种不同情况中进行相应的处理,而手指松开是最重要的情况,因为每一个TouchEvent最终都会经历这一步,onTouchEvent方法的具体代码如下所示:
public boolean onTouchEvent(MotionEvent event) {
final int viewFlags = mViewFlags;
//这里首先判断这个View是不是可用的,防止之前被设置成禁用
if ((viewFlags & ENABLED_MASK) == DISABLED) {
…………
//下面一句话是说无论是否可点击,但总要把这次事件先消耗掉
return (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
}
//下面是当View可以被点击时的状况
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
switch (event.getAction()) {
//第一个case分句处理按下的时候
case MotionEvent.ACTION_DOWN:
/*首先遍历整棵View树,isInScrollingContainer方法内部用一个循环判断当前View外层的父类是不是一个可滑动的容器*/
booleanisInScrollingContainer = isInScrollingContainer();
/*如果是一个可滑动的容器,就设置一下相关标志位,mPendingCheckForTap表示一个消息线程,它在postDelayed()方法中会被延时发送出去,因为要确定用户在这个容器下是点击还是滑动,如果一定时间内用户没有滑动会自动执行mPendingCheckForTap消息线程,该消息内部接着会判断是不是长按操作,如果在这段时间内用户进行了滑动那么就立即删除掉这个消息。*/
if (isInScrollingContainer) {
mPrivateFlags |= PREPRESSED;
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
postDelayed(mPendingCheckForTap);
} else {
/*如果父类的容器不可滑动说明一定是点击操作。接着用上一个子句同样的思想“等等再执行”检查是不是长按的操作,如果一段时间用户没有松开就会触发长按消息的线程checkForLongClick。反之,就将这个消息移除。在这里,可以体会到判断长按短按,滑动点击独有的算法:“等等再执行”。*/
mPrivateFlags |= PRESSED;
refreshDrawableState();
checkForLongClick(0);
}
break;
//处理移动的情况
case MotionEvent.ACTION_MOVE:
final int x = (int) event.getX();
final int y = (int) event.getY();
//在这里记录下用户移动的位置坐标,其中mTouchSlop表示判断触摸点是否在此View中时,向上下左右增大mTouchSlop个像素(允许的误差范围)
if (!pointInView(x, y, mTouchSlop)) {
//如果不在View的范围里面,那么将处理点击的消息移除
removeTapCallback();
if ((mPrivateFlags & PRESSED) != 0) {
//进一步判断如果都已经准备长按了,则将长按的消息移除,并将View的按下状态设置为false
removeLongPressCallback();
mPrivateFlags &= ~PRESSED;
refreshDrawableState();
}
}
break;
//处理松开的情况
case MotionEvent.ACTION_UP:
………
if (!mHasPerformedLongPress) {
//执行到该标志量表明了这不是长按,是一个点击,所以移除长按的相关消息。
removeLongPressCallback();
if (!focusTaken) {
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
//直到最后我们看到了performClick()被执行。
performClick();
}
}
}
…………
}
break;
}
return true;
}
return false;
}
// 在performClick()中回调方法被执行,回调的所有流程到此结束。
public booleanperformClick() {
………
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
//回调方法在这里被执行
li.mOnClickListener.onClick(this);
return true;
}
return false;
}
4 Android系统应用层中折射的事件处理模型
从应用层的角度上来说,View与用户交互过程中体现的不仅仅是之前在框架层里面详细分析的回调机制,同时也是沿袭了java可视化编程开发的一种经典的事件处理方式。这种机制本身包含触发事件、事件源、事件监听器、响应事件四个方面。触发事件指的是用户操作手机的交互方式,如长按、点击、触摸、回滚等操作;事件源是指产生事件的组件,也就是各种各样的View控件(按钮、图片等等);事件监听器是组件产生事件时响应的接口,如OnClickListener;最后响应事件指的是OnClickListener里面的回调方法onClick()。
图三 现实场景图
这种方式非常类似现实生活中的一种场景(如图三 现实场景图):现在的高级轿车都有防盗功能,当车产生剧烈震动时就会报警,报警时报警灯会不断闪烁发出提示音。在这里,事件源是车,事件监听器是车上的报警器,触发事件就是感应到的震动,而报警灯的闪烁及发出提示音指的是响应事件。联系到实际的Android开发中,Button按钮就是一个接收事件的事件源;事件监听器就是OnClickListener接口,需要用户实例化传入;触发事件就是用户的点击操作;点击之后发生的回调onClick就是响应事件。这样子的类比应该更能让读者理解这种机制。
接下来编写一段实际的java代码模拟汽车防盗的经典事件处理模型:
public classCar {//事件源
intColor;
intstyle;
floatprice;
String name;
PreventTheftListenerlistener;
staticboolean isDanger=true;
//监听器的定义
interfacePreventTheftListener{
publicvoid onTheft(Car car);//触发事件
}
publicCar(String name) {
this.name=name;
}
privateboolean isDanger() {
returnisDanger;
}
publicvoid setPrevent_Theft_Listener(PreventTheftListener listener) {
this.listener=listener;
}
publicvoid doListener(){
if(listener!=null)
listener.onTheft(this);
}
//监听器的实例化
staticclass Wuhan_CarListener implements PreventTheftListener{
privateint color;
privatefloat price;
Stringname;
publicWuhan_CarListener(String name) {
isDanger=true;
this.name=name;
System.out.println("我已经安装到你的汽车上,我已经对周围的环境处于监听状态\n");
}
publicvoid onTheft(Car car) {//回调事件
System.out.println("主人,有震动,小偷要偷你的汽车");
noise();
flash();
}
privatevoid noise() {
System.out.println("模拟报警器的功能之一-----发出响声");
}
privatevoid flash() {
System.out.println("模拟报警器的功能之一-----不断闪光");
}
}
publicstatic void main(String[] args) {
Car c=newCar(“甲壳虫”);
c.setPrevent_Theft_Listener(newWuhan_CarListener("##牌防盗监听器"));//装上防盗监听器
if(c.isDanger()==true){
c.doListener(); }
}
}
图四 事件处理模型图
(如图四所示 事件处理模型图)Android中View点击事件的回调都存在事件源对象(Button),监听器对象(OnClickListener),事件的触发者和事件的响应这四样东西,四者实际上构成了一种经典的事件处理模型,它能够灵敏的捕捉到外界的触发动作并在合适的场合执行回调方法,这样子就实现了接口定义和接口实现的分离,突出了接口和对象的组合,也体现了软件设计中的松耦合性和多态性。只不过这个经典的模型在实际的Android系统中,需要考虑更多实际系统的元素,因此显得复杂,但在应用层可以看出其本质是相似的。
汽车里面给用户留下一个接口,试想,如果把防盗的功能直接作为一个实际类成员绑定在汽车上面,那么在有震动的时候当然也能利用该成员调用防盗的动作,但这样就不够灵活,不能够让每个设计防盗器的厂商设计自己的防盗器性能,可扩展性非常的差。所以,在车里面定义一个监听器的接口,而接口的实现交给每一个专门设计防盗器的厂商来做,在合适的时候利用监听器对象来调用不同厂商的实现方式,这也体现出了多态。这样子,在汽车行业,做防盗器的公司就和做汽车的公司分开了,其经济效益和生产效率也会加倍。在Android系统中,接口是系统框架提供的,接口的实现是各种公司不同的上层应用开发者实现的。如(图五 接口设计图)这样就可以达到接口统一,而实现不同,系统只需要在不同状态回调我们的实现类来达到接口和实现的分离。
图五 接口设计图
5 结语
方法回调是功能定义和功能实现相分离的一种手段,是一种松耦合的设计思想。Android作为一种优秀的移动智能终端系统架构,有自己的运行环境同时也在不同的场合为开发者设置了不同的开发接口,而操作系统通过在不同的情形“回调”开发者对接口的不同实现来达到接口定义和接口实现的统一。在Android应用程序中用户与View的交互是非常普遍和重要的,本文详细研究了Android系统底层的框架代码到上层的应用代码,构建出了虚拟世界和物理世界的联系,介绍了 Android系统下的回调机制、用户和View交互时触发的回调方法在系统框架的详细执行过程以及基于这一机制下的经典事件处理模型。借助这篇文章一方面希望读者借此理解Android中的回调机制和经典的事件处理模型,另一方面希望读者多从系统源码的角度研究和解决问题。但是,关于这一课题的研究毕竟目前还只是剖析Android框架的一小部分,在这一课题或其他框架理论的发现和创新还有待于进一步的探索和研究。