第23章 統一編程接口——外觀模式

23.1 外觀模式介紹

外觀模式(Facade)在開發過程中的運用頻率非常高,尤其是在現階段,各種第三方SDK"充斥”在我們的周邊,而這些SDK大多會使用外觀模式。通過一個外觀類使得整個系統的接口只有一個統一的高層接口,這樣能夠降低用戶的使用成本,也對用戶屏蔽了很多實現細節。當然,在我們的開發過程中,外觀模式也是我們封裝API的常用手段,例如網絡模塊、ImageLoader模塊等。 可能你已經在開發中運用過無數次外觀模式,只是沒有在理論層面認識它,本章我們就從理論結合實踐上學習這個模式。

23.2 外觀模式定義

要求一個子系統的外部與其內部的通信必須通過一個統一的對象進行。門面模式(Facade模式)提供一個高層次的接口,使得子系統更易於使用。

23.3 外觀模式的使用場景

  1. 爲一個複雜子系統提供一個簡單接口。子系統往往因爲不斷演化而變得越來越複雜,甚至可能被替換。大多數模式使用時都會產生更多、更小的類,這使子系統更具可重用性的同時也更容易對子系統進行定製、修改,這種易變性使得隱藏子系統的具體實現變得尤爲重要。Facade可以提供一個簡單統一的接口,對外隱藏子系統的具體實現、隔離變化。
  2. 當你需要構建一個層次結構的子系統時,使用Facade模式定義子系統中每層的入口點。如果子系統之間是相互依賴的,你可以讓它們僅通過Facade接口進行通信,從而簡化了它們之間的依賴關係。

23.4 外觀模式的UML類圖

UML類圖如圖23-1所示。
在這裏插入圖片描述
角色介紹

  • Facade:系統對外的統一接口,系統內部系統地工作。
  • SystemA、SystemB、SystemC:子系統接口(實現部分未在圖23-1中給出)。
    外觀模式接口比較簡單,就是通過一個統一的接口對外提供服務,使得外部程序只通過一個類就可以實現系統內部的多種功能,而這些實現功能的內部子系統之間可能也有交互,或者說完成一個功能需要幾個子系統之間進行協作,如果沒有封裝,那麼用戶就需要操作幾個子系統的交互邏輯,容易出現錯誤。而通過外觀類來對外屏蔽這些複雜的交互,降低用戶的使用成本。它的結構如圖23-2所示。

23.5 外觀模式的簡單示例

生活中使用外觀模式的例子非常多,任何一個類似中央調度結構的組織都類似外觀模式。舉個簡單的例子,手機就是一個外觀模式的例子,它集合了電話功能、短信功能、GPS、拍照等於一身,通過手機你就可以完成各種功能。而不是當你打電話時使用一個諾基亞1100,要拍照時非得用一個相機,如果每使用一個功能你就必須操作特定的設備,會使得整個過程很繁瑣。而手機給了你一個統 一的入口,集電話、上網、拍照等功能於一身,使用方便,操作簡單。結構圖如圖23-3所示。

下面我們來簡單模擬一下手機的外觀模式實現,首先我們建立一個MobilePhone代碼大致如下。
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
MobilePhone類中含有兩個子系統,也就是撥號系統和拍照系統,MobilePhone將這兩個系統封裝起來,爲用戶提供一個統一的操作接口,也就是說用戶只需要通過MobilePhone這個類就可以操作打電話和拍照這兩個功能。用戶不需要知道有Phone這個接口以及它的實現類是Phonelmpl,同樣也不需要知道Camera相關的信息,通過MobilePhone就可以包攬一切。而在MobilePhone中也封裝了兩個子系統的交互,例如視頻電話時需要先打開攝像頭,然後再開始撥號,如果沒有這一步的封裝,每次用戶實現視頻通話功能時都需要手動打開攝像頭、進行撥號,這樣會增加用戶的使用成本,外觀模式使得這些操作更加簡單、易用。



