深入理解Flash的沙箱 – Application Domains_上

应用程序域

Application Domains 应用程序域

和安全域一样,不同安全沙箱下的SWF有着自己独立的类定义。这种在安全域下面进行划分和管理类定义(函数、接口和命名空间的定义也类似)的子域就是应用程序域。应用程序域只存在于安全域内,并且只能属于唯一的一个安全域。但是安全域可以包含多个应用程序域。

Application Domains in Security Domains
安全域内的应用程序域

虽然安全域沙箱用于保护数据安全,应用程序沙箱域用于划分定义。但是他们都用于解决定义的冲突和判断代码的继承关系。

安全域彼此之间是相互独立的,相比之下,应用程序域之间的关系则较为复杂。应用程序域通过类似于Flash中的显示列表那样的层级关系链接在一起。应用程序域可以包含任意的子域,而子域只能有一个父域。子域继承了来自父域中的定义,就像是显示列表中父对象的位置和缩放属性被子对象继承一样。

应用程序域的根节点是一个系统域,这个域包含了Flash Player API的原生定义(Array,XML,flash.display.Sprite等等)。系统域与安全域是一一对应的关系,当安全域初始化的时候这个唯一的系统域也被建立。

当一个Flash Player的实例初始化的时候,SWF文件加到它对应的安全域内。同时也创建了一个包含了这个文件中所有编译过的ActionScript定义的应用程序域。这个应用程序域就成为安全域下的系统域的第一个子域。Flash Player API的原生定义就通过这种继承关系对所有子域开放。

Child Application Domain
在系统域下新建了一个SWF应用程序域

我们将在应用程序域的继承章节中进行更多关于继承的讨论。

Application Domain Placement 应用程序域的位置

第一个实例化Flash Player的SWF文件所包含的定义总是被加载为系统域的直接子域。父SWF去加载子SWF的时候,可以控制子SWF内的定义所要放置的位置。可选的位置共有以下4种:

  1. 父SWF的应用程序域的新建子域 (默认方式)
  2. 子SWF 与父SWF的应用程序域合并
  3. 作为父域的系统域下的新建子域
  4. 在其他安全域下的系统域的新建子域

前三种情况都是把子SWF加载到父域所处的安全域下,只有第四种是唯一一种把SWF加载到其他安全域下的方法。

Application Domain Placement
加载子SWF时放置应用程序域的4种选择

还有一种没提到的方式,是你为某个已加载的SWF创建了应用程序域,再把其他子SWF中的定义合并到(或者继承)这个域的情况。这种特殊的放置方式需要复杂的应用程序域层级管理,你需要掌握ApplicationDomain.parentDomain的用法,在此提醒读者小心:这种方法通常在不同的安全沙箱下(本地或者网络)会有不同的行为。这种方式很不常见,所以在此不进行更深的探讨。

LoaderContext对象的applicationDomain属性定义了放置应用程序域的方式。你可以用ApplicationDomain.currentDomain(类似于安全域的SecurityDomain.currentDomain)或者用new关键字新建一个ApplicationDomain实例来作为参数。在ApplicationDomain的构造函数里可以为新建的域指定父域,如果这个参数没有指定,则表示将该域直接作为系统域的子域。

// 将定义放置到父SWF所在的应用程序域(当前应用程序域)
var current:ApplicationDomain = ApplicationDomain.currentDomain;
 
// 将定义放置到父SWF所在的应用程序域的的子域
var currentChild:ApplicationDomain = new ApplicationDomain(current);<em>?</em>
 
// 将定义放置到父SWF所在的应用程序域的系统域
var systemChild:ApplicationDomain = new ApplicationDomain();

下面的代码演示了使用LoaderContext对象传递ApplicationDomain实例给Loader.load方法,把一个子SWF加载到父SWF所处的应用程序域的子域下的例子。这种方式也是默认的加载行为。

var context:LoaderContext = new LoaderContext();
// 把子应用程序域作为当前应用程序域的子域
var current:ApplicationDomain = ApplicationDomain.currentDomain;
context.applicationDomain = new ApplicationDomain(current);
 
var loader:Loader = new Loader();
var url:String = "child.swf";
loader.load(new URLRequest(url), context);

ApplicationDomain实例在内部包含了不对ActionScript开放的层级位置信息。每个ApplicationDomain实例都是一个唯一引用,彼此之间不能相互比较。

