[Android研究手记3]组件间的交互和进程间IPC通信

27
Jun
1

在Android中窗体与窗体之间如何互相调用和交换数据?窗体(Activity)和后台的服务(Service)如何通信?基于Unix(Linux)的系统都有一个很优秀的传统,就是倡导非常轻便的进程间通信(IPC)机制;倡导进程通过IPC来互相协作;倡导功能单一,小巧而强壮的进程,而不是又大又复杂的“万金油”。同样,在Android中我们可以将我们的Activity和Service放在不同的进程中运行,我们可以在我们的Task 中加载其他进程的Activity,这些机制都鼓励我们“尽量利用已有的功能,利用IPC和包含这些已有功能的程序协作,来完成一个完整的应用”,例如在我们的程序中充分利用Google Map的相关窗体和服务。所有这些都建立在一套轻便好用的IPC机制上。


Android的组件和进程间通信都建立在一种基于称为Intent的消息基础之上。Intent就是一种消息,它包含了两个重要的内容:1. 消息的目的,即这个消息是发给哪个组件的?(消息的目的中不会包含“消息是发给哪个进程”这样的信息,这里Android有意淡化进程的概念,而只让我们关心组件,因为了解太多关于进程的具体信息会加大复杂度,而又如何做到进程间的消息传递呢?下文会说到一种Android中关于这点比较特别的设计方式,我认为是一种简捷有用又符合手机特点的设计);2. 消息所携带的数据内容,即需要传递给目标的数据。下面是一个简单的利用Intent来启动一个Service并向其传递数据的代码示例:


Intent serviceIntent = new Intent(context, svrMain.class);
serviceIntent.putExtra(“Network_Report”, networkStatus);
context.startService(serviceIntent);


上面的代码首先构造了一个Intent对象,并在构造的时候指定了这个Intent的目的地,即“svrMain.class”,表示这个Intent是要传递给一个类名叫svrMain的Service。然后向这个Intent中放入了一个数据,数据的key为“Network_Report”,value为一个叫networkStatus的int类型变量,用来指明当前网络的状态。最后我们使用系统提供的上下文API,将这个Intent传递给指定的Service。


Intent的消息目的地分为两种模式,一种是显式的,一种是隐式的。我们上面的例子中看到的就是一个显式消息的例子。显式消息直接指定消息目的地组件的类元信息,例如上面例子中svrMain就是我们写的一个类名为svrMain的Service,class操作符就是获取其类元信息。这种模式的消息由于已经确切知道了消息目标的确切信息,所以只适用于同一进程内的不同组件之间通信,例如打开一个子窗体,和同一进程中的service通信等。对应的,隐式消息就一般用于跨进程的通信了,隐式消息没有确定的消息目的地,除了数据外,隐式消息只是包含了一些用于表征消息特征的描述字段。而一些需要收到某种特定特征消息的某个程序中的某个组件,需要通过在其所在程序的AndroidMainifest.xml中注册一种被称为intent-filter的消息特征筛选器,然后Android系统会按照一定的匹配规则来匹配发出的消息特征和所有拥有响应这种特征的intent-filter的组件(无论是同一进程内的组件还是不同程序中的组件),匹配到的组件就会接受到相应的消息。前面的描述多少有些拗口,我们举个实际的例子来说明,如果我们想开启一个子窗体(无论这个窗体来自同一进程还是不同进程),我们除了使用显式消息外,我们还可以使用隐式消息:


Intent openSomeDiagIntent = new Intent();
openSomeDiagIntent.setAction(“edwin.demo.fooActivity”);
this.startActivity(openSomeDiagIntent);


上面的隐式消息不包含具体的目的地,而是仅包含一个名位“Action”的特征字符串,Action就是上文所说的Intent特征的一种。从字面上来理解,可以理解为这个消息所代表的是完成某一个动作的含义,由action来标明动作的名字。所有能够处理这种动作的Activity都可以收到该消息。对应的,可能在同一个程序或者另外的某个程序的AndroidMainifest.xml中声明了下面这样的一个Activity:


<activity android:name=”.fooActivity”>
<intent-filter>
<action android:name=”edwin.demo.fooActivity” />
</intent-filter>
</activity>