我們來看看Phone接口和Phonelmpl。
在這裏插入圖片描述
代碼很簡單,就是單純的抽象與實現。Camera也是類似的實現,具體代碼如下。
在這裏插入圖片描述
在這裏插入圖片描述
運行結果:
在這裏插入圖片描述
從上述代碼中可以看到,外觀模式就是統一接口封裝。將子系統的邏輯、交互隱藏起來,爲用戶提供一個高層次的接口,使得系統更加易用,同時也對外隱藏了具體的實現,這樣即使具體的子系統發生了變化,用戶也不會感知到,因爲用戶使用的是Facade高層接口,內部的變化對於用戶來說並不可見。這樣一來就將變化隔離開來,使得系統也更爲靈活。






23.6 Android源碼中的外觀模式

在用Android開發過程中,Context是最重要的一個類型,Context意爲上下文,也就是程序的運行環境。它封裝了很多重要的操作,如startActivity、sendBroadcast()、bindService等,因此,Context對開發者來說是最重要的高層接口。Context只是一個定義了很多接口的抽象類,這些接口的功能實現並不是在Context及其子類中,而是通過其他子系統來完成,例如startActivity的真正實現是通過ActivityManagerService,獲取應用包相關信息則是通過PackageManagerService。Context只是做了一個高層次的統一封裝,正如上文所示,Context只是一個抽象類,它的真正實現在Contextlmpl類中,Contextlmpl就是今天我們要分析的外觀類。

在本書的前面章節中已經提到多次,在應用啓動時,首先會fork一個子進程,並且調用ActivityThread.main方法啓動該進程。ActivityThread又會構建Application對象,然後和Activity、Contextlmpl關聯起來,最後會調用Activity的onCreate、onStart、onResume函數使Activity運行起來,此時應用的用戶界面就呈現在我們面前了。main函數會間接地調用ActivityThread中的handleLaunchActivity函數啓動默認的Activity, handleLaunchActivity代碼如下。
在這裏插入圖片描述
在handleLaunchActivity函數中會調用 perfromLaunchActivity函數執行Applicaton、Contextlmpl、Activity的創建工作,並且通過Activity類的attach函數將這3者關聯起來,相關代碼在註釋4處。 而Activity本身又是Context的子類,因此,Activity就具有了Context定義的所有方法。但Activity並不實現具體的功能,它只是繼承了Context的接口,並且將相關的操作轉發給Contextlmpl對象

這個Contextlmpl存儲在Activity的上兩層父類ContextWrapper中,變量名爲mBase,具體代碼如下。
在這裏插入圖片描述
在ActivityThread類的perfromLaunchActivity函數中會調用Activity的attach方法將Contextlmpl等對象關聯到Activity中,這個Contextlmpl最終會被Contentwrapper類的mBase字段引用。我們先看看attach方法的內部實現。
在這裏插入圖片描述
attach函數主要就是一些賦值操作,這裏我們只關心mBase的初始化。在attach函數中,第一句就調用了attachBaseContext函數,該函數定義在ContextWrapper類,它就是簡單地將Context參數傳遞給mBase字段。此時,我們的Activity內部就持有了Contextlmpl的引用



Activity在開發過程中部分充當了代理的角色,例如,當我們通過Activity對象調用sendBroadcast、getResource等函數時,實際上Activity只是代理了ContextImpl的操作,也就是內部都調用了mBase對象的相應方法來處理,這些操作被封裝在Activity的父類的ContentWrapper中。代碼如下所示。在這裏插入圖片描述
在這裏插入圖片描述
既然Contextlmpl那麼重要,包含了各個系統服務的調用與操作,那麼我們來看看它的相關實現。
在這裏插入圖片描述
在這裏插入圖片描述
從上述程序中可以看到,Contextlmpl內部封裝了很多不同子系統的操作,例如,Activity的跳轉、發送廣播、啓動服務、設置壁紙等,這些工作並不是在Contextlmpl中實現,而是轉交給了具體的子系統進行處理。通過Context這個抽象了、定義了一組接口,Contextlmpl實現Context定義的接口,這使得用戶可以通過Context這個接口統一進行與Android系統的交互,這樣用戶通常情況下就不需要對每個子系統進行了解,例如啓動Activity時用戶不需要手動調用mMainThread.getInstrurnentation().execStartActivity函數進行執行,發送廣播時也不需要直接操作ActivityManagerNative類。用戶與系統服務的交互都通過Context的高層接口,這樣對用戶屏蔽了具體實現的細節,降低了使用成本。




