[ActionScript 3.0] AS3 深刻理解Flash的 應用程序域Application Domains

簡介

網上有不少flash,一般都不須要顯示的使用應用程序域,由於默認的應用程序域就夠用了。其實複雜的狀況下須要用到應用程序域,好比說有兩個不一樣的swf,一個是舊版本的,一個是新版的,這兩個文件裏的類有相同的徹底限定明,以前的設計是舊的swf不要了,用新的swf代替,後來又要這兩個swf同時加載到一個主程序裏,那麼這時就須要使用到應用程序域了。
html

目錄

  • Application Domains 應用程序域
  • Application Domain Placement 應用程序域的位置
  • Application Domain Inheritance 應用程序域的繼承
  • Child Domains: Definition Versioning 子域:定義的版本管理
  • Separate Domains: Preventing Conflicts 域分離:避免衝突
  • Same Domain: Runtime Shared Libraries 相同的域:運行時共享庫
  • Getting Definitions Dynamically 動態獲取定義
  • Same-definition Collisions 相同定義的衝突
  • Conclusion 總結

Application Domains 應用程序域

和安全域同樣,不一樣安全沙箱下的SWF有着本身獨立的類定義。這種在安全域下面進行劃分和管理類定義(函數、接口和命名空間的定義也相似)的子域就是應用程序域。應用程序域只存在於安全域內,而且只能屬於惟一的一個安全域。可是安全域能夠包含多個應用程序域。跨域

安全域內的應用程序域
application_in_security.png安全

雖然安全域沙箱用於保護數據安全,應用程序沙箱域用於劃分定義。可是他們都用於解決定義的衝突和判斷代碼的繼承關係。
安全域彼此之間是相互獨立的,相比之下,應用程序域之間的關係則較爲複雜。應用程序域經過相似於Flash中的顯示列表那樣的層級關係連接在一塊兒。應用程序域能夠包含任意的子域,而子域只能有一個父域。子域繼承了來自父域中的定義,就像是顯示列表中父對象的位置和縮放屬性被子對象繼承同樣。
應用程序域的根節點是一個系統域,這個域包含了Flash Player API的原生定義(Array,XML,flash.display.Sprite等等)。系統域與安全域是一一對應的關係,當安全域初始化的時候這個惟一的系統域也被創建。
當一個Flash Player的實例初始化的時候,SWF文件加到它對應的安全域內。同時也建立了一個包含了這個文件中全部編譯過的ActionScript定義的應用程序域。這個應用程序域就成爲安全域下的系統域的第一個子域。Flash Player API的原生定義就經過這種繼承關係對全部子域開放。網絡

在系統域下新建了一個SWF應用程序域
child_application_domain.png架構

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

Application Domain Placement 應用程序域的位置

第一個實例化Flash Player的SWF文件所包含的定義老是被加載爲系統域的直接子域。父SWF去加載子SWF的時候,能夠控制子SWF內的定義所要放置的位置。可選的位置共有如下4種:框架

  1. 父SWF的應用程序域的新建子域 (默認方式)
  2. 子SWF 與父SWF的應用程序域合併
  3. 做爲父域的系統域下的新建子域
  4. 在其餘安全域下的系統域的新建子域

前三種狀況都是把子SWF加載到父域所處的安全域下,只有第四種是惟一一種把SWF加載到其餘安全域下的方法。dom

加載子SWF時放置應用程序域的4種選擇
app_domain_placement.png編輯器

還有一種沒提到的方式,是你爲某個已加載的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);

// 將定義放置到父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_inherit_definitions.png

這是由於你不能改變一個已經存在的實例的類定義。若是新的定義在被加載進來之前就已經用舊的定義生成過實例,那麼這個實例原先的類定義和新的類定義之間就會產生衝突。因此Flash Player保護原先的類定義不被重寫來避免衝突。
這也意味着開發者不可能覆蓋ActionScript API的原生定義。由於SWF所處的應用程序域確定是系統域的子域,而系統域包含了全部原生的定義。因此就算子SWF中包含了同名的定義,也會被系統域中的定義所覆蓋。
當向一個已經存在的應用程序域合併定義時,上述規則一樣適用。只有與原先的域裏的定義無衝突的定義纔會被合併。

新增到應用程序域裏的定義不會覆蓋現有的定義
combine_definitions.png

這種狀況下,那些發生衝突但卻被覆蓋的定義就徹底獲取不到了。可是若是是繼承的方式,就算子域中的那些衝突定義被父域中的定義覆蓋掉,仍是能夠經過getDefinition方法從子域中提取出來,關於這點將在 動態獲取定義 章節中討論。
加載到應用程序域中的定義在應用程序域的生命期裏一直存在。在SWF卸載後,用於保存這個SWF內的定義的應用程序域也會從內存中卸載。但若是該SWF的定義是放在其餘某個已經存在的應用程序域內的話,那麼這些定義將一直存在於內存中,除非目標應用程序域所關聯的那個SWF被卸載。若是一直把新的定義加載到一個已經存在的域內,好比爲第一個被加載的SWF建立的域,那麼定義所佔用的內存就會一直增長。若是是一個不停加載子SWF的滾動廣告應用的話,持續增長定義到相同的應用程序域內引發的內存增加問題顯然不是預期的結果。
並且,用這種方式加載的定義不會隨着子SWF的卸載而卸載,而是在第二次加載相同的子SWF的時候重用第一次加載時建立的定義。這一般不會有什麼問題,可是這意味着再次加載相同SWF的時候靜態類的狀態不會重置。靜態變量有多是上次使用過的值,必定會和第一次加載進來的時候保持一致。
因此,不一樣的狀況須要不一樣的解決方法。

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

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

子應用程序域繼承自父域
child_domain_inherits_parent.png

考慮如下情形:一個基於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;
            title.selectable = false; // 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_system_domain_child.png

因爲父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的定義
parent_domain_adds_child.png

運行時共享庫(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共享庫在運行時加載
compile_shared_library.png

另外一個準備工做須要編寫代碼。使用外部庫的時候,發佈的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直接共享和重用定義的方法。
在安全域和應用程序域的概念上有不少容易犯的錯誤。但願這篇教程可以幫你對此有所準備。你不只應當瞭解他們的運做方式,還要知道如何正確運用它們以達成你想要的效果。

譯者注:

經過這篇文章的翻譯,我才真正體會了翻譯工做的難作。雖然平時看英文資料的速度還挺快,可是用中文詳細複述一遍須要多花好幾十倍的時間。在這篇教程的翻譯中,我沒有使用全文翻譯等輔助工具,徹底靠手打,一邊翻譯一遍領會做者的意圖。感受收穫仍是比單純看一遍要來得更多一些。
本文介紹的知識至關重要,特別是從AS2時代成長起來的開發者很容易就掉進文中提到的一些陷阱。徹底掌握這部分知識對設計模塊架構,管理內存等方面都有很大的幫助。在此要再次感謝原做者爲咱們帶來這麼精彩的教程。
譯文轉自:http://kevincao.com/2010/11/application-domains/

看完了應用程序域,若是還想再看點安全方面的,請看 教程:深刻理解Flash的安全沙箱 – Security Domains
» 原文連接地址:http://www.litefeel.com/application-domains/

相關文章
相關標籤/搜索