Flex 应用程序的生命阶段
所有的生物生命周期我们都分为:出生,生长,成熟,死亡。正如生物的生命周期,Flex框架和它的组件也有相似的过程。我们将检验出生过程,看组件是如何被创建的,如何生长及成熟,到最后将从系统中它们移除时即为生命的终止。
在查看完组件后,我们将查看Flex应用程序框架的生命周期。我们将重新查看Flex框架是如何开始,及与Flash Player结合,创建它们的程序和它们的子组件。我们将会关注程序在生长阶段会发生什么现象及在成熟阶段当与用户交互时会发生什么。最后我们看一下应用程序的销毁阶段。
理解这些所有的因素和它们发生的顺序将会帮助你设计和开发出更好的组件和应用程序。一旦你完全了解它们的生命过程,利用它们的优点来为你的程序添加新的功能。
组件生命周期的介绍
在讨论Flex 应用程序如何开始和创建它们的孩子之前,我们就先花点时间看一看一般组件的生命周期。首先理解组件的生命周期是很重要的,因为Flex应用程序的生命周期是组件生命周期的扩展,再加上一些重要的修改来帮助性能和其它所需要的系统功能。
组件阶段:概述
一个Flex组件都经过七个不同的阶段,我们可以划分到4个生命阶段。由于组件整个生命周期主要有七个阶段,一些阶段是重复的出现,有的只出现一次。这七个阶段是构建,添加,初始化,失效,验证,更新和清除。
这并不是Adobe官方的条目,但我们把这些阶段组织在一起并分类,有利于帮我们定义组件什么时候做什么样的事件。我们的七个阶段按四个生命阶段来分。出生阶段由构建,添加,初始化组成。生长和成熟阶段由失效,验证,更新来组成。死亡就是清除阶段。来看一下第一个阶段:构建。
组件阶段:构建(出生)
构建阶段是组件的第一个阶段,定义为组件的构建。Adobe Flex 文档用Button 作为它们的示例控件,如下:
var myButton:Button = new Button(); //这是我们的构建阶段
很明显的,对吧?什么是不很明显的,在这一阶段很少发生。在构建阶段会发生什么呢?
如果我们从Button's的构造器中开始,跟进super()方法链,先到UIComponent类,然后到FlexSprite,最后到Sprite(到这里为止,因为它的源码包含在playerglobal.swc中)。FlexSprite是在继承链中是足够的顶端了,可以从这里看清整个构建过程了,所以我们就从这里开始。
查看FlexSprite的构造器所做的工作仅是给name属性赋值了一个唯一的文本值。因为FlexSprite构造器所发生的情况已结束,我们再往下看UIComponent构造器。UIComponent在构造时做得稍微多一点,也不很多。设置焦点管理,开始跟踪自己的添加和删除事件,侦听焦点事件、键盘事件,定义自己的资源管理索引,保存当前组件自己的私有宽度和高度值,这些宽度和高度也许已在父类中定义。只做了这么多,没有布局、样式和子类的创建。
最后,我们回到Button 类。Button 的构造函数里所做的是注册侦听MouseEvents的事件来处理用户的交互。只有这么多。
关于在自定义组件开发中使用构造器,在后面的章节‘使用构造’最佳实践会有更详细的信息说明。以上是我们的构造阶段,下一阶段是什么?
组件阶段:添加(出生)
当你将组件添加上父组件时,就发生了‘添加’阶段:
this.addChild(myButton);//这是添加阶段的开始
将组件添加到父类这个过程中,父类要执行大量的功能,这也是生命周期进行更高层次的阶段的地方。让我们查看一下UIComponent的addChild()方法,看它是如何定义添加阶段。
addChild()方法被分解成3个子方法:addingChild(),$addChild()和childAdded().将一个阶段分解成几个子步骤来完成,这种模式在Flex框架中是很常见的。一个复杂的过程分解成几个小节来完成,这样做能更好地控制整个过程。我们在以后的章节中将会继续讲解这种模式的好处,另外在Flex 组件开发最佳实践章节中会有理详细的讲解。
当addChild()方法被调用时,先执行addingChild()方法。addingChild()方法为我们做了大量的工作,比如子类的父类引用,设置子类的document(父类 Application)引用,定义布局管理器(这对以后的阶段来说是很重要的),定义哪些字体是可用的,开始样式的传播及组件样式的管理。
一旦 addingChild()方法完成,addChild()方法将调用Flash Player 级别的将组件添加到显示列表的$addChild()方法,确保播放器的Flash 帧在渲染阶段将组件绘制在屏幕上。
最后调用的子方法是childAdded(),它决定着子类控件是否已初始化完成,如果没有初始化完成,则调用子类的initialize()方法,完成添加阶段后,进行初始化阶段。
组件的阶段:初始化(出生)
初始化阶段主要负责组件的子类的创建和为进行第一个失效和验证环节做准备。Initialize()方法是被父类的childAdded()方法所调用,它被分解成4个子方法:createChildren(),childrenCreated(),initializedAccessibility()和initializationComplete().
当initialize()方法被调用时,UIComponent先派发FlexEvent.PREINITIALIZE事件,然后调用createChildren()方法来确保相关的组件被创建,接着为自己添加子组件。
例如,Button组件重写了UIComponent的createChildren()方法来生成TextField的实例作为Button的标签文本,设置TextField实例的styleName属性,然后将TextField的实例做为孩子通过addChild()方法进行添加。
当createChildren()方法完成后,将调用childrenCreated()方法。这个方法主要是为进入失效-验证环节做准备,就是下一面我们要谈的内容。
Initialize()方法最后调用的两个方法是initializeAccessibility()和initializationComplete(). initializaeAccessibility()方法是用Flash Player的无障碍系统来设置组件的相关属性。initalizationComplete()方法是最后一个被调用的方法,给processedDescriptors 设置器进行赋值,从而派发FlexEvent.INITIALIZED事件。
尽管初始化已经结束,但在使用这个组件之前还有些任务要做。关于如何使用初始化,请参考后面的"使用初始化"章节。
组件状态:失效(生长/成熟)
失效是在组件生命中第一个可重复生命周期,也是生长和成熟的第一个阶段。在createChildren()方法被调用后,UIComponent类的childrenCreated()方法被调用。在这个方法里,在失效阶段3个子方法被调用:invalidateProperties(),invalidateSize()和invalidateDisplayList().
失效和生效环节
不讨论与生效阶段的交互和定义,我们就不能深入地了解失效阶段。
在前面我们提到了Flex框架在程序会使用‘等待下一帧’来管理性能。失效-生效环节是Flex框架在整个生命周期中第一次显示它权力的地方。
失效-生效周期给我们提供了一个解耦的过程。从值处理过程中单独地改变值。解耦这一过程,我们可以得到很多方便之处,比如推迟大数量的处理直到代码处理的最后阶段,还有就是阻止没必要的代码重复执行。 看下面的例子:
myButton.width = 20;
myButton.width = 25;
在上面的代码中,让我们想像一下,设置myButton的width后立即计算更新控件的尺寸;这种改变将导致它的所有者的宽度需要重新布局,设置孩子组件的宽度将更新它们的布局,然后通知他们的父类的宽度改变,可能引起其它相关组件的宽度属性或重新计算。
现在,执行到下一条时,设置宽度后将把所有的改变再重新执行一遍。这样执行时间就会变成双倍的了,若设置宽度是多次的话,执行时间就会大幅度增长从而在没有必要的条件导致Flash 帧的延长。我们如何能让它只执行一次,而且只执行最后一次设置宽度的代码呢。
失效和生效周期使用标识和推迟执行所需的代码来解决这个问题。我们看一下UIComponent的width设置器:
view plaincopy to clipboardprint?
Public function set width(value:Number):void{
if(explicitWidth != value){
explicitWidth = value;
invalidateSize();
}
//other code follows...
Public function set width(value:Number):void{
if(explicitWidth != value){
explicitWidth = value;
invalidateSize();
}
//other code follows...
这个方法首先检查一下值是否发生了变化,若没则忽略本次更新。若有变化,存储新值并调用invalidateSize()。再看一下invalidateSize()方法:
view plaincopy to clipboardprint?
public function invalidateSize():void
{
if(!invalidateSizeFlag)
{
invalidateSizeFlag = true;
if(parent && UIComponentGlobals.layoutManager)
UIComponentGlobals.layoutManager.invalidateSize(this);
}
}
public function invalidateSize():void
{
if(!invalidateSizeFlag)
{
invalidateSizeFlag = true;
if(parent && UIComponentGlobals.layoutManager)
UIComponentGlobals.layoutManager.invalidateSize(this);
}
}
invalidateSize()这个方法是比较简单,但作用很强大。它首先使用invalidateSizeFlag这个值来检查是否已调用过invalidateSize()方法。若是第一次被调用,invalidateSizeFlag为false并设它为true,检查若有父类,然后将自己注册了LoayoutManager的实例。若我们在前面已调用过invalidateSize(),并没有进入生效阶段的话,我们不用担心无法注册layoutManager,因为我们已经在下一个生效阶段中注册了对size的生效。这时我们对Button的width在生效阶段到来之前进次N次的改变,也不会影响性能。
现在已把我们的组件注册到LayoutManager上了,一些独特的代码已经执行了。LayoutManager主要是做什么的呢,一旦stage派发Event.RENDER事件后,就进行生效阶段,这时它将调用一个隐藏的UIComponent实例的callLater()方法。如果你还记得Sean Christmann的文章,当RENDER事件派发后并在Flash Player往屏幕上绘制显示列表之前,是允许执行用户代码的。通过运行验证阶段向保证我们,最后的代码将在屏幕上呈现前执行。
LayoutManager作为一个排除系统,在下一个RENDER事件之前将跟踪所有已注册失效改变的组件。当LoayoutManager得到RENDER事件,它将检查所有在失效阶段注册他的组件,并在这些组件上开始执行生效阶段。若LayoutManager在一个组件的生效阶段过程中进行设置时,又有新组件的注册到LayoutManager上,这些新组件将在排除等到下一个RENDER事件。这有助于防止在一个单一的过程进入一个无限循环的更新。
失效和生效的过程是一个周期,是因为组件一个属性在任何时候被失效后;生效过程必须执行一次,它组件设置成可用的状态。
三种类型的失效
再回到Button例子,在第一个失效阶段我们调用三个失效方法:invalidateProperties(),invalidateSize(),和invalidateDisplayList().
重要的是要知道在失效阶段过程中有什么样的改变,会调用这些方法。当组件重新计算和布局前属性的改变,则会调用invalidateProperties()方法。一些(属性或操作)需要组件重新计算它们的尺寸或它们孩子的尺寸时,invalidateSize()方法被调用。最后,当一些改变需要组件更新显示列表时,调用invalidateDisplayList()方法。
组件阶段:生效(生长成熟)
当改变了一个属性,比如:data,它不需要组件改变大小或更新列表,它改变时,只需数据执行一些计算。生效阶段有4个子方法:commitProperties(),measure(),layoutChrome(),和updateDisplayList()。
不像失效阶段,生效阶段在执行这些方法时有一个预定的顺序:先commitProperties()方法,再执行layoutChrome(), 第3个是执行layoutChrome()方法(若有必要的话),最后执行updateDisplayList()方法。生效阶段有这种线性安排是非常重要的,因为我们的顺序验证生效(应用设置)。
例如,我们使用invalidateProperties()方法来表明一个或多个属性已改变,我们得确保在开始布局我们的控件之前使属性生效,因为在布局阶段时可能需要这些新的属性值。
每个失效的方法都有一个相应的生效方法:invalidateProperties()对应commitProperties(),invalidateSize()对应measure()和invalidateDisplayList()对应updateDisplayList()。当我们使一个组件失效时,LayoutManager会跟踪到组件的哪个失效方法,这样在生效阶段时,只调用相应的生效方法。
例如,假设我们的组件data属性改变了,若这个值没有影响到尺寸或组件的显示列表,然后,在失效阶段我们只需要调用invalidateProperties()方法就可以了。也就是说,当下一个RENDER事件被派发时,LayoutManager检查组件,只查看生效的属性,然后调用commitProperties()方法。
生效阶段有第四个方法,正如我们在图中看到的,叫layoutChrome()。layoutChrome()方法没在UIComponent中定义,而在Container类中定义了。当创建一个容器时,你会经常创建一个边框或给子组件设置padding样式,我们必须执行这个方法。layoutChrome()方法在measure()方法后执行,这样就明白子组件的尺寸了并应用'chrome'。
目前我们已定义了失效-生效阶段,让我回头看一下Button组件的创建过程。若能回忆起,在初始化阶段孩子被创建(标签的TextField),然后调用childrenCreated()方法中的3个失效阶段的方法。
这时候,组件正好有parent并访问LayoutManager,所以3个失效的方法用LayoutManager注册到我们新的Button,并等待下一个RENDER事件来处理这些改变。3个方法被调用,因为在组件的生命周期中这是第一个生效阶段。
在LayoutManager在组件上执行完所需的生效方法后,LayoutManager来检查组件是否被标识成已初始化,若没有,则将组件标识成已初始化。此时,我们的组件被认为已初始化和已更新。LayoutManager将派发FlexEvent.UPDATE_COMPLETE事件。
组件状态:更新(成熟)
一个组件在失效后,进入生效阶段中,任何时候都是更新阶段。一旦组件被生效,LayoutManager就会再次派发UPDATE_COMPLETE事件。更新周期会一直重复直到组件被移除。最重要的是要知道,更新阶段在组件生命周期中花费时间最多的。程序与用户的交互过程都被认为是更新阶段的改变。关于如果管理更新阶段更新的情况,请参考后面的章节(生效阶段和方法)。
组件的阶段:移除(死亡)
组件的最后一个阶段是移除阶段。当组件不再有父组件时这个阶段就发生了。在多数情况下,这种阶段发生在父组件调用removeChild()方法时,将组件作为参数进行传递。为什么说是大多数情况,因为组件的父类组件或更深的父类组件,当被从显示列表的堆栈中移动时可能会进行移除阶段,取决于这些组件在程序中是否有引用。关于这一点我们再多介绍一下:
当使用removeChild()来删除一个孩子时,例如:
this.removeChild(myButton);
父组件的removeChild()方法调用3个子方法,就像addChild()方法一样。第1个方法是removingChild()默认是不做任何事件的(UIComponent级别的控件)。它可以被重写,这样在组件从显示列表前做一些特定的功能。
接下来调用Flash Player级别的$removeChild()方法,在显示列表中做真正移除并检查该组件在内存中是否有引用存在。若该组件在内存中已没有引用,则将它标识成可供垃圾回收的。(例如其它对象指引该对象,事件的侦听者是强引用等,都表明存在引用)
最后,childRemoved()方法被调用,主要是清空该组件的parent和document属性。这样能确保访组件在需要时再次被指定父组件,并阻止该组件进行失效-生效的阶段。
此时,该组件处于在最后的阶段。若没有其它引用指向该组件的话,它将被垃圾回收器回收并从内存中删除。如该组件已被回收,则它的所有子类组件也被回收。也就是说,会发生在前面描述的那样,移除的发生并不直接通过removeChild()方法。
若父组件,祖父组件或任何父类被移除,并没有任何引用这组件在移除组件父类时,这是所有的组件都会被垃圾回收。
例如,我们有一个以Button作为孩子的HBox。如果我们要从显示堆栈中移除HBox时,HBox则进入移除阶段。这一过程中该HBox不会移除任何它的孩子,且它的任何孩子都不会进入移除阶段。但是,若这个HBox和这个Button都不存在引用时,当一定时候垃圾回收器会对这两个组件进行回收。