Android Activity/View/Window/Dialog/Fragment 深層次關(guān)聯(lián)(白話解析)

前言

Activity/Fragment/View 系列文章:

Android Activity 與View 的互動(dòng)思考
Android Activity 生命周期詳解及監(jiān)聽
Android onSaveInstanceState/onRestoreInstanceState 原來要這么理解
Android Fragment 要你何用?
Android Activity/View/Window/Dialog/Fragment 深層次關(guān)聯(lián)(白話解析)

很早就想就這幾個(gè)UI 組件關(guān)系梳理一篇博客,但由于之前一些基礎(chǔ)博客沒梳理好,因此耽擱了。這些UI 組件不論對于初學(xué)者還是有一定開發(fā)經(jīng)驗(yàn)的同學(xué)來說都是經(jīng)常用到的,但是可能沒有深究其中差異,而網(wǎng)上也沒有統(tǒng)一梳理這方面知識(shí)的文章。
本篇文章嘗試用簡單的語言精確描述個(gè)中關(guān)聯(lián)與差異,通過本篇文章你將了解到:

1、Window 與 Window.java/PhoneWindow.java 有啥關(guān)系?
2、Window 與 View 是如何關(guān)聯(lián)上的?
3、View 與 ViewGroup 父與子嵌套交錯(cuò)?
4、Activity 與 Window、View 如何牽線搭橋?
5、Activity 與 Dialog/PopupWindow/Toast 該怎么選?
6、Activity 與Fragment 的聯(lián)系與區(qū)別。
7、一個(gè)串起來的小故事

1、Window 與 Window.java/PhoneWindow 有啥關(guān)系?

系統(tǒng)里的Window

從最簡單的Android Demo 開始:編寫一個(gè)顯示 "Hello World" 的App。

1、定義一個(gè)MainActivity。
2、MainActivity 指定加載(setContentView(xx))布局文件。

編寫完成并運(yùn)行到手機(jī)上,當(dāng)點(diǎn)擊桌面上該Demo 的圖標(biāo)后將會(huì)顯示"Hello World"字符串,可以看出僅僅只需要簡單的幾步就可以在手機(jī)上顯示一段文字,對于開發(fā)者來說是很簡單,而簡單的原因是開發(fā)者不需要關(guān)注底層顯示問題,系統(tǒng)已經(jīng)幫忙我們搞定了這一切。

每一個(gè)Activity 啟動(dòng)時(shí)都會(huì)向系統(tǒng)申請創(chuàng)建一個(gè)Window 用來展示界面,我們的App是運(yùn)行在獨(dú)立的進(jìn)程,這此處的"系統(tǒng)"指的是系統(tǒng)進(jìn)程:system_server。
App 進(jìn)程通過Binder(Android 跨進(jìn)程通信方式) 告訴系統(tǒng)服務(wù):WMS(WindowManagerService),請為我創(chuàng)建一個(gè)Window(窗口)用來顯示我的UI。


image.png

WMS 收到請求后,將會(huì)創(chuàng)建WindowState 對象,該對象用來描述Window 的一切屬性,也是WMS 里表示"窗口"的實(shí)體。

應(yīng)用里的Window

在App 進(jìn)程也會(huì)涉及到Window,只不過這并不是真正意義上的"窗口",它叫:Window.java,可以看出這是個(gè)抽象類,它的唯一實(shí)現(xiàn)子類就是咱們熟知的PhoneWindow.java。
Window.java/PhoneWindow.java 作用:

1、將Activity 部分邏輯提取放在Window.java實(shí)現(xiàn)。
2、比如設(shè)置狀態(tài)欄、導(dǎo)航欄、標(biāo)題、主題等。
3、處理按鍵事件分發(fā)。

用到Window.java 的組件常見的有Activity與Dialog,它倆都使用DecorView 作為整個(gè)ViewTree的根,而PhoneWindow.java 持有DecorView實(shí)例,也就是說Activity與Dialog 對DecorView的部分操作放在PhoneWindow.java里完成了。
網(wǎng)上大部分文章通常舉例說:

Activity 包含Window,Window 包含View。

