1. 程式人生 > >《深入理解Android 卷III》第八章深入理解Android桌布(節選)

《深入理解Android 卷III》第八章深入理解Android桌布(節選)

                      第8章 深入理解Android桌布(節選)

本章主要內容:

·  討論動態桌布的實現。

·  在動態桌布的基礎上討論靜態桌布的實現。

·  討論WMS對桌布視窗所做的特殊處理。

本章涉及的原始碼檔名及位置:

·  WallpaperManagerService.java

frameworks/base/services/java/com/android/server/WallpaperManagerService.java

·  WallpaperService.java

frameworks/base/core/java/android/service/wallpaper/WallpaperService.java

·  ImageWallpaper.java

frameworks/base/packages/SystemUI/src/com/android/systemui/ImageWallpaper.java

·  WallpaperManager.java

frameworks/base/core/java/android/app/WallpaperManager.java

·  WindowManagerService.java

frameworks/base/services/java/com/android/server/wm/WindowManagerService.java

·  WindowStateAnimator.java

frameworks/base/services/java/com/android/server/wm/WindowStateAnimator.java

·  WindowAnimator.java

frameworks/base/services/java/com/android/server/wm/WindowAnimator.java

8.1 初識Android桌布

本章將對桌布的實現原理進行討論。在Android中,桌布分為靜態與動態兩種。靜態桌布是一張圖片,而動態桌布則以動畫為表現形式,或者可以對使用者的操作作出反應。這兩種形式看似差異很大,其實二者的本質是統一的。它們都以一個Service的形式執行在系統後臺,並在一個型別為TYPE_WALLPAPER

的視窗上繪製內容。進一步講,靜態桌布是一種特殊的動態桌布,它僅在視窗上渲染一張圖片,並且不會對使用者的操作作出反應。因此本章將首先通過動態桌布的實現討論Android桌布的實現與管理原理,然後在對靜態桌布的實現做介紹。

Android桌布的實現與管理分為三個層次:

·  WallpaperService與Engine同SystemUI一樣,桌布執行在一個Android服務之中,這個服務的名字叫做WallpaperService。當用戶選擇了一個桌布之後,此桌布所對應的WallpaperService便會啟動並開始進行桌布的繪製工作,因此繼承並定製WallpaperService是開發者進行桌布開發的第一步Engine是WallpaperService中的一個內部類,實現了桌布視窗的建立以及Surface的維護工作。另外,Engine提供了可供子類重寫的一系列回撥,用於通知桌布開發者關於桌布的生命週期、Surface狀態的變化以及對使用者的輸入事件進行響應。可以說,Engine類是桌布實現的核心所在。桌布開發者需要繼承Engine類,並重寫其提供的回撥以完成桌布的開發。這一層次的內容主要體現了桌布的實現原理。

·  WallpaperManagerService,這個系統服務用於管理桌布的執行與切換,並通過WallpaperManager類向外界提供操作桌布的介面。當通過WallpaperManagaer的介面進行桌布的切換時,WallpaperManagerService會取消當前桌布的WallpaperService的繫結,並啟動新桌布的WallpaperService。另外,Engine類進行視窗建立時所使用的視窗令牌也是由WallpaperManagerService提供的。這一層次主要體現了Android對桌布的管理方式。

·  WindowManagerService,用於計算桌布視窗的Z序、可見性以及為桌布應用視窗動畫桌布視窗(TYPE_WALLPAPER)的Z序計算不同於其他型別的視窗。其他視窗依照其型別會有固定的mBaseLayer以及mSubLayer,並結合它們所屬的Activity的順序或建立順序進行Z序的計算,因此這些視窗的Z序相對固定。而桌布視窗則不然,它的Z序會根據FLAG_SHOW_WALLPAPER標記在其它視窗的LayoutParams.flags中的存在情況而不斷地被調整。這一層次主要體現了Android對桌布視窗的管理方式。

本章將通過對動態桌布切換的過程進行分析揭示WallpaperService、Engine以及WallpaperManagerService三者的實現原理以及協作情況。靜態桌布作為動態桌布的一種特殊情況,將會在完成動態桌布的學習之後於8.3節進行討論。而WindowManagerService對桌布視窗的處理將在8.4節進行介紹。

8.2 深入理解動態桌布

8.2.1 啟動動態桌布的方法

啟動動態桌布可以通過呼叫WallpaperManager.getIWallpaperManager().setWallpaperComponent()方法完成。它接受一個ComponentName型別的引數,用於將希望啟動的桌布的WallpaperService的ComponentName告知WallpaperManagerServiceWallpaperManager.getIWallpaperManager()方法返回的是WallpaperManagerService的Bp端。因此setWallpaperComponent()方法的實現位於WallpaperManagerService之中。參考其實現:

