1. 程式人生 > >深入理解Flash Player的應用程式域(Application Domains)

深入理解Flash Player的應用程式域(Application Domains)

轉:http://riaoo.com/?p=1970


深入理解Flash Player的應用程式域(Application Domains)


這篇是《深入理解Flash Player的安全域(Security Domains)》的下文。轉載過來備份一下。原文地址:http://kevincao.com/2010/11/application-domains/

目錄

Application Domains 應用程式域

和安全域一樣,不同安全沙箱下的SWF有著自己獨立的類定義。這種在安全域下面進行劃分和管理類定義(函式、介面和名稱空間的定義也類似)的子域就是應用程式域。應用程式域只存在於安全域內,並且只能屬於唯一的一個安全域。但是安全域可以包含多個應用程式域。


安全域內的應用程式域

雖然安全域沙箱用於保護資料安全,應用程式沙箱域用於劃分定義。但是他們都用於解決定義的衝突和判斷程式碼的繼承關係。

安全域彼此之間是相互獨立的,相比之下,應用程式域之間的關係則較為複雜。應用程式域通過類似於Flash中的顯示列表那樣的層級關係連結在一起。應用程式域可以包含任意的子域,而子域只能有一個父域。子域繼承了來自父域中的定義,就像是顯示列表中父物件的位置和縮放屬性被子物件繼承一樣。

應用程式域的根節點是一個系統域,這個域包含了Flash Player API的原生定義(Array,XML,flash.display.Sprite等等)。系統域與安全域是一一對應的關係,當安全域初始化的時候這個唯一的系統域也被建立。

當一個Flash Player的例項初始化的時候,SWF檔案加到它對應的安全域內。同時也建立了一個包含了這個檔案中所有編譯過的ActionScript定義的應用程式域。這個應用程式域就成為安全域下的系統域的第一個子域。Flash Player API的原生定義就通過這種繼承關係對所有子域開放。


在系統域下新建了一個SWF應用程式域

我們將在應用程式域的繼承章節中進行更多關於繼承的討論。

Application Domain Placement 應用程式域的位置

第一個例項化Flash Player的SWF檔案所包含的定義總是被載入為系統域的直接子域。父SWF去載入子SWF的時候,可以控制子SWF內的定義所要放置的位置。可選的位置共有以下4種:

  1. 父SWF的應用程式域的新建子域 (預設方式)
  2. 子SWF 與父SWF的應用程式域合併
  3. 作為父域的系統域下的新建子域
  4. 在其他安全域下的系統域的新建子域

前三種情況都是把子SWF載入到父域所處的安全域下,只有第四種是唯一一種把SWF載入到其他安全域下的方法。


載入子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 應用程式域的繼承

定義的繼承和類繼承有點類似,兩者都是子級可以訪問父級的定義,而反之則不行。

區別在於,應用程式域的繼承不允許子級覆蓋父級的定義。如果子域中包含有與父域一樣的定義(指的是完全限定名稱一致,包括包路徑)。那麼父域中的定義會取代掉子域。


子域中的定義被父域覆蓋

這是因為你不能改變一個已經存在的例項的類定義。如果新的定義在被載入進來以前就已經用舊的定義生成過例項,那麼這個例項原先的類定義和新的類定義之間就會產生衝突。所以Flash Player保護原先的類定義不被重寫來避免衝突。

這也意味著開發者不可能覆蓋ActionScript API的原生定義。因為SWF所處的應用程式域肯定是系統域的子域,而系統域包含了所有原生的定義。所以就運算元SWF中包含了同名的定義,也會被系統域中的定義所覆蓋。

當向一個已經存在的應用程式域合併定義時,上述規則同樣適用。只有與原先的域裡的定義無衝突的定義才會被合併。


新增到應用程式域裡的定義不會覆蓋現有的定義