通過Contextlmpl的封裝之後,用戶與系統服務之間的交互如圖23-4所示。
在這裏插入圖片描述
從上述示例來看,外觀模式的實現非常簡單, 沒有複雜的類型結構,只是通過一組高層接口封裝了各個子系統的操作,並且統一提供給用戶。試想一下如果沒有外觀模式的封裝,那麼用戶就必須知道各個子系統的相關細節,甚至它們之間的協作流程,當子系統較多時這些相互之間的關係就會很亂,導致用戶在使用相關功能時難度也會變大,易於出錯。而使用外觀模式的封裝就避免了用戶需要與多個子系統進行交互,降低了用戶的使用成本,對外也屏蔽了具體細節,保證了系統的易用性、穩定性。

23.7 深度拓展

23.7.1 Android資源的加載與匹配

在Android開發中,我們爲了屏幕適配通常會爲一個應用做多套資源,使得在不同分辨率的設備上能夠儘量保持一致的UI效果。那麼我們不禁要問Android的資源在哪?讀取資源的形式又是怎樣?下面我們就來簡單分析一下Android的資源機制。

其實在Android應用程序資源的編譯和打包之後就生成一個資源索引表文件resources.arsc,這個應用程序資源會被打包到APK文件中。Android應用程序在運行的過程中,通過一個稱爲Resource來獲取資源,但實際上Resource內部又是通過一個叫AssetManager的資源管理器來讀取打包在APK文件裏面的資源文件。那麼AssetManager如何與應用關聯在一起,它又是如何找到應用的資源,這就是我們本節要關注的重點。

我們在上文中說到,獲取資源的操作實際上是由Contextlmpl來完成的,Activity、Service等組件的getResource函數最終都轉發給了ContextImpl類型的mBase字段。也就是調用了Contextlmpl的getResource函數,而這個Resource在Contextlmpl關聯到Activity之前就會初始化Resource對象,相關代碼如下。
在這裏插入圖片描述
在上述performLaunchActivity函數中,首先創建了Activity與Application,然後通過 createBaseContextForActivity創建了一個Contextlmpl對象。而在createBaseContextForActivity函數中又調用了Contextlmpl類的createActivityContext靜態函數,我們看看源碼。
在這裏插入圖片描述
在這裏插入圖片描述
createActivityContext函數中最終調用了Contextlmpl的構造函數,在該函數中會初始化該進程的各個字段,例如資源、包信息、屏幕配置等。這裏只關心與資源相關的代碼。在通過mPackagelnfo得到對應的資源之後,會調用ResourcesManager的getTopLevelResources來根據設備配置等相關信息獲取到對應的資源,也就是資源的適配。getTopLevelResources代碼如下。
在這裏插入圖片描述
在這裏插入圖片描述
首先會以APK路徑、屏幕設備id、配置等構建一個資源key,根據這個key到ActivityThread類的mActiveResources(HashMap類型)中查詢是否已經加載過該APK的資源,如果含有緩存那麼直接使用緩存。這個mActiveResources維護了當前應用程序進程中加載的每一個APK文件及其對應的Resources對象的對應關係。如果沒有緩存,那麼就會新創建一個,並且保存在mActiveResources中







在沒有資源緩存的情況下,ActivityThread會新創建一個AssetManager對象,並且調用AssetManager對象的addAssetPath函數來將參數resDir作爲它的資源目錄,這個resDir就是APK文件的絕對路徑。創建了一個新的AssetManager對象之後,會將這個AssetManager對象作爲Resource構造函數的第一個參數來構建一個新的Resources對象。這個新創建的Resources對象會以前面所創建的ResourcesKey對象爲鍵值緩存在mActiveResources所描述的一個HashMap中,以便重複使用該資源時無需重複創建