[WallpaperManagerService.java-->WallpaperManagerService.setWallpaperComponent()]

public void setWallpaperComponent(ComponentNamename) {

    // 設定動態桌布需要呼叫者擁有一個簽名級的系統許可權

    checkPermission(android.Manifest.permission.SET_WALLPAPER_COMPONENT);

   synchronized (mLock) {

        /* ① 首先從mWallpaperMap中獲取桌布的執行資訊WallpaperData。

         WallpaperManagerService支援多使用者機制,因此裝置上的每一個使用者可以設定自己

          的桌布。mWallpaperMap中為每一個使用者儲存了一個WallpaperData例項,這個例項

          中儲存了和桌布執行狀態相關的資訊。例如WallpaperService的ComponentName,

          到WallpaperService的ServiceConnection等。於是當發生使用者切換時,

          WallpaperManagerService可以從mWallpaperMap中獲取新使用者的WallpaperData,

          並通過儲存在其中的ComponentName重新啟動該使用者所設定的桌布。因此,

          當通過setWallpaperComponent()設定新桌布時,需要獲取當前使用者的WallpaperData,

          並在隨後更新其內容使之儲存新桌布的資訊 */

        intuserId = UserHandle.getCallingUserId();

       WallpaperData wallpaper = mWallpaperMap.get(userId);

       ......

       final long ident = Binder.clearCallingIdentity();

        try{

           ......

           // ② 啟動新桌布的WallpaperService

           bindWallpaperComponentLocked(name, false, true, wallpaper, null);

        }finally {

           Binder.restoreCallingIdentity(ident);

        }

    }

}

注意 WallpaperManager.getIWallpaperManager()並沒有作為SDK的一部分提供給開發者。因此第三方應用程式是無法進行動態桌布的設定的。

8.2.2 桌布服務的啟動原理

(1)桌布服務的驗證與啟動

bindWallpaperComponentLocked()方法將會啟動由ComponentName所指定的WallpaperService,並向WMS申請用於新增桌布視窗的視窗令牌。不過在此之前,bindWallpaperComponentLocked()會對ComponentName所描述的Service進行一系列的驗證,以確保它是一個桌布服務。而這一系列的驗證過程體現了一個Android服務可以被當作桌布必要的條件。

[WallpaperManagerService.java-->WallpaperManagerService.setWallpaperComponentLocked()]

boolean bindWallpaperComponentLocked(ComponentNamecomponentName, boolean force,

       boolean fromUser, WallpaperData wallpaper, IRemoteCallback reply) {

    ......

    try {

        /* 當componentName為null時表示使用預設桌布。

          這裡會將componentName引數改為預設桌布的componentName */

        if(componentName == null) {

           /* 首先會嘗試從com.android.internal.R.string.default_wallpaper_component

              中獲取預設桌布的componentName。這個值的設定位於res/values/config.xml中,

              開發者可以通過修改這個值設定預設桌布*/

           String defaultComponent = mContext.getString(

                         com.android.internal.R.string.default_wallpaper_component);

           if (defaultComponent != null) {

               componentName = ComponentName.unflattenFromString(defaultComponent);

           }

           /* 倘若在上述的資原始檔中沒有指定一個預設桌布,即default_wallpaper_component的

              值被設定為@null),則使用ImageWallpaper代替預設桌布。ImageWallpaper就是前文

              所述的靜態桌布 */

           if (componentName == null) {

               componentName = IMAGE_WALLPAPER;

           }

        }

        /* 接下來WallpaperMangerService會嘗試從PackageManager中嘗試獲取ComponentName所

          指定的Service的描述資訊,獲取此資訊的目的在於確認該Service是一個符合要求的桌布服務 */

        intserviceUserId = wallpaper.userId;

       ServiceInfo si = mIPackageManager.getServiceInfo(componentName,

                             PackageManager.GET_META_DATA |

                              PackageManager.GET_PERMISSIONS,serviceUserId);

        /* ① 第一個檢查,要求這個Service必須宣告其訪問許可權為BIND_WALLPAPER。這個簽名級的系

          統許可權這是為了防止桌布服務被第三方應用程式啟動而產生混亂 */

        if(!android.Manifest.permission.BIND_WALLPAPER.equals(si.permission)) {

           if (fromUser) {

               throw new SecurityException(msg);

           }

           return false;

        }

       WallpaperInfo wi = null;

        /* ② 第二個檢查,要求這個Service必須可以用來處理

           android.service.wallpaper.WallpaperService這個Action。

WallpaperManagerService會使用這個Action對此服務進行繫結。

           其檢查方式是從PackageManager中查詢所有可以處理

android.service.wallpaper.WallpaperService的服務,然後檢查即將啟動的服務

           是否在PackageManager的查詢結果之中 */

       Intent intent = new Intent(WallpaperService.SERVICE_INTERFACE);

        if(componentName != null && !componentName.equals(IMAGE_WALLPAPER)) {

           // 獲取所有可以處理android.service.wallpaper.WallpaperService的服務資訊

           List<ResolveInfo> ris =

                   mIPackageManager.queryIntentServices(intent,

                           intent.resolveTypeIfNeeded(mContext.getContentResolver()),

                           PackageManager.GET_META_DATA, serviceUserId);

            /* ③ 第三個檢查,要求這個Service必須在其meta-data中提供關於桌布的描述資訊如果

             即將啟動的服務位於查詢結果之中,便可以確定這是一個桌布服務。此時會建立一

             個WallpaperInfo的例項以解析並存儲此桌布服務的描述資訊桌布服務的描述資訊包含

             了桌布的開發者、縮圖、簡單的描述文字以及用於對此桌布進行引數設定的Activity的

             名字等。桌布開發者可以在AndroidManifest.xml中將一個包含了上述資訊的xml檔案

             置在名為android.service.wallpaper的meta-data中以提供這些資訊 */

           for (int i=0; i<ris.size(); i++) {

               ServiceInfo rsi = ris.get(i).serviceInfo;

               if (rsi.name.equals(si.name) &&

                        rsi.packageName.equals(si.packageName)){

                   try {

                        wi = newWallpaperInfo(mContext, ris.get(i));

                   } catch (XmlPullParserException e) {......}

                   break;

               }

           }

            if (wi == null) {

               /* wi為null表示即將啟動的服務沒有位於查詢結果之中,或者沒有提供必須的meta-data。

                 此時返回false表示繫結失敗 */

               return false;

           }

        }

       ......

    }

    ......

}

