BT

如何利用碎片时间提升技术认知与能力? 点击获取答案

深入浅出Flex组件生命周期Part4 ─ 引擎LayoutManager

| 作者 董龙飞 关注 0 他的粉丝 发布于 2011年9月30日. 估计阅读时间: 49 分钟 | Google、Facebook、Pinterest、阿里、腾讯 等顶尖技术团队的上百个可供参考的架构实例!

从Part4,我们开始真正的Spark组件生命周期探险旅程。

一. 小结

我们已经知道,Spark组件实际上由两个UIComponent构成,一个是所谓的Skin类,一个是所谓的Component类。Component类负责管理数据和逻辑,Skin类负责管理可视化。

在不同的场合和微博上,在Spark组件发布已近两年的今天,我仍然听到很多人在抱怨Spark和Halo的不同。那么,阅读过本系列文章之后,你会发现,Spark只不过为传统的Halo组件增加了一个UIComponent(即Skin类),把一个UIComponent组件的工作拆分给了两个UIComponent组件。并不是你所想象的那样翻天覆地的变化。

既然二者都是UIComponent,那么本质上,两个UIComponent都要遵从UICompnent组件的生命周期,就是那个著名的三阶段:commitProperties(), measure()和updateDisplayList(),当然也包括对应的invalidate方法。如果你还不甚了解三阶段构成的组件生命周期和对应的invalidate方法,那么请就此打住,先参考Using Adobe Flex4.5理解组件生命周期的基本知识,然后再回到本文。

如果我们已经对此达成共识,那么让我们开始吧。

二. 引擎:mx.managers.LayoutManager

每个稍有经验的Flex开发者,都知道组件生命周期的三阶段commitProperties(),measure()和updateDisplayList()。但很少有人深究过其自何处,这导致Flex组件的生命周期对很多开发者几乎成为一个神秘的神话。实际上,mx.managers.LayoutManager是这一切的驱动者。

2.1/ 引自ActionScript3语言参考:关于LayoutManager

看一下LayoutManager类提供的部分主要方法:

LayoutManager+Method

 

invalidateProperties(), invalidateSize() , invalidateDisplayList() , validateProperties() , validateSize() , validateDisplayList()… 你是否发现这些组件生命周期中的神秘方法?

如ActionScript3参考中所说:LayoutManager 是 Flex 的度量和布局策略所基于的引擎。在ActionScript3语言参考之LayoutManager类中讲到:

LayoutManager 是 Flex 的度量和布局策略所基于的引擎。布局分三个阶段执行:提交、度量和布局。

这三个阶段互不相同,并且,只有在处理完当前阶段的所有 UIComponent 之后才会进入下一阶段。在某个阶段中处理 UIComponent 期间,可能出现另一阶段请求重新处理 UIComponent 的情况。这些请求将进行排队,并且只在下次运行此阶段时才得到处理。

提交阶段从调用 validateProperties() 开始,该方法将遍历一个对象列表(该列表按嵌套级别反向排序),并调用每个对象的 validateProperties() 方法。

列表中的对象是按与嵌套级别正相反的顺序接受处理的,所以最先访问的是嵌套深度最浅的对象。这也可以称为自上而下或从外到内的顺序。

在此阶段中,允许内容依据属性设置而定的组件在进入度量和布局阶段之前进行自我配置。为了获得更好的性能,组件的属性 setter 方法有时不执行更新到新属性值所需的全部操作。但是,属性 setter 会调用 invalidateProperties() 方法,并在运行此阶段之前延迟此操作。这样,可以在多次设置属性时避免执行不必要的操作。

度量阶段从调用 validateSize() 开始,该方法将遍历一个对象列表(该列表按嵌套级别排序),并调用每个对象的 validateSize() 方法,以确定对象大小是否已更改。

如果之前调用过对象的 invalidateSize() 方法,则调用 validateSize() 方法。如果对象的大小或位置因调用 validateSize() 而发生了更改,则会调用对象的 invalidateDisplayList() 方法,这就会将该对象添加到处理队列中,等待下次运行布局阶段时进行处理。此外,已分别调用 invalidateSize() 和 invalidateDisplayList() 为度量和布局这两个阶段标记了对象的父项。