那么这就表示这个Activity能够收到并处理action为“edwin.demo.fooActivity”的消息。所以上面的代码串起来的效果就是,打开了这个名为.fooActivity的窗口,无论这个窗口是在当前的进程中还是另外的一个程序中。


除了Action这种消息特征外,Intent还有category,data这两个特征描述属性。Category同样是一个字符串,从字面上理解就是“消息的分类特征”。从程序上看其和Action的不同在于,一个Intent只能有惟一的一个Action名称,但是却可以包含多个Category字符串;一个Intent-Filter可以包含多个Action节点但至少要包含一个,另一方面一个Intent-Filter可以包含零到多个Category节点。Android在做Intent-Filter匹配的时候,Intent的Action属性匹配到Intent-Filter中的任何一个action节点,就表明拥有这个Intent-Filter的组件能够处理这种消息;而对于Category来讲一个Intent中的所有的Category都必须存在于Intent-Filter中的Category节点中时,才表明匹配成功。Data属性可以描述一个Intent所要传递的数据类型和URI,每一个Intent只能包含一个Data属性。其中数据的类型使用MIME类型描述方式来描述,例如video/mpeg表示编码格式为mpeg的视频,这里也可以使用通配符,例如video/*表示任意格式的视频文件类型;数据的URI由scheme(协议),host,port,path四部分组成:scheme://host:port/path,例如http://test.com:8080/file/file1 或者content://edwin.demo.contentProvider:100/forder/content1,其中path部分也是可以支持通配符的。Data属性是一个很有用的描述特征,例如下面这样的一个包含data节点的Intent-Filter:


<activity android:name=”.actHttpVideoMan”>
<intent-filter>
<action android:name=”edwin.demo.actHttpVideoMan.Main” />
<data android:scheme=”http” android:type=”video/*” />
</intent-filter>
</activity>


它表示窗体actHttpVideoMan能够处理来自web服务器的视频文件。这样的filter有什么作用呢?最典型的情况就是配合浏览器工作。浏览器在打开一个链接的时候首先会尝试显示这个链接对应的html页面,如果这个链接不是一个html页面,而是一个视频文件或者其他浏览器本身不能处理的格式的话,浏览器会使用隐式消息尝试开启一个能够处理这种数据格式的Activity来处理,浏览器发出的隐式消息就是一个包含data属性,其中URI scheme为http,数据类型为video/*的消息,如果有能够匹配这个intent的组件,例如我们上面的那个activity,浏览器就会启动这个窗体,接着这个窗体会根据data属性指定的URI去播放在线视频,如果没有可以处理这个intent的Activity,浏览器才会调用下载管理器下载文件。


隐式消息这个设计简单有效,它忽略了进程的细节,让IPC在一个更高的更接近人脑思维模式的层次工作,让系统中的不同进程协作看起来就像是同一程序中的协作一样,这种简单的IPC机制在很大的程度上鼓励我们和其他进程协作,通过协作的进程来完成一个复杂的任务,而不是把什么功能都做到一个大而全的程序里面。不过上文还有一些细节没有提到,例如如果一个intent有多个可匹配的处理组件,系统如何处理?这就要分响应消息的组件类型来说了,如果是service,那么这些service都可以启动并处理消息,如果是Activity,则android会弹出一个对话框让用户进行选择。比如我们安装了多个可以处理在线视频的软件,当我们在浏览器中点击一个在线视频的链接时,系统会让用户选择使用哪个软件来观看。另外大家一定会想到安全性的问题,如果不同进程间的组件可以通过隐式消息互相通信,那我们的程序不是可以轻易调用到其他的程序或者系统中的一些敏感程序的组件,这样会不会很不安全呢?其实Android在安全方面有一个统一,完备和轻便的安全策略模型,Intent的安全自然是被考虑在内的,关于android的安全模型我会在后续的系列blog中专门说明。


最后,除了Intent这种基于消息的进程内和进程间通信模型外,android中也有一种相比起来稍显笨重一些的IPC机制,它采用类似远程方法调用的方案,通过接口定义文件AIDL来定义一个IPC接口,然后通过接收方实现接口,调用方调用接口的本地代理实现来完成IPC。这种模型只适用于Activity和Service间的通信,之所以需要这种稍显重量的模式,是因为Activity除了发送intent去启动一个service外,可能还需要能够在Service的运行过程中连接到service,对Service发送一些控制请求。例如音乐播放程序,其后台的播放服务往往独立运行,以方便我们在使用其他程序界面时也能听到音乐。同时这个后台播放服务也会定义一个控制接口,包含比如播放,暂停,快进之类的方法,任何时候播放程序的界面都可以通过使用bindService API连接到播放服务,获取这个接口的包含IPC细节的实现代理,通过这组控制接口方法对其进行控制,这时这种IPC的方案就显的更方便更直观一些了。有关使用AIDL这种IPC的更详细描述,Google的官方文档已做了详细的讲解。


下篇预告:《[Android研究手记4] Service的运行时生命周期模型》

Filed under: Android

难用到令人抓狂的w3c标准DOM解析API

20
Jun
3

被.Net毒害太深,以至于离开.Net就发现自己原来是个菜鸟,一件在.net中可以很简单完成的事情,到了java下面可能就要让你抓狂。要解析Xml,Android提供的jar中包含了最基本的w3c DOM API和标准的SAX(Simple API for XML),因为常年沉侵在.Net的温床中,导致了对这些标准API的从来不闻不问,当使用到这些API时,也就只有抓狂了。。。没有XPath支持不说,看看下面这个Xml片段在标准DOM API中的Node结构便知道抓狂的含义了:

<root>
<!–xxx–>
<sub>123</sub>
</root>

首先,DOM里的Node不是在.net理解中的<a></a>这个叫一个node,任何xml中的元素都是node,<a></a>只是一种type为ELEMENT_NODE的node,此外还有很多type的node,常用的还有TEXT_NODE,上面XML片段的Node模型如下(*表示缩进):


root (NodeType: ELEMENT_NODE)
****\n (NodeType: TEXT_NODE,抓狂1:<root>后面的换行符\n也是一个Node!一个文本类型的Node,标准API中可以用getNodeValue()得到“\n”)
****<!–xxx–> (NodeType: COMMENT_NODE)
****\n (NodeType: TEXT_NODE,还是\n)
****sub (NodeType: ELEMENT_NODE)
********123 (NodeType: TEXT_NODE,抓狂2:<sub>中的文字也是一个node,getNodeValue()得到”123″。这个文字不是sub节点的nodeValue,而是一个新的node,sub节点的nodeValue会直接返回null)
****\n (NodeType: TEXT_NODE,sub节点后的换行符)


如果之前不了解w3c标准DOM API,很抱歉,上面的知识得花费你个把小时调试你才能学会。幸运的是除了标准API,我们还有更多的选择,DOM4J就是一个不那么让人抓狂的开源XML解析工具包。

Filed under: Language

[Android研究手记2]Activity的运行时生命周期模型

15
Jun
7

由于在Android中,进程的生命周期大多数时候是由系统管理的;另外也由于手机应用的一些特殊性,所以我们需要更多的去关注各个Android Component的运行时生命周期模型。(所谓手机应用的特殊性主要是指这样2点: 1. 手机应用的大多数情况下我们只能在手机上看到一个程序的一个界面 ,用户除了通过程序界面上的功能按钮来在不同的窗体间切换,还可以通过Back键和Home键来返回上一个窗口,而用户使用Back或者Home的时机是非常不确定的,任何时候用户都可以使用Home或Back来强行切换当前的界面。 2. 往往手机上一些特殊的事件发生也会强制的改变当前用户所处的操作状态,例如无论任何情况,在手机来电时,系统都会优先显示电话接听界面。)了解这些Component的生命周期模型一方面是让我们对软件在手机中的运行情况做到心中有数,更重要的,对于程序开发来说,生命周期中的每一个关键事件都会有我们可以覆写于各种Component对应基类型的事件处理方法,了解各Component的生命周期就是让我们在开发程序时明白我们该怎样去编写各种事件的处理代码。例如Activity的Create,就会有对应的事件处理函数onCreate,我们可以从Activity基类覆写这个事件处理函数完成我们需要的相关事件处理:

public class actMain extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
…… //我们的事件处理代码
}
……

这篇Post我们就来看看最常用的Activity的运行时生命周期模型(Service的运行时生命周期模型在下一篇讲述了如何启动一个Service并和其通信后再做描述)。 Activity的生命周期模型在Google提供的官方文档上有比较详细的一个图示(请自行翻墙查看)。其一共包含7个我们需要关心的关键事件,下面对其分别详细说明(文字中的粗体字表示后文中会经常用到的概念在第一次出现时会给出解释,之后后文不再详细说明):


1. void onCreate(Bundle savedInstanceState)
当Activity被第首次加载时执行。我们新启动一个程序的时候其主窗体的onCreate事件就会被执行。如果Activity被销毁后(onDestroy后),再重新加载进Task时,其onCreate事件也会被重新执行。注意这里的参数savedInstanceState(Bundle类型是一个键值对集合,大家可以看成是.Net中的Dictionary)是一个很有用的设计,由于前面已经说到的手机应用的特殊性,一个Activity很可能被强制交换到后台(交换到后台就是指该窗体不再对用户可见,但实际上又还是存在于某个Task中的,比如一个新的Activity压入了当前的Task从而“遮盖”住了当前的Activity,或者用户按了Home键回到桌面,又或者其他重要事件发生导致新的Activity出现在当前Activity之上,比如来电界面),而如果此后用户在一段时间内没有重新查看该窗体(Android通过长按Home键可以选择最近运行的6个程序,或者用户直接再次点击程序的运行图标,如果窗体所在的Task和进程没有被系统销毁,则不用重新加载Process, Task和Task中的Activity, 直接重新显示Task顶部的Activity, 这就称之为重新查看某个程序的窗体),该窗体连同其所在的Task和Process则可能已经被系统自动销毁了,此时如果再次查看该窗体,则要重新执行onCreate事件初始化窗体。而这个时候我们可能希望用户继续上次打开该窗体时的操作状态进行操作,而不是一切从头开始。例如用户在编辑短信时突然来电,接完电话后用户又去做了一些其他的事情,比如保存来电号码到联系人,而没有立即回到短信编辑界面,导致了短信编辑界面被销毁,当用户重新进入短信程序时他可能希望继续上次的编辑。这种情况我们就可以覆写Activity的void onSaveInstanceState(Bundle outState)事件,通过向outState中写入一些我们需要在窗体销毁前保存的状态或信息,这样在窗体重新执行onCreate的时候,则会通过savedInstanceState将之前保存的信息传递进来,此时我们就可以有选择的利用这些信息来初始化窗体,而不是一切从头开始。

2. void onStart()
onCreate事件之后执行。或者当前窗体被交换到后台后,在用户重新查看窗体前已经过去了一段时间,窗体已经执行了onStop事件,但是窗体和其所在进程并没有被销毁,用户再次重新查看窗体时会执行onRestart事件,之后会跳过onCreate事件,直接执行窗体的onStart事件。

3. void onResume()
onStart事件之后执行。或者当前窗体被交换到后台后,在用户重新查看窗体时,窗体还没有被销毁,也没有执行过onStop事件(窗体还继续存在于Task中),则会跳过窗体的onCreate和onStart事件,直接执行onResume事件。

4. void onPause()
窗体被交换到后台时执行。

5. void onStop()
onPause事件之后执行。如果一段时间内用户还没有重新查看该窗体,则该窗体的onStop事件将会被执行;或者用户直接按了Back键,将该窗体从当前Task中移除,也会执行该窗体的onStop事件。

6. void onRestart()
onStop事件执行后,如果窗体和其所在的进程没有被系统销毁,此时用户又重新查看该窗体,则会执行窗体的onRestart事件,onRestart事件后会跳过窗体的onCreate事件直接执行onStart事件。

7. void onDestroy()
Activity被销毁的时候执行。在窗体的onStop事件之后,如果没有再次查看该窗体,Activity则会被销毁。


最后用一个实际的例子来说明Activity的各个生命周期。假设有一个程序由2个Activity A和B组成,A是这个程序的启动界面。当用户启动程序时,Process和默认的Task分别被创建,接着A被压入到当前的Task中,依次执行了onCreate, onStart, onResume事件被呈现给了用户;此时用户选择A中的某个功能开启界面B,界面B被压入当前Task遮盖住了A,A的onPause事件执行,B的onCreate, onStart, onResume事件执行,呈现了界面B给用户;用户在界面B操作完成后,使用Back键回到界面A,界面B不再可见,界面B的onPause, onStop, onDestroy执行,A的onResume事件被执行,呈现界面A给用户。此时突然来电,界面A的onPause事件被执行,电话接听界面被呈现给用户,用户接听完电话后,又按了Home键回到桌面,打开另一个程序“联系人”,添加了联系人信息又做了一些其他的操作,此时界面A不再可见,其onStop事件被执行,但并没有被销毁。此后用户重新从菜单中点击了我们的程序,由于A和其所在的进程和Task并没有被销毁,A的onRestart和onStart事件被执行,接着A的onResume事件被执行,A又被呈现给了用户。用户这次使用完后,按Back键返回到桌面,A的onPause, onStop被执行,随后A的onDestroy被执行,由于当前Task中已经没有任何Activity,A所在的Process的重要程度被降到很低,很快A所在的Process被系统结束。


下篇预告:《[Android研究手记3] Component间的交互和进程间IPC通信》

Filed under: Android

[Android研究手记1]基本概念和模型

8
Jun
21

前几天已经给大家做过广告了,从这篇开始,我准备每周更新一篇Android研究手记,分享我学习Android开发过程中的一些经验和知识,同时也是给自己进行备忘。由于我研究Android的过程也是我在Android上的第一个完整项目BlogMessage的开发过程,所以我尽量将这个实际项目的开发过程穿插在整个系列中,并试图以此将Google提供的各种文档中比较难理解的部分用尽量简单的方式表达出来。就不继续打广告了,下面进入正题:


学习在一个全新的平台上开发最重要的第一步是什么?我觉得当然不是语言的问题,也更不是工具的问题。学习Windows开发,我们得了解Win32窗口的运行原理,消息队列等机制,不了解这些背后的机制和基本的运行模型,只靠RAD工具我们还是只能停留在普通应用的阶段。学习Web开发,不了解HTML DOM,HTTP协议原理等内容,只靠掌握如ASP.Net,JSP等厂商技术,我们也做不到Web开发的“心想事成”,“胸有成竹”;同样如Anrs Blog中说到的正则表示式的应用,我们还得了解其背后的匹配原理。所以,我觉得面对一个全新的平台,我们得首先了解其工作和运行的基本模型和原理,才能做到以后在这个平台上的“得心应手”。


Android开发平台的架构模型,Google官方已经用一个很简单的结构图清晰的进行了说明,简单来说Android开发平台就是 Linux + Google在其上自己开发的Java虚拟机和运行时 + Android SDK 构成,这些内容供我们了解就可以了。就开发一个完整的Android应用来说,我将对我们十分重要而Google官方文档又说的有些复杂的,关于一个完整的Android程序的静态组成模型,和动态运行时模型,整理成了一张图形如下,我们就从这张图说起:

点击图片看大图

各位看官先不要被图形中的生词吓死,接下来我会详细解释每一个概念。


先来看图形中的灰色部分,这部分描述了一个完整的Android应用程序可以包含的各个组成部分,我们将组成一个Android程序的组件称为Android Component(图中中间部分的基类),由若干个Android Component就组成了一个完整的Android应用程序。先看图中左下方的Activity,这个组件我们可以认为它是Windows中的窗体概念,这是Android程序的基本组成部分,也就是程序的人机交互界面。比如一个简单的短信程序就应该包含三个Activity,一个短信列表界面,一个阅读短信详细内容的界面和一个编辑短信的界面。图中左上角的Service顾名思议就是服务,一个Android程序中哪些部分是服务呢?举例来说,短信程序并不只是在我们打开短信界面的时候才去收取短信,我们退出界面后,手机仍然会去收取短信,并在新的短信到达时通知我们,所以一定有某个任务在后台运行着,这就是Service了;再比如说音乐播放功能,当我们从播放界面返回手机待机界面的时候仍然可以继续听音乐,这也是一个Service的例子。其实Activity + Service是非常常见的手机软件应用,比如我要做的BlogMessage同样也是这样的结构。左边中间部分的“Broadcast Receiver”是用于接收各种系统定义事件或自定义事件的接收器,如果我们的程序想侦测一些系统事件的发生,我们就需要写一个Broadcast Receiver。例如我们的程序想在手机打开Wifi的时候立即去刷新最新的数据,或者我们想在手机来电时执行某个动作,这些都可以由Broadcast Receiver订阅特定的事件来完成。图中左边剩下的“Content Provider”,我们可以把它理解成一种特殊的Service,一种可以给其他程序提供数据的Service,例如手机中的联系人信息,我们任何程序都可以和其通信去获取联系人的信息,这就可实现为一个典型的Content Provider。


再来看图中蓝色的部分,这是一个静态的部署概念,就如同我们.Net开发的程序集的概念一样。Apk是我们Android程序发布和部署的基本单位,一个完整的Android程序就可以打包为一个或多个Apk进行发布,我们从Android Marketing上下载安装的程序也是一个个的Apk包,我们在Eclipse 中的一个Project的最终Build结果也就是一个Apk文件。一个Apk中包含了上面介绍的4种Android Component。


最后,图中黄色的部分就是系统运行时的概念了。由于Android平台是基于Linux的,所以Process(进程)和Thread(线程)的概念和Linux中的一致,在代码中我们可以编写一个普通的Java Thread来实现多线程。需要注意的是,Android中的Process是受系统自动管理的,并不是说我们在一个程序界面中按了手机上的Back键或者Home键程序就结束了,大家也很难在Android的各种程序中找到类似Symbian程序中的“退出”功能。Android系统会给每一个进程都计算出一个“重要程度”等级,在系统运行的某个时候例如资源不足的时候,系统会根据各个进程的“重要程度”来决定先释放哪个进程。(进程“重要程度”的判断在Google的官方文档还是说的比较清楚的,实际上各个Android Component都有很完整的运行时生命周期,由于我们不太清楚进程结束的时机,了解各个Android Component的运行时生命周期以及相关事件就对我们的开发来说非常重要,我会陆续在后续的手记中详细阐述这些内容)。一个Apk中包含的Android Component在运行时可以运行在同一个进程中,也可以运行在不同的进程中,这取决于我们在Apk的AndroidManifest.xml上进行的配置(大家可以将这个AndroidManifest.xml看成是Apk的全局配制信息,其中会描述这个Apk中包含了哪些Android Component以及各个Component的运行和启动方式等,我会在后续的Post中讲解这些内容)。最后,图中下面中间部分的“Task”是Android中一个很特殊的运行时概念,也是很复杂的一个概念,Google的官方文档用了很大的篇幅来说明这个概念。它有别于进程和线程,并且只和Activity的运行时有关系。我们可以将其理解成“窗口栈”,这是由手机上的特殊操作方式所引出的概念。由于手机上的程序,用户一般只能在同一时间看到一个界面,例如在编辑短信的时候一般就不能看到短信列表的界面。而一个完整的程序一般会由多个Activity组成,所以这些Activity会在运行时随着打开的先后顺序会被放到同一个窗口栈(Task)中,当前活动窗口栈中最上面的Activity就是用户当前看到的界面,按手机上的“Back”则是销毁当前栈顶的Activity,回到上一个界面。然而Task这个概念之所以复杂,是因为不同Process中的Activity可以被放到同一个Task中,例如在我们的程序中可能会打开Google Map的地图界面。具体Activity在运行时该被放到哪个Task中,这会由Activity的taskAffinity属性决定,一般情况下一个Apk中的所有Activity在运行时会被放到同一个Task中,但是运行时Activity的taskAffinity是可以修改的。例如上面说的Google Map的例子,地图显示界面默认是存在于Google Map这个程序的默认Task中的,但是我们却可以在运行时将这个界面带到我们自己程序的当前Task中来。窗口在Task中的“入栈”和“出栈”操作和Activity的运行时生命周期息息相关,后面我也会用更详细的篇幅来介绍Task和Activity运行时生命周期的关系。


第一篇手记就写到这里了,下篇预告:《[Android研究手记2] Activity和Service的运行时生命周期模型》。

Filed under: Android