可見WallpaperManagerService要求被啟動的目標Service必須滿足以下三個條件:

·  該服務必須要以android.permission.BIND_WALLPAPER作為其訪問許可權。桌布雖然是一個標準的Android服務,但是通過其他途徑(如第三方應用程式)啟動桌布所在的服務是沒有意義的。因此Android要求作為桌布的Service必須使用這個簽名級的系統許可權進行訪問限制,以免被意外的應用程式啟動。

·  該服務必須被宣告為可以處理android.service.wallpaper.WallpaperService這個Action。WallpaperManagerService會使用這個Action對此服務進行繫結。

·  該服務必須在其AndroidManifest.xml中提供一個名為android.service.wallpaper的meta-data,用於提供動態桌布的開發者、縮圖與描述文字。

一旦目標服務滿足了上述條件,WallpaperManagerService就會著手進行目標服務的啟動與繫結。

參考setWallpaperComponentLocked()方法的後續程式碼:

[WallpaperManagerService.java-->WallpaperManagerService.setWallpaperComponentLocked()]

boolean bindWallpaperComponentLocked(ComponentNamecomponentName, boolean force,

       boolean fromUser, WallpaperData wallpaper, IRemoteCallback reply) {

       ...... // 檢查服務是否符合要求的程式碼

        /* ① 建立一個WallpaperConnection它不僅實現了ServiceConnection介面用於監

          聽WallpaperService之間的連線狀態,同時還實現了IWallpaperConnection.Stub,

          也就是說它支援跨程序通訊。

          在服務繫結成功後的WallpaperConnection.onServiceConnected()方法呼叫中,

          WallpaperConnection的例項會被髮送給WallpaperService,使其作為WallpaperService

          向WallpaperManagerService進行通訊的橋樑 */

       WallpaperConnection newConn = new WallpaperConnection(wi, wallpaper);

        // 為啟動桌布服務準備Intent

       intent.setComponent(componentName);

       intent.putExtra(Intent.EXTRA_CLIENT_LABEL,

               com.android.internal.R.string.wallpaper_binding_label);

       intent.putExtra(Intent.EXTRA_CLIENT_INTENT,PendingIntent.getActivityAsUser(

               mContext, 0,

               Intent.createChooser(new Intent(Intent.ACTION_SET_WALLPAPER),

                 mContext.getText(com.android.internal.R.string.chooser_wallpaper)),

               0, null, new UserHandle(serviceUserId)));

        /* ② 啟動制定的桌布服務。當服務啟動完成後,剩下的啟動流程會在

          WallpaperConnection.onServiceConnected()中繼續 */

        if(!mContext.bindService(intent,

                              newConn,Context.BIND_AUTO_CREATE, serviceUserId)) {

        }

        // ③ 新的的桌布服務啟動成功後,便通過detachWallpaperLocked()銷燬舊有的桌布服務

        if(wallpaper.userId == mCurrentUserId && mLastWallpaper != null) {

           detachWallpaperLocked(mLastWallpaper);

        }

        // ④ 將新的桌布服務的執行資訊儲存到WallpaperData中

       wallpaper.wallpaperComponent = componentName;

       wallpaper.connection = newConn;

        /* 設定wallpaper.lastDiedTime。這個成員變數與其說描述桌布的死亡時間戳,不如說是

          描述其啟動的時間戳。它用來在桌布服務意外斷開時(即桌布服務非正常停止)檢查此桌布服務

          的存活時間。當存活時間小於一個特定的時長時將會認為這個桌布的軟體質量不可靠

          從而選擇使用預設桌布,而不是重啟這個桌布服務 */

       wallpaper.lastDiedTime = SystemClock.uptimeMillis();

       newConn.mReply = reply;

        /* ④ 最後向WMS註冊一個WALLPAPER型別的視窗令牌。這個令牌會在onServiceConnected()

          之後被傳遞給WallpaperService用於作為後者新增視窗的通行證 */

        try{

           if (wallpaper.userId == mCurrentUserId) {

                mIWindowManager.addWindowToken(newConn.mToken,

                       WindowManager.LayoutParams.TYPE_WALLPAPER);

               mLastWallpaper = wallpaper;

           }

        } catch (RemoteException e) {}

    } catch(RemoteException e) {}

    returntrue;

}