從方便理解層次關(guān)系的角度來看上面這句話沒問題,因?yàn)閺拇a角度出發(fā):Activity 持有PhoneWindow.java 實(shí)例,PhoneWindow.java 又持有RootView(DecorView),這么看起來就是包含關(guān)系。不過你真要較真的話:
Activity 并不是窗口,Window.java 也沒表示窗口,它倆就不存在所謂的窗口包含關(guān)系。
而Window.java/PhoneWindow.java 與系統(tǒng)里的Window 沒有什么直接聯(lián)系,兩者不是一個(gè)概念。

后續(xù)提及的Window沒有特殊說明指的是系統(tǒng)里的Window。

2、Window 與 View 是如何關(guān)聯(lián)上的?

Window 提供給應(yīng)用進(jìn)程的對象

既然說了Window.java與系統(tǒng)里的Window不是同一個(gè)概念,那么在Activity里的布局文件如何展示到系統(tǒng)里的Window上的呢?
當(dāng)App 進(jìn)程請求系統(tǒng)創(chuàng)建Window時(shí),調(diào)用棧如下:

WindowManager.addView(xx)-->WindowManagerImpl.addView(xx)-->WindowManagerGlobal.addView(xx)-->ViewRootImpl.setView(xx)
-->Session.addToDisplay(xx)-->WindowManagerService.addWindow(xx)

其中Session.addToDisplay(xx) 及其之后的方法是在系統(tǒng)進(jìn)程里執(zhí)行的,addWindow(xx)里會(huì)構(gòu)造WindowState。
以上流程調(diào)用結(jié)束,系統(tǒng)里的Window 就創(chuàng)建完畢了。
現(xiàn)在需要將該Window與應(yīng)用進(jìn)程關(guān)聯(lián)起來。

我們知道Android 是事件驅(qū)動(dòng)的,當(dāng)提交界面刷新動(dòng)作后,這些動(dòng)作將會(huì)被緩存,等待屏幕刷新信號(hào)到來時(shí)才會(huì)真正執(zhí)行這些刷新動(dòng)作,而此處的執(zhí)行入口即為:ViewRootImpl.performTraversals()。
該方法里調(diào)用了:ViewRootImpl.relayoutWindow(xx),進(jìn)而調(diào)用了,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 已經(jīng)有效,填充mSurface
            mSurface.copyFrom(mSurfaceControl);
        } else {
            destroySurface();
        }

mSurface 是ViewRootImpl.java 里的成員變量,定義如下:

    @UnsupportedAppUsage
    public final Surface mSurface = new Surface();

總結(jié)來說:

在執(zhí)行relayout(xx)之后,WMS 端的surface 實(shí)例存放在SurfaceControl里,然后再賦值給應(yīng)用進(jìn)程里的mSurface變量。
此時(shí)應(yīng)用進(jìn)程間接擁有了Window 的Surface。

image.png

此處涉及到Binder通信,更詳細(xì)的Binder請移步:Binder"秒懂"系列

應(yīng)用進(jìn)程繪制到Window上

應(yīng)用進(jìn)程拿到了WMS的Surface,接下來就需要將界面繪制到該Surface上,問題的重點(diǎn)是如何繪制到Surface上。
Android 繪制分為軟件繪制與硬件繪制,不論是哪種繪制方式最終都要通過Surface,以軟件繪制為例:

1、通過Surface.lockCanvas(xx)拿到Canvas對象。
2、通過Canvas繪制任意的界面。

最開始的"Hello World"是個(gè)字符串,因此我們可以用TextView來展示它,而TextView本質(zhì)是通過Canvas.drawText("Hello World")來繪制的。
Surface可以理解為一個(gè)展示的面,而Canvas則是畫布(繪制各種圖形的API集合)。

image.png

通過畫布的各種操作,最終效果呈現(xiàn)在Surface上。

至此我們知道Window和View的關(guān)聯(lián)過程:

1、應(yīng)用進(jìn)程請求WMS 創(chuàng)建Window。
2、應(yīng)用進(jìn)程拿到WMS Surface。
3、應(yīng)用進(jìn)程通過Surface拿到Canvas。
4、應(yīng)用進(jìn)程通過將Canvas傳遞給ViewTree的根(RootView)。
5、ViewTree將Canvas一層層傳遞給各個(gè)ViewGroup/View。
6、ViewGroup/View 在onDraw(Canvas)里拿到Canvas進(jìn)行繪制。
7、最終效果將呈現(xiàn)在Surface上,也就是說Window 有了內(nèi)容。