接下來,我們首先分析AssetManager類的構造函數和成員函數addAssetPath的實現,接着再分析Resources類的構造函數的實現,以便可以瞭解用來訪問應用程序資源的AssetManager對象和Resources對象的創建以及初始化過程。
在這裏插入圖片描述
在這裏插入圖片描述
在AssetManager的構造函數中,首先會調用init函數進行初始化然後再調用ensureSystemAssets函數來加載系統資源,這些系統資源存儲在mSystem對象中,mSystem也是AssetManager類型。需要注意的是,init函數並不是一個Java函數,而是一個Native層的方法,它的實現在android_util_AssetManager.cpp文件中,具體代碼如下。
在這裏插入圖片描述
在init函數中首先創建了一個Native層的AssetManager對象,然後添加了默認的系統資源,最後將這個AssetManager對象轉換爲整型並且傳遞給Java層的AssetManager的mObject字段,這樣Java層就相當於存有了一個Native層AssetManager的句柄。這裏我們關注的是addDefaultAssets函數,具體代碼如下。
在這裏插入圖片描述
代碼也比較簡單,就是拼接系統資源路徑,最好將該路徑傳遞給addAssetPath函數中,實際上就是將系統資源路徑添加到AssetManager的資源路徑列表中。
在這裏插入圖片描述
在這裏插入圖片描述
此時,在ActivityThread的getTopLevelResources函數中的new AssetManager的過程就執行完畢了,然後繼續執行AssetManager對象的addAssetPath函數,具體代碼如下。
在這裏插入圖片描述
在Java層的AssetManager的addAssetPath函數中實際上調用的是Native層的addAssetPathNative
函數
。需要注意的是,這個path參數必須是一個目錄或者是一個zip壓縮文件(APK本質上就是1個zip文件)路徑。這個addAssetPathNative函數在Native層對應的函數爲android_content_AssetManager_addAssetPath,該函數定義在android_util_AssetManager.cpp文件中
在這裏插入圖片描述
在android_content_AssetManager_addAssetPath函數中會調用assetManagerForJavaObject函數先從Java層的AssetManager對象中獲取到mObject字段,該字段存儲了由Native層AssetManager指針轉換而來的整型值。此時需要通過這個整型值逆向地轉換爲Native層的AssetManager對象。然後將APK路徑添加到資源路徑中,這樣也就含有了應用本身的資源。













我們再來分析這個過程,首先在Java層的AssetManager構造函數中調用了init函數初始化系統資源,創建與初始化完畢之後又調用了Java層AssetManager對象的addAssetPath函數將應用APK的路徑傳遞給Native層的AssetManager,使得應用的資源也被添加到AssetManager中,這樣資源路徑就構建完畢了

最後在ResourcesManager的getTopLevelResources函數中將初始化好的AssetManager、設備配置等作爲參數來構造Java層的Resource對象,也就是上文中getTopLevelResources函數中的註釋4處。Resource的構造函數如下。
在這裏插入圖片描述
Resources類的構造函數首先將參數assets所指向的一個AssetManager對象保存在成員變量mAssets中,我們獲取資源就是通過這個AssetManager對象進行操作。接下來調用updateConfiguration函數來設置設備配置信息,最後調用AssetManager的成員函數ensureStringBlocks來創建字符串資源池

我們來看看updateConfiguration函數的實現。
在這裏插入圖片描述
在這裏插入圖片描述
Resources類的成員函數updateConfiguration首先是根據參數config和metrics來更新設備的當前配置信息,例如,屏幕大小和密碼、國家(地區)和語言、鍵盤配置情況等,接着再調用成員變量mAssets所指向的一個Java層的AssetManager對象的成員函數setConfiguration來將這些配置信息設置到與之關聯的C++層的AssetManager對象中。這樣一來,在我們通過Resource獲取資源時,Native層就會根據這個配置信息尋找最適合的資源返回,從而達到多屏幕適配的效果