bindWallpaperComponentLocked()主要做了如下幾件事情:

·  建立WallpaperConnection。由於實現了ServiceConnection介面,因此它將負責監聽WallpaperManagerService與桌布服務之間的連線狀態。另外由於繼承了IWallpaperConnection.Stub,因此它具有跨程序通訊的能力。在桌布服務繫結成功後,WallpaperConnection例項會被傳遞給桌布服務作為桌布服務與WallpaperManagerService進行通訊的橋樑。

·  啟動桌布服務。通過Context.bindService()方法完成。可見啟動桌布服務與啟動一個普通的服務沒有什麼區別。

·  終止舊有的桌布服務。

·  將屬於當前桌布的WallpaperConnection例項、componentName機器啟動時間戳儲存到WallpaperData中。

·  向WMS註冊WALLPAPER型別的視窗令牌。這個視窗令牌儲存在WallpaperConnection.mToken中,並隨著WallpaperConnection的建立而建立。

僅僅將指定的桌布服務啟動起來尚無法使得桌布得以顯示,因為新啟動起來的桌布服務由於沒有可用的視窗令牌而導致其無法新增視窗。WallpaperManagerService必須通過某種方法將視窗令牌交給桌布服務才行。所以桌布顯示的後半部分的流程將在WallpaperConnection.onServiceConnected()回撥中繼續。同其他服務一樣,WallpaperManagerService會在這個回撥之中獲得一個Binder物件。因此在進行onServiceConnected()方法的討論之前,必須瞭解WallpaperManagerService在這個回撥中將會得到一個什麼樣的Binder物件。

現在把分析目標轉移到WallpaperService中。和普通服務一樣,WallpaperService的啟動也會經歷onCreate()、onBind()這樣的生命週期回撥。為了瞭解WallpaperManagerService可以從onServiceConnected()獲取怎樣的Binder物件,需要看下WallpaperService.onBind()的實現:

[WallpaperService.java-->WallpaperService.onBind()]

public final IBinder onBind(Intent intent) {

    /*onBind()新建了一個IWallpaperServiceWrapper例項,並將

      其返回給WallpaperManagerService */

    return new IWallpaperServiceWrapper(this);

}

IWallpaperServiceWrapper類繼承自IWallpaperService.Stub。它儲存了WallpaperService的例項,同時也實現了唯一的一個介面attach()。很顯然,當這個Binder物件返回給WallpaperManagerService之後,後者定會呼叫這個唯一的介面attach()以傳遞顯示桌布所必須的包括視窗令牌在內的一系列的引數。

(2)向桌布服務傳遞建立視窗所需的資訊

重新回到WallpaperManagerService,當WallpaperService建立了IWallpaperServiceWrapper例項並返回後,WallpaperManagerService將會在WallpaperConnection.onServiceConnected()中收到回撥。參考其實現:

[WallpaperManagerService.java-->WallpaperConnection.onServiceConnected()]