明顯地可以看出,以上過程與Window.java/PhoneWindow.java/Activity 并無關(guān)系,我們可以脫離三者將任意想要顯示內(nèi)容顯示在Window上。
顯示任意Window 攻略可移步:Window/WindowManager 不可不知之事

3、View 與 ViewGroup 父與子嵌套交錯(cuò)?

為什么需要View

當(dāng)需要往Window上展示界面時(shí),我們除了需要準(zhǔn)備繪制的內(nèi)容,如文本、圖片。還需要知道繪制在Window的哪個(gè)位置,繪制的內(nèi)容展示的尺寸有多大。
這些在Canvas里都有對應(yīng)的方法:

繪制文本、圖片
Canvas.drawText(xx)、Canvas.drawBitmap(xx)。

繪制的位置
Canvas.translate(xx)

繪制的尺寸
Canvas.clipRect(xx)

雖然都是可以通過Canvas控制,但是若是界面元素很多的話,那么重復(fù)的工作就比較多,尤其是繪制的位置與尺寸這倆步驟顯然可以抽出作為公共的步驟。
而View 的作用之一就是封裝了以上三個(gè)步驟,就是View里典型的三大步驟:

測量、擺放、繪制。

當(dāng)我們需要在Window上展示不同的界面元素時(shí),只需要定義不同的View對象即可,這樣就方便了許多。
系統(tǒng)提供的文字View(TextView),圖片View(ImageView)等即是View的具象化。

View 三大過程請移步:View測量/擺放/繪制 終于懂了

為什么需要ViewGroup

再考慮一個(gè)場景:想要在Window里展示3個(gè)界面元素,并且是縱向排布的。


image.png

這種場景的使用范圍很廣,沒必要每次都重新設(shè)置View 之間的排列位置,于是將線性縱向排列拎出來作為一個(gè)公共組件,此時(shí)就引入了ViewGroup。
ViewGroup 描述了一組View的展示規(guī)則,系統(tǒng)提供的LinearLayout、FrameLayout等就是ViewGroup的具象化。

ViewGroup 顧名思義就是個(gè)組,其內(nèi)部可以存放View也可以存放ViewGroup,通過嵌套包含,構(gòu)成了ViewTree,最終展示在Window上。


image.png

4、Activity 與 Window、View 如何牽線搭橋?

Activity 的引入

Window與View 是通過Surface/Canvas 關(guān)聯(lián)上的,換句話說想要在Window上展示界面元素,實(shí)際上只需要調(diào)用一個(gè)方法:

    //textView 表示待展示的View
    //layoutParams 表示對textView 布局的約束
    windowManager.addView(textView, layoutParams)

windowManager 實(shí)例通過獲取系統(tǒng)服務(wù)而得到:

        //獲取WindowManager實(shí)例,這里的App是繼承自Application
    WindowManager wm = (WindowManager) App.getApplication().getSystemService(Context.WINDOW_SERVICE);

添加構(gòu)建單個(gè)Window很簡單,試想一下若是添加多個(gè)Window,那么這些Window之間的關(guān)系是如何處理呢?比如Window2顯示后需要將Window1隱藏,點(diǎn)擊事件該流轉(zhuǎn)到哪個(gè)Window上,狀態(tài)欄、導(dǎo)航欄的設(shè)置,頁面的生命周期是如何確定的等問題,單靠Window顯得力不從心。
Activity 作為一個(gè)獨(dú)立頁面的承載者,擁有完整的生命周期,當(dāng)需要展示一個(gè)頁面時(shí),我們僅僅只需要將待展示的頁面布局(View)關(guān)聯(lián)到Activity,最終Window展示的結(jié)果即是我們的布局文件。
因此問題的重點(diǎn)是:
1、Activity 與View 如何關(guān)聯(lián)?
2、Activity 與Window 如何關(guān)聯(lián)?

Activity 與 View的關(guān)聯(lián)

通常編寫一個(gè)Activity時(shí),需要重寫其onCreate(xx)方法,在該方法里調(diào)用:

setContentView(R.layout.activity_main)