從上述的分析中我們可以知道,Activity、Contextlmpl、Resource、AssetManager的結構圖如圖23-5所示。在這裏插入圖片描述
經過上述的分析,我們也可以得出一個結論:應用的資源存儲在APK中,一個AssetManager可以管理多個路徑的資源文件,Resource通過AssetManager獲取資源。那就是說我們可以通過AssetManager的addAssetPath方法添加APK路徑以達到一些資源替換或者換膚的效果,還有類似於動態加載一個未安裝的APK時也可以通過這種形式加載插件APK的資源,使得插件Activity等組件可以通過資源類R來訪問應用資源

23.7.2 動態加載框架的實現

在上一節中我們學習了Android中的資源機制,本節我們將資源機制作爲基本知識點來實現一個簡單的動態加載框架。在開始之前我們需要知道,未安裝的APK文件是可以通過DexClassLoader進行加載、運行的,具體代碼如下。
在這裏插入圖片描述
其中mContext是一個Context類型的對象,apkPath就是這個APK在手機中的絕對路徑,dexOutputDir就是APK解壓出的dex文件的存放目錄。通過這種形式就有了一個可以加載這個APK的ClassLoader。然後通過這個ClassLoader加載這個APK默認啓動的Activity,並且通過反射調用這個插件Activity的onCreate、onStart、onResume函數就會加載這個應用

我們的實現原理當然沒有上面所說的那麼簡單,還需要處理的問題包括加載插件APK的資源、處理它的生命週期函數等。不過在此之前,首先做的還是構建能夠加載插件APK的ClassLoader和資源,我們新創建了一個PluginManager類來完成這些工作,具體代碼如下。
在這裏插入圖片描述
在PluginManager類中以插件APK的包名爲key,插件APK信息爲值緩存了插件相關的信息。加載插件APK時會從map中檢測是否含有緩存,如果有則使用緩存的APK,否則通過DexClassLoader和資源創建一個PluginApk對象,最後將這個對象緩存到map中。創建PluginApk的代碼是createApk函數,具體代碼如下:
在這裏插入圖片描述
在這裏插入圖片描述
在createApk函數中首先通過AssetManager對象添加了插件APK所在的絕對路徑,使得AssetManager可以獲取到該插件APK的資源,然後又通過該AssetManager和設備配置等創建了Resource對象,並且將這個對象存儲到PluginApk中,最後構建插件APK的DexClassLoader並存儲在PluginApk對象中。最後這個PluginApk對象會緩存到PluginManager的信息表中




在啓動插件之前,用戶需要調用PluginManager的loadApk函數加載插件APK的相關信息,也就是上述所說的資源、DexClassLoader等,然後再調用PluginManager的startActivity函數啓動插件,具體代碼如下。
在這裏插入圖片描述
需要注意的是,啓動插件時需要在Intent中傳遞插件APK的包名和默認加載的Activity類名,否則會拋出異常。因爲PluginManager需要知道插件APK默認的啓動Activity才能將插件APK運行起來。還有一個重要的地方是,我們在startActivity中會重新構建一個Intent,這個Intent啓動的卻是ActivityProxy這個類。我們是要啓動插件APK的默認Activity,怎麼換成了ActivityProxy呢?

實際上我們動態加載的並不是插件APK的Activity,而是一個Activity空殼,也就是這裏的ActivityProxy。在這個空殼Activity中包裝了插件Activity的聲明週期,也就說插件Activity和ActivityProxy是共存的。相關代碼如下。
在這裏插入圖片描述
在ActivityProxy中有一個LifbCircleControIler,這個類就負責通過DexClassLoader加載插件APK的Activity,我們看到ActivityProxy的聲明週期函數中都調用了LifeCircleController對象對應的方法,而LifeCircleController中實際上又調用了插件Activity的生命週期方法。我們看看LifeCircleController的實現。
在這裏插入圖片描述
在這裏插入圖片描述
在 ActivityProxy 的 onCreate 函數中調用了 LifeCircleController 的 onCreate 函數,在 LifeCircleControIler的onCreate函數中主要做了如下幾步操作




  1. 獲取插件Activity的類路徑、包名
  2. 通過包名到PluginManager中獲取PluginApk信息
  3. 通過PluginApk中的DexClassLoader加載插件Activity
  4. 將ActivityProxy對象注入到插件Activity中
  5. 調用插件Activity的onCreate函數

