前言
Activity/Fragment/View 系列文章:
Android Activity 與View 的互動思考
Android Activity 生命周期詳解及監聽
Android onSaveInstanceState/onRestoreInstanceState 原來要這么理解
Android Fragment 要你何用?
Android Activity/View/Window/Dialog/Fragment 深層次關聯(白話解析)
很早就想就這幾個UI 組件關系梳理一篇博客,但由于之前一些基礎博客沒梳理好,因此耽擱了。這些UI 組件不論對于初學者還是有一定開發經驗的同學來說都是經常用到的,但是可能沒有深究其中差異,而網上也沒有統一梳理這方面知識的文章。
本篇文章嘗試用簡單的語言精確描述個中關聯與差異,通過本篇文章你將了解到:
1、Window 與 Window.java/PhoneWindow.java 有啥關系?
2、Window 與 View 是如何關聯上的?
3、View 與 ViewGroup 父與子嵌套交錯?
4、Activity 與 Window、View 如何牽線搭橋?
5、Activity 與 Dialog/PopupWindow/Toast 該怎么選?
6、Activity 與Fragment 的聯系與區別。
7、一個串起來的小故事
1、Window 與 Window.java/PhoneWindow 有啥關系?
系統里的Window
從最簡單的Android Demo 開始:編寫一個顯示 "Hello World" 的App。
1、定義一個MainActivity。
2、MainActivity 指定加載(setContentView(xx))布局文件。
編寫完成并運行到手機上,當點擊桌面上該Demo 的圖標后將會顯示"Hello World"字符串,可以看出僅僅只需要簡單的幾步就可以在手機上顯示一段文字,對于開發者來說是很簡單,而簡單的原因是開發者不需要關注底層顯示問題,系統已經幫忙我們搞定了這一切。
每一個Activity 啟動時都會向系統申請創建一個Window 用來展示界面,我們的App是運行在獨立的進程,這此處的"系統"指的是系統進程:system_server。
App 進程通過Binder(Android 跨進程通信方式) 告訴系統服務:WMS(WindowManagerService),請為我創建一個Window(窗口)用來顯示我的UI。
WMS 收到請求后,將會創建WindowState 對象,該對象用來描述Window 的一切屬性,也是WMS 里表示"窗口"的實體。
應用里的Window
在App 進程也會涉及到Window,只不過這并不是真正意義上的"窗口",它叫:Window.java,可以看出這是個抽象類,它的唯一實現子類就是咱們熟知的PhoneWindow.java。
Window.java/PhoneWindow.java 作用:
1、將Activity 部分邏輯提取放在Window.java實現。
2、比如設置狀態欄、導航欄、標題、主題等。
3、處理按鍵事件分發。
用到Window.java 的組件常見的有Activity與Dialog,它倆都使用DecorView 作為整個ViewTree的根,而PhoneWindow.java 持有DecorView實例,也就是說Activity與Dialog 對DecorView的部分操作放在PhoneWindow.java里完成了。
網上大部分文章通常舉例說:
Activity 包含Window,Window 包含View。
從方便理解層次關系的角度來看上面這句話沒問題,因為從代碼角度出發:Activity 持有PhoneWindow.java 實例,PhoneWindow.java 又持有RootView(DecorView),這么看起來就是包含關系。不過你真要較真的話:
Activity 并不是窗口,Window.java 也沒表示窗口,它倆就不存在所謂的窗口包含關系。
而Window.java/PhoneWindow.java 與系統里的Window 沒有什么直接聯系,兩者不是一個概念。
后續提及的Window沒有特殊說明指的是系統里的Window。
2、Window 與 View 是如何關聯上的?
Window 提供給應用進程的對象
既然說了Window.java與系統里的Window不是同一個概念,那么在Activity里的布局文件如何展示到系統里的Window上的呢?
當App 進程請求系統創建Window時,調用棧如下:
WindowManager.addView(xx)-->WindowManagerImpl.addView(xx)-->WindowManagerGlobal.addView(xx)-->ViewRootImpl.setView(xx)
-->Session.addToDisplay(xx)-->WindowManagerService.addWindow(xx)
其中Session.addToDisplay(xx) 及其之后的方法是在系統進程里執行的,addWindow(xx)里會構造WindowState。
以上流程調用結束,系統里的Window 就創建完畢了。
現在需要將該Window與應用進程關聯起來。
我們知道Android 是事件驅動的,當提交界面刷新動作后,這些動作將會被緩存,等待屏幕刷新信號到來時才會真正執行這些刷新動作,而此處的執行入口即為:ViewRootImpl.performTraversals()。
該方法里調用了:ViewRootImpl.relayoutWindow(xx),進而調用了,Session.relayout(xx):
#ViewRootImpl.java
int relayoutResult = mWindowSession.relayout(mWindow, mSeq, params,
(int) (mView.getMeasuredWidth() * appScale + 0.5f),
(int) (mView.getMeasuredHeight() * appScale + 0.5f), viewVisibility,
insetsPending ? WindowManagerGlobal.RELAYOUT_INSETS_PENDING : 0, frameNumber,
mTmpFrame, mPendingOverscanInsets, mPendingContentInsets, mPendingVisibleInsets,
mPendingStableInsets, mPendingOutsets, mPendingBackDropFrame, mPendingDisplayCutout,
mPendingMergedConfiguration, mSurfaceControl, mTempInsets);
if (mSurfaceControl.isValid()) {
//mSurfaceControl 已經有效,填充mSurface
mSurface.copyFrom(mSurfaceControl);
} else {
destroySurface();
}
mSurface 是ViewRootImpl.java 里的成員變量,定義如下:
@UnsupportedAppUsage
public final Surface mSurface = new Surface();
總結來說:
在執行relayout(xx)之后,WMS 端的surface 實例存放在SurfaceControl里,然后再賦值給應用進程里的mSurface變量。
此時應用進程間接擁有了Window 的Surface。
此處涉及到Binder通信,更詳細的Binder請移步:Binder"秒懂"系列
應用進程繪制到Window上
應用進程拿到了WMS的Surface,接下來就需要將界面繪制到該Surface上,問題的重點是如何繪制到Surface上。
Android 繪制分為軟件繪制與硬件繪制,不論是哪種繪制方式最終都要通過Surface,以軟件繪制為例:
1、通過Surface.lockCanvas(xx)拿到Canvas對象。
2、通過Canvas繪制任意的界面。
最開始的"Hello World"是個字符串,因此我們可以用TextView來展示它,而TextView本質是通過Canvas.drawText("Hello World")來繪制的。
Surface可以理解為一個展示的面,而Canvas則是畫布(繪制各種圖形的API集合)。
通過畫布的各種操作,最終效果呈現在Surface上。
至此我們知道Window和View的關聯過程:
1、應用進程請求WMS 創建Window。
2、應用進程拿到WMS Surface。
3、應用進程通過Surface拿到Canvas。
4、應用進程通過將Canvas傳遞給ViewTree的根(RootView)。
5、ViewTree將Canvas一層層傳遞給各個ViewGroup/View。
6、ViewGroup/View 在onDraw(Canvas)里拿到Canvas進行繪制。
7、最終效果將呈現在Surface上,也就是說Window 有了內容。
明顯地可以看出,以上過程與Window.java/PhoneWindow.java/Activity 并無關系,我們可以脫離三者將任意想要顯示內容顯示在Window上。
顯示任意Window 攻略可移步:Window/WindowManager 不可不知之事
3、View 與 ViewGroup 父與子嵌套交錯?
為什么需要View
當需要往Window上展示界面時,我們除了需要準備繪制的內容,如文本、圖片。還需要知道繪制在Window的哪個位置,繪制的內容展示的尺寸有多大。
這些在Canvas里都有對應的方法:
繪制文本、圖片
Canvas.drawText(xx)、Canvas.drawBitmap(xx)。
繪制的位置
Canvas.translate(xx)
繪制的尺寸
Canvas.clipRect(xx)
雖然都是可以通過Canvas控制,但是若是界面元素很多的話,那么重復的工作就比較多,尤其是繪制的位置與尺寸這倆步驟顯然可以抽出作為公共的步驟。
而View 的作用之一就是封裝了以上三個步驟,就是View里典型的三大步驟:
測量、擺放、繪制。
當我們需要在Window上展示不同的界面元素時,只需要定義不同的View對象即可,這樣就方便了許多。
系統提供的文字View(TextView),圖片View(ImageView)等即是View的具象化。
View 三大過程請移步:View測量/擺放/繪制 終于懂了
為什么需要ViewGroup
再考慮一個場景:想要在Window里展示3個界面元素,并且是縱向排布的。
這種場景的使用范圍很廣,沒必要每次都重新設置View 之間的排列位置,于是將線性縱向排列拎出來作為一個公共組件,此時就引入了ViewGroup。
ViewGroup 描述了一組View的展示規則,系統提供的LinearLayout、FrameLayout等就是ViewGroup的具象化。
ViewGroup 顧名思義就是個組,其內部可以存放View也可以存放ViewGroup,通過嵌套包含,構成了ViewTree,最終展示在Window上。
4、Activity 與 Window、View 如何牽線搭橋?
Activity 的引入
Window與View 是通過Surface/Canvas 關聯上的,換句話說想要在Window上展示界面元素,實際上只需要調用一個方法:
//textView 表示待展示的View
//layoutParams 表示對textView 布局的約束
windowManager.addView(textView, layoutParams)
windowManager 實例通過獲取系統服務而得到:
//獲取WindowManager實例,這里的App是繼承自Application
WindowManager wm = (WindowManager) App.getApplication().getSystemService(Context.WINDOW_SERVICE);
添加構建單個Window很簡單,試想一下若是添加多個Window,那么這些Window之間的關系是如何處理呢?比如Window2顯示后需要將Window1隱藏,點擊事件該流轉到哪個Window上,狀態欄、導航欄的設置,頁面的生命周期是如何確定的等問題,單靠Window顯得力不從心。
Activity 作為一個獨立頁面的承載者,擁有完整的生命周期,當需要展示一個頁面時,我們僅僅只需要將待展示的頁面布局(View)關聯到Activity,最終Window展示的結果即是我們的布局文件。
因此問題的重點是:
1、Activity 與View 如何關聯?
2、Activity 與Window 如何關聯?
Activity 與 View的關聯
通常編寫一個Activity時,需要重寫其onCreate(xx)方法,在該方法里調用:
setContentView(R.layout.activity_main)
注:調用該方法之前PhoneWindow實例已經創建。
Activity 關聯了布局文件:R.layout.activity_main,我們統稱為View。
setContentView(xx) 主要功能如下:
1、創建DecorView,作為整個ViewTree的RootView,DecorView下還掛了其它的View/ViewGroup,有個ViewGroup叫做"content"。
2、PhoneWindow持有該DecorView。
3、將R.layout.activity_main 布局文件加載到內存,實例化為ViewGroup/View。
4、將實例化后的布局文件加入到DecorView里子布局"content"里。
5、此時一個完整的ViewTree建立完畢。
至此,Activity 已經關聯了PhoneWindow,通過PhoneWindow間接持有了DecorView。
Activity 到View 的流轉更詳細請移步:Android Activity創建到View的顯示過程
Activity 與 Window的關聯
Activity 執行完成onCreate(xx)后,經過"Start"階段到達"Resume"階段,此時界面需要真正展示了。
在"Resume"階段會調用:
ActivityThread.handleResumeActivity(xx)
該方法里有個重要的操作:
1、找到當前的Activity 實例,進而找到所持有的PhoneWindow實例。
2、通過PhoneWindow實例,找到關聯的ViewTree根View:DecorView。
3、通過WindowManager.addView(DecorView,layoutparam)將DecorView 添加到Widnow里。
可以看出,Activity 內部實現了一些基礎頁面配置(DecorView 里處理了狀態欄、導航欄、一些主題設置等),實現了ViewTree的構建(自定義的布局掛到DecorView某個子布局里),實現了將整個ViewTree添加到Window的操作。
大部分場景下,我們僅僅只需要關注布局文件(R.layout.activity_main)的編寫與邏輯處理即可,剩下的工作交給Activity處理,最終我們想要展示的效果將會在Window里呈現。
更詳細的DecorView分析請移步:Android DecorView 一窺全貌(上)
5、Activity 與 Dialog/PopupWindow/Toast 該怎么選?
有時候我們并不需要一個完整的頁面,僅僅需要一個彈框提示即可,這個時候會考慮使用Dialog/PopupWindow/Toast 等組件。
請記住一個點:不管是什么樣的UI 組件,最終都需要通過WindowManager.add(xx)添加到Window里。
Dialog/PopupWindow/Toast 最終都是通過WindowManager.add(xx)加載的,只不過是它們設定的Window尺寸沒有占滿整個屏幕,而是由外部設定的Window尺寸。
它們沒有生命周期,其中Dialog/PopupWindow 依賴于Activity 上下文(Context,Token限制)。
當不需要占滿屏幕、UI 簡單、邏輯簡單、偏重于提示/選擇之類的場景時,Dialog/PopupWindow/Toast 是比較好的選擇。
至于它們內部的區別,請移步:Dialog/PopupWindow/Toast 到底該怎么選
6、Activity 與Fragment 的聯系與區別。
有時我們想要在不同的case下顯示不同的頁面,例如頁面頂部有幾個Tab欄,新聞、娛樂、學習等板塊,點擊不同的tab展示不同的頁面。用Activity 作為頁面承載的話有點大材小用,并且過渡沒那么流暢。用Dialog的話因為沒有生命周期管控,一些邏輯沒法閉環(比如后臺的網絡請求,數據庫加載等)。
此時就需要考慮使用Fragment。
Fragment 有如下特點:
1、跟隨Activity 擁有生命周期。
2、將View 封裝并擁有獨立的處理邏輯。
3、Fragment 構建、切換速度比Activity 快。
但也有缺點:
1、生命周期復雜。
2、必須依賴于Activity。
更多Fragment 解析請移步:Android Fragment 要你何用?
7、一個串起來的小故事
1、WMS 能夠構建并展示窗口,它作為一個公共的服務,需要提供給其它App(應用)創建并填充窗口,于是他提供了Surface給其它App使用。
2、應用拿到Surface并從中取出Canvas后就可以繪制任意的圖形了。
3、Canvas 繪制需要設定繪制的起點,繪制的尺寸以及繪制的內容,這些都是必經之路,每次展示都需要設定這些參數有點冗余,于是View出現了。
4、View 封裝了測量、布局、繪制 等操作,應用開發者只需要關注如何繪制即可,甚至繪制都無需過多關心,比如TextView、ImageView 都是系統封裝好了的,僅僅需要關注具體的內容即可。
5、有一些布局的排列比較常見且規律,比如線性垂直排列View,這個時候就引入了ViewGroup,有了它各個View的順序、位置編排都能實現,甚至我們都不需要關心這些,比如LinearLayout、FrameLayout 都是系統封裝好了的排列規則。
6、此時通過View/ViewGroup 已經能夠填充Window的內容了,但還是不夠,因為還是要處理多個Window的交互,這個時候Activity 出現了。
7、Activity 能夠設定默認的RootView(DecorView),我們僅僅只需要將布局文件關聯到Activity就可以有一個統一的展示風格,比如統一的主題、標題欄等。
8、Activity 承載越來越多的工作量,為了減輕它的負擔,PhoneWindow.java/Window.java 出現了,能夠幫Activity 處理狀態欄、導航欄、事件分發等一些操作。
9、Activity 還是太重了,一些場景并不適合,比如只想彈出一個提示框、選擇框等,殺雞焉用牛刀? 于是Dialog/PopupWindow/Toast 出現了。
10、而Dialog/PopupWindow/Toast 沒有生命周期,用Activity 直接控制View又增加了Activity的工作量,于是Fragment 幫助Activity 管理了一些較為獨立的View。
本文基于Android 10.0
若有疑問或是想要了解更細節的知識,請留言或私信我。
您若喜歡,請點贊、關注,您的鼓勵是我前進的動力
持續更新中,和我一起步步為營系統、深入學習Android
1、Android各種Context的前世今生
2、Android DecorView 必知必會
3、Window/WindowManager 不可不知之事
4、View Measure/Layout/Draw 真明白了
5、Android事件分發全套服務
6、Android invalidate/postInvalidate/requestLayout 徹底厘清
7、Android Window 如何確定大小/onMeasure()多次執行原因
8、Android事件驅動Handler-Message-Looper解析
9、Android 鍵盤一招搞定
10、Android 各種坐標徹底明了
11、Android Activity/Window/View 的background
12、Android Activity創建到View的顯示過
13、Android IPC 系列
14、Android 存儲系列
15、Java 并發系列不再疑惑
16、Java 線程池系列
17、Android Jetpack 前置基礎系列