public void onServiceConnected(ComponentName name,IBinder service) {

   synchronized (mLock) {

        if (mWallpaper.connection == this) {

           // 更新桌布的啟動時間戳

           mWallpaper.lastDiedTime = SystemClock.uptimeMillis();

           // ① 將WallpaperService傳回的IWallpaperService介面儲存為mService

           mService = IWallpaperService.Stub.asInterface(service);

           /* ② 繫結桌布服務。attachServiceLocked()會呼叫IWallpaperService.attach()

             方法以將桌布服務建立視窗所需的資訊傳遞過去 */

           attachServiceLocked(this, mWallpaper);

           // ③ 儲存當前桌布的執行狀態到檔案系統中,以便在系統重啟或發生使用者切換時可以恢復

           saveSettingsLocked(mWallpaper);

        }

    }

}

進一步地,attachServiceLocked()方法會呼叫IWallpaperService.attach()方法,將建立桌布視窗所需的資訊傳送給桌布服務。

[WallpaperManagerService.java-->WallpaperManagerService.attachServiceCLocked()]

void attachServiceLocked(WallpaperConnection conn,WallpaperData wallpaper) {

    try {

        /* 呼叫IWallpaperService的唯一介面attach(),將建立桌布視窗所需要的引數傳遞

          給WallpaperService */

       conn.mService.attach(conn, conn.mToken,

               WindowManager.LayoutParams.TYPE_WALLPAPER, false,

               wallpaper.width, wallpaper.height);

    } catch(RemoteException e) {......}

}

attach()方法的引數很多,它們的意義如下:

·  conn即WallpaperConnection,WallpaperService將通過它向WallpaperManagerService進行通訊。WallpaperConnection繼承自IWallpaperConnection,只提供了兩個介面的定義,即attachEngine()以及engineShown()。雖說WallpaperManager是WallpaperManagerService向外界提供的標準介面,但是這裡仍然選擇使用WallpaperConnection實現這兩個介面的原因是由於attachEngine()以及engineShown()是隻有WallpaperService才需要用到而且是它與 WallpaperManagerService之間比較底層且私密的交流,將它們的實現放在通用的介面WallpaperManager中顯然並不合適。這兩個介面中比較重要的當屬attachEngine()了。如前文所述,Engine類是實現桌布的核心所在,而WallpaperService只是一個用於承載桌布的執行的容器而已。因此相對於WallpaperService,Engine是WallpaperManagerService更加關心的物件。所以當WallpaperService完成了Engine物件的建立之後,就會通過attachEngine()方法將Engine物件的引用交給WallpaperManagerService。

·  conn.mToken就是在bindWallpaperComponent()方法中向WMS註冊過的視窗令牌。是WallpaperService有權新增桌布視窗的憑證。

·  WindowManager.LayoutParams.TYPE_WALLPAPER指明瞭WallpaperService需要新增TYPE_WALLPAPER型別的視窗。讀者可能會質疑這個引數的意義:桌布除了是TYPE_WALLPAPER型別以外難道還有其他的可能麼?的確在實際的桌布顯示中WallpaperService必然需要使用TYPE_WALLPAPER型別新增視窗。但是有一個例外,即桌布預覽。在LivePicker應用中選擇一個動態桌布時,首先會使得使用者對選定的桌布進行預覽。這一預覽並不是真的將桌布設定給了WallpaperManagerService,而是LivePicker應用自行啟動了對應的桌布服務,並要求桌布服務使用TYPE_APPLICATION_MEDIA_OVERLAY型別建立視窗。這樣一來,桌布服務所建立的視窗將會以子視窗的形式襯在LivePicker的視窗之下,從而實現了動態桌布的預覽。

·  false的引數名是isPreview. 用以指示啟動桌布服務的意圖。當被實際用作桌布時取值為false,而作為預覽時則為true。僅當LivePicker對桌布進行預覽時才會使用true作為isPreview的取值。桌布服務可以根據這一引數的取值對自己的行為作出調整。

當WallpaperManagerService向WallpaperService提供了用於建立桌布視窗的足夠的資訊之後,WallpaperService便可以開始著手進行Engine物件的建立

(3)Engine的建立

呼叫IWallpaperService.attach()是WallpaperManagerService在桌布服務啟動後第一次與桌布服務進行聯絡。參考其實現:

[WallpaperService.java-->IWallpaperServiceWrapper.attach()]

public void attach(IWallpaperConnection conn,IBinder windowToken,

        intwindowType, boolean isPreview, int reqWidth, int reqHeight) {

    // 使用WallpaperManagerService提供的引數,構造一個IWallpaperEngineWarapper例項

    new IWallpaperEngineWrapper(mTarget, conn, windowToken,

           windowType, isPreview, reqWidth, reqHeight);

}