注:調(diào)用該方法之前PhoneWindow實(shí)例已經(jīng)創(chuàng)建。
Activity 關(guān)聯(lián)了布局文件:R.layout.activity_main,我們統(tǒng)稱為View。
setContentView(xx) 主要功能如下:

1、創(chuàng)建DecorView,作為整個(gè)ViewTree的RootView,DecorView下還掛了其它的View/ViewGroup,有個(gè)ViewGroup叫做"content"。
2、PhoneWindow持有該DecorView。
3、將R.layout.activity_main 布局文件加載到內(nèi)存,實(shí)例化為ViewGroup/View。
4、將實(shí)例化后的布局文件加入到DecorView里子布局"content"里。
5、此時(shí)一個(gè)完整的ViewTree建立完畢。

至此,Activity 已經(jīng)關(guān)聯(lián)了PhoneWindow,通過PhoneWindow間接持有了DecorView。

Activity 到View 的流轉(zhuǎn)更詳細(xì)請移步:Android Activity創(chuàng)建到View的顯示過程

Activity 與 Window的關(guān)聯(lián)

Activity 執(zhí)行完成onCreate(xx)后,經(jīng)過"Start"階段到達(dá)"Resume"階段,此時(shí)界面需要真正展示了。
在"Resume"階段會(huì)調(diào)用:

ActivityThread.handleResumeActivity(xx)

該方法里有個(gè)重要的操作:

1、找到當(dāng)前的Activity 實(shí)例,進(jìn)而找到所持有的PhoneWindow實(shí)例。
2、通過PhoneWindow實(shí)例,找到關(guān)聯(lián)的ViewTree根View:DecorView。
3、通過WindowManager.addView(DecorView,layoutparam)將DecorView 添加到Widnow里。

可以看出,Activity 內(nèi)部實(shí)現(xiàn)了一些基礎(chǔ)頁面配置(DecorView 里處理了狀態(tài)欄、導(dǎo)航欄、一些主題設(shè)置等),實(shí)現(xiàn)了ViewTree的構(gòu)建(自定義的布局掛到DecorView某個(gè)子布局里),實(shí)現(xiàn)了將整個(gè)ViewTree添加到Window的操作。
大部分場景下,我們僅僅只需要關(guān)注布局文件(R.layout.activity_main)的編寫與邏輯處理即可,剩下的工作交給Activity處理,最終我們想要展示的效果將會(huì)在Window里呈現(xiàn)。

更詳細(xì)的DecorView分析請移步:Android DecorView 一窺全貌(上)

5、Activity 與 Dialog/PopupWindow/Toast 該怎么選?

有時(shí)候我們并不需要一個(gè)完整的頁面,僅僅需要一個(gè)彈框提示即可,這個(gè)時(shí)候會(huì)考慮使用Dialog/PopupWindow/Toast 等組件。
請記住一個(gè)點(diǎn):不管是什么樣的UI 組件,最終都需要通過WindowManager.add(xx)添加到Window里。

Dialog/PopupWindow/Toast 最終都是通過WindowManager.add(xx)加載的,只不過是它們設(shè)定的Window尺寸沒有占滿整個(gè)屏幕,而是由外部設(shè)定的Window尺寸。
它們沒有生命周期,其中Dialog/PopupWindow 依賴于Activity 上下文(Context,Token限制)。

當(dāng)不需要占滿屏幕、UI 簡單、邏輯簡單、偏重于提示/選擇之類的場景時(shí),Dialog/PopupWindow/Toast 是比較好的選擇。
至于它們內(nèi)部的區(qū)別,請移步:Dialog/PopupWindow/Toast 到底該怎么選

6、Activity 與Fragment 的聯(lián)系與區(qū)別。

有時(shí)我們想要在不同的case下顯示不同的頁面,例如頁面頂部有幾個(gè)Tab欄,新聞、娛樂、學(xué)習(xí)等板塊,點(diǎn)擊不同的tab展示不同的頁面。用Activity 作為頁面承載的話有點(diǎn)大材小用,并且過渡沒那么流暢。用Dialog的話因?yàn)闆]有生命周期管控,一些邏輯沒法閉環(huán)(比如后臺(tái)的網(wǎng)絡(luò)請求,數(shù)據(jù)庫加載等)。
此時(shí)就需要考慮使用Fragment。
Fragment 有如下特點(diǎn):