列表中的对象是按嵌套级别的顺序进行处理的,所以最先访问的是嵌套深度最深的对象。这也可以称为自下而上或从内到外的顺序。

布局阶段从调用 validateDisplayList() 方法开始,该方法将遍历一个对象列表(该列表按嵌套级别反向排序),并调用每个对象的 validateDisplayList() 方法,以请求对象确定它所包含的所有组件(即其子对象)的大小和位置。

如果之前调用过对象的 invalidateDisplayList() 方法,则调用 validateDisplayList() 方法。

列表中的对象是按与嵌套级别正相反的顺序接受处理的,所以最先访问的是嵌套深度最浅的对象。这也可以称为自上而下或从外到内的顺序。

通常情况下,组件不会覆盖 validateProperties()、validateSize() 或 validateDisplayList() 方法。对于 UIComponent 而言,大部分组件都会覆盖分别由validateProperties()、validateSize() 或 validateDisplayList() 方法调用的 commitProperties()、measure() 或 updateDisplayList() 方法。

当应用程序启动时,将创建一个 LayoutManager 实例并将其存储在 UIComponent.layoutManager 属性中。所有组件都应使用此实例。如果您无权访问 UIComponent 对象,也可以使用静态LayoutManager.getInstance() 方法访问 LayoutManager。

2.2/ LayoutManager来自何处

所有的UIComponent组件都通过 layoutManager 属性访问其LayoutManager,更重要的是,这些LayoutManager都指向一处。

是的,我要说的其实是:LayoutManager 类是一个单体类。

当应用程序启动时,将创建一个 LayoutManager 实例并将其存储在 UIComponent.layoutManager 属性中。所有组件都应使用此实例。

查看spark.components.Application类的构造器,可以看到:

public function Application()
    {
        UIComponentGlobals.layoutManager = ILayoutManager(
            Singleton.getInstance("mx.managers::ILayoutManager"));
        UIComponentGlobals.layoutManager.usePhasedInstantiation = true;
 
        if (!FlexGlobals.topLevelApplication)
            FlexGlobals.topLevelApplication = this;
 
        super();
 
        showInAutomationHierarchy = true;
 
        initResizeBehavior();
   }

需要注意的是,这里默认设置了LayoutManager的 usePhasedInstantiation 属性为true。我们在下文中将会谈到该属性。

而查看LayoutManager类,你会看到如下 getInstance() 方法:

public static function getInstance():LayoutManager 
{ 
 if (!instance)
 instance = new LayoutManager(); 
 return instance; 
}

可以想象,LayoutManager是一个多么繁忙而重要的类,Flex应用程序中所有可视化组件的度量和布局都由这一个类实例推动。

2.3/ invalidate方法

在本文中,当说起invalidate方法,我的意思是invalidateProperties(),invalidateSize()和invalidateDisplayList()方法。

然而,如果更严谨地说,有两套invalidate方法。LayoutManager的invalidate方法和UIComponent的invalidate方法。

2.3.1/ UIComponent的invalidate方法

Flex开发者通常调用的是UIComponent类的invalidate方法。调用该方法来确保Flex在Flash下一帧调用对应的commitProperties,measure和updateDisplayList方法。

我们以UIComponent类的invalidateProperties()方法为例:

 public function invalidateProperties():void { 
 if (!invalidatePropertiesFlag) { 
 invalidatePropertiesFlag = true; 
 if (nestLevel && UIComponentGlobals.layoutManager) UIComponentGlobals.layoutManager.invalidateProperties(this);
  } 
  }
 

实际上,UIComponent的invalidate方法是个"假李逵",UIComponent的invaliate方法最终会调用LayoutManager类的invalidate方法。

因此,更严谨地说,在本文中,当说器invalidate方法时,我的意思是LayoutManager类的invalidateProperties() ,invalidateSize()和invalidateDisplayList()方法。