顧名思義,在attach()方法中所建立的IWallpaperEngineWrapper將會建立並封裝Engine例項IWallpaperEngineWrapper繼承自IWallpaperEngine.Stub,因此它也支援跨Binder呼叫。在隨後的程式碼分析中可知,它將會被傳遞給WallpaperManagerService,作為WallpaperManagerService與Engine進行通訊的橋樑。

另外需要注意的是,attach()方法的實現非常奇怪,它直接建立一個例項但是並沒有將這個例項賦值給某一個成員變數,在attach()方法結束時豈不是會被垃圾回收?不難想到,【在IWallpaperEngineWrapper的建構函式】中一定有些動作可以使得這個例項不被釋放。參考其實現:

[WallpaperService.java-->IWallpaperEngineWrapper.IWallpaperEngineWrapper()]

IWallpaperEngineWrapper(WallpaperService context,

       IWallpaperConnection conn, IBinder windowToken,

        intwindowType, boolean isPreview, int reqWidth, int reqHeight) {

    /* 建立一個HandlerCaller。

      HandlerCaller是Handler的一個封裝,而它與Handler的區別是額外提供了

      一個executeOrSendMessage()方法。當開發者在HandlerCaller所在的執行緒

      執行此方法時會使得訊息的處理函式立刻得到執行,在其他執行緒中執行此方法的效果

      則與Handler.sendMessage()別無二致。除非閱讀程式碼時遇到這個方法,讀者

      只需要將其理解為Handler即可。

      注意意通過其建構函式的引數可知HandlerCaller儲存了IWallpaperEngineWrapper的例項 */

    mCaller= new HandlerCaller(context,

           mCallbackLooper != null

                   ? mCallbackLooper : context.getMainLooper(),

           this);

    // 將WallpaperManagerService所提供的引數儲存下來

   mConnection = conn; // conn即是WallpaperManagerService中的WallpaperConnection

   mWindowToken = windowToken;

   mWindowType = windowType;

   mIsPreview = isPreview;

   mReqWidth = reqWidth;

   mReqHeight = reqHeight;

    // 傳送DO_ATTACH訊息。後續的流程轉到DO_ATTACH訊息的處理中進行

    Messagemsg = mCaller.obtainMessage(DO_ATTACH);

   mCaller.sendMessage(msg);

}

注意 在這裡貌似並沒有儲存新建的IWallpaperEngineWrapper例項,它豈不是有可能在DO_ATTACH訊息執行前就被Java的垃圾回收機制回收了?其實不是這樣。HandlerCaller的建構函式以及最後的sendMessage()操作使得這個IWallpaperEngineWrapper的例項得以堅持到DO_ATTACH訊息可以得到處理的時刻。sendMessage()方法的呼叫使得Message被目標執行緒的MessageQueue引用,並且對應的Handler被Message引用,而這個Handler是HandlerCaller的內部類,因此在Handler中有一個隱式的指向HandlerCaller的引用,最後在HandlerCaller中又存在著IWallpaperEngineWrapper的引用因此IWallpaperEngineWrapper間接地被HandlerCaller所線上程的MessageQueue所引用著,因此在完成DO_ATTACH訊息的處理之前,IWallpaperEngineWrapper並不會被回收。雖然這是建立在對Java引用以及Handler工作原理的深刻理解之上所完成的精妙實現,但是它確實已經接近危險的邊緣了。

在這裡所建立的mCaller具有十分重要的地位。它是一個重要的執行緒排程器,所有桌布相關的操作都會以訊息的形式傳送給mCaller,然後在IWallpaperEngineWrapper的executeMessage()方法中得到處理,從而這些操作轉移到mCaller所在的執行緒上進行(如桌布繪製、事件處理等)。可以說mCaller的執行緒就是桌布的工作執行緒。預設情況下這個mCaller執行在桌布服務的主執行緒上即context.getMainLooper()。不過當WallpaperService.mCallbackLooper不為null時會執行在mCallbackLooper所在的執行緒。mCaller執行在桌布服務的主執行緒上聽起來十分合理,然而提供手段以允許其執行在其他執行緒的做法卻有些意外。其實這是為了滿足一種特殊的需求,以ImageWallper桌布服務為例,它是SystemUI的一部分而SystemUI的主執行緒主要用來作為狀態列、導航欄的管理與繪製的場所,換句話說其主執行緒的工作已經比較繁重了。因此ImageWallpaper可以通過這一手段將桌布的工作轉移到另外一個執行緒中進行。不過因為這一機制可能帶來同步上的問題,因此在Android 4.4及後續版本中被廢除了。

接下來分析DO_ATTACH訊息的處理:

[WallpaperService.java-->IWallpaperEngineWrapper.executeMessage()]