這種情況下,那些發生衝突但卻被覆蓋的定義就完全獲取不到了。但是如果是繼承的方式,就運算元域中的那些衝突定義被父域中的定義覆蓋掉,還是可以通過getDefinition方法從子域中提取出來,關於這點將在動態獲取定義章節中討論。

載入到應用程式域中的定義在應用程式域的生命期裡一直存在。在SWF解除安裝後,用於儲存這個SWF內的定義的應用程式域也會從記憶體中解除安裝。但如果該SWF的定義是放在其他某個已經存在的應用程式域內的話,那麼這些定義將一直存在於記憶體中,除非目標應用程式域所關聯的那個SWF被解除安裝。如果一直把新的定義載入到一個已經存在的域內,比如為第一個被載入的SWF建立的域,那麼定義所佔用的記憶體就會一直增加。如果是一個不停載入子SWF的滾動廣告應用的話,持續增加定義到相同的應用程式域內引起的記憶體增長問題顯然不是預期的結果。

而且,用這種方式載入的定義不會隨著子SWF的解除安裝而解除安裝,而是在第二次載入相同的子SWF的時候重用第一次載入時建立的定義。這通常不會有什麼問題,但是這意味著再次載入相同SWF的時候靜態類的狀態不會重置。靜態變數有可能是上次使用過的值,一定會和第一次載入進來的時候保持一致。

所以,不同的情況需要不同的解決方法。

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

定義的繼承機制使得子域可以很方便的共享父域內的定義。也由於子域中的重名定義會被父域所覆蓋的原因,父應用程式域擁有控制在子域中使用哪個版本的定義的權力。


子應用程式域繼承自父域

考慮以下情形:一個基於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的定義放到新的系統域的子域下。


系統域下的不同子應用程式域

由於父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;

同樣的,你也必須留心類似這樣的值有可能在你的程式之外被人所改動。

Same Domain: Runtime Shared Libraries 相同的域:執行時共享庫

把新增的定義增加到現有的應用程式域下可能是應用程式域最大的用處。因為繼承只能把父域內的定義對子域共享,而合併定義到相同的應用程式域內則可以對所有使用這個域的SWF共享,包括父級和子級。


父應用程式域包括了子SWF的定義

執行時共享庫(RSLs)正是運用了這種機制。RSLs是可以在執行時被載入的獨立的程式碼庫。通過RSLs,其他SWF可以共用其中的程式碼而不需要編譯到自身,從而排除了冗餘,減小了檔案量,也讓程式碼更容易維護。我們在主應用程式域中載入RSL,從而可以在整個程式中共享定義。

使用RSLs之前需要做些準備工作。首先,ActionScript編譯器需要在釋出SWF檔案的時候知道哪些定義不需要被編譯。

原生的Flash Player API定義就不需要編譯。雖然每個SWF都需要用到原生的定義(Array,XML,Sprite等),但是這些定義只存在於Flash Player的可執行檔案中,不需要也不會被編譯到SWF檔案中。編譯器使用一個叫做playerglobal.swc的特殊SWC(預先編譯的SWF類庫)來識別原生定義。它包含了原生定義的介面,包括定義的名字和資料型別等。編譯器通過它來編譯SWF,而且不會把這些定義編譯到最終的SWF中。

編譯器還可以引用其他類似playerglobal.swc一樣的SWC庫。這些庫作為“外部”類庫,其中包含的定義只是用於編譯,不會包含到SWF內部。

這裡不詳細討論在編輯工具中如何進行庫連結的設定。不同版本的編輯器的設定有些不同,具體方法請參考Flash文件。

雖然我們用SWCs來編譯SWF,但實際上他們本身就是SWF檔案,和其他被載入的SWF內容類似。在進行庫編譯的時候,同時生成了SWF和SWC檔案。SWF用於執行時載入,而SWC在編譯時用做外部庫。


編譯器使用SWCs共享庫,SWF共享庫在執行時載入