2.3.2/ LayoutManager的invalidate方法

我们以invalidateProperties()方法为例,看一下invalidate方法的具体工作:

public function invalidateProperties(obj:ILayoutManagerClient ):void 
{
if (!invalidatePropertiesFlag && systemManager) { 
invalidatePropertiesFlag = true; 
if (!listenersAttached) attachListeners(systemManager);
 } // trace("LayoutManager adding " + Object(obj) + " to invalidatePropertiesQueue");
 if (targetLevel < = obj.nestLevel)
  invalidateClientPropertiesFlag = true; 
  invalidatePropertiesQueue.addObject(obj, obj.nestLevel); 
  // trace("LayoutManager added " + Object(obj) + " to invalidatePropertiesQueue");
   }

invalidateProperties首先设置invalidatePropertiesFlag为true,然后注册监听器attachListeners(systemManager),最后把自己加入了invalidatePropertiesQueue队列 invalidatePropertiesQueue.addObject(obj, obj.nestLevel)。

设置invalidatePropertiesFlash为true以及attacheListeners方法实现了延迟计算。而通过invalidatePropertiesQueue,LayoutManager维护了不同的invalidated对象队列。

我们接下来逐一分析。

i/ invalidate方法实现延迟计算

invalidate方法的主要工作就是实现延迟计算。查看LayoutManager的attachListeners(systemManager)方法,就会理解他是如何做到的:

public function attachListeners(systemManager:ISystemManager):void
    {
        if (!waitedAFrame)
        {
            systemManager.addEventListener(Event.ENTER_FRAME, waitAFrame);
        }
        else
        {
            systemManager.addEventListener(Event.ENTER_FRAME, doPhasedInstantiationCallback);
            if (!usePhasedInstantiation)
            {
                if (systemManager && (systemManager.stage || usingBridge(systemManager)))
                {
                    systemManager.addEventListener(Event.RENDER, doPhasedInstantiationCallback);
                    if (systemManager.stage)
                        systemManager.stage.invalidate();
                }
            }
        }
 
        listenersAttached = true;
    }

attachListeners方法最重要的是通过 systemManager.addEventListener(Event.ENTER_FRAME, waitAFrame)实现了三阶段的延迟计算。这里我们看到了传统Flash开发者很熟悉的Event.ENTER_FRAME事件,这行命令指定FlashPlayer在下一帧执行waitAFrame方法。该方法其余部分帮助确保"在某个阶段中处理 UIComponent 期间,可能出现另一阶段请求重新处理 UIComponent 的情况。这些请求将进行排队,并且只在下次运行此阶段时才得到处理。"。

waitAFrame方法如下:

private function waitAFrame(event:Event):void
    {
        // trace(">>LayoutManager:WaitAFrame");
 
        systemManager.removeEventListener(Event.ENTER_FRAME, waitAFrame);
        systemManager.addEventListener(Event.ENTER_FRAME, doPhasedInstantiationCallback);
        waitedAFrame = true;
 
        // trace("< <LayoutManager:WaitAFrame");
    }

waitAFrame()方法通过systemManager.addEventListener(Event.ENTER_FRAME, doPhasedInstantiationCallback) 又一次"延迟"调用了doPhasedInstantiationCallback方法。doPhasedInstantiationCallback方法则回调用真正执行三阶段任务的doPhasedInstantiation方法。

通过EventENTER_FRAME事件,LayoutManager保证了当调用invalidate方法时,Flex在下下帧执行真正的计算(并不像有些文档所说在下一帧),即执行validate方法。

按照huang.xinghui的评论,我修改了此处:

通过Event.ENTER_FRAME事件,LayoutManager保证了当调用invalidate方法时,Flex在下帧执行真正的计算,即执行validate方法。

huang.xinghui的注释:

在第一次进入时,因为waitedAFrame变量是false,而等待两帧去执行doPhasedInstantiationCallback函数,但是后续就不会了,waitedAFrame变量设置为true,就直接是在下一帧执行doPhasedInstantiationCallback函数。waitedAFrame重头到尾只有在waitAFrame函数里进行了赋值。第一次的两帧执行,不知道是不是和swf加载时的两帧(第一帧加载preloader,第二帧加载application)有关系?