public void executeMessage(Message message) {

    switch(message.what) {

        case DO_ATTACH: {

           try {

               /* ① 把IWallpaperEngineWrapper例項傳遞給WallpaperConnection進行儲存。

儲存在WallpaperConnection.mEngine成員之中

                至此這個例項便名花有主,再也不用擔心被回收了,而且WallpaperManagerService

                還可以通過它與實際的Engine進行通訊 */

               mConnection.attachEngine(this);

           } catch (RemoteException e) {}

           /* ② 通過onCreateEngine()方法建立一個Engine。

             onCreateEngine()是定義在WallpaperService中的一個抽象方法。

             WallpaperService的實現者需要根據自己的需要返回一個自定義的Engine的子類 */

           Engine engine = onCreateEngine();

           mEngine = engine;

           /* ③ 將新建的Engine新增到WallpaperService.mActiveEngines列表中。

             讀者可能會比較奇怪,為什麼是列表?難道一個Wallpaper可能會有多個Engine麼?

            這個奇怪之處還是桌布預覽所引入的。當桌布A已經被設定為當前桌布之時,系統中會存

            在一個它所對應的WallpaperService,以及在其內部會存在一個Engine。

            此時當LivePicker或其他桌布管理工具預覽桌布A時,它所對應的WallpaperService

            仍然只有一個,但是在其內部會變成兩個Engine。

            這一現象更能說明,WallpaperService僅僅是提供桌布執行的場所,而Engine才是真正

            的桌布的實現 */

           mActiveEngines.add(engine);

           // ④ 最後engine.attach()將會完成視窗的建立、第一幀的繪製等工作

            engine.attach(this);

           return;

        }

    }

}

正如前文所述,作為擁有跨Binder呼叫的IWallpaperEngineWrapper通過attachEngine()方法將自己傳遞給了WallpaperConnection,後者將其儲存在WallpaperConnection.mEngine成員之中。從此之後,WallpaperManagerService便可以通過WallpaperConnection.mEngine與桌布服務程序中的IWallpaperEngineWrapper進行通訊,而IWallpaperEngineWrapper進一步將來自WallpaperManagerService中的請求或設定轉發給Engine物件,從而實現了WallpaperManagerService對桌布的控制。

到目前為止,WallpaperManagerService與桌布服務之間已經出現了三個用於跨Binder通訊的物件。它們分別是:

·  IWallpaperService,實現在桌布服務程序之中,它所提供的唯一的方法attach()用於在桌布服務啟動後接收視窗建立所需的資訊(Token等等),或者說為了完成桌布的初始化工作。除此之外IWallpaperService不負責任何功能,WallpaperManagerService對桌布進行的請求與設定都交由在attach()的過程中所建立的IWallpaperEngineWrapper例項完成。

·  WallpaperConnection,實現在WallpaperManagerService中,並通過IWallpaperService.attach()方法傳遞給了IWallpaperEngineWrapper。桌布服務通過WallpaperConnection的attachEngine()方法將IWallpaperEngineWrapper例項傳遞給WallpaperManagerService進行儲存。另外桌布服務還通過它的engineShown()方法將桌布顯示完成的事件通知給WallpaperManagerService。

·  IWallpaperEngineWrapper,實現在桌布程序中。Engine例項是桌布實現的核心所在。作為Engine例項的封裝者,它是WallpaperManagerService對Engine進行請求或設定的唯一介面。

總體來說,IWallpaperService與WallpaperConnection主要服務於桌布的建立階段,而IWallpaperEngineWrapper則用於在桌布的執行階段對Engine進行操作與設定。

說明 按照常規的思想來推斷,WallpaperManagerService與WallpaperService之間應該僅僅需要IWallpaperService提供介面對桌布進行操作與設定。為什麼要增加一個IWallpaperEngineWrapper呢?這得從WallpaperService與Engine之間的關係說起。IWallpaperService在WallpaperManagerService看來表示的是WallpaperService,而IWallpaperEngineWrapper則表示的是Engine。WallpaperService是Engine執行的容器,因此它所提供的唯一的方法attach()用來在WallpaperService中建立新的Engine例項(由建立一個IWallpaperEngineWrapper例項來完成)。Engine則是桌布的具體實現,因此IWallpaperEngineWrapper所提供的方法用來對桌布進行操作與設定。從這個意義上來講IWallpaperService與IWallpaperEngineWrapper的同時存在是合理的。另外,將IWallpaperService與IWallpaperEngineWrapper分開還有著簡化實現的意義。從DO_ATTACH訊息的處理過程可知,WallpaperService中可以同時執行多個Engine例項。而WallpaperManagerService或LivePicker所關心的只是某一個Engine,而不是WallpaperService中的所有Engine,因此相對於使用IWallpaperService的介面時必須在引數中指明所需要操作的Engine,直接操作IWallpaperEngineWrapper更加簡潔直接。

