深入理解Android卷III 第7章 深入理解SystemUI (節選)
多謝華章圖書與鄧凡平先生的幫助,《深入理解Android卷III〉終於上市了。歡迎大家來這裡一起探討文中的問題或與Android系統有關的任何話題。
第7章深入理解SystemUI
本章主要內容:
-
探討狀態列與導航欄的啟動過程
-
介紹狀態列中的通知資訊、系統狀態圖示等資訊的管理與顯示原理
-
介紹導航欄中的虛擬按鍵、SearchPanel的工作原理
-
介紹SystemUIVisibility
本章涉及的原始碼檔名及位置:
-
SystemServer.java
frameworks/base/services/java/com/android/server/SystemServer.java
-
SystemUIService.java
frameworks/base/packages/SystemUI/src/com/android/systemui/SystemUIService.java
-
PhoneWindowManager.java
frameworks/base/policy/src/com/android/internal/policy/impl/PhoneWindowManager.java
-
PhoneStatusBar.java
frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
-
BaseStatusBar.java
frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java
-
StatusBarManager.java
frameworks/base/core/java/android/app/StatusBarManager.java
-
StatusBarManagerService.java
frameworks/base/services/java/com/android/server/StatusBarManagerService.java
-
NotificationManager.java
frameworks/base/core/java/android/app/NotificationManager.java
-
NotificationManagerService.java
frameworks/base/services/java/com/android/server/NotificationManagerService.java
-
KeyButtonView.java
frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyButtonView.java
-
NavigationBarView.java
frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java
-
DelegateViewHelper.java
frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/DelegateViewHelper.java
-
SearchPanelView.java
frameworks/base/packages/SystemUI/src/com/android/systemui/SearchPanelView.java
-
PhoneWindow.java
frameworks/base/policy/src/com/android/internal/policy/impl/PhoneWindow.java
-
InputMethodService.java
frameworks/base/core/java/android/inputmethodservice/InputMethodService.java
-
View.java
frameworks/base/core/java/android/view/View.java
-
ViewRootImpl.java
frameworks/base/core/java/android/view/ViewRootImpl.java
-
WindowManagerService.java
frameworks/base/services/java/com/android/server/wm/WindowManagerService.java
7.1初識SystemUI
顧名思義,SystemUI是為使用者提供系統級別的資訊顯示與互動的一套UI元件,因此它所實現的功能包羅永珍。螢幕頂端的狀態列、底部的導航欄、圖片桌布以及RecentPanel(近期使用的APP列表)都屬於SystemUI的範疇。SystemUI中還有一個名為TakeScreenshotService的服務,用於在使用者按下音量下鍵與電源鍵時進行截圖操作。在第5章曾介紹了PhoneWindowManager監聽這一組合鍵的機制,當它捕捉到這一組合鍵時便會向TakeScreenShotService傳送請求從而完成截圖操作。SystemUI還提供了PowerUI和RingtonePlayer兩個服務。前者負責監控系統的剩餘電量並在必要時為使用者顯示低電警告,後者則依託AudioService為向其他應用程式提供播放鈴聲的功能。SystemUI的博大不止如此,讀者可以通過檢視其AndroidManifest.xml來了解它所實現的其他功能。本章將著重介紹其中最重要的兩個功能的實現:狀態列和導航欄。
7.1.1 SystemUIService的啟動
儘管SystemUI的表現形式與普通的Android應用程式大相徑庭,但它卻是以一個APK的形式存在於系統之中,即它與普通的Android應用程式並沒有本質上的區別。無非是通過Android四大元件中的Activity、Service、BroadcastReceiver接受外界的請求並執行相關的操作,只不過它們所接受到的請求主要來自各個系統服務而已。
SystemUI包羅永珍,並且大部分功能之間相互獨立,比如RecentPanel、TakeScreenshotService等均是按需啟動,並在完成其既定任務後退出,這與普通的Activity以及Service別無二致。比較特殊的是狀態列、導航欄等元件的啟動方式。它們運行於一個稱之為SystemUIService的一個Service之中。因此討論狀態列與導航欄的啟動過程其實就是SystemUIService的啟動過程。
1.SystemUIService的啟動時機
那麼SystemUIService在何時由誰啟動的呢?作為一個系統級別的UI元件,自然要在系統的啟動過程中來尋找答案了。
在負責啟動各種系統服務的ServerThread中,當核心系統服務啟動完成後ServerThread會通過呼叫ActivityManagerService.systemReady()方法通知AMS系統已經就緒。這個systemReady()擁有一個名為goingCallback的Runnable例項作為引數。顧名思義,當AMS完成對systemReady()的處理後將會回撥這一Runnable的run()方法。而在這一run()方法中可以找到SystemUI的身影:
[SystemServer.java-->ServerThread]
ActivityManagerService.self().systemReady(newRunnable() {
publicvoid run() {
// 呼叫startSystemUi()
if(!headless) startSystemUi(contextF);
......
}
}
進一步地,在startSystemUI()方法中:
[SystemServer.java-->ServerThread.startSystemUi()]
static final void startSystemUi(Context context) {
Intentintent = new Intent();
// 設定SystemUIService作為啟動目標
intent.setComponent(new ComponentName("com.android.systemui",
"com.android.systemui.SystemUIService"));
// 啟動SystemUIService
context.startServiceAsUser(intent, UserHandle.OWNER);
}
可見,當核心的系統服務啟動完畢後,ServerThread通過Context.startServiceAsUser()方法完成了SystemUIService的啟動。
2.SystemUIService的建立
參考SystemUIService的onCreate()的實現:
[SystemUIService.java-->SystemUIService.onCreate()]
/*①SERVICES陣列定義了運行於SystemUIService之中的子服務列表。當SystemUIService服務啟動
時將會依次啟動列表中所儲存的子服務 */
final Object[] SERVICES = new Object[] {
0,// 0號元素儲存的其實是一個字串資源號,這個字串資源儲存了實現了狀態列/導航欄的類名
com.android.systemui.power.PowerUI.class,
com.android.systemui.media.RingtonePlayer.class,
};
public void onCreate() {
......
IWindowManager wm = WindowManagerGlobal.getWindowManagerService();
try {
/* ②根據IWindowManager.hasSystemNavBar()的返回值選擇一個合適的
狀態列與導航欄的實現*/
SERVICES[0] = wm.hasSystemNavBar()
? R.string.config_systemBarComponent
: R.string.config_statusBarComponent;
} catch(RemoteException e) {......}
finalint N = SERVICES.length;
//mServices陣列中儲存了子服務的例項
mServices = new SystemUI[N];
for (inti=0; i<N; i++) {
Class cl = chooseClass(SERVICES[i]);
try{
//③例項化子服務並將其儲存在mServices陣列中
mServices[i] = (SystemUI)cl.newInstance();
}catch (IllegalAccessException ex) {......}
// ④設定Context,並通過呼叫其start()方法執行它
mServices[i].mContext = this;
mServices[i].start();
}
}
除了onCreate()方法之外,SystemUIService沒有其他有意義的程式碼了。顯而易見,SystemUIService是一個容器。在其啟動時,將會逐個例項化定義在SERVICIES列表中的繼承自SystemUI抽象類的子服務。在呼叫了子服務的start()方法之後,SystemUIService便不再做任何其他的事情,任由各個子服務自行執行。而狀態列導航欄則是這些子服務中的一個。
值得注意的是,onCreate()方法根據IWindowManager.hasSystemNavBar()方法的返回值為狀態列/導航欄選擇了不同的實現。進行這一選擇的原因為了能夠在大尺寸的裝置中更有效地利用螢幕空間。在小螢幕裝置如手機中,由於螢幕寬度有限,Android採取了狀態列與導航欄分離的佈局方案,也就是說導航欄與狀態列佔用了更多的垂直空間,使得導航欄的虛擬按鍵尺寸足夠大以及狀態列的資訊量足夠多。而在大螢幕裝置如平板電腦中,由於螢幕寬度比較大,足以在一個螢幕寬度中同時顯示足夠大的虛擬按鍵以及足夠多的狀態列資訊量,此時可以選擇將狀態列與導航欄功能整合在一起成為系統欄作為大螢幕下的佈局方案,以節省對垂直空間的佔用。
hasSystemNavBar()的返回值取決於PhoneWindowManager.mHasSystemNavBar成員的取值。因此在PhoneWindowManager.setInitialDisplaySize()方法中可以得知Android在兩種佈局方案中進行選擇的策略。
[PhoneWindowManager.java-->PhoneWindowManager.setInitialDisplaySize()]
public void setInitialDisplaySize(Display display,int width
, intheight, int density) {
......
// ① 計算螢幕短邊的DP寬度
intshortSizeDp = shortSize * DisplayMetrics.DENSITY_DEFAULT / density;
// ②螢幕寬度在720dp以內時,使用分離的佈局方案
if(shortSizeDp < 600) {
mHasSystemNavBar= false;
mNavigationBarCanMove = true;
} elseif (shortSizeDp < 720) {
mHasSystemNavBar = false;
mNavigationBarCanMove = false;
}
......
}
在SystemUI中,分離佈局方案的實現者是PhoneStatusBar,而整合佈局方案的實現者則是TabletStatusBar。二者的本質功能是一致的,即提供虛擬按鍵、顯示通知資訊等,區別僅在於佈局的不同、以及由此所衍生出的定製行為而已。因此不難想到,它們是從同一個父類中繼承出來的。這一父類的名字是BaseStatusBar。本章將主要介紹PhoneStatusBar的實現,讀者可以類比地對TabletStatusBar進行研究。
7.1.2狀態列與導航欄的建立
如7.1.1節所述,狀態列與導航欄的啟動由其PhoneStatusBar.start()完成。參考其實現:
[PhoneStatusBar.java-->PhoneStatusBar.start()]
public void start() {
......
// ①呼叫父類BaseStatusBar的start()方法進行初始化。
super.start();
// 建立導航欄的視窗
addNavigationBar();
// ②建立PhoneStatusBarPolicy。PhoneStatusBarPolicy定義了系統通知圖示的設定策略
mIconPolicy= new PhoneStatusBarPolicy(mContext);
}
參考BaseStatusBar.start()的實現,這段程式碼比較長,並且涉及到了本章隨後會詳細介紹的內容。因此倘若讀者閱讀起來比較吃力可以僅關注那三個關鍵步驟。在完成本章的學習之後再回過頭來閱讀這部分程式碼便會發現十分簡單了。
[BaseStatusBar-->BaseStatusBar.start()]
public void start() {
/* 由於狀態列的視窗不屬於任何一個Activity,所以需要使用第6章所介紹的WindowManager
進行視窗的建立 */
mWindowManager = (WindowManager)mContext
.getSystemService(Context.WINDOW_SERVICE);
/* 在第4章介紹視窗的佈局時曾經提到狀態列的存在對窗口布局有著重要的影響。因此狀態列中
所發生的變化有必要通知給WMS */
mWindowManagerService = WindowManagerGlobal.getWindowManagerService();
......
/*mProvisioningOberver是一個ContentObserver。
它負責監聽Settings.Global.DEVICE_PROVISIONED設定的變化。這一設定表示此裝置是否已經
歸屬於某一個使用者。比如當用戶開啟一個新購買的裝置時,初始化設定嚮導將會引導使用者閱讀使用條款、
設定帳戶等一系列的初始化操作。在初始化設定嚮導完成之前,
Settings.Global.DEVICE_PROVISIONED的值為false,表示這臺裝置並未歸屬於某
一個使用者。
當裝置並未歸屬於某以使用者時,狀態列會禁用一些功能以避免資訊的洩露。mProvisioningObserver
即是用來監聽裝置歸屬狀態的變化,以禁用或啟用某些功能 */
mProvisioningObserver.onChange(false); // set up
mContext.getContentResolver().registerContentObserver(
Settings.Global.getUriFor(Settings.Global.DEVICE_PROVISIONED), true,
mProvisioningObserver);
/* ①獲取IStatusBarService的例項。IStatusBarService是一個系統服務,由ServerThread
啟動並常駐system_server程序中。IStatusBarService為那些對狀態列感興趣的其他系統服務定
義了一系列API,然而對SystemUI而言,它更像是一個客戶端。因為IStatusBarService會將操作
狀態列的請求傳送給SystemUI,並由後者完成請求 */
mBarService = IStatusBarService.Stub.asInterface(
ServiceManager.getService(Context.STATUS_BAR_SERVICE));
/* 隨後BaseStatusBar將自己註冊到IStatusBarService之中。以此宣告本例項才是狀態列的真正
實現者,IStatusBarService會將其所接受到的請求轉發給本例項。
“天有不測風雲”,SystemUI難免會因為某些原因使得其意外終止。而狀態列中所顯示的資訊並不屬於狀態
欄自己,而是屬於其他的應用程式或是其他的系統服務。因此當SystemUI重新啟動時,便需要恢復其
終止前所顯示的資訊以避免資訊的丟失。為此,IStatusBarService中儲存了所有的需要狀態列進行顯
示的資訊的副本,並在新的狀態列例項啟動後,這些副本將會伴隨著註冊的過程傳遞給狀態列並進行顯示,
從而避免了資訊的丟失。
從程式碼分析的角度來看,這一從IstatusBarService中取回資訊副本的過程正好完整地體現了狀態列
所能顯示的資訊的型別*/
/*iconList是向IStatusBarService進行註冊的引數之一。它儲存了用於顯示在狀態列的系統狀態
區中的狀態圖示列表。在完成註冊之後,IStatusBarService將會在其中填充兩個陣列,一個字串
陣列用於表示狀態的名稱,一個StatusBarIcon型別的陣列用於儲存需要顯示的圖示資源。
關於系統狀態區的工作原理將在7.2.3節介紹*/
StatusBarIconList iconList = new StatusBarIconList();
/*notificationKeys和StatusBarNotification則儲存了需要顯示在狀態列的通知區中通知資訊。
前者儲存了一個用Binder表示的通知傳送者的ID列表。而notifications則儲存了通知列表。二者
通過索引號一一對應。關於通知的工作原理將在7.2.2節介紹 */
ArrayList<IBinder> notificationKeys = newArrayList<IBinder>();
ArrayList<StatusBarNotification> notifications
= newArrayList<StatusBarNotification>();
/*mCommandQueue是CommandQueue類的一個例項。CommandQueue繼承自IStatusBar.Stub。
因此它是IStatusBar的Bn端。在完成註冊後,這一Binder物件的Bp端將會儲存在
IStatusBarService之中。因此它是IStatusBarService與BaseStatusBar進行通訊的橋樑。
*/
mCommandQueue = new CommandQueue(this, iconList);
/*switches則儲存了一些雜項:禁用功能列表,SystemUIVisiblity,是否在導航欄中顯示虛擬的
選單鍵,輸入法視窗是否可見、輸入法視窗是否消費BACK鍵、是否接入了實體鍵盤、實體鍵盤是否被啟用。
在後文中將會介紹它們的具體影響 */
int[]switches = new int[7];
ArrayList<IBinder> binders = new ArrayList<IBinder>();
try {
// ②向IStatusBarServie進行註冊,並獲取所有儲存在IStatusBarService中的資訊副本
mBarService.registerStatusBar(mCommandQueue, iconList,
notificationKeys,notifications,
switches, binders);
} catch(RemoteException ex) {......}
// ③