ii/ LayoutManager管理的invalidate对象队列

LayoutManager类实现了延迟计算,但是在下下帧,Flex需要知道针对哪些对象执行validate操作。这些对象就是所谓的"invalidated"对象,即通过invalidate方法打上了 invalidatePropertiesFlag=true, invalidateSizeFlag=true和invalidateDisplayListFlag=true的对象。

LayoutManager维护了三个数组:invalidatePropertiesQueue,invalidateSizeQueue和invalidateDisplayListQueue。

在invalidate方法中,LayoutManager把每一个调用了invalidate方法的UIComponent都置入了对应的队列中,比如调用了invalidateProperties方法的对象被置入了invaliatePropertiesQueue队列,随之加入的还有其在DisplayList的位置。在invalidateProperties方法中,完成该操作的代码如下:

invalidatePropertiesQueue.addObject(obj, obj.nestLevel);

上述代码中,nestLevel实际上是UIComponent在DisplayList上的位置。

当调用invalidate方法时会把UIComponent对象加入对应的队列,而当调用validate方法时,则会把该对象从队列中移除。

2.4/ 三阶段计算:doPhasedInstantiation

真正的三阶段计算发生在doPhasedInstantiation方法。在我们继续之前,先来整理一下目前为止我们已经开始的"探险旅程"。

UIComponent一旦invalidate,就会调用LayoutManager的invalidate方法,invalidate方法先设置invalidate标记为true,然后添加 Event.ENTER_FRAME的侦听器,以在下一帧执行waitAFrame方法。之后,又把调用invalidate的UIComponent对象添加到对应的队列中。

而在下一帧执行时,waitAFrame方法"人如其名",再次添加 Event.ENTER_FRAME侦听器方法doPhasedInstantiationCallback,而该方法会最终调用doPhasedInstantiation。

现在,我们来到了doPhaseInstantiation门前,马上就要揭开组件 "三阶段" 生命周期神秘的面纱。

喘一口气,别激动。

我们先列出doPhaseInstantiation方法代码如下:

/**
     *  @private
     */
    private function doPhasedInstantiation():void
    {
        // trace(">>DoPhasedInstantation");
 
        // If phasing, do only one phase: validateProperties(),
        // validateSize(), or validateDisplayList().
        if (usePhasedInstantiation)
        {
            if (invalidatePropertiesFlag)
            {
                validateProperties();
 
                // The Preloader listens for this event.
                systemManager.document.dispatchEvent(
                    new Event("validatePropertiesComplete"));
            }
 
            else if (invalidateSizeFlag)
            {
                validateSize();
 
                // The Preloader listens for this event.
                systemManager.document.dispatchEvent(
                    new Event("validateSizeComplete"));
            }
 
            else if (invalidateDisplayListFlag)
            {
                validateDisplayList();
 
                // The Preloader listens for this event.
                systemManager.document.dispatchEvent(
                    new Event("validateDisplayListComplete"));
            }
        }
 
        // Otherwise, do one pass of all three phases.
        else
        {
            if (invalidatePropertiesFlag)
                validateProperties();
 
            if (invalidateSizeFlag)
                validateSize();
 
            if (invalidateDisplayListFlag)
                validateDisplayList();
        }
 
        // trace("invalidatePropertiesFlag " + invalidatePropertiesFlag);
        // trace("invalidateSizeFlag " + invalidateSizeFlag);
        // trace("invalidateDisplayListFlag " + invalidateDisplayListFlag);
 
        if (invalidatePropertiesFlag ||
            invalidateSizeFlag ||
            invalidateDisplayListFlag)
        {
            attachListeners(systemManager);
        }
        else
        {
            usePhasedInstantiation = false;
 
			listenersAttached = false;
 
			var obj:ILayoutManagerClient = ILayoutManagerClient(updateCompleteQueue.removeLargest());
            while (obj)
            {
                if (!obj.initialized && obj.processedDescriptors)
                    obj.initialized = true;
                if (obj.hasEventListener(FlexEvent.UPDATE_COMPLETE))
                    obj.dispatchEvent(new FlexEvent(FlexEvent.UPDATE_COMPLETE));
                obj.updateCompletePendingFlag = false;
                obj = ILayoutManagerClient(updateCompleteQueue.removeLargest());
            }
 
            // trace("updateComplete");
 
            dispatchEvent(new FlexEvent(FlexEvent.UPDATE_COMPLETE));
        }
 
        // trace("< <DoPhasedInstantation");
    }