Engine建立完畢之後會通過Engine.attach()方法完成Engine的初始化工作。參考其程式碼:

[WallpaperService.java-->Engine.attach()]

void attach(IWallpaperEngineWrapper wrapper) {

    ......

    // 儲存必要的資訊

   mIWallpaperEngine = wrapper;

    mCaller= wrapper.mCaller;

   mConnection = wrapper.mConnection;

   mWindowToken = wrapper.mWindowToken;

    /* ① mSurfaceHolder是一個BaseSurfaceHolder型別的內部類的例項。

      Engine對其進行了簡單的定製。開發者可以通過mSurfaceHolder定製所需要的Surface型別 */

   mSurfaceHolder.setSizeFromLayout();

   mInitializing = true;

    // 獲取WindowSession,用於與WMS進行通訊

    mSession= WindowManagerGlobal.getWindowSession(getMainLooper());

    //mWindow是IWindow的實現,視窗建立之後它將用於接收來自WMS的回撥

   mWindow.setSession(mSession);

    //Engine需要監聽螢幕狀態。這是為了保證在螢幕關閉之後,動態桌布可以停止動畫的渲染以節省電量

   mScreenOn =

           ((PowerManager)getSystemService(Context.POWER_SERVICE)).isScreenOn();

   IntentFilter filter = new IntentFilte r();

    filter.addAction(Intent.ACTION_SCREEN_ON);

   filter.addAction(Intent.ACTION_SCREEN_OFF);

   registerReceiver(mReceiver, filter);

    /* ② 呼叫Engine.onCreate()。

      Engine的子類往往需要重寫此方法以修改mSurfaceHolder的屬性,如畫素格式,尺寸等。

      注意此時  尚未建立視窗,在這裡所設定的SurfaceHolder的屬性將會在建立視窗時生效 */

   onCreate(mSurfaceHolder);

   mInitializing = false;

   mReportedVisible = false;

    /* ③ 最後updateSurface將會根據SurfaceHolder的屬性建立視窗以及Surface,並進行

      桌布的第一次繪製 */

   updateSurface(false, false, false);

}

Engine.attach()方法執行的結束標誌著桌布啟動工作的完成,至此在最後的updateSurface()方法結束之後新的桌布便顯示出來了。

(4)桌布的建立流程

可見,桌布的建立過程比較複雜。在這個過程中存在著多個Binder物件之間的互相呼叫。因此有必要對此過程進行一個簡單的整理:

·  首先,桌布管理程式(如LivePicker)呼叫IWallpaperManager.setWallpaperComponent()要求WallpaperManagerService設定指定的桌布

·  WallpaperManagerService通過呼叫bindWallpaperComponentLocked()將給定的桌布服務啟動起來。同時舊有的桌布服務會被終止。

·  WallpaperManagerService成功連線桌布服務後,呼叫桌布服務的attach()方法將視窗令牌等引數交給桌布服務。

·  桌布服務響應attach()的呼叫,建立一個Engine。

·  Engine的updateSurface()方法將會建立桌布視窗及Surface,並進行桌布的繪製。

而在這個過程中,WallpaperManagerService中存在如下重要的資料結構:

·  WallpaperInfo儲存了動態桌布的開發者、縮圖與描述資訊。這個資料結構創建於WallpaperManagerService.bindWallpaperComponentLocked()方法,其內容來自於桌布所在應用程式的AndroidManifest.xml中名為android.service.wallpaper的meta-data。

·  WallpaperConnection它不僅僅是桌布服務與WallpaperManagerService進行通訊的渠道,它同時也儲存了與桌布服務相關的重要的執行時資訊,如IWallpaperService、IWallpaperEngineWrapper、WallpaperInfo以及用於建立視窗所需的視窗令牌。WallpaperConnection創建於WallpaperManagerService.bindWallpaperComponentLocked()方法。

·  WallpaperData它儲存了一個桌布在WallpaperManagerService中可能用到的所有資訊,包括桌布服務的ComponentName,WallpaperConnection,桌布服務的啟動時間等。WallpaperData被儲存在一個名為mWallpaperMap的SparseArray中,而且裝置中每一個使用者都會擁有一個固定的WallpaperData例項。當前使用者進行桌布切換時會更新WallpaperData的內容,而不是新建一個WallpaperData例項另外,WallpaperData中還儲存了與靜態桌布相關的一些資訊,關於靜態桌布的內容將在8.3節進行介紹。

桌布的建立過程同時體現了桌布服務與WallpaperManagerService之間的關係,如圖8-1所示。

圖 8 - 1 桌布服務與WallpaperManagerService之間的關係

---------------------