《深入理解Flash Player的安全域(Security Domains)》(下)

轉自  : http://www.2cto.com/Article/201301/181648.html



目錄

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::MyClass@51e1101 to com.example.MyClass.

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

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

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

Conclusion 總結

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

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

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


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章