在普通的组件生命周期中,我们需要关注的是779行至807行。默认情况下,LayoutManager的usePhasedInstantiation为true,则执行此段代码,完成ActionScript3语言参考中所描述的:

LayoutManager 允许在各个阶段之间更新屏幕。usePhasedInstantiation=true,则在各阶段都会进行度量和布局,每个阶段结束后都会更新一次屏幕。所有组件都将调用其validateProperties() 和 commitProperties() 方法,直到验证完各自的所有属性。屏幕将随之更新。

然后,所有组件都将调用其 validateSize() 和 measure() 方法,直到测量完所有组件,屏幕也将再次更新。

最后,所有组件都将调用其 validateDisplayList() 和 updateDisplayList() 方法,直到验证完所有组件,屏幕也将再次更新。如果正在验证某个阶段,并且前面的阶段失效,则会重新启动 LayoutManager。当创建和初始化大量组件时,此方法更为高效。框架负责设置此属性。

在应用程序启动时,已经默认设置了该属性为true。参见上文中《 LayoutManager()来自何处》。

在 doPhasedInstantiation方法中,对于每一个阶段,Flex执行了同样的操作:首先通过flag变量判断是否需要执行该阶段,如果需要则执行对应的validate方法,最后抛出对应的validate完成事件。

2.5/ validate方法

2.5.1/ LayoutManager的valiate方法

亘古不变的真理是,有"山东"那么一定就有"山西"。有invalidate那么一定有validate。

说到validate方法,实际上意味着三阶段对应的validateProperties(),validateSize()和validateDisplayList()方法。

与invalidate方法一样,也存在着两套validate方法:LayoutManager类的validate方法和UIComponent类的validate方法。但是不同的是,此时,LayoutManager类的validate方法看起来更像"假李逵"。

让我们以LayoutManager类的validateProperties()方法为例:

  private function validateProperties():void
    {
        // trace("--- LayoutManager: validateProperties --->");
        CONFIG::performanceInstrumentation
        {
            var perfUtil:PerfUtil = PerfUtil.getInstance();
            perfUtil.markTime("validateProperties().start");
        }
 
        // Keep traversing the invalidatePropertiesQueue until we've reached the end.
        // More elements may get added to the queue while we're in this loop, or a
        // a recursive call to this function may remove elements from the queue while
        // we're in this loop.
        var obj:ILayoutManagerClient = ILayoutManagerClient(invalidatePropertiesQueue.removeSmallest());
        while (obj)
        {
            // trace("LayoutManager calling validateProperties() on " + Object(obj) + " " + DisplayObject(obj).width + " " + DisplayObject(obj).height);
 
            CONFIG::performanceInstrumentation
            {
                var token:int = perfUtil.markStart();
            }
 
			if (obj.nestLevel)
			{
				currentObject = obj;
	            obj.validateProperties();
	            if (!obj.updateCompletePendingFlag)
	            {
	                updateCompleteQueue.addObject(obj, obj.nestLevel);
	                obj.updateCompletePendingFlag = true;
	            }
			}            
            CONFIG::performanceInstrumentation
            {
                perfUtil.markEnd(".validateProperties()", token, 2 /*tolerance*/, obj);
            }
 
            // Once we start, don't stop.
            obj = ILayoutManagerClient(invalidatePropertiesQueue.removeSmallest());
        }
 
        if (invalidatePropertiesQueue.isEmpty())
        {
            // trace("Properties Queue is empty");
 
            invalidatePropertiesFlag = false;
        }
 
        // trace("< --- LayoutManager: validateProperties ---");
        CONFIG::performanceInstrumentation
        {
            perfUtil.markTime("validateProperties().end");
        }
    }