另一個準備工作需要編寫程式碼。使用外部庫的時候,釋出的SWF中不包含庫中的定義。如果Flash Player嘗試執行其中程式碼,就會產生核查錯誤,整個SWF基本上就癱瘓了。

Flash Player會在類第一次使用的時候校驗其定義。如果應用程式域中不包括該定義,那麼校驗錯誤就會產生。

實際上缺少定義產生的錯誤有兩種。校驗錯誤是兩種之中最糟的,表示類無法正常工作的災難性失敗。另一種是引用錯誤,當某種資料型別被引用但是卻不可用的情況下發生。雖然缺失定義也會造成引用錯誤,但這種錯誤只會在已經經過核查的類內部打斷程式碼執行的正常過程。

var instance:DoesNotExist;
// VerifyError: Error #1014: Class DoesNotExist could not be found.
// 當Flash Player校驗包含該定義的類時發生校驗錯誤
var instance:Object = new DoesNotExist();
// ReferenceError: Error #1065: Variable DoesNotExist is not defined.
// 當代碼執行到這一行的時候發生引用錯誤

主要的區別在於校驗錯誤與類定義有關,而引用錯誤與程式碼執行相關。在類內部的程式碼要執行之前,必須要先通過校驗。上面的例子中instance物件宣告為Object型別,校驗可以正常通過(只是在執行的時候就會遇到引用錯誤)。

Note: Strict Mode 注意:嚴格模式

外部庫是引用定義而不需將其編譯到SWF中的一種方法。另一種方法是關閉嚴格模式,這將大大放寬了對變數使用的檢查。對於類的使用來說,你可以引用一個不存在的類而不會引起編譯器報錯。你不能直接把不存在的類用作變數型別(這樣做會在執行時產生校驗錯誤),但是你可以像上面的“引用錯誤”例子中那樣去引用。在非嚴格模式下,編譯器也許會檢測不到一些可能發生的錯誤,所以通常不建議用這種模式。

使用了RSLs的SWF檔案必須保證先載入好RSLs,才能使用這些外部定義。我們應該在主應用程式開始執行之前用一個預載入器來載入RSLs。

下面演示了一個SWF載入包含Doughnut類的外部RSL的例子。雖然在SWF中直接引用了這個類,但是它卻是編譯在外部庫中,並通過SWC的方式來引用的。RSL在Doughnut類第一次使用之前就被載入進來,所以不會造成校驗錯誤。

Doughnut.as (編譯為 doughnutLibrary.swc 和 doughnutLibrary.swf):

package {
	import flash.display.Sprite;

	public class Doughnut extends Sprite {
		public function Doughnut(){

			// draw a doughnut shape
			graphics.beginFill(0xFF99AA);
			graphics.drawCircle(0, 0, 50);
			graphics.drawCircle(0, 0, 25);
		}
	}
}

ShapesMain.as (Shapes.swf的主類):

package {
	import flash.display.Sprite;

	public class ShapesMain extends Sprite {
		public function ShapesMain(){

			// 雖然並沒有編譯到Shapes.swf中,
			// 但是我們通過doughnutLibrary.swc外部庫
			// 可以獲得對Doughnut類的引用
			var donut:Doughnut = new Doughnut();
			donut.x = 100;
			donut.y = 100;
			addChild(donut);
		}
	}
}

Shapes.swf (RSL loader):

var rslLoader:Loader = new Loader();
rslLoader.contentLoaderInfo.addEventListener(Event.INIT, rslInit);

// 把RSL中的定義載入到當前應用程式域中
var context:LoaderContext = new LoaderContext();
context.applicationDomain = ApplicationDomain.currentDomain;

var url:String = "doughnutLibrary.swf";
rslLoader.load(new URLRequest(url), context);

function rslInit(event:Event):void {
	// 只有當RSL中的定義匯入到當前應用程式域以後
	// 我們才能用其中的Doughnut定義通過ShapesMain類的校驗
	addChild(new ShapesMain());
}

