5.3 组件的生命周期模型
组件的生命周期,指的是组件从被构造运行开始,直至被销毁的整个时段内,组件的状态变化。在意图模式和进程托管模型下,每个组件对象都是被系统托管的,系统会根据需求对组件进行构造、状态变更及销毁。
当组件状态发生改变时,Android系统会调用该组件对象的特定函数进行通知,开发者需要派生实现这些函数,对相关事件进行监听和处理,从而掌控组件的生命周期。
5.3.1 界面组件的生命周期
在Android中,界面组件是和用户直接交互的组件,它的生命周期也是与用户交互紧密相关的。在上一节中已经介绍过,根据界面组件与用户交互状态的不同,可以分成三类,分别是:
1)前台界面组件,指的是当前与用户进行交互的界面组件。
2)可视界面组件,指的是虽不与用户直接交互,但仍然处于用户的可视区间内的界面组件。
3)后台界面组件,指的是用户无法交互和感知的界面组件,它属于某个任务,位于该任务的组件栈中,可能会回到前台或可视状态。
界面组件在三种状态间切换时,可能引起其所在进程优先级的变化。因此,当界面组件的状态发生改变时,Android会调用界面组件的相关函数。开发者可以通过重载这些函数去掌控界面组件的生命周期。
图5-8描述了界面组件的状态与Activity中函数的对应关系。当组件被系统构造运行时,该组件对象的Activity.onCreate函数会被调用;而该组件销毁前,其Activity.onDestroy函数则会被调用。从Activity.onCreate到Activity.onDestroy,便是界面组件的整个生命周期范围。
当组件进入可视状态前,组件对象的Activity.onStart函数会被调用,而当组件从可视状态重新切换到后台状态前,组件的Activity.onStop方法会被调用。
与之类似,当Activity.onResume函数被调用,界面组件从可视状态晋升为前台状态,直到该组件的Activity.onPause方法被调用,才宣告组件重回可视状态。
只要组件离开前台状态,变成可视或后台状态(即组件的Activity.onPause、Activity.onStop或Activity.onDestroy函数被调用后),组件对象就处于可回收的状态。在系统资源紧张的情况下,系统会暂时销毁该界面组件对象,等用户再次使用时,再重新调用Activity.onCreate函数重构该组件。
图 5-8 界面组件的生命周期
了解界面组件的生命周期以及生命周期中的相关事件函数,是为了能够选择正确的时机,妥善管理界面组件中的数据和线程等资源。
❑数据管理
界面组件有时需要从存储设备中读取或写入数据。比如在词典中查词,就需要从文件中读取词典数据信息,以供用户查询,同时还需要读写相关的设置信息,以记录用户对词典的使用习惯。
对于只读数据,一种常用的管理模式是在Activity.onCreate函数中进行数据的加载,直到组件的Activity.onDestroy函数被调用时再进行释放,一个简单的示例如下:
private Object data;//缓存只读的数据
protected void onCreate(Bundle savedInstanceState){
data=readData();//读取数据到内存中
}
protected void onDestroy(){
data=null;//将数据置空,加速回收
}
而如果数据需要支持读写操作,则通常需要在Activity.onResume或Activity.onCreate函数中进行读取,而在Activity.onPause函数中实现存储。因为当Activity.onPause函数被调用后,该界面组件就处于可回收的状态。当资源紧张时,系统会强行销毁组件对象,对象中所有未持久化的修改就会丢失。对读写数据进行处理的模型示例如下:
private Object data;//会被读写访问的数据
protected void onResume(){
if(data==null){
data=readData();//读取数据到内存中
}
}
protected void onPause(){
writeData(data);//将修改过的数据写入到持久化设备中
}
❑状态管理
在界面组件中,界面的状态数据也是需要妥善维护的。状态数据,指的是用户与界面组件交互过程中产生的临时数据。它们与当前界面的交互状态相关,不会被持久化到存储设备中。比如,用户正向文本框中输入的文字、滚动条的位置、游戏应用中的当前积分和关卡等信息,都属于状态数据。由于状态数据都是暂存在内存中的,一旦其组件被系统强制回收,所有数据都将丢失,从而将大大影响用户体验。想象一下,用户正在热火朝天地玩着游戏,此时一个电话打进来暂时中断了他的游戏之旅,等他接完电话回到游戏中,由于系统强制回收了该组件对象,之前的游戏数据全部丢失,这是一件多么令人沮丧的事情。
Android对于这种状况当然不会坐视不管。界面组件提供了相关的事件函数,帮助开发者保护这些状态数据。如图5-9所示,当系统将界面组件切离前台状态(即Activity.onPause函数调用前),会先行调用Activity.onSaveInstanceState函数。在该函数中,开发者可以将组件中的状态数据写入参数outState对象中。outState对象的类型是android.os.Bundle,它通过键值对的方式进行数据的存储。
图 5-9 界面组件的状态保存
但与Activity.onPause函数的调用有所不同,Activity.onSaveInstanceState函数不一定在每次离开前台状态时都会被调用。如果此次操作是用户主动期望离开该界面组件,比如,点击了键盘上的回退键,或者Activity.finish函数被主动调用,那么,组件对象就不会调用Activity.onSaveInstanceState函数来保存状态,因为这意味着当前的状态信息已经被用户主动放弃,无需再保存。
状态管理和界面组件生命周期这样的关系,常被开发者用来询问用户是否需要保存编辑过的信息。在Android原生的邮件应用中,当用户修改了邮件内容并主动退出编辑页面之前,系统会弹出对话框询问用户是否需要保存编辑过的邮件;而如果是系统回收组件导致的被动退出,则不会突兀的弹出对话框来干扰用户[1]:
//记录是否需要保存邮件草稿
boolean needSaveDraft=true;
…
protected void onSaveInstanceState(Bundle outState){
super.onSaveInstanceState();
//先保存邮件修改状态,用于重新启动时恢复该信息
outState.putBoolean("NEED_SAVE_DRAFT",needSaveDraft);
//将是否需要保存设置为false,表示在被动退出时无需保存草稿
needSaveDraft=false;
…
}
protected void onPause(){
//如果用户修改了邮件草稿,并且不是被动退出,则询问用户是否保存
if(needSaveDraft){
ShowAskSaveDraftDialog();
…
}
}
当Activity.onSaveInstanceState函数调用完成后,存储着状态信息的outState对象中的数据就由系统进程代为保管,不论该应用进程是否被系统回收,这些数据都不会丢失。
小贴士 在界面组件的开发中,时常需要分辨退出组件这个操作是由系统自动发出的还是用户主动操作的。一个简单的策略,可以通过Activity.onSaveInstanceState函数的调用状况来判定,如果在Activity.onPause函数调用前Activity.onSaveInstanceState被调用了,说明这是一次系统回收,反之则是用户主动退出。
如果组件在切换到后台后被系统回收,那么缓存的状态数据会在该界面组件重新被构造时通过Activity.onCreate函数传入。Activity.onCreate函数有一个名为savedInstanceState的参数,其中的信息与之前保存的outState对象完全一致。如果savedInstanceState为空,说明这是一次全新的构造,反之则说明这是一次恢复性构造,界面组件可以利用该参数中的信息将界面状态恢复到被系统回收前的状态。
区分是恢复性构造还是全新的构造,是开发中需要妥善处理的细节。如果是全新构造,界面组件需要分析调用者发送来的Intent对象,控制业务流程;而如果是恢复性构造,则需要将上次缓存的信息一一恢复。还是以Android原生的邮件应用为例,当用户打开编辑邮件界面时,邮件应用会对不同的状态进行判断,确保邮件状态是正确的:
public void onCreate(Bundle savedInstanceState){
…
if(savedInstanceState!=null){
//这是一次恢复性构造,尝试从中读出之前的草稿状态
needSaveDraft=savedInstanceState.getBoolean(
"NEED_SAVE_DRAFT",false);
…
}else{
//这是一次全新的启动,根据不同的Intent对象,进行不同的初始化
String action=intent.getAction();
if(Intent.ACTION_VIEW.equals(mAction)
||Intent.ACTION_SENDTO.equals(mAction)
||Intent.ACTION_SEND.equals(mAction)
||Intent.ACTION_SEND_MULTIPLE.equals(mAction)){
//这是用户选择发送新邮件,从Intent对象中读取发件人、邮件内容等信息,初始化发送邮
件界面
…
}else if(ACTION_REPLY.equals(mAction)
||ACTION_REPLY_ALL.equals(mAction)
||ACTION_FORWARD.equals(mAction)){
//这是用户选择回复或转发邮件,那么就需要从Intent对象中读取原始邮件的id,并将原
始邮件内容添加上去
…
}else{
…
}
}
}
除了Activity.onCreate函数,该savedInstanceState对象还会通过Activity.onRestore-InstanceState函数进行传入,开发者也可以在该函数中将状态信息恢复。
为了降低开发者的负担,Android中大部分的系统控件都实现了状态缓存的逻辑。在Activity.onSaveInstanceState函数调用前,界面组件会遍历整个控件树,将各个控件的状态保存下来,等到Activity.onRestoreInstanceState函数被调用时再进行恢复。比如,文本控件android.widget.EditText对象会将其中的文本信息作为状态信息进行保存,当界面组件再次恢复时,文本控件中已输入的信息就会按原貌自动恢复。
如果系统内置的控件状态缓存逻辑不符合开发者的需求,开发者可以调用View.setSaveEnabled函数关闭对应控件对象的自动缓存,在Activity.onSaveInstanceState函数中自行管理控件的状态。
用于状态管理的函数Activity.onSaveInstanceState和Activity.onRestoreInstanceState并不属于基本的生命周期函数,因为它们的调用不仅和组件状态相关联,而且与用户行为也有密切的联系。但状态管理相关的操作还是与组件的生命周期有必然的联系,开发者同样需要妥善利用这些函数,处理由于生命周期的变更而引起的变化。
❑注册管理
界面组件在与用户交互的过程中,有时需要随着系统状态的变化及时地更新信息。比如地理信息相关的应用,就需要时刻监听用户当前位置的变化,及时更新周边信息。
界面组件可以通过监听相关的事件信息来捕获这些变化。如果所监听事件的变化,仅当组件在前台状态时才需要生效(比如广播事件的监听、地理位置的变更等),那么就需要在Activity.onResume中进行绑定,在Activity.onPause中进行注销:
LocationManager locationManager;//位置服务对象
LocationListener locationListener;//位置信息监听对象
protected void onResume(){
//开始监听位置信息的变化
locationManager.requestLocationUpdates("gps",
0,0,locationListener);
}
protected void onPause(){
//停止监听位置信息的变化
locationManager.removeUpdates(locationListener);
}
❑线程管理
在应用开发中,网络通信、数据库操作、复杂计算等操作都需要耗费大量的时间,如果放在界面组件所在的主线程中执行,势必会导致界面不响应。因此,应用通常采用多线程的设计,在后台线程中执行此类耗时的操作。
Android的组件生命周期,是一个典型的同步处理逻辑,对多线程架构没有提供良好的支持模型。这就需要开发者根据自己的需求,充分利用组件的生命周期,合理地安排线程的构造及销毁。
如果线程的生命周期和该界面组件的生命周期紧密联系,就需要在界面组件的生命周期中管理该线程,一旦线程被界面组件构造出来,就需要在Activity.onDestroy函数中明确终止该线程,回收其线程空间,否则,将导致线程资源泄漏。
仅在Activity.onDestroy函数中回收线程依然不够完美,因为在资源紧张的情况下,系统会强行回收组件,此时该组件的Activity.onDestroy函数可能并没有被调用,从而导致线程资源泄漏。
一个更好的线程管理方案,是将线程的句柄信息当作界面组件的状态信息缓存下来。如果系统强行回收组件对象,则需要在组件再次被构造时,根据缓存的线程句柄信息找到该线程,从而避免线程泄漏。实现的基本代码框架如下[2]:
//线程Id的状态键值
private static final String WORKER_KEY="thread_id";
private Thread worker;//一个后台线程,执行耗时操作
protected void onCreate(Bundle savedInstanceState){
//如果组件不是全新构造,尝试找到之前的线程
if(savedInstanceState!=null){
final long threadId=savedInstanceState.getLong(
WORKER_KEY);
worker=findThreadById(threadId);
}
//如果没有老的线程,则构造新的
if(thread==null){
worker=createNewWorker();
worker.start();
}
}
protected void onSaveInstanceState(Bundle outState){
//如果后台线程还在运行,缓存其线程Id
if(worker!=null){
outState.putLong(worker.getId());
}
}
protected void onDestroy(){
//组件生命周期终止,停止线程执行,释放线程资源
if(worker!=null){
worker.interrupt();
worker=null;
}
}
[1]Android 4.0版本的Android原生邮件应用已经不再询问用户是否需要保存,而是默认保存用户的任何修改,也许是设计师认为这样可以彻底避免对用户的干扰。
[2]关于如何根据线程Id找到线程,可以参见:http://nadeausoftware.com/articles/2008/04/java_tip_how_list_and_find_threads_and_thread_groups#Gettingalistofallthreads。