validateProperties()方法的主要工作就是遍历invalidatePropertiesQueue队列,对队列中的每个UIComponent对象执行其validateProperties方法。

 

暂停!再回头看一下上面这句话。

这句话中提到的两个valiadteProperties方法中,第一个validateProperties方法指的是LayoutManager类的方法,而第二个则是UIComponent类的方法。

那么LayoutManager的valiateProperties方法是如何执行遍历的呢?通过invalidatePropertiesQueue.removeSmallest()方法,LayoutManager从DisplayList的最顶层开始,从上之下开始遍历。

让我们回顾一下,在invalidate方法中,我们把UICompoennt对象加入了相应的invalidate队列中(比如invalidatePropertiesQueue),同时传入了nestLevel。我们说过,nestLevel表示了UIComponent对象在显示列表中的位置,Application的nestLevel值为3,从3开始,加入到显示列表中的组件,每低一级,则nestLevel指加一。这意味着,在显示列表中,最低一级组件的nestLevel值最大。

回到validateProperties方法,在while循环之前,通过下面代码从队列中移除并获取了nestLevel值最小的UIComponent,即意味着显示列表中调用过invalidateProperties方法的最外层的组件。在while循环的尾部,又一次调用该方法,来保证循环 invalidatePropertiesQueue 队列。

var obj:ILayoutManagerClient = ILayoutManagerClient(invalidatePropertiesQueue.removeSmallest())

如果你查看LayoutManager的valiadteSize方法,你会发现其调用的是removeLargest()方法,意味着validateSize是从最底层级组件开始循环,一直到最顶部。或者称之为从内到外。

查看LayoutManager的validateDisplayList方法,你会发现调用的是removeSmallest()方法,意味着其同validateProperties()方法一样,是从外向内遍历。

在while循环中,validateProperties方法对invalidatePropertiesQueue中的每个UIComponent都执行了validateProperties()方法。

2.5.2/ UIComponent的validate方法

下面,我们深入到UIComponent类,以其validateProperties()方法为例,看一下其是如何执行的。UIComponent类的validateProperties方法代码如下:

public function validateProperties():void
    {
        if (invalidatePropertiesFlag)
        {
            commitProperties();
 
            invalidatePropertiesFlag = false;
        }
    }

不出所料,UIComponent类的validate方法调用了commitProperties()方法。你也可以想象得到,validateSize()方法调用了measure()方法,而validateDisplayList()方法调用了updateDisplayList()方法。

三. 总结

至此,你应该更加深入的理解了Flex组件生命周期的三阶段commitProperties, measure以及updateDisplayList,以及相对应的invalidate方法。如我们开篇所说的,尽管这些方法存在于UIComponent类,但是其真正的发动机引擎实际上是单体类LayoutManager。

此时,如果你再次回顾本文开篇引用的ActionScript3参考中对于LayoutManager类的解释,你也许会有更深入的理解。

在接下来的系列文章中,我们将进一步深入理解Spark组件生命周期中的其他问题。

查看原文:深入浅出Flex组件生命周期Part4 ─ 引擎LayoutManager

评价本文

专业度
风格

您好,朋友!

您需要 注册一个InfoQ账号 或者 才能进行评论。在您完成注册后还需要进行一些设置。

获得来自InfoQ的更多体验。

告诉我们您的想法

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我
社区评论

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

讨论

登陆InfoQ,与你最关心的话题互动。


找回密码....

Follow

关注你最喜爱的话题和作者

快速浏览网站内你所感兴趣话题的精选内容。

Like

内容自由定制

选择想要阅读的主题和喜爱的作者定制自己的新闻源。

Notifications

获取更新

设置通知机制以获取内容更新对您而言是否重要

BT