在這個例子中,Shapes.swf是主程式,當RSL載入完畢後例項化主類ShapesMain。如果沒有匯入RSL中的定義,建立ShapesMain例項的時候就會因為在應用程式域中找不到對應的類而發生校驗錯誤。

注意:Flex中的RSL

這裡討論的方法是最底層的方法,不應該用於Flex開發。Flex框架中有自己的一套RSLs處理機制,更多關於RSL在Flex中的應用,請參考Flex Runtime Shared Libraries (Flex 4)

Getting Definitions Dynamically 動態獲取定義

我們可以用Application.getDefinition方法獲取不在應用程式域內的定義,或者被父域覆蓋的定義。這個方法返回應用程式域及其任意父域內的定義引用。在當前應用程式域內使用getDefinition方法的效果等同於全域性函式getDefinitionByName

我們也可以通過SWF的LoaderInfo.applicationDomain來獲得在ApplicationDomain.currentDomain以外的應用程式域。在下面的例子中我們用Loader載入了一個SWF檔案,然後在載入的那個應用程式域中提取com.example.Box類的定義。

try {
	var domain:ApplicationDomain = loader.contentLoaderInfo.applicationDomain;
	var boxClass:Class = domain.getDefinition("com.example.Box") as Class;
	var boxInstance:Object = new boxClass();
}catch(err:Error){
	trace(err.message);
}

以上的例子中包含了兩個知識點。首先,getDefinition方法的返回值被顯式的轉換為Class型別,這是因為getDefinition預設返回的是Object型別,有可能代表了除了類型別以外的其他型別(函式,名稱空間,介面)。其次,這個操作應該要放在try-catch函式體內,因為如果getDefinition查詢定義失敗將會丟擲錯誤。或者你也可以在使用getDefinition之前用ApplicationDomain.hasDefinition方法檢測是否能夠成功找到某個定義。

用動態方式去獲取的定義,而不是那些在當前應用程式域(及繼承的程式域內)的定義,是不能用作變數型別的。就像RSL一樣,在應用程式域內找不到的類定義會在校驗的時候報錯。所以上面的例子中boxInstance變數宣告為Object型別而不是Box型別,就是因為Box類的定義在應用程式域內不存在。

Same-definition Collisions 相同定義的衝突

有些時候可能會發生你引用的定義匹配到另外的應用程式域裡的定義的交叉情況。這種情況將會產生如下強制轉換型別錯誤:

TypeError: Error #1034: Type Coercion failed: cannot convert
	com.example::[email protected] to com.example.MyClass.

你可以看到在不同記憶體空間裡的定義用@符號進行了區分。雖然它們內部的程式碼可能是完全相同的(或不同),但是由於它們存在不同的應用程式域(或安全域)內,所以它們是兩個不同的定義。

只有像Object那樣的原生Flash Player定義才可以將位於不同域(甚至是跨安全域的)的定義關聯起來。實際上,大多數時候宣告一個跨域的變數型別的時候都需要用Object型別。

雖然我們可以用Object這種通用型別來解決定義衝突錯誤,實際上我們更應該合理安排應用程式域的位置來消除這種不匹配的情況。

Conclusion 總結

這篇教程包含了很多方面的資訊。前半部分討論了什麼是安全域,以及它如何影響來自不同域的內容。Flash Player用這種安全沙箱機制保護使用者的資料。Flash開發者應該瞭解併合理利用這種限制。

第二部分討論了應用程式域——另一種用於在安全沙箱內劃分ActionScript定義的沙箱型別。應用程式域的層級機制提供了在不同的SWF直接共享和重用定義的方法。

在安全域和應用程式域的概念上有很多容易犯的錯誤。希望這篇教程能夠幫你對此有所準備。你不僅應當瞭解他們的運作方式,還要知道如何正確運用它們以達成你想要的效果。