var current1:ApplicationDomain = ApplicationDomain.currentDomain;
var current2:ApplicationDomain = ApplicationDomain.currentDomain;
trace(current1 == current2); // false

你也不能通过parentDomain属性得到系统域的引用,只有通过new ApplicationDomain()才可以。

Application Domain Inheritance 应用程序域的继承

定义的继承和类继承有点类似,两者都是子级可以访问父级的定义,而反之则不行。

区别在于,应用程序域的继承不允许子级覆盖父级的定义。如果子域中包含有与父域一样的定义(指的是完全限定名称一致,包括包路径)。那么父域中的定义会取代掉子域。

Child Application Domain Inheritance
子域中的定义被父域覆盖

这是因为你不能改变一个已经存在的实例的类定义。如果新的定义在被加载进来以前就已经用旧的定义生成过实例,那么这个实例原先的类定义和新的类定义之间就会产生冲突。所以Flash Player保护原先的类定义不被重写来避免冲突。

这也意味着开发者不可能覆盖ActionScript API的原生定义。因为SWF所处的应用程序域肯定是系统域的子域,而系统域包含了所有原生的定义。所以就算子SWF中包含了同名的定义,也会被系统域中的定义所覆盖。

当向一个已经存在的应用程序域合并定义时,上述规则同样适用。只有与原先的域里的定义无冲突的定义才会被合并。

Adding Definitions in Application Domain
新增到应用程序域里的定义不会覆盖现有的定义

这种情况下,那些发生冲突但却被覆盖的定义就完全获取不到了。但是如果是继承的方式,就算子域中的那些冲突定义被父域中的定义覆盖掉,还是可以通过getDefinition方法从子域中提取出来,关于这点将在动态获取定义章节中讨论。

加载到应用程序域中的定义在应用程序域的生命期里一直存在。在SWF卸载后,用于保存这个SWF内的定义的应用程序域也会从内存中卸载。但如果该SWF的定义是放在其他某个已经存在的应用程序域内的话,那么这些定义将一直存在于内存中,除非目标应用程序域所关联的那个SWF被卸载。如果一直把新的定义加载到一个已经存在的域内,比如为第一个被加载的SWF创建的域,那么定义所占用的内存就会一直增加。如果是一个不停加载子SWF的滚动广告应用的话,持续增加定义到相同的应用程序域内引起的内存增长问题显然不是预期的结果。

而且,用这种方式加载的定义不会随着子SWF的卸载而卸载,而是在第二次加载相同的子SWF的时候重用第一次加载时创建的定义。这通常不会有什么问题,但是这意味着再次加载相同SWF的时候静态类的状态不会重置。静态变量有可能是上次使用过的值,一定会和第一次加载进来的时候保持一致。

所以,不同的情况需要不同的解决方法。

Child Domains: Definition Versioning 子域:定义的版本管理

定义的继承机制使得子域可以很方便的共享父域内的定义。也由于子域中的重名定义会被父域所覆盖的原因,父应用程序域拥有控制在子域中使用哪个版本的定义的权力。

Child Application Domain Inherits Parent Definitions
子应用程序域继承自父域

考虑以下情形:一个基于SWF的网站使用不同的SWF文件来代表不同的页面。主SWF负责加载这些子页面。每个页面SWF基于一个相同的类库开发,具有相似的行为。比如都有一个PageTitle类来表示页面的标题文本。

假如在相同域下有另一个SWF也用到这些相同的子页面,但是需要把子页面的标题文本变为不可选(假设原先的属性是可选择)。要实现这个例子里的目的,在PageTitle类中,我们需要把TextField的selectable属性改为false。但这样改动的问题是会影响原先的SWF文件保持其本来的行为。

为了解决这个问题,我们可以把每个子页面都复制一份并重新编译。但这么做的话会占用更多的空间和网站流量。更好的办法是只编译第二个主SWF,把更新过的PageTitle类定义一起编译进去。然后在子页面在加载到子应用程序域的时候,这个类的定义就会被父域里的定义给覆盖。

原先所有子页面用的PageTitle类如下:

package {
	import flash.display.Sprite;
	import flash.text.TextField;
 
	public class PageTitle extends Sprite {
 
		private var title:TextField;
 
		public function PageTitle(titleText:String){
			title = new TextField();
			title.text = titleText;
			addChild(title);
		}
	}
}

