SystemUI 開發(fā)總結(jié)
| 目錄-
SystemUI 有哪內(nèi)容
應(yīng)用窗口的 flag 是如何影響狀態(tài)欄的戴已?
后續(xù):SystemUI 能否脫離對系統(tǒng)源碼依賴本橙?
SystemUI 有哪內(nèi)容
從表面上看扳躬, 我們看到的狀態(tài)欄、通知欄甚亭、下拉菜單贷币、導(dǎo)航欄、鎖屏亏狰、最近任務(wù)役纹、低電提示等系統(tǒng)頁面都是 SystemUI 的。SystemUI骚揍,在源碼目錄中位于: framework/base/packages 目錄下字管, 可見 SystemUI 和 framework 是關(guān)聯(lián)的, SystemUI 依賴了很多內(nèi)部 API 信不, 系統(tǒng)資源嘲叔, SystemUI 編譯是要依賴系統(tǒng)源碼的。
SystemUI 也是一個應(yīng)用抽活,不過這個應(yīng)用特殊之處在于他沒有啟動圖標(biāo)硫戈、也沒有入口 Activity 。他的入口程序是一個服務(wù):SystemUIService下硕。 這個服務(wù)會被系統(tǒng)服務(wù)拉起來丁逝, 這個服務(wù)起來汁胆, SystemUI 應(yīng)用進程就創(chuàng)建起來了,具體啟動過程后面會分析霜幼。除了 SystemUIService 嫩码, SystemUI 還有很多服務(wù), 例如: 負(fù)責(zé)鎖屏的KeyguardService罪既、負(fù)責(zé)最近任務(wù)的 RecentsSystemUserService铸题、負(fù)責(zé)壁紙的 ImageWallpaper 、負(fù)責(zé)截屏的TakeScreenshotService 等琢感。
系統(tǒng)移植 丢间、UI 改造
如果要做系統(tǒng)移植, SystemUI 改造這塊的資料還是挺少驹针,大部分情況下都是啃源碼烘挫,連蒙帶猜的修改,然后再編譯出來驗證柬甥。通常我們會從布局著手看看哪個布局長得像就著手去改饮六,不過這塊完全是可以沉淀一下經(jīng)驗出來讓后人去節(jié)省時間的。這里我也不再贅述了苛蒲, 有人已經(jīng)梳理過了喜滨, 我借花獻佛吧:https://blog.csdn.net/azhengye/article/details/50419409。
架構(gòu)關(guān)系
在系統(tǒng)服務(wù)中撤防,有一個服務(wù)是專門為 SystemUI 的狀態(tài)欄服務(wù)的, 這個服務(wù)就是 StatusbarManagerService (簡稱:SMS)棒口,和這個服務(wù)關(guān)系比較密切的服務(wù)是 WindowManagerService(簡稱:WMS)寄月, SMS 主要管控的是狀態(tài)欄、導(dǎo)航欄无牵, 例如:我們可以設(shè)置全屏漾肮、沉浸式狀態(tài)欄都是 SMS 在起作用。
初次開發(fā) SystemUI 有哪些彎路 (環(huán)境上的坑)
失敗方案1
IDE獨立編譯 SystemUI 茎毁, 把 SystemUI 所依賴的系統(tǒng) jar 都拷貝帶 IDE 下克懊,使用 provided 方式依賴。 在 6.0 以下版本還勉強可行 七蜘, 8.0 以后就基本不可能了谭溉, 8.0 以后 SystemUI 合入了鎖屏模塊,依賴了太多的系統(tǒng)資源橡卤, 編譯不過是一個問題扮念, 就算編譯過了, 所依賴的系統(tǒng)資源 ID 也會不一致碧库。 經(jīng)過 1~2 兩天的嘗試柜与, 這個方案失敗了巧勤。
失敗方案2
使用 Google 源碼編譯, 然后在源碼中修改 SystemUI , 將編譯的 SystemUI 安裝到 MTK 系統(tǒng)的版子上弄匕。 發(fā)現(xiàn)安裝到 MTK 的板子以后跑不起來颅悉, 原因是某些服務(wù)啟動不了, 同時也存在資源 ID 不一致的問題迁匠。 經(jīng)過 2~3 天的這條這個方案失敗了剩瓶。
最終方案
最終不得不麻煩系統(tǒng)同學(xué), 幫忙提供源碼: 在 MTK 源碼中編譯柒瓣。
為了提高效率儒搭, 使用一臺昨晚編譯機, 另一臺作為編輯機芙贫, 通過 ssh 搭建通道配合完成開發(fā)搂鲫、編譯、安裝三個流程磺平。
SystemUI 是如何啟動的魂仍?
前面介紹過 SystemUIService 是 SystemUI 的入庫程序。 SystemUIService 是在服務(wù)進程中啟動的拣挪,我們來看下源碼:
SystemServer.java 中 SystemServer 是 zygote 進程起來的啟動的第一個服務(wù)擦酌, 然后在這個服務(wù)的 run 方法方法中會一次啟動 Android 系統(tǒng)服務(wù)。
private void run() {
// ... 省略一堆代碼
startBootstrapServices();
startCoreServices();
startOtherServices();
// ... 省略一堆代碼
}
其中 AMS 是在 startOtherServices() 這個方法中啟動的:
private void startOtherServices() {
// ... 省略一堆代碼
mActivityManagerService = mSystemServiceManager.startService(
ActivityManagerService.Lifecycle.class).getService();
// ... 省略一堆代碼
mActivityManagerService.systemReady(() -> {
// ... 省略一堆代碼
try {
startSystemUi(context, windowManagerF);
} catch (Throwable e) {
reportWtf("starting System UI", e);
}
// ... 省略一堆代碼
});
}
在 AMS 啟動啟動完成之后菠劝,會回調(diào)一個 systemReady() 傳遞進去的方法赊舶, 在其中調(diào)用 startSystemUi() 方法啟動了 SystemUI :
static final void startSystemUi(Context context, WindowManagerService windowManager) {
Intent intent = new Intent();
intent.setComponent(new ComponentName("com.android.systemui",
"com.android.systemui.SystemUIService"));
intent.addFlags(Intent.FLAG_DEBUG_TRIAGED_MISSING);
//Slog.d(TAG, "Starting service: " + intent);
context.startServiceAsUser(intent, UserHandle.SYSTEM);
windowManager.onSystemUiStarted();
}
SystemUIService 邏輯也是相當(dāng)簡單, 啟動之后主要調(diào)用 SystemUIApplication 的 startServicesIfNeeded()|
@Override
public void onCreate() {
super.onCreate();
((SystemUIApplication)
getApplication()).startServicesIfNeeded();
// For debugging RescueParty
if (Build.IS_DEBUGGABLE &&
SystemProperties.getBoolean("debug.crash_sysui", false)) {
throw new RuntimeException();
}
}
在 SystemUIApplication 中啟動了 SystemUI 的各個 UI 模塊:
public void startServicesIfNeeded() {
startServicesIfNeeded(SERVICES);
}
例如 : SERVICES 包含了狀態(tài)欄赶诊、電量笼平、畫中畫、 鎖屏等舔痪。
通知視圖是如何夸進程顯示的寓调?
跨進程通訊的基礎(chǔ)是 IPC ,通知服務(wù)(NotificationManagerService, 簡稱 NMS)也不離開 IPC 锄码,核心架構(gòu)還是 IPC 架構(gòu)夺英。
消息通道
- 應(yīng)用做作為通知的發(fā)送端扯罐, 需要調(diào)用 NMS 奈籽,發(fā)通知。例如:
String channelId = "channel_1";
String tag = "ailabs";
int id = 10086;
int importance = NotificationManager.IMPORTANCE_LOW;
NotificationChannel channel = new NotificationChannel(channelId, "123", importance);
NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
manager.createNotificationChannel(channel);
Notification notification = new Notification.Builder(MainActivity.this, channelId)
.setCategory(Notification.CATEGORY_MESSAGE)
.setSmallIcon(R.mipmap.ic_launcher)
.setContentTitle("This is a content title")
.setContentText("This is a content text")
.setAutoCancel(true)
.build();
// 通知欄要顯示的視圖布局
RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.layout_remoteviews);
notification.contentView = remoteViews;
manager.notify(tag, id , notification);
- SystemUI 作為通知的接收放需要注冊監(jiān)聽器 INotificationListener 是監(jiān)聽通通知的一個 AIDL 接口颖侄,
NotificationListenerService 是一個監(jiān)聽管理服務(wù)炬太,他的內(nèi)部類 NotificationListenerWrapper 實現(xiàn)了
INotificationListener 接口灸蟆。 例如:
/** @hide */
protected class NotificationListenerWrapper extends INotificationListener.Stub {
@Override
public void onNotificationPosted(IStatusBarNotificationHolder sbnHolder,
NotificationRankingUpdate update) {
// 接收通知
....
省略了很多代碼
}
@Override
public void onNotificationRemoved(IStatusBarNotificationHolder sbnHolder,
NotificationRankingUpdate update, NotificationStats stats, int reason) {
// 刪除通知
....
// 省略了很多代碼
}
這個通知監(jiān)聽需要向 NMS 注冊:
@SystemApi
public void registerAsSystemService(Context context, ComponentName componentName,
int currentUser) throws RemoteException {
if (mWrapper == null) {
mWrapper = new NotificationListenerWrapper();
}
mSystemContext = context;
INotificationManager noMan = getNotificationInterface();
mHandler = new MyHandler(context.getMainLooper());
mCurrentUser = currentUser;
noMan.registerListener(mWrapper, componentName, currentUser);
}
以上是 Android 為我們提供的通知接收管理服務(wù)類, SystemUI 有個NotificationListenerWithPlugins 類繼承了 NotificationListenerService
類。 并在 SystemUI 進程起來的時候調(diào)用 registerAsSystemService() 方法完成了注冊:
NotificationListenerWithPlugins mNotificationListener = new NotificationListenerWithPlugins();
mNotificationListener.registerAsSystemService();
這樣通道就建立起來了炒考。
消息傳遞過程可缚,大家可以按照這個思路器走讀源碼
<a name="13yage"></a>
RemoteViews
以上只是講解了應(yīng)用怎么把一個消息傳遞到 SystemUI , 理解 IPC 通訊的不難理解。 而神奇之處在于顯示的視圖布局明明是定義在一個應(yīng)用中斋枢,為何能跨進程顯示到 SystemUI 進程中呢帘靡?
發(fā)送通知, 傳遞的通知實體是 Notification 的實例瓤帚, Notification 實現(xiàn)了 Parcelable 接口描姚。 Notification 有個 RemoteViews 的成員變量
RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.layout_remoteviews); notification.contentView = remoteViews;
RemoteViews 也實現(xiàn)了 Parcelable 接口, 主要是封裝了通知欄要展示的視圖信息戈次, 例如轩勘, 應(yīng)用包名、布局ID怯邪。我們都知道實現(xiàn)了 Parcelable 這個接口就可以在 IPC 通道上夸進程傳遞绊寻。 RemoteView 支持的布局類型也是有限的,例如在 8.0 上僅支持如下類型:
- android.widget.AdapterViewFlipper
*android.widget.FrameLayout - android.widget.GridLayout
- android.widget.GridView
- android.widget.LinearLayout
- android.widget.ListView
- android.widget.RelativeLayout
- android.widget.StackView
- android.widget.ViewFlipper
RemoteView 攜帶了視圖信息悬秉, 進程間傳遞的并不是真實的視圖對象澄步, 而主要是布局的 id ,那么顯示在通知欄上的視圖對象又是如何創(chuàng)建出來的呢和泌?
通知視圖創(chuàng)建
在通知的接收端創(chuàng)建的村缸,上文說過 NotificationManagerService 內(nèi)部類 NotificationListenerWrapper 監(jiān)聽通知消息, 在收到消息之后就在里面解析消息武氓,并創(chuàng)建視圖了梯皿。
protected class NotificationListenerWrapper extends INotificationListener.Stub {
@Override
public void onNotificationPosted(IStatusBarNotificationHolder sbnHolder,
NotificationRankingUpdate update) {
StatusBarNotification sbn;
try {
sbn = sbnHolder.get();
} catch (RemoteException e) {
Log.w(TAG, "onNotificationPosted: Error receiving StatusBarNotification", e);
return;
}
try {
// convert icon metadata to legacy format for older clients
createLegacyIconExtras(sbn.getNotification());
// 創(chuàng)建視圖
maybePopulateRemoteViews(sbn.getNotification());
maybePopulatePeople(sbn.getNotification());
} catch (IllegalArgumentException e) {
// warn and drop corrupt notification
Log.w(TAG, "onNotificationPosted: can't rebuild notification from " +
sbn.getPackageName());
sbn = null;
}
// ... 省略代碼
}
@Override
public void onNotificationRemoved(IStatusBarNotificationHolder sbnHolder,
NotificationRankingUpdate update, NotificationStats stats, int reason) {
StatusBarNotification sbn;
//... 省略代碼
}
}
在 maybePopulateRemoteViews 這個方法中會去檢查布局是否要加載, **其實我們比較好奇的是布局資源在應(yīng)用進程中县恕,
SystemUI 如何加載遠程進程的布局資源索烹?**
有兩個關(guān)鍵的信息: 包名、布局ID弱睦。知道了包名 SystemUI 進程是有權(quán)限創(chuàng)建對應(yīng)包名的上下文對象的,進而可以拿到對應(yīng)應(yīng)用的
資源管理器渊额, 然后就可以加載布局資源創(chuàng)建對象了况木。 maybePopulateRemoteViews 方法跟蹤下去, 會走到 RemoteViews 的
private View inflateView(Context context, RemoteViews rv, ViewGroup parent) {
// RemoteViews may be built by an application installed in another
// user. So build a context that loads resources from that user but
// still returns the current users userId so settings like data / time formats
// are loaded without requiring cross user persmissions.
final Context contextForResources = getContextForResources(context);
Context inflationContext = new RemoteViewsContextWrapper(context, contextForResources);
// If mApplyThemeResId is not given, Theme.DeviceDefault will be used.
if (mApplyThemeResId != 0) {
inflationContext = new ContextThemeWrapper(inflationContext, mApplyThemeResId);
}
LayoutInflater inflater = (LayoutInflater)
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
// Clone inflater so we load resources from correct context and
// we don't add a filter to the static version returned by getSystemService.
inflater = inflater.cloneInContext(inflationContext);
inflater.setFilter(this);
View v = inflater.inflate(rv.getLayoutId(), parent, false);
v.setTagInternal(R.id.widget_frame, rv.getLayoutId());
return v;
}
其中 getContextForResources 中的 context 對象就是通過應(yīng)用包名創(chuàng)建的上下文對象旬迹,創(chuàng)建過程:
private static ApplicationInfo getApplicationInfo(String packageName, int userId) {
if (packageName == null) {
return null;
}
// Get the application for the passed in package and user.
Application application = ActivityThread.currentApplication();
if (application == null) {
throw new IllegalStateException("Cannot create remote views out of an aplication.");
}
ApplicationInfo applicationInfo = application.getApplicationInfo();
if (UserHandle.getUserId(applicationInfo.uid) != userId
|| !applicationInfo.packageName.equals(packageName)) {
try {
Context context = application.getBaseContext().createPackageContextAsUser(
packageName, 0, new UserHandle(userId));
applicationInfo = context.getApplicationInfo();
} catch (NameNotFoundException nnfe) {
throw new IllegalArgumentException("No such package " + packageName);
}
}
return applicationInfo;
}
只有 SystemUI 才能接收通知嗎火惊?
答案是否定的, 只要有權(quán)限注冊通知監(jiān)聽的應(yīng)用都可以奔垦。 具體權(quán)限是: <uses-permission android:name="android.permission.STATUS_BAR_SERVICE"/>
只要應(yīng)用有這個權(quán)限就可以注冊通知監(jiān)聽了屹耐, 這個權(quán)限只有系統(tǒng)應(yīng)用才能申請, 也就是說椿猎,只要是系統(tǒng)應(yīng)用都可以監(jiān)聽并顯示通知的惶岭。 可以寫一個簡單的 demo 測試一下:
一寿弱、 申請權(quán)限
<uses-permission android:name="android.permission.STATUS_BAR_SERVICE"/>
二、 在布局中定義一個容器來裝遠程通知視圖
...
<FrameLayout
android:layout_width="match_parent"
android:layout_height="92px"
android:id="@+id/notification">
</FrameLayout>
...
三按灶、注冊監(jiān)聽并處理通知顯示邏輯症革。
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final ViewGroup notificationContainer = findViewById(R.id.notification);
NotificationListenerService listenerService = new NotificationListenerService() {
@SuppressLint("LongLogTag")
@Override
public void onNotificationPosted(StatusBarNotification sbn) {
super.onNotificationPosted(sbn);
Log.d("NotificationListenerService", "onNotificationPosted" + sbn);
if (sbn.getNotification().contentView != null) {
View view = sbn.getNotification().contentView.apply(MainActivity.this, null);
notificationContainer.addView(view);
view.setVisibility(View.VISIBLE);
Log.d("NotificationListenerService", "add contentView");
}
if (sbn.getNotification().bigContentView != null) {
View view = sbn.getNotification().bigContentView.apply(MainActivity.this, null);
notificationContainer.addView(view);
view.setVisibility(View.VISIBLE);
Log.d("NotificationListenerService", "add bigContentView");
}
if (sbn.getNotification().headsUpContentView != null) {
sbn.getNotification().headsUpContentView.apply(MainActivity.this, null);
Log.d("NotificationListenerService", "add headsUpContentView");
}
}
@SuppressLint("LongLogTag")
@Override
public void onNotificationRemoved(StatusBarNotification sbn) {
super.onNotificationRemoved(sbn);
Log.d("NotificationListenerService", "onNotificationRemoved" + sbn);
}
@SuppressLint("LongLogTag")
@Override
public void onListenerConnected() {
super.onListenerConnected();
Log.d("NotificationListenerService", "onNotificationRemoved");
}
@Override
public void onListenerDisconnected() {
super.onListenerDisconnected();
}
};
// 調(diào)用注冊方法 registerAsSystemService 不是公開的 API 反射
try {
Method method =
NotificationListenerService.class.getMethod("registerAsSystemService", Context.class, ComponentName.class, int.class);
method.setAccessible(true);
method.invoke(listenerService, this,
new ComponentName(getPackageName(), getClass().getCanonicalName()),
-1);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
運行起來后,注冊成功鸯旁, 然后任意應(yīng)用發(fā)通知噪矛, 這里就能顯示出來了。
應(yīng)用窗口的 flag 是如何狀態(tài)欄铺罢?
在系統(tǒng)服務(wù)中艇挨,有一個服務(wù)是專門為 SystemUI 的狀態(tài)欄服務(wù)的, 這個服務(wù)就是 StatusbarManagerService (簡稱:SMS)韭赘,和這個服務(wù)關(guān)系比較密切的服務(wù)是 WindowManagerService(簡稱:WMS)缩滨, SMS 主要管控的是狀態(tài)欄、導(dǎo)航欄辞居, 例如:我們可以設(shè)置全屏楷怒、沉浸式狀態(tài)欄都是 SMS 在起作用。 我們看一下 window flag 是如何一步一步的影響系統(tǒng)狀態(tài)欄的瓦灶。
通常我們這樣添加窗口屬性鸠删,例如設(shè)置 flag 讓 SystemUI 狀態(tài)欄支持繪制背景:
Window window = activity.getWindow();
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
我們都知道 Android 系統(tǒng)為我們提供的 window 實現(xiàn)類是 PhoneWindow (不清楚的可以參考文章:http://www.reibang.com/p/b4c23dee9206), flag 其實被僅僅是 WindowManager.LayoutParams 的一個標(biāo)記而已贼陶。
public void setFlags(int flags, int mask) {
final WindowManager.LayoutParams attrs = getAttributes();
attrs.flags = (attrs.flags&~mask) | (flags&mask);
mForcedWindowFlags |= mask;
dispatchWindowAttributesChanged(attrs);
}
所有窗口的 View 和 LayoutParams 最終會被添加到 WindowManagerService 中刃泡,WindowManagerService 會記錄著窗口信息,包括 flag 屬性 碉怔。 (View烘贴、Window 和 ViewRootImpl 的關(guān)系:參考:http://www.reibang.com/p/47421ec56795)
每次窗口布局、焦點發(fā)生變化的時候撮胧,都會去重新計算當(dāng)前窗口的屬性桨踪, 包括 flag。
public int addWindow(Session session, IWindow client, int seq,
WindowManager.LayoutParams attrs, int viewVisibility, int displayId,
Rect outContentInsets, Rect outStableInsets, Rect outOutsets,
InputChannel outInputChannel) {
//...省略一堆代碼
// 計算屬性
mPolicy.adjustWindowParamsLw(win.mAttrs);
//...省略一堆代碼
updateFocusedWindowLocked(UPDATE_FOCUS_WILL_ASSIGN_LAYERS,
false /*updateInputWindows*/);
}
mPolicy 是 PhoneWindowManager 的一個實例芹啥, adjustWindowParamsLw 主要是根據(jù)窗口的屬性來決定接下來要展示什么樣的
SystemUI锻离。例如:
@Override
public void adjustWindowParamsLw(WindowManager.LayoutParams attrs) {
// 省略一堆代碼
if ((attrs.flags & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0
|| forceWindowDrawsStatusBarBackground
&& attrs.height == MATCH_PARENT && attrs.width == MATCH_PARENT) {
attrs.subtreeSystemUiVisibility |= View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
}
}
接下來, 會調(diào)用 PhoneWindowManager 的 focusChangedLw()墓怀, 在這里調(diào)用了更新 SystemUI 樣式的方法 updateSystemUiVisibilityLw汽纠。
@Override
public int focusChangedLw(WindowState lastFocus, WindowState newFocus) {
mFocusedWindow = newFocus;
if ((updateSystemUiVisibilityLw()&SYSTEM_UI_CHANGING_LAYOUT) != 0) {
// If the navigation bar has been hidden or shown, we need to do another
// layout pass to update that window.
return FINISH_LAYOUT_REDO_LAYOUT;
}
return 0;
}
private int updateSystemUiVisibilityLw() {
mHandler.post(new Runnable() {
@Override
public void run() {
StatusBarManagerInternal statusbar = getStatusBarManagerInternal();
if (statusbar != null) {
statusbar.setSystemUiVisibility(visibility, fullscreenVisibility,
dockedVisibility, 0xffffffff, fullscreenStackBounds,
dockedStackBounds, win.toString());
statusbar.topAppWindowChanged(needsMenu);
}
}
});
return diff;
}
statusbar 就是 StatusbarManagerService 的一個實例。 在 SystemUI 的啟動過程中傀履, SystemUI 會向 StatusbarManagerService
服務(wù)注冊一個回調(diào)虱朵, 專門用來接收 StatusbarManagerService 的調(diào)用, 這個回調(diào)器就是 CommandQueue。
public void setSystemUiVisibility(int vis, int fullscreenStackVis, int dockedStackVis,
int mask, Rect fullscreenStackBounds, Rect dockedStackBounds) {
synchronized (mLock) {
// Don't coalesce these, since it might have one time flags set such as
// STATUS_BAR_UNHIDE which might get lost.
SomeArgs args = SomeArgs.obtain();
args.argi1 = vis;
args.argi2 = fullscreenStackVis;
args.argi3 = dockedStackVis;
args.argi4 = mask;
args.arg1 = fullscreenStackBounds;
args.arg2 = dockedStackBounds;
mHandler.obtainMessage(MSG_SET_SYSTEMUI_VISIBILITY, args).sendToTarget();
}
}
CommandQueue 收到調(diào)用之后就會將消息發(fā)送到 SystemUI 的視圖碴犬, 視圖再根據(jù)收到的 vis 屬性改變樣式絮宁。