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

  • 董龙飞

2011 年 9 月 29 日

话题:语言 & 开发

从 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

语言 & 开发