在PluginManager的startActivity函數中我們強調過,跳轉到插件Activity的Intent必須包含插件Activity的類路徑、包名就是爲了在這一步使用。獲取到類名之後再得到DexClassLoader,最後動態加載插件Activity,並且將ActivityProxy注入到插件Activity中。最後調用插件Activity的onCreate函數。

對於插件Activity我們也定義了一個基類,叫做PluginActivity,具體代碼如下。
在這裏插入圖片描述
在這裏插入圖片描述
從上述程序中可以看到,在PluginActivity中所有的函數幾乎都轉交給了ActivityProxy處理,比如setContentView,實際上也是設置給了ActivityProxy對象。此時我們可以思考一下,我們在開發插件時會繼承自PluginActivity,然後在onCreate函數中設置setContentView爲插件Activity設置佈局內容,而PluginActivity調用的卻是ActivityProxy的setContentView函數,就相當於是爲ActivityProxy對象設置內容佈局當我們要啓動插件Actiivty時實際上又是啓動ActivityProxy,又在ActivityProxy的onCreate函數中通過LifeCircleController動態地用DexClassLoader加載插件Activity,並且調用它的onCreate函數、setContentView方法等,而setContentView實際上調用了ActivityProxy的setContentView,在調用 ActivityProxy的onStart、onResume之後ActivityProxy就顯示到我們面前了,它的視圖就是通過插件Activity設置的內容


一句話概括就是:ActivityProxy加載的是插件Activity的內容與資源。這樣一來,插件Activity就會以ActivityProxy的形式展現出來。

例如,我們有一個插件APK在SD卡的plugins目錄下,它的包名爲com.example.plugin,默認啓動的Activity類路徑爲com.example.plugin.MainActivity。首先需要一個宿主應用來加載插件應用,宿主應用中要加載插件APK時代碼調用如下。
在這裏插入圖片描述
在這裏插入圖片描述
我們再創建一個插件工程,這個工程需要引用動態加載框架的代碼,然後新創建一個繼承自PluginActivity的插件Activity,具體代碼如下。
在這裏插入圖片描述
生成APK之後將該插件APK命名爲plugin.apk,放到SD卡的plugins目錄下,然後運行宿主APK,而在宿主APK中又會加載plugin.apk,此時得到的效果如圖23-6和圖23-7所示。
在這裏插入圖片描述
在宿主中,我們通過PluginManager啓動了插件APK。但是,實際上啓動是ActivityProxy,只是我們將插件Activity的contentview設置給了ActivityProxy,這樣看起來像是啓動了插件Activity。






在這個過程中最終的兩點就是contentview的設置和資源的替換。當然這只是一個最基本的實現,通過這個示例只是讓大家對於動態加載有一個基本的認識,更完善的動態加載框架請查看https://github.com/singwhatiwanna/dynamic-load-apk,也希望更多的人蔘與到這個開源項目來。

23.8 外觀模式實戰

對於SDK和開源庫來說,外觀模式通常是使用率最高的模式,這些庫通過外觀類爲用戶提供統一的高層接口,使得用戶不必瞭解一些更細節的實現。例如在使用ImageLoader時我們通常只操作一個ImageLoader類就可以完成,而不要了解網絡請求類、緩存類以及它們的交互細節。又比如在使用友盟統計SDK時,我們基本上通過MobclickAgent這個類就可以完成我們所需的功能,至於MobclickAgent的內部有其他的什麼類型、它們的具體交互是什麼我們都不需要關心,這個MobclickAgent類也就是一個外觀類。可見,外觀模式實際上在我們的開發中無處不在,今天我們還是以小民的ImageLoader(類名爲SimplelmageLoader,簡稱ImageLoader)爲例來講述外觀模式的運用。

