第3章Android应用程序模型
本章重点介绍Android应用程序的模型和重要组件。Android在设计之初,就针对资源受限的嵌入式设备进行了大量的优化工作,这一点可以从Android的应用程序文件结构中了解到;Android的开放性显示了其强大的生命力,第三方应用程序可以使用和内置应用程序一样的编程接口,这无疑有利于软件厂商和个人开发者开发出更加出色的应用程序。本章从Android的运行环境和APK结构谈起,并通过实例介绍Android应用程序的4个重要组成部分:
Activity
Service
Content Provider
BroadcastReceiver
3.1 Android应用程序基础
↘ 3.1.1 Android应用程序运行环境
Android应用程序使用Java编程语言开发,Java源文件经过编译器编译得到class文件,然后使用aapt工具把class文件转换成dex文件。dex文件相比于普通的class文件,结构更加紧凑,更适合运行在嵌入式设备上。dex文件和相关的资源文件一起打包生成apk文件,也就是Android应用程序最终的发布文件。
应用程序运行时,Android系统会启动一个Linux进程,应用程序就运行在自己的进程之中。每一个进程都拥有自己的一个Java虚拟机,不同应用程序的代码都是单独运行的,不会相互影响:也不会出现一个应用程序崩溃,影响整个虚拟机,进而影响其他应用程序的情况。Android系统会监视所有的进程,如果系统资源匮乏,在可用内存低于某个值的情况下,系统可能会主动地结束应用程序所在的进程,并回收系统资源。
每个应用程序会被分配一个Linux用户ID。应用程序文件只在程序内部可见,其他程序是无法访问的。如果希望将数据暴露给其他应用程序,则需要借助Android提供的Content Provider机制(稍后介绍)。
那么Android平台是如何实现一个应用程序、一个进程、一个虚拟机实例呢?Zygote是一个虚拟机进程,同时也是一个虚拟机实例的孵化器,每当系统要求执行一个Android应用程序时,Zygote就会fork出一个子进程来执行该应用程序。这样做的好处显而易见:Zygote进程是在系统启动时产生的,完成虚拟机的初始化、库的加载、预置类库的加载和初始化等操作。在系统需要一个新的虚拟机实例时,Zygote通过复制自身,快速地提供一个新的虚拟机实例。另外,对于一些只读的系统库,所有虚拟机实例都和Zygote共享一块内存区域,大大节省了内存开销。Android应用程序的运行情况如图3-1所示。
图3-1 一个应用程序、一个进程、一个虚拟机实例
↘ 3.1.2 Android应用程序的组成
Android应用程序最终是以apk文件形式发布的,apk是一种与zip文件格式兼容的文件。Android SDK提供了aapt工具,可以把类文件和资源文件打包成apk文件,打包过程包括class文件到dex文件的转换、生成资源表、优化文本格式的XML文件等工作。转换过程如图3-2所示。本节重点分析一下apk文件的组成。
图3-2 aapt转换工具
以第2章创建的chapter2_2项目为例,分析bin目录下chapter2_2.apk文件的组成。由于aapt命令不在<your_sdk_dir>/tools目录下,如果希望使用此命令,则需要将<your_sdk_dir>/ platform-tools目录加入到环境变量PATH中。启动命令行工具,进入到chapter2_2项目的bin目录下,输入aapt list chapter2_2.apk,可以看到如下的输出,其中每一行代表一个文件。
E:\workspace\chapter2_2\bin>aapt list chapter2_2.apk res/layout/main.xml AndroidManifest.xml resources.arsc res/drawable-hdpi/android_logo.jpg res/drawable-hdpi/ic_launcher.png res/drawable-ldpi/ic_launcher.png res/drawable-mdpi/ic_launcher.png classes.dex META-INF/MANIFEST.MF META-INF/CERT.SF META-INF/CERT.RSA
尽管chapter2_2.apk文件包含的内容并不是很多,但已经足够用来解释apk文件的组成了。
1.图片和原始数据文件
当然,apk文件中应该包含图片和音频文件等数据,这些数据本身就是压缩过的,aapt工具直接把它们放在了apk包的res的三个子目录下。以chapter2_2.apk文件为例,可以看到ic_launcher.png和android_logo.png两个图片文件。
2.layout文件
图片文件下面是layout目录下的main.xml文件,需要注意的是,这里的main.xml与项目中的main.xml是不同的。项目中的main.xml是文本格式文件,开发者可以很容易阅读此文件文件;而chapter2_2.apk文件里面的main.xml文件是二进制格式。相比文本格式文件,二进制格式文件更适合嵌入式设备的处理器和内存环境,解析效率更高,且支持索引和查找。图3-3和图3-4对比了两种不同格式的main.xml文件。
图3-3 项目中文本格式的main.xml文件
图3-4 apk文件中二进制格式的main.xml文件(UltraEdit视图)
从main.xml的转换这一细节可以感觉到,Android平台对应用程序的优化达到了极点。这样做的目的是为了降低对目标设备的处理器、内存等硬件环境的要求;同时,获得更快的响应速度,提高用户体验。
3.AndroidManifest.xml文件
AndroidManifest.xml文件是整个应用程序的信息描述文件,定义了应用程序中包含的Activity、Service、Content Provider和BroadcastReceiver组件信息。每个应用程序在根目录下都必须包含一个AndroidManifest.xml文件,且文件名不能修改。
AndroidManifest.xml文件主要提供了如下的信息描述:
命名应用程序的java包,这个包名将用来唯一标识这个应用程序。
描述了应用程序中包含的Activity、Service、BroadcastReceiver和Content Provider组件。
定义了应用程序运行的进程。
声明了应用程序需要访问受限API所需的权限。
声明其他程序如果希望访问本程序组件所需要的权限。
声明应用程序能够正常运行所需要的最小级别的Android API。
列出应用程序运行所需要连接的库。
需要注意的一点是,Android系统只执行在AndroidManifest.xml中注册的组件。如果创建了一个Activity却发现它无法接收Intent并启动,那么可能是忘记了在AndroidManifest. xml中声明这个Activity。为了优化应用程序,AndroidManifest.xml也经过了aapt工具的转换,在apk中以二进制文件形式存在。
AndroidManifest.xml的文件结构如下所示,文件以<?xml version="1.0" encoding="utf-8"?>声明开始,并在随后的标签中至少包含<manifest>和<application>。
< ?xml version="1.0" encoding"utf-8" ? > <manifest> <uses-permission /> <permission /> <permission-tree /> <permission-group /> <instrumentation /> <uses-sdk /> <application> <activity> <intent-filter> <action /> <category /> <data /> </intent-filter> <meta-data /> </activity> <activity-alias> <intent-filter> . . . </intent-filter> <meta-data /> </activity-alias> <service> <intent-filter> . . . </intent-filter> <meta-data/> </service> <receiver> <intent-filter> . . . </intent-filter> <meta-data /> </receiver> <provider> <grant-uri-permission /> <meta-data /> </provider> <uses-library /> <uses-configuration /> </application> </manifest>
这里不再逐一地介绍每个标签的意义,在后续的章节中会慢慢地介绍其中的标签。读者也可以自行参考Android开发文档中关于AndroidManifest.xml文件介绍的部分。
4.Resources.arsc文件
Resources.arsc文件位于apk的根目录下,是整个文件的资源表。通过对此文件的解析,可以获得包名称、整个资源包含的资源类型、每种资源类型包括的元素、元素的ID等信息。Resources.arsc的文件格式并未公开,但是可以借助aapt工具列出资源表的大概结构。关于Resources.arsc的文件格式,有兴趣的读者可以进一步分析。下面是使用aapt工具显示HelloActivity.apk的资源表的输出信息。
E:\workspace\chapter2_2\bin>aapt dump resources chapter2_2.apk Package Groups (1) Package Group 0 id=127 packageCount=1 name=com.doodev.chapter2_2 Package 0 id=127 name=com.doodev.chapter2_2 typeCount=5 type 0 configCount=0 entryCount=0 type 1 configCount=3 entryCount=2 spec resource 0x7f020000 com.doodev.chapter2_2:drawable/android_logo: flag s=0x00000000 spec resource 0x7f020001 com.doodev.chapter2_2:drawable/ic_launcher: flags =0x00000100 config 0 density=120 sdk=4 resource 0x7f020001 com.doodev.chapter2_2:drawable/ic_launcher: t=0x03 d =0x00000001 (s=0x0008 r=0x00) config 1 density=160 sdk=4 resource 0x7f020001 com.doodev.chapter2_2:drawable/ic_launcher: t=0x03 d =0x00000002 (s=0x0008 r=0x00) config 2 density=240 sdk=4 resource 0x7f020000 com.doodev.chapter2_2:drawable/android_logo: t=0x03 d=0x00000000 (s=0x0008 r=0x00) resource 0x7f020001 com.doodev.chapter2_2:drawable/ic_launcher: t=0x03 d =0x00000003 (s=0x0008 r=0x00) type 2 configCount=1 entryCount=1 spec resource 0x7f030000 com.doodev.chapter2_2:layout/main: flags=0x000000 00 .......其他内容省略
在Android应用程序中,如果想访问某个资源,就必须知道该资源的类型和ID。在Java程序中,应用程序自带的资源使用R.resource_type.resource_name的方式来访问,系统自带的资源则使用android.R.resource_type.resource_name来访问。在资源文件中,应用程序自带的资源使用@resource_type/resource_name来访问,系统自带的资源则使用@android:resource_type/resource_name来访问。
5.classes.dex文件
Dalvik虚拟机运行dex文件,而不是传统的class文件,DX工具将编译后的class文件转换成一个dex文件。在程序运行时,Dalvik虚拟机从dex中装载读取指令和数据。
传统的Java应用程序,往往包含多个class文件,这样就不可避免地增加了冗余信息。将class文件整合到一起,可以减小类文件的尺寸、IO操作,提高类的查找速度。除此之外,dex文件经过了精心设计,以适应嵌入式设备资源受限的环境。该文件结构设计简洁,且使用等长的指令,提高了解析速度。为了提高跨进程的数据共享,尽量扩大了只读结构的大小。
6.META-INF文件夹
META-INF文件夹只存在于签名后的apk文件中,其中包含MANIFEST.MF、CERT.SF和CERT.RSA文件。MANIFEST.MF文件包含了apk文件中所有文件的名称和此文件的SHA1摘要值。而CERT.SF和CERT.RSA文件是使用jarsigner工具生成的签名文件和签名块文件,在CERT.SF文件中默认会包含整个MANIFEST.MF文件的SHA1 摘要值;CERT.RSA文件存放对CERT.SF文件的签名,同时还包含从keystore文件中生成的证书或者证书链。在应用程序管理器安装apk文件的过程中会检查证书,对比每个文件的摘要值是否匹配,防止应用程序被篡改。关于数字签名的更多内容,将在本章的3.7 节“数字签名”中介绍。
至此,我们已经逐一分析了Android应用程序的组成,感受到了平台架构师在打造平台时的精心设计。接下来的部分将向大家介绍构成Android应用程序的重要组件,包括Activity、Service、Content Provider和BroadcastReceiver。
3.2 Activity
从表面上讲,Activity是Android应用程序的一个界面,用户可以通过这个界面操作播放器,查看联系人或者玩游戏。图3-5列举出了Android内置的电话和Home应用程序界面,每个界面都是由一个Activity构成的。在一个Android应用程序中,可以只包含一个Activity,也可以包含多个Activity。
图3-5 电话和移动随身听界面
对开发者而言,Activity是Android应用程序的入口,Android应用程序模型没有定义像main()这样的入口方法,而是在Activity类中定义了一系列的生命周期方法,比如onCreate()、onResume()、onStart()、onPause()、onStop() 和onDestroy(),Android系统会在适当的时候调用对应的生命周期方法。Activity是与用户沟通的窗口,Activity类实现了Window.Callback、KeyEvent.Callback和ComponentCallbacks等多个接口,以便能够处理按键事件,并在出现内存不足等情况时做出响应。Activity上呈现的用户界面是由View或者ViewGroup构成的,因此Activity可以看做是View的载体,在Android系统中已经实现了很多友好易用的组件,包括按钮、文本框、多选框、列表等。
下面通过一个例子,介绍Activity的声明、Activity之间的数据传递,以及Activity的生命周期等内容。
↘ 3.2.1 Activity创建与声明
1.声明Activity
在Eclipse中创建一个项目chapter3_1,按照图3-6 所示填写项目的属性,然后单击“Finish”按钮,ADT插件会自动创建ContactsActivity.java,并将此组件注册到AndroidManifest. xml文件中。
图3-6 创建chapter3_1项目
AndroidManifest.xml的内容如下所示:
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.doodev.chapter3_1" android:versionCode="1" android:versionName="1.0" > <uses-sdk android:minSdkVersion="10" /> <application android:icon="@drawable/ic_launcher" android:label="@string/app_name" > <activity android:name=".ContactsActivity" android:label="@string/app_name" > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
其中,定义在<manifest>标签内的“package="com.doodev.chapter3_1"”用来唯一标识这个应用程序。这个包名也是其他组件的前缀,应用程序安装后会创建/data/data/com. doodev.chapter3_1目录,用来存放应用程序的私有文件,比如数据库文件、Preference文件等。<activity>标签必须包含属性android:name,其值应该指向这个Activity类,在这里.ContactsActivity则代表com.doodev.chapter3_1.ContactsActivity类。通过项目向导创建的Activity默认作为了应用程序入口,ContactsActivity将被放置在手机的启动面板上(Launcher)。在AndroidManifest.xml中可能定义了多个Activity,那么系统是如何知道哪个Activity作为应用程序的入口呢?答案是通过分析<activity>的下一级标签<intent-filter>,由于ContactsActivity的标签中声明了如下的intent-filter标签,因此系统判断出它就是程序的入口。
<intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter>
2.创建Activity
接下来我们修改刚刚创建的ContactsActivity,目的是让它可以在屏幕上显示一组联系人。为了简单起见,这里使用一个数组来表示联系人。ContantsActivity的源代码如下所示:
public class ContactsActivity extends ListActivity { private String[] peoples = { "Eric", "Monica", "Jim", "John", "Hanks" }; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); //初始化屏幕的布局 setContentView(R.layout.contacts_list); //绑定到ArrayAdapter setListAdapter(new ArrayAdapter<String>(this, R.layout.contacts_item, peoples)); } }
ListActivity是Activity的子类,如果想在界面中采用列表的方式向用户展示数据,那么可以选择扩展此类。因为Android系统已经为ListActivity绑定了一个ListView,可以显示来自数据库或者数组的数据,大大加快了开发的速度。
onCreate()方法是Activity定义的生命周期方法,在Activity创建时,这个方法将首先被调用。在这里,通过调用setContentView(R.layout.contacts_list)初始化了Activity的界面布局。Contacts_list.xml是定义在res/layout目录下的XML文件,内容如下所示:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" android:paddingLeft="8dip" android:paddingRight="8dip"> <ListView android:id="@android:id/list" android:layout_width="fill_parent" android:layout_height="0dip" android:layout_weight="1" android:drawSelectorOnTop="false"/> <TextView android:id="@android:id/empty" android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="@string/empty" /> </LinearLayout>
LinearLayout是ViewGroup的子类,它总是将子View排成一行或者一列。通过setOrientation()方法或者在XML文件中定义android:orientation属性可以修改子元素的排列方式。本例中android:orientation="vertical"表明联系人以垂直的方式一行一行显示。ListView和TextView的ID都是由Android系统提供的,这两个组件可以看做是相互替换的,在某一时间只能显示其中一个。只有当列表的数据为空时,才会显示TextView的内容。
setListAdapter(new ArrayAdapter<String>(this,R.layout.contacts_item,peoples))被调用之后,peoples数组中定义的数据就被绑定到ListView上。Adapter模式在Android的图形用户界面框架中被广泛使用,Adapter作为桥梁连接起AdapterView和数据源。这里,我们使用了简单的ArrayAdapter。ArrayAdapter构造器的一个重要参数是R.layout.contacts_item,通过定义res/layout/contacts_item.xml可以控制列表中每一行的外观,比如定义行高、字体大小、字体颜色等。Contacts_item.xml的内容如下所示,从中可以看出列表的每一行是一个TextView。当然,也可以自己定义更美观的View,在第4章“图形用户界面”中将会详细介绍如何使用各种系统的UI组件定义自己的View等内容。
<?xml version="1.0" encoding="utf-8"?> <TextView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/contactItem" android:layout_width="fill_parent" android:layout_height="48sp" android:textSize="20sp" android:textStyle="normal" android:gravity="center_vertical" android:paddingLeft="8dip" />
需要注意的一点事,android:id的值中包括一个“+”,@后面的这个“+”号代表如果contactItem不存在就创建一个新的id,随后,在程序中可以通过R.id.contactItem来访问此组件。运行chapter3_1项目,ContactsActivity界面如图3-7所示。
图3-7 ContactsActivity界面
↘ 3.2.2 Activity的生命周期
在Android系统中,Activity的实例被放在一个堆栈里面。当一个新的Activity启动之后,就会被放置在堆栈的顶部,成为正在运行的Activity;而先前的Activity则变成不可见的,位于新Activity的下面。只有当新的Activity退出时,先前的Activity才会出现在堆栈顶部。
Activity有4种状态:运行、暂停、停止和销毁。
当Activity位于堆栈的顶部时,它就处于运行状态(active)。
当Activity失去了焦点,但是依然可见时,例如,一个半透明的Activity覆盖了当前的Activity就会出现这种情况,此时被覆盖的Activity就处于暂停状态(paused),维持着成员信息和所有状态。当系统处于内存严重不足的情况下时,暂停的Activity可能会被系统销毁。
当Activity完全被其他的Activity覆盖时,它就处于停止状态(stopped),处于停止状态的Activity依然维持着成员信息和所有状态,只是变得不可见了。当其他模块需要内存时,停止的Activity可能会被销毁。
当Activity处于停止或者暂停状态时,系统可能要求它结束生命周期,或者直接把它所在的进程杀死,进而从内存中删除它,此时的Activity就被销毁了。
如图3-8所示是Activity的生命周期图。Activity定义了一系列的生命周期方法,如下所示,系统在适当的时候会回调它们。在定义自己的Activity的生命周期方法时,应该首先调用父类的方法。
图3-8 Activity的生命周期图
public class Activity extends ApplicationContext { protected void onCreate(Bundle savedInstanceState){} protected void onStart(){} protected void onRestart(){} protected void onResume(){} protected void onPause(){} protected void onStop(){} protected void onDestroy(){} }
Activity的整个生命周期始于onCreate()方法而止于onDestroy()方法。通常在onCreate()方法中构建Activity所需的资源,并在onDestroy()方法中释放资源。Activity的可视化生命周期始于onStart()方法而止于onStop()方法,此时的Activity是可见的,可能无法和用户进行交互操作。在onStart()方法中可以注册BroadcastReceiver,并且在onStop()方法中注销BroadcastReceiver。Activity的前台生命周期始于onResume()方法而止于onPause()方法,此时的Activity是可见的,位于堆栈的顶部。通常,需要在这两个方法里面处理外部事件,比如电话呼入,当电话呼入时,Phone应用程序会进入前台,而当前运行的Activity被覆盖。
接下来,以ContactsActivity为例演示一下Activity的生命周期,为清楚起见,这里新建了项目chapter3_2。为了演示Activity的生命周期,需要实现Activity类的所有生命周期方法,并在方法中通过Log记录方法执行。为了记录生命周期方法执行的顺序,在ContactsActivity类中定义了一个整型的成员变量seq,每次方法执行seq时会自动加一。ContactsActivity还覆盖了toString()方法,返回ContactsActivity对象的字符串表示,可以根据返回的字符串判断系统创建了新的ContactsActivity还是复用了以前的对象。
修改后的ContactsActivity.java代码如下所示:
public class ContactsActivity extends ListActivity { private static final String TAG = "ContactsActivity"; private String[] peoples = { "Eric", "Monica", "Jim", "John", "Hanks" }; private int seq = 1; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // 初始化界面布局 setContentView(R.layout.contacts_list); // 绑定到ArrayAdapter setListAdapter(new ArrayAdapter<String>(this, R.layout.contacts_item, peoples)); Log.e(TAG, toString() + " onCreate is called " + seq++); } @Override protected void onDestroy() { super.onDestroy(); Log.e(TAG, toString() + " onDestroy is called " + seq++); } @Override protected void onPause() { super.onPause(); Log.e(TAG, toString() + " onPause is called " + seq++); } @Override protected void onRestart() { super.onRestart(); Log.e(TAG, toString() + " onRestart is called " + seq++); } @Override protected void onResume() { super.onResume(); Log.e(TAG, toString() + " onResume is called " + seq++); } @Override protected void onStart() { super.onStart(); Log.e(TAG, toString() + " onStart is called " + seq++); } @Override protected void onStop() { super.onStop(); Log.e(TAG, toString() + " onStop is called " + seq++); } public String toString() { String s = super.toString(); int index = s.lastIndexOf("."); if (index != -1) { return s.substring(index + 1, s.length()); } return s; } }
为了更直观地看到生命周期方法的执行情况,启动Log日志输出,从命令行输入:
adb logcat ContactsActivity:E *:S
然后运行chapter3_2项目,从日志控制台可以看到如下的输出信息:
E/ContactsActivity( 832): ContactsActivity@43a00d18 onCreate is called 1 E/ContactsActivity( 832): ContactsActivity@43a00d18 onStart is called 2 E/ContactsActivity( 832): ContactsActivity@43a00d18 onResume is called 3
onResume()方法执行结束后,ContactsActivity已经显示在模拟器屏幕上,进入了可视化阶段。我们向模拟器发送一个电话呼入事件,当电话呼入时,电话应用程序会启动,如图3-9所示,电话的界面会覆盖ContactsActivity,因此ContactsActivity进入到停止状态。启动命令行终端,执行:
telnet localhost 5554 gsm call 13810000086
图3-9 电话呼入
模拟器会弹出电话应用程序,挂断电话后,ContactsActivity再次显示到前台。从控制台查看日志输出,可以看到电话呼入时onPause()和onStop()方法被依次调用了;而电话结束后,onRestart()、onStart()和onResume()方法又被调用了。可以得出结论:如果应用程序需要处理电话呼入类似的事件,则应该在onPause()方法中保存某些状态,并在onResume()方法中读取状态并恢复。
E/ContactsActivity( 832): ContactsActivity@43a00d18 onCreate is called 1 E/ContactsActivity( 832): ContactsActivity@43a00d18 onStart is called 2 E/ContactsActivity( 832): ContactsActivity@43a00d18 onResume is called 3 E/ContactsActivity( 832): ContactsActivity@43a00d18 onPause is called 4 E/ContactsActivity( 832): ContactsActivity@43a00d18 onStop is called 5 E/ContactsActivity( 832): ContactsActivity@43a00d18 onRestart is called 6 E/ContactsActivity( 832): ContactsActivity@43a00d18 onStart is called 7 E/ContactsActivity( 832): ContactsActivity@43a00d18 onResume is called 8
单击屏幕右上角的“返回”按钮,退出ContactsActivity,可以从日志的控制台看到onStop()和onDestroy()方法被依次调用了,Activity进入到销毁状态。
E/ContactsActivity( 832): ContactsActivity@43a00d18 onCreate is called 1 E/ContactsActivity( 832): ContactsActivity@43a00d18 onStart is called 2 E/ContactsActivity( 832): ContactsActivity@43a00d18 onResume is called 3 E/ContactsActivity( 832): ContactsActivity@43a00d18 onPause is called 4 E/ContactsActivity( 832): ContactsActivity@43a00d18 onStop is called 5 E/ContactsActivity( 832): ContactsActivity@43a00d18 onRestart is called 6 E/ContactsActivity( 832): ContactsActivity@43a00d18 onStart is called 7 E/ContactsActivity( 832): ContactsActivity@43a00d18 onResume is called 8 E/ContactsActivity( 832): ContactsActivity@43a00d18 onPause is called 9 E/ContactsActivity( 832): ContactsActivity@43a00d18 onStop is called 10 E/ContactsActivity( 832): ContactsActivity@43a00d18 onDestroy is called 11
用户使用手机过程中,可能通过按“Home”键直接从ContactsActivity进入到启动面板中,这时候会发生什么情况呢?从Home运行“联系人”应用程序,ContactsActivity启动后,单击模拟器上的“Home”键,进入到手机的启动面板,然后再次从启动面板选择“联系人”应用程序运行,如图3-10所示。
图3-10 从启动面板再次进入Contacts应用程序
从日志的输出可以看到,当“Home”键被按下时,onPause()和onStop()被依次调用了。ContactsActivity进入到停止状态,当从启动面板再次进入到ContactsActivity时,onRestart()、onStart()和onResume()被依次调用。通过此例,再次印证了前面关于Activity生命周期的介绍。
↘ 3.2.3 Activity和Intent
在一个Activity中启动一个新的Activity,需要调用Context.startActivity(Intent intent)方法,新启动的Activity将被放在堆栈的顶部。Intent作为Activity之间的纽带起着重要的作用,事实上它不仅用于启动Activity,还可以用来启动Service和绑定Service等工作。
1.Intent的定义
Intent是对某一操作的抽象描述,除此之外,还可以在Intent中附带数据。Intent主要包含如下的信息:
Action,执行的动作,比如ACTION_VIEW、ACTION_MAIN等。
Data,操作的数据,通常是以Uri的形式表现的。
Category,执行动作的分类,例如,CATEGORY_LAUNCHER代表Activity应该出现在启动面板中。
Type,指定Data的MIME类型。
Component,指定Intent指向的组件名称。通常情况下,系统会根据Intent包含的信息来决定哪个组件处理这个Intent。但是,当Intent的这个属性被设置时,其他的属性就认为是可选的了。
Extras,存放Intent附带的额外信息。在Intent的定义中,信息是存放在Bundle类中的。如果想在Activity之间传递一些数据,可以将其放置在Extras中。
每启动一个Activity就像通过鼠标点击了一个HTML页面,浏览器组装了符合HTTP协议的数据发送给另外一个URL,另外一个页面接收到数据之后,渲染界面并显示到用户面前。前面一个页面则被浏览器存放在历史记录中,通过工具栏的“返回”按钮还可以返回;而Intent就用在Activity之间的切换工作中,指明下一个Activity的地址,附带需要在两个Activity之间传递的数据。
Android系统已经在Intent类中定义了一系列的标准Action和Category,应用程序可以直接使用。当然,应用程序也可以自己定义Action,需要确保每个Action必须是唯一的,形式应该遵循Java编码的样式,例如定义“com.doodev.action.VIEW_CONTACTS”。
2.解析Intent
当ACTION_MAIN和CATEGORY_HOME联合使用时,就可以启动手机的主屏。示例代码如下:
Intent intent = new Intent(); intent.setAction(Intent.ACTION_MAIN); intent.addCategory(Intent.CATEGORY_HOME); startActivity(intent);
上述代码中出现的Intent并没有指定component的值,因此系统通过Intent中的action、type和category信息来决定由哪个Activity来处理到来的Intent。这一工作是由PackageManager来完成的,PackageManager根据AndroidManifest.xml中定义的<intent-filter>标签来对应用程序中包含的Activity进行匹配。这种情况我们称为“隐性解析”。
与“隐性解析”对应的称做“显性解析”。顾名思义,Intent对象已经通过调用setComponent()或者setClass()方法指定了component属性,这时候系统不再需要其他的信息来判断由哪个Activity来处理到来的Intent了。
在实际开发中,两种方式都经常用到。下面扩展一下chapter3_1项目,增加一个DetailActivity,当用户点击列表的某一行时,从ContactsActivity启动DetailActivity。为了响应用户点击ContactsActivity列表事件,需要在ContactsActivity中覆盖onListItemClick()方法,如下所示:
@Override protected void onListItemClick(ListView l, View v, int position, long id) { super.onListItemClick(l, v, position, id); Intent intent = new Intent(this, DetailActivity.class); intent.putExtra("name", peoples[position]); startActivity(intent); }
这里把用户点击的联系人名字放到Extras中一起发送给DetailActivity。DetailActivity在onCreate()方法中,调用getIntent()即可获得Intent对象,并从Extras中读取联系人的名字显示在屏幕上。DetailActivity的代码如下所示:
public class DetailActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); //获得Intent并从中读取附带的数据 Intent intent = getIntent(); String name = intent.getStringExtra("name"); TextView view = new TextView(this); view.setText("您选择了"+name); setContentView(view); } }
不要忘记,将DetailActivity注册到AndroidManifest.xml中,以便可以通过startActivity()将其启动。
<activity android:name=".DetailActivity" android:label="@string/information"> <intent-filter> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> </intent-filter> </activity>
运行chapter3_3,如图3-11所示。
图3-11 从一个Activity启动另一个Activity
↘ 3.2.4 使用Intent调用系统服务
Intent的引入不但使得Activity之间的跳转变得很容易,还使得调用系统服务成为可能,通常应用程序只需要发送一个Intent,就可以轻松调用系统提供的浏览器、电话等服务,还可以完成应用程序安装和卸载等操作。chapter3_4列举了部分常用的系统功能调用Intent,供开发者参考。
1.调用浏览器
调用浏览器打开网站的代码如下所示:
Uri uri = Uri.parse("http://www.google.com"); Intent intent = new Intent(Intent.ACTION_VIEW, uri); startActivity(intent); break;
2.使用Google地图定位
Google为android的开发者提供了一套相当完善的地图应用接口,开发者可以很方便地使用这些接口来做一些LBS应用程序。下面的代码用于调用Google地图显示某点的位置信息。
Intent intent = new Intent(android.content.Intent.ACTION_VIEW, Uri.parse("http:// ditu.google.cn/maps?hl=zh&mrt=loc&q=31.1198723,121.1099877")); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK&Intent.FLAG_ACTIVITY_EXCLUDE_FROM_ RECENTS); intent.setClassName(this,"com.google.android.maps.MapActivity"); startActivity(intent);
需要说明的是,Google Map API并不是标准的Android API,如果需要在程序里面使用,那么必须在AndroidManifest.xml的<application>标签里面声明如下内容。最终程序能否在手机上正常运行,还要看手机是否支持Google Map API。
<uses-library android:name="com.google.android.maps"/>
3.电话呼叫功能
使用Intent可以方便地调用系统的Phone应用程序,包括进入到拨号盘或者直接拨打电话。直接拨打电话的代码如下所示:
Uri uri = Uri.parse("tel:10086"); Intent intent = new Intent(Intent.ACTION_CALL, uri); startActivity(intent);
4.发送短消息
发送短消息的代码如下所示:
Uri uri = Uri.parse("smsto:10086"); Intent intent = new Intent(Intent.ACTION_SENDTO, uri); intent.putExtra("sms_body", "welcome to android world!"); startActivity(intent);
5.发送电子邮件
发送电子邮件的代码如下所示。需要说明的是,用户的手机上可能安装了多个邮件客户端,都能够处理发送邮件的操作。这时候可以使用Intent.createChooser()方便地创建一个ACTION_CHOOSER类型的Intent。
Uri uri = Uri.parse("mailto:13810018823@139.com"); Intent intent = new Intent(Intent.ACTION_SEND,uri); intent.putExtra(Intent.EXTRA_EMAIL, "eric.zhan@gmail.com"); intent.putExtra(Intent.EXTRA_TEXT, "Android Mail"); intent.setType("text/plain"); startActivity(Intent.createChooser(intent, "选择Email客户端"));
6.播放多媒体文件
播放多媒体文件的代码如下所示,系统会启动默认的媒体播放器播放MP3文件。
Intent intent = new Intent(Intent.ACTION_VIEW); Uri uri = Uri.parse("file:///sdcard/01.mp3"); intent.setDataAndType(uri, "audio/mp3"); startActivity(intent);
7.安装APK文件
如果系统通过程序安装APK文件,那么只需要获得APK文件的路径,代码如下所示:
String fileName = Environment.getExternalStorageDirectory() + "/chapter3_5.apk"; Intent intent = new Intent(Intent.ACTION_VIEW); intent.setDataAndType(Uri.fromFile(new File(fileName)), "application/vnd.android. package-archive"); startActivity(intent);
8.卸载应用程序
卸载系统已经安装的应用程序,需要获得应用程序的包名,并将Intent的Action属性设置为ACTION_DELETE,代码如下所示:
Uri uri = Uri.fromParts("package", "com.doodev.chapter3_5", null); Intent intent = new Intent(Intent.ACTION_DELETE, uri); startActivity(intent);
9.访问联系人
访问手机联系人的代码如下所示:
Intent intent = new Intent(); intent.setAction(Intent.ACTION_VIEW); intent.setData(ContactsContract.Contacts.CONTENT_URI); startActivity(intent);
10.从图库选择图片
发微博的时候,我们经常需要从图库插入一张图片,这个功能是如何实现的呢?首先需要构建一个Action属性为ACTION_GET_CONTENT的Intent。由于我们需要获得所选图片的内容,因此可以调用startActivityForResult()来获得内容,代码如下所示:
Intent intent = new Intent(Intent.ACTION_GET_CONTENT); intent.addCategory(Intent.CATEGORY_OPENABLE); intent.setType("image/*"); startActivityForResult(intent, 0);
当用户选择了某个图片之后,系统会回调onActivityResult()方法,并将图片的具体信息存储在onActivityResult()方法的Intent变量中,这样我们就能很容易地从中读取路径信息了。
@Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (resultCode == Activity.RESULT_OK && requestCode == 0) { Uri uri = data.getData(); //显示所选图片的URI路径 Toast.makeText(this, uri.getPath(), Toast.LENGTH_LONG).show(); } }
如果需要获得图片文件的数据,还可以通过下面的代码读取。
Uri uri = data.getData(); Log.e("uri", uri.toString()); ContentResolver cr = this.getContentResolver(); try { Bitmap bitmap = BitmapFactory.decodeStream(cr.openInputStream(uri)); ImageView imageView = (ImageView) findViewById(R.id.iv01); /* 将Bitmap设定到ImageView */ imageView.setImageBitmap(bitmap); } catch (FileNotFoundException e) { Log.e("Exception", e.getMessage(),e); }
↘ 3.2.5 Activity和Task
Activity不仅可以启动定义在本应用程序之内的Activity,还可以启动定义在其他应用程序中的Activity。这一特性极大地丰富了组件复用的程度,也提高了用户体验。对用户而言,他们并不关心展现在眼前的界面定义在哪个应用程序之中,而是在乎系统是否提供了流畅、友好的操作界面。对于Android系统而言,用户看到的这些Activity会被放到一个堆栈之内,称之为“Task”。这个堆栈最底层的Activity就是启动这个Task的Activity,通常来说,就是排列在启动面板上的Activity。当有新的Activity启动时,前一个Activity进入暂停状态;当用户按返回键时,当前的Activity从堆栈弹出,而前一个Activity的onResume()方法被调用,重新进入到运行状态。
一个Task里的所有Activity组成一个单元,整个Task(整个Activity堆栈)可以在前台,也可以在后台(应用程序的切换就是Task的前后台的切换)。假设当前的Task有4个Activity在堆栈里,当用户按下“Home”键,去开启另一个应用(实际上是一个新的Task)时,那么当前的Task就退到后台继续运行,新开启的Root Activity此时就显示出来了,然后,过了一段时间,用户回到主界面,又重新选择了以前的那个应用(先前的那个Task),那么先前的那个Task此时也回到了前台,当用户按下“Back”键时,屏幕不是显示刚刚关闭的那个应用,而是移除回到前台的这个Task堆栈栈顶Activity,将下一个Activity显示出来。
刚才描述的情况是Activity和Task默认的行为,但是有很多的方法可以对这些行为进行修改,如Activity和Task的联系。Task里Activity的行为,是受启动它的Intent对象的flag和在manifest文件中的Activity的属性集合共同影响的。
前面介绍了Intent的action、data和type等属性。其实,在Intent中还定义了flag属性和如下4个常量:
FLAG_ACTIVITY_NEW_TASK。
FLAG_ACTIVITY_CLEAR_TOP。
FLAG_ACTIVITY_RESET_TASK_IF_NEEDED。
FLAG_ACTIVITY_SINGLE_TOP。
在<activity>标签中,可以使用如下6个属性,系统将根据属性值调整Task内Activity的执行方式。
taskAffinity。
launchMode。
allowTaskReparenting。
clearTaskOnLaunch。
alwaysRetainTaskState。
finishOnTaskLaunch。
1.Task与Activity的亲属关系
在默认情况下,一个应用程序内的所有Activity都有亲属关系,它们属于同一个Task。但这不是绝对的,通过设置<activity>标签的taskAffinity属性可以修改Task和Activity的关系。在一个应用程序内的Activity可以有不同的affinity,在不同应用程序内的Activity也可以共享一个affinity。亲属关系只有在Intent中包含了FLAG_ACTIVITY_NEW_TASK标志或者<activity>的allowTaskReparenting设置为“true”时才会起作用。
(1)FLAG_ACTIVITY_NEW_TASK
通常,Activity会与调用startActivity()的调用者处在一个Task之内。但是,如果Intent中包含了FLAG_ACTIVITY_NEW_TASK标志,那么系统会查找其他的Task来存放这个Activity;如果已经有与Activity的亲属关系相同的Task,就复用这个Task,将Activity放在Task的顶部;如果不存在,系统会创建一个新的Task。
(2)allowTaskReparenting
如果<activity>的allowTaskReparenting属性设置为“true”,这表明这个Activity可以从启动它的那个Task移动到与它有相同亲属关系的Task,当然是在与它有相同亲属关系的Task重新回到前台时。举例来讲,某个应用程序A包含了B Activity,且B的allowTaskReparenting属性设置为“true”,那么当应用程序C通过startActivity调用B时,B被放置在了C的Task中。但是,当A重新回到前台时,B可以重新回到A的Task之中。
2.启动模式
在<activity>标签中定义了4种launchMode的属性值,分别为 “standard”、“singleTop”、“singleTask”和“singleInstance”。通过设置不同的启动模式,可以更改Activity在Task中的创建方式,以及如何响应Intent等行为。
为了更好地理解这4种属性,可以把它们分为两组:“standard”和“singleTop”一组,“singleTask”和“singleInstance”一组。
具有“standard”或者“singleTop”启动模式的Activity可以实例化很多次。这些实例可以属于任何Task并且可以位于Activity stack的任何位置。相反,具有“singleTask”或者“singleInstance”启动模式的Activity只能有一个实例,它们总是位于Activity stack的底部。
“standard”和“singleTop”模式只在一种情况下有差别:每次有一个新的启动,“standard”Activity的Intent就会创建一个新的实例来响应这个Intent,每个实例处理一个Intent。但是对于一个“singleTop”的Activity来说,如果目标Task已经有一个存在的“singleTop”的Activity实例并且位于stack的顶部,那么这个实例就会接收到这个新的Intent(调用onNewIntent()),而不会创建新的实例。在其他情况下——例如,如果存在的“singleTop”的Activity实例在目标Task中,但不是在stack的顶部,或者它在一个stack的顶部,但不是在目标Task中,那么新的实例都会被创建并压入stack中。
“singleTask”和“singleInstance”模式也只在一种情况下有差别:“singleTask”的Activity允许其他Activity成为它的Task的一部分。“singleTask”的Activity位于Activity stack的底部,其他Activity(必须是“standard”和“singleTop”的Activity)可以启动加入到相同的Task中。“singleInstance”的Activity不允许其他Activity成为它的Task的一部分。“singleInstance”的Activity是Task中唯一的Activity。如果它启动其他的Activity,这个被启动的Activity会被放置到另一个Task中——好像Intent中包含了FLAG_ACTIVITY_NEW_TASK标志。这个差异还导致它们在处理新来的Intent(在Activity实例已经存在的情况下,新来的Intent)时的方式不同:一个具有singleInstance属性的Activity总在栈顶(因为Task里就只有一个Activity),所以它会处理所有的Intent(onNewIntent()被调用)。但是一个具有singleTask属性的Activity不能确定它是否在栈顶(它的上面是否还有其他的Activity),如果具有singleTask属性的Activity位于栈顶,这时有一个Intent来启动它,它将处理这个Intent(onNewIntent()被调用)。否则,这个Intent将会被丢掉(即使是这个Intent被丢掉,它还是会导致这个Task回到前台)。
当创建一个类(Activity)的实例来处理一个新的Intent时,用户可以按下“Back”键回到上一个Activity,但是如果是用已经存在的栈顶的Activity来处理Intent(onNewIntent()被调用),按下“Back”键是不能回到以前的状态的(在没处理这个Intent之前)。
(1)standard(默认启动模式)
“standard”启动模式说明Activity放在调用startActivity()方法的Activity的Task之中,此类Activity允许有多个实例存在,也可以属于不同的Task;在它所处的Task中允许存在其他的Activity。当Intent到来时,系统总是创建一个新的Activity实例来处理。
(2)singleTop
“singleTop”启动模式与“standard”启动模式相类似,不同之处在于,当Intent到来时,如果已经有一个Activity的实例存在,并且位于目标Task堆栈的顶部,系统会复用此Activity来响应Intent,而不是重新创建一个实例。
(3)singleTask
“singleTask”启动模式说明Activity永远是处在新创建的Task中,并且它们总是位于Task的底部。此类的Activity不允许存在多个实例,运行时总是以单例形式存在;在它所处的Task中允许存在其他的Activity。当Intent到来时,系统会根据Activity的位置来处理Intent,当Activity处于堆栈的顶部时,则由其处理Intent,否则直接丢弃Intent。
(4)singleInstance
“singleInstance”启动模式与“singleTask”有些类似,不同之处在于它所在的Task不允许存在其他的Activity,它永远是Task中的唯一Activity。当Intent到来时,系统总是由这唯一的实例来处理Intent。
3.清除堆栈
在默认情况下,如果用户离开一个应用程序较长一段时间,系统会自动清除除了根Activity之外的所有Activity,也就是只保留了初始的Activity。背后的思想就是,当用户长期离开应用之后再次回来,他们希望放弃以前做的事情并重新开始这个应用。当然,开发者可以通过设置Task内的根Activity的某些属性来改变这种行为。
(1)alwaysRetainTaskState
顾名思义,系统会保留Task的状态,即便经过了很长时间也不会主动清除堆栈。
(2)clearTaskOnLaunch
一旦用户离开应用程序再次返回时,系统会清除除根Activity之外的所有Activity。
(3)finishOnTaskLaunch
与上面clearTaskOnLaunch属性类似,但是这个属性是可以用在所有的Activity上的,而不局限于根Activity。当finishOnTaskLaunch设置为“true”时,一旦用户离开Task,那么Activity就不存在了。
除了设置Activity的属性之外,还可以通过设置Intent的FLAG_ACTIVITY_CLEAR_TOP来删除Task内的Activity。当含有FLAG_ACTIVITY_CLEAR_TOP标志的Intent到来,并且在Task内已经存在了处理Intent的Activity时,目标Activity上面的所有Activity都会被删除。如果目标Activity的启动模式为standard,目标Activity也会被先删除,然后再创建一个新的Activity来处理到来的Intent。
3.3 Content Provider
Android平台提供了4 种数据持久化存储方案,分别是文件、Preference、数据库和Content Provider。和另外3种不同,Content Provider存储的数据允许应用程序之间共享。在Android系统中已经预置了几种Content Provider,向开发者提供音频、视频、图片、联系人和呼叫记录等数据。很明显,如果这些数据使用数据库接口来存储,那么将无法提供给其他的应用程序使用。当然,如果数据只是想在应用程序内部使用,就不应该使用Content Provider,而使用数据库或者文件等可以获得更高效的读/写操作。
Content Provider的接口是抽象的,通过这些接口可以很容易地从Content Provider中查询数据,向Content Provider中写入数据。而底层数据的存储形式对调用者是透明的,它们可能是以文件形式存储的,也可能存储在数据库里。在android.provider包内定义了一些类和接口,它们主要描述了内置的几个Content Provider的数据结构。例如,MediaStore.Audio定义了音频数据的信息,CallLog.Calls则定义了通话记录的信息。具体内容可以查看Android的开发文档。
本节通过一个例子说明如何查询Android平台上的多媒体存储信息。由于需要挂载SD卡,所以首先创建一个SD卡,并向SD卡上传输一些MP3歌曲。随后,使用-sdcard选项启动模拟器,模拟器启动时,系统会对SD卡进行扫描,并更新平台上的多媒体存储数据。事实上,这些数据是存储在数据库之内的。
chapter3_5项目中的MusicActivity演示了如何从Content Provider中读取数据,MusicActivity的源代码如下所示:
public class MusicActivity extends ListActivity { private Cursor cursor; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); //设置界面布局 setContentView(R.layout.songs); ContentResolver resolver = getContentResolver(); //从Content Provider中获得SD卡上的音乐列表 cursor =resolver.query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, null, null, null, MediaStore.Audio.Media.DEFAULT_SORT_ORDER); String[] cols = new String[] { MediaStore.Audio.Media.TITLE, MediaStore.Audio.Media.ARTIST, }; int[] ids = new int[] { R.id.track_name, R.id.artist }; if (cursor != null) startManagingCursor(cursor); //创建Adapter并绑定到ListView SimpleCursorAdapter adapter = new SimpleCursorAdapter(this, R.layout.songs_list, cursor, cols, ids); setListAdapter(adapter); } }
读取Content Provider的内容之前,必须首先获得ContentResolver的实例,获得实例后,通过ContentResolver的接口即可与后端的Content Provider进行交互操作。
ContentResolver cr = getContentResolver();
ContentResolver的接口方法返回的参数都是Cursor,因此使用SimpleCursorAdapter将Cursor中的列映射到XML文件中定义的TextView、ImageView等视图上。本例中使用SimpleCursorAdapter的构造器将MediaStore.Audio.Media.TITLE和MediaStore.Audio.Media. ARTIST映射到定义在/res/layout/songs_list.xml中的TextView上。songs_list.xml的内容如下所示:
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="?android:attr/listPreferredItemHeight" android:paddingLeft="5dip" android:paddingRight="7dip" android:paddingTop="3dip" > <TextView android:id="@+id/track_name" android:layout_width="270dip" android:layout_height="wrap_content" android:includeFontPadding="false" android:background="@null" android:singleLine="true" android:ellipsize="end" android:textSize="20sp" android:textColor="#FFFFFF" android:textStyle="normal" android:textColorHighlight="#FFFF9200" /> <TextView android:id="@+id/artist" android:layout_width="200dip" android:layout_height="wrap_content" android:textSize="14sp" android:textColor="#FF565555" android:textStyle="normal" android:textColorHighlight="#FFFF9200" android:includeFontPadding="false" android:background="@null" android:scrollHorizontally="true" android:singleLine="true" android:ellipsize="end" android:layout_alignParentBottom="true" android:layout_below="@+id/track_name" /> </RelativeLayout>
运行chapter3_5,MusicActivity界面显示如图3-12所示。
图3-12 MusicActivity运行界面
事实上,Content Provider的运行机制是比较复杂的,涉及跨进程调用。本例只是介绍了如何查询Content Provider中的内容,更多关于Content Provider的知识将在本书第5章中详细介绍。
3.4 BroadcastReceiver
应用程序的运行环境不是一成不变的,SD卡插拔、电池电量低等事件会影响应用程序的运行。为了能够做出正确的响应,应用程序必须能够监听此类事件并做出正确的处理。在Android系统中,BroadcastReceiver就是我们需要的那个组件。BroadcastReceiver没有界面显示,但是它却可以通过AndroidManifest.xml或者在代码中进行注册,以监听应用程序感兴趣的事件,这有点类似Java ME平台的Push注册机制,但是比Push注册更简单,功能更加强大。当广播事件到来时,BroadcastReceiver的onReceive()方法会被调用。
BroadcastReceiver是一个抽象类,定义了一个抽象方法onReceive()。
void onReceive(Context curContext, Intent broadcastMsg)
在onReceive()方法的执行过程中,Android系统认为Receiver处在活动状态;onReceive()方法执行结束后,系统就认为Receiver已经处在非活动状态,可以在任意时间销毁此Receiver实例。因此,BroadcastReceiver的生命周期就对应onReceive()方法的执行过程。在实现onReceive()方法时需要注意,避免在onReceive()方法中进行异步调用,因为调用结果返回之前,BroadcastReceiver的实例可能已经被系统销毁了。显示Dialog、绑定Service等动作属于异步调用范畴,因此不适合在onReceive()方法中调用。
下面通过扩展chapter3_5,演示如何在应用程序中注册BroadcastReceiver。有些时候,我们希望手机启动后就自动运行某应用程序,为了实现此功能,可以在AndroidManifest.xml中注册BroadcastReceiver并监听android.intent.action.BOOT_COMPLETED动作。首先创建一个BroadcastReceiver的子类BootReceiver,并在onReceive()方法中启动MusicActivity。代码如下所示:
public class BootReceiver extends BroadcastReceiver { @Override public void onReceive(Context arg0, Intent arg1) { if (arg1.getAction().equals(Intent.ACTION_BOOT_COMPLETED)) { Intent intent = new Intent(arg0, com.doodev.chapter3_5.MusicActivity. class); //在Activity之外调用startActivity() intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); arg0.startActivity(intent); } } }
Android系统启动后,会广播Intent.ACTION_BOOT_COMPLETED事件,BootReceiver收到Intent之后启动MusicActivity。需要注意的一点是,由于是在Activity之外调用startActivity来启动一个Activity,因此需要在Intent中设置Intent.FLAG_ACTIVITY_NEW_TASK。那么Android是如何知道BootReceiver正在监听系统启动完成这一事件呢?答案是BootReceiver在AndroidManifest.xml中完成了注册。下面的代码完成了注册工作:
<receiver android:name=".BootReceiver" > <intent-filter> <action android:name="android.intent.action.BOOT_COMPLETED" /> </intent-filter> </receiver>
除此之外,还需要在AndroidManifest.xml中增加权限声明,如果不声明应用程序所需要的权限,那么在运行时会抛出安全异常。
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
使用adb install chapter3_6.apk命令将应用程序安装到模拟器上,然后重新启动模拟器。可以看到,在模拟器启动完成后,MusicActivity自动运行了。
除了在AndroidManifest.xml中注册BroadcastReceiver之外,还可以在代码中注册。一般来说,在Activity启动过程中注册BroadcastReceiver,在进入到停止或者销毁状态前注销BroadcastReceiver。为了减轻系统的负载,注销注册的BroadcastReceiver是一个良好的编程习惯。
3.5 Service
Service是在系统中运行的一段代码,它没有界面显示,也无法和用户交互。但是,当希望Activity退出之后某些工作还在后台继续进行的话,例如,用户可以在后台听音乐的同时编写短消息,就必须使用Service组件。本书第7章将详细介绍Service相关的开发知识。
3.6 安全与许可
↘ 3.6.1 安全架构
Android系统基于Linux内核,自然从Linux那里继承了部分安全设计。例如,每个Android应用程序都被赋予了唯一的用户ID,系统为每个应用程序创建了一个沙箱,以阻止它触及其他应用程序,当然也阻止了其他程序。用户ID是在安装过程中由系统指定的,并且在应用程序卸载之前保持不变。
Android系统的安全核心是,在默认情况下,应用程序没有任何特权访问那些可能影响操作系统、其他应用程序或者用户的API。一般来说,这些API都是一些敏感的操作,比如读取或者写入用户的私有数据区、访问网络连接等。如果应用程序希望访问此类的API,那么必须要在AndroidManifest.xml中请求,在安装过程中获得用户的许可后才可以访问。一旦用户同意了这些请求,那么应用程序在执行过程中不会再询问用户。Java ME平台与Android在这点上是不同的,MIDlet套件在安装过程中并不会询问用户是否允许访问某些敏感的API;而是在运行过程中,当调用敏感API的时候再弹出对话框询问用户。相比之下,Android的安全设计获得了更好的用户体验,但是存在潜在的危险,开发者可以较容易地编写一个程序偷偷地发送短信,而用户可能对此一无所知。图3-13 演示了在笔者的HTV G11上安装chapter3_6.apk过程中询问用户是否允许程序开机后自动启动的情况。
图3-13 安装过程中向用户询问权限许可
↘ 3.6.2 许可
1.使用许可
在默认情况下,Android应用程序没有任何权限使用敏感API。如果希望赋予应用程序相关的权限,则需要在AndroidManifest.xml中使用<uses-permission>标签来声明。例如,如果希望应用程序能够接收短消息,那么应该按照下面的形式在AndroidManifest.xml中标明。通常,许可失败将会导致系统抛出SecurityException给应用程序。但是,sendBroadcast(Intent intent)是个例外。由于此方法是在调用返回后才检查权限,因此,即使许可失败了,你也不会得到SecurityException,失败信息仅仅记录在系统日志中。
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.doodev.chapter3_6 " > <uses-permission android:name="android.permission.RECEIVE_SMS" /> </manifest>
Android系统中已经定义了完备的许可列表,每个许可使用唯一的字符串来标识。这些许可定义在android.Manifest.permission类中,也可以借助adb工具查询系统中支持的许可列表。在命令行环境下,输入“adb shell pm list permissions”,输出如下所示,如果编写程序时不记得某个Permission了,那么不妨使用这个命令查看一下。
C:\Documents and Settings\eric>adb shell pm list permissions All Permissions: permission:android.permission.CLEAR_APP_USER_DATA permission:android.permission.SHUTDOWN permission:android.permission.BIND_INPUT_METHOD permission:android.permission.ACCESS_DRM permission:android.permission.INTERNAL_SYSTEM_WINDOW permission:android.permission.SEND_DOWNLOAD_COMPLETED_INTENTS permission:android.permission.ACCESS_CHECKIN_PROPERTIES permission:android.permission.READ_INPUT_STATE permission:android.permission.DEVICE_POWER permission:android.permission.DELETE_PACKAGES permission:android.permission.ACCESS_CACHE_FILESYSTEM permission:android.permission.REBOOT permission:android.permission.STATUS_BAR permission:android.permission.ACCESS_DOWNLOAD_MANAGER_ADVANCED permission:android.permission.STOP_APP_SWITCHES permission:android.permission.ACCESS_DOWNLOAD_MANAGER permission:android.permission.CONTROL_LOCATION_UPDATES permission:android.permission.MANAGE_APP_TOKENS permission:android.permission.DELETE_CACHE_FILES permission:android.permission.BATTERY_STATS permission:android.permission.MASTER_CLEAR permission:android.permission.BRICK permission:android.permission.SET_ACTIVITY_WATCHER permission:android.permission.BACKUP //省略部分内容
2.声明许可
除了使用系统提供的许可之外,应用程序也可以自己声明许可。声明应用程序的许可是有意义的,利用声明的许可可以控制谁能启动应用程序中的Activity,提高应用程序的安全性。声明许可需要在AndroidManifest.xml中使用<permission>标签,例如,下面的代码声明了一个自定义的许可,其中android:name作为Permission的主键,以一个唯一的字符串表示。android:protectionLevel是必须声明的,因为系统根据此项声明来决定如何向用户显示此项权限声明。
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.doodev.chapter3_6"> <permission android:name="com.doodev.chapter3_6.MUSIC_ACTIVITY" android:label="@string/label" android:description="@string/desciption" android:permissionGroup="android.permission-group.COST_MONEY" android:protectionLevel="dangerous" /> </manifest>
如果想控制外部程序对本应用程序组件的访问,可以在相关的组件标签中增加android:permission属性,并指定需要的许可。以<activity>为例,如果想强制调用者必须获得com.doodev.chapter3_6.MUSIC_ACTIVITY许可才可以启动,那么需要设置android:permission属性。
<activity android:name=".MusicActivity" android:label="@string/app_name" android:permission="com.doodev.chapter3_6.MUSIC_ACTIVITY"> </activity>
这样,如果其他Activity想调用MusicActivity时,则必须使用<uses-permission>标签声明com.doodev.chapter3_6.MUSIC_ACTIVITY。对于Activity,许可检查发生在Activity. startActivity() 方法和Activity.startActivityForResult()方法调用过程中。如果调用者没有声明适当的许可,则会抛出SecurityException。Service、Content Provider和BroadcastReceiver等组件检查许可与Activity稍有不同,读者可参考Android开发文档获得更详细的信息。
3.7 数字签名
只有使用证书进行数字签名之后的应用程序才能安装到Android平台上,之所以这样做是为了能够追溯应用程序的作者,并且在应用程序之间建立信任。有经验的开发者可能感到了不安,因为申请证书往往需要数个工作日,并且搭上数千元不等的现金。事实上,大可不必担心,因为证书不需要被证书认证机构签名,只是自己生成的证书就可以了。
↘ 3.7.1 签名策略
通常来说,开发者可能要开发多款应用程序,此时,Android建议的签名策略是使用一个唯一的证书来为这些应用程序签名。为什么推荐这么做呢?举一个例子,当应用程序需要升级时,你一定希望旧的应用程序能够无缝升级到新版本的应用程序。但是,如果你使用了不同的证书签名新的应用程序,那么你只能给应用程序分配一个重新的包名,也就意味着用户不得不安装一个全新的应用程序。只有当新老版本的应用程序使用相同的证书时,系统才允许应用程序升级安装。
除此之外,还有一些应用程序模块化、密钥的有效期等原因,因此使用一个唯一的证书为公司或者个人的多个应用程序签名是最佳实践策略。
↘ 3.7.2 签名步骤
可能有开发者问:我从来没有接触过签名这一步,但是我的应用程序安装和运行都没有问题。那是因为在默认情况下,ADT使用调试密钥(debug key)为应用程序签名了。如果想最终发布Android应用程序,那么创建自己的密钥、为应用程序签名是不可或缺的。本节介绍使用keytool和jarsigner工具签名的步骤,这两个工具是Java 2 SDK自带的,请读者确认自己电脑上已经安装了Java 2 SDK。
1.生成keystore文件
启动命令行工具,输入下面的命令:
C:\Documents and Settings\eric>keytool -genkey -v -keystore mingjava.keystore -a lias eric -keyalg RSA -validity 10000
系统会要求输入keystore的密码、单位、所在区域和国家代码等内容。例如:
输入keystore密码: doodev@10086 您的名字与姓氏是什么? [Unknown]: mingjava 您的组织单位名称是什么? [Unknown]: doodev 您的组织名称是什么? [Unknown]: doodev 您所在的城市或区域名称是什么? [Unknown]: beijing 您所在的州或省份名称是什么? [Unknown]: beijing 该单位的两字母国家代码是什么 [Unknown]: CN CN=mingjava, OU=doodev, O=doodev, L=beijing, ST=beijing, C=CN正确吗? [否]: y 创建1,024比特RSA键值对及针对CN=mingjava, OU=doodev, O=doodev, L=beijing, ST=beijing, C=CN的自我签署的认证 (MD5WithRSA) : 输入<eric>的主密码 (如果和keystore密码相同,按回车): [正在存储mingjava.keystore]
最后,keytool工具在当前目录生成了mingjava.keystore文件,用来为应用程序签名。keytool工具的选项含义,请参考http://java.sun.com/j2se/1.5.0/docs/tooldocs/#security。
2.签名应用程序
生成了keystore文件之后,就可以使用它为应用程序签名了。运行下面的命令:
F:\>jarsigner -verbose -keystore mingjava.keystore chapter3_2.apk eric
然后输入前面设置的密码,即可生成签名后的应用程序。例如:
输入密钥库的口令短语: doodev@10086 正在添加: META-INF/MANIFEST.MF 正在添加: META-INF/ERIC.SF 正在添加: META-INF/ERIC.RSA 正在签名: res/layout/songs.xml 正在签名: res/layout/songs_list.xml 正在签名: AndroidManifest.xml 正在签名: resources.arsc 正在签名: classes.dex ........
保持密钥的安全性至关重要,建议在使用keytool和jarsigner时都不要使用-storepass和-keypass选项,避免密码存储在shell的历史记录中。