编译到第二个主文件里的更新版本的PageTitle类:

package {
	import flash.display.Sprite;
	import flash.text.TextField;
 
	public class PageTitle extends Sprite {
 
		private var title:TextField;
 
		public function PageTitle(titleText:String){
			title = new TextField();
			title.text = titleText;
			<strong>title.selectable = false;</strong> // changed
			addChild(title);
		}
	}
}

把更新过的PageTitle类定义编译到新的主文件里面,并加载所有子页面到它们自己的子应用程序域中。

PageTitle; // 虽然没有直接用到PageTitle,但我们可以包含一个引用,让它被一同编译进来 
 
// 加载子页面到它们自己的子应用程序域中
// 加载的SWF将会用父域里的PageTitle定义取代掉它们自带的
function addChildPage(url:String):void {
	var context:LoaderContext = new LoaderContext();
	var current:ApplicationDomain = ApplicationDomain.currentDomain;
	context.applicationDomain = new ApplicationDomain(current);
 
	var loader:Loader = new Loader();
	addChild(loader);
	loader.load(new URLRequest(url), context);
}

这种方法可以在不用重新编译子内容的前提下改变其中的类行为,这都是由于父应用程序域中的定义会覆盖子域中的定义的原因。

注意在上面的例子也可以省略LoaderContext的使用,效果是一样的。

即便子SWF无需用作多重使用目的,更新主文件中的定义也比更新所有子文件的更加简单。实际上,子文件中甚至可以完全不用包含这些定义,只依赖于主文件提供。这就是我们将在相同的域:运行时共享库章节里将展开讨论的。

Separate Domains: Preventing Conflicts 域分离:避免冲突

某些情形下,你可能不希望加载的子SWF内容被父应用程序域里的定义继承关系所影响。因为有可能你甚至不知道父域中存在哪些定义。不论哪种情况,最好都要避免主SWF和子SWF中的定义共享。在这种情况下,应该把子SWF的定义放到新的系统域的子域下。

Child SWF Domain as Child of System Domain
系统域下的不同子应用程序域

由于父SWF和子SWF的定义之间没有继承关系,所以这时候即使存在相同的定义也不会引起冲突,因为二者属于不同的沙箱。

举个例子:比如你有个培训程序,通过加载外部SWF来代表不同的培训模块。这个程序已经有些年头了,许多开发者开发了成百上千个培训模块。这些模块,甚至培训主程序自身都是基于不同版本的基础代码库进行开发。所以主程序要保证自己使用的基础代码库不会对其他模块造成不兼容的情况。这就必须把这些培训模块加载到他们独立的系统域下的子域,而不是把他们加载到主应用程序域的子域下面。

trainingapplication.swf:

var moduleLoader:Loader = new Loader();
addChild(moduleLoader);
 
// 把模块加载到系统域的子域下,与当前的应用程序域区分开
function loadModule(url:String):void {
	var context:LoaderContext = new LoaderContext();
	context.applicationDomain = new ApplicationDomain();
 
	moduleLoader.load(new URLRequest(url), context);
}

不足的是,这种定义的划分方式还不是完全隔离的。由于在同一个安全域下的内容都处于一个相同的系统域下,任何对系统域内定义的修改都将影响同一个安全域下的所有应用程序域。即使是将子SWF加载到一个单独的系统域的子域下,父SWF对系统域的更改还是会对其造成影响。

我们可以通过改动XML.prettyIndent属性来验证这一点:不管处于应用程序域层级的哪个SWF对系统域里的定义作出改变,都会影响到相同安全域下的所有文件。

parent.swf:

trace(XML.prettyIndent); // 2
XML.prettyIndent = 5;
trace(XML.prettyIndent); // 5
 
var loader:Loader = new Loader();
 
var context:LoaderContext = new LoaderContext();
// 新建一个独立的应用程序域
context.applicationDomain = new ApplicationDomain();
 
var url:String = "child.swf";
loader.load(new URLRequest(url), context);

child.swf:

trace(XML.prettyIndent); // 5

所以最佳实践是对定义做的改动应该在使用后及时还原,这样可以避免对其他文件的影响。

var originalPrettyIndent:int = XML.prettyIndent;
XML.prettyIndent = 5;
trace(myXML.toXMLString());
XML.prettyIndent = originalPrettyIndent;

同样的,你也必须留心类似这样的值有可能在你的程序之外被人所改动。