在本書的前面章節中我們講到,用戶在使用ImageLoader時只需要操作ImageLoader類,其他的類型基本不用關心。ImageLoader封裝了內部邏輯,通過ImageLoader的接口用戶就可以完成圖片加載的操作。核心代碼如下。
在這裏插入圖片描述
在ImageLoader類中對外暴露的函數基本只有displayImage,也就是加載圖片的函數。最簡單的情況下,用戶只需要傳遞ImageView和圖片的uri即可實現圖片加載。在該函數中,首先會將傳遞進來的參數轉換爲一個BitmapRequest對象,然後將該對象傳遞到請求隊列中。請求隊列的初始化就在ImageLoader的初始化函數中。該請求隊列初始化之後就會啓動CPU數量+1個的請求處理線程,在這些線程的run中不斷地從請求隊列中獲取請求、加載請求、最後將圖片投遞到UI線程更新ImageView。首先我們來看消息隊列的初始化。
在這裏插入圖片描述
在RequestQueue中會啓動指定數量的RequestDispatcher線程,每個RequestDispatcher本質上是一個線程,在它們的run函數中有一個死循環不斷地查詢請求隊列中是否含有請求,並且處理這些請求。
在這裏插入圖片描述
在RequestDispatcher的run方法中主要分爲4步。





  1. 從消息隊列中獲取消息
  2. 根據請求的uri地址獲取圖片的uri schema
  3. 根據schema獲取對應的圖片Loader
  4. 加載圖片,並且將結果傳遞給UI線程,更新ImageView

從上述代碼看,RequestDispatcher中又封裝了LoaderManager系統,而在LoaderManager中又管理了各個Loader類,而Loader類中又管理了圖片的加載流程、圖片緩存等邏輯。而這些相關的類型、邏輯都被封裝在ImageLoader類的外衣之下,對於用戶來說它們根本不知道這些類型的存在,幾乎所有的操作都是通過SimplelmageLoader這個類完成。它們的結構如圖23-8所示。
在這裏插入圖片描述
從圖23-8中我們可以看到, ReuqestQueue等類型都被封裝在了SimplelmageLoader類之下,用戶根本不知道這些類型的存在,而只需要操作SimplelmageLoader的接口就可以完成圖片加載的操作。這樣就避免了暴露過多的實現細節,也使得ImageLoader的使用更爲簡單,試想一下,每次都需要手動構建一個BitmapRequest類,然後添加到請求隊列中,那麼整個過程就增加了用戶的使用成本,用戶也直接依賴了具體的實現,這對於系統的升級、維護造成了一定的困難。通過SimplelmageLoder類將這些具體細節都隱藏起來,在降低用戶使用成本的同時也增加了靈活性,例如,當你需要替換RequestDispatcher爲線程池實現時,用戶並不能感知到,因爲原來的用戶並不知道內部實現有RequestDispatcher這個類型,這就能很好地在不對用戶產生影響的情況下對產品進行升級、維護。

23.9 小結

外觀模式是一個使用頻率較高的設計模式,它的精髓就在於“封裝”二字。通過一個高層次結構爲用戶提供統一的API入口,使得用戶通過一個類型就基本能夠操作整個系統,這樣減少了用戶的使用成本,也能夠提升系統的靈活性。

優點

  • 對客戶程序隱藏子系統細節,因而減少了客戶對於子系統的耦合,能夠擁抱變化。
  • 外觀類對子系統的接口封裝,使得系統更易於使用。

缺點

  • 外觀類接口膨脹。由於子系統的接口都有外觀類統一對外暴露,使得外觀類的API接口較多,在一定程度上增加了用戶使用成本。
  • 外觀類沒有遵循開閉原則,當業務出現變更時,可能需要直接修改外觀類。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章