1、跟隨Activity 擁有生命周期。
2、將View 封裝并擁有獨(dú)立的處理邏輯。
3、Fragment 構(gòu)建、切換速度比Activity 快。

但也有缺點(diǎn):

1、生命周期復(fù)雜。
2、必須依賴于Activity。

更多Fragment 解析請移步:Android Fragment 要你何用?

7、一個(gè)串起來的小故事

1、WMS 能夠構(gòu)建并展示窗口,它作為一個(gè)公共的服務(wù),需要提供給其它App(應(yīng)用)創(chuàng)建并填充窗口,于是他提供了Surface給其它App使用。
2、應(yīng)用拿到Surface并從中取出Canvas后就可以繪制任意的圖形了。
3、Canvas 繪制需要設(shè)定繪制的起點(diǎn),繪制的尺寸以及繪制的內(nèi)容,這些都是必經(jīng)之路,每次展示都需要設(shè)定這些參數(shù)有點(diǎn)冗余,于是View出現(xiàn)了。
4、View 封裝了測量、布局、繪制 等操作,應(yīng)用開發(fā)者只需要關(guān)注如何繪制即可,甚至繪制都無需過多關(guān)心,比如TextView、ImageView 都是系統(tǒng)封裝好了的,僅僅需要關(guān)注具體的內(nèi)容即可。
5、有一些布局的排列比較常見且規(guī)律,比如線性垂直排列View,這個(gè)時(shí)候就引入了ViewGroup,有了它各個(gè)View的順序、位置編排都能實(shí)現(xiàn),甚至我們都不需要關(guān)心這些,比如LinearLayout、FrameLayout 都是系統(tǒng)封裝好了的排列規(guī)則。
6、此時(shí)通過View/ViewGroup 已經(jīng)能夠填充Window的內(nèi)容了,但還是不夠,因?yàn)檫€是要處理多個(gè)Window的交互,這個(gè)時(shí)候Activity 出現(xiàn)了。
7、Activity 能夠設(shè)定默認(rèn)的RootView(DecorView),我們僅僅只需要將布局文件關(guān)聯(lián)到Activity就可以有一個(gè)統(tǒng)一的展示風(fēng)格,比如統(tǒng)一的主題、標(biāo)題欄等。
8、Activity 承載越來越多的工作量,為了減輕它的負(fù)擔(dān),PhoneWindow.java/Window.java 出現(xiàn)了,能夠幫Activity 處理狀態(tài)欄、導(dǎo)航欄、事件分發(fā)等一些操作。
9、Activity 還是太重了,一些場景并不適合,比如只想彈出一個(gè)提示框、選擇框等,殺雞焉用牛刀? 于是Dialog/PopupWindow/Toast 出現(xiàn)了。
10、而Dialog/PopupWindow/Toast 沒有生命周期,用Activity 直接控制View又增加了Activity的工作量,于是Fragment 幫助Activity 管理了一些較為獨(dú)立的View。

本文基于Android 10.0
若有疑問或是想要了解更細(xì)節(jié)的知識(shí),請留言或私信我。

您若喜歡,請點(diǎn)贊、關(guān)注,您的鼓勵(lì)是我前進(jìn)的動(dòng)力

持續(xù)更新中,和我一起步步為營系統(tǒng)、深入學(xué)習(xí)Android

1、Android各種Context的前世今生
2、Android DecorView 必知必會(huì)
3、Window/WindowManager 不可不知之事
4、View Measure/Layout/Draw 真明白了
5、Android事件分發(fā)全套服務(wù)
6、Android invalidate/postInvalidate/requestLayout 徹底厘清
7、Android Window 如何確定大小/onMeasure()多次執(zhí)行原因
8、Android事件驅(qū)動(dòng)Handler-Message-Looper解析
9、Android 鍵盤一招搞定
10、Android 各種坐標(biāo)徹底明了
11、Android Activity/Window/View 的background
12、Android Activity創(chuàng)建到View的顯示過
13、Android IPC 系列
14、Android 存儲(chǔ)系列
15、Java 并發(fā)系列不再疑惑
16、Java 線程池系列
17、Android Jetpack 前置基礎(chǔ)系列

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容