前言
Activity/Fragment/View 系列文章:
Android Activity 與View 的互動思考
Android Activity 生命周期詳解及監(jiān)聽
Android onSaveInstanceState/onRestoreInstanceState 原來要這么理解
Android Fragment 要你何用音比?
Android Activity/View/Window/Dialog/Fragment 深層次關(guān)聯(lián)(白話解析)
很早就想就這幾個UI 組件關(guān)系梳理一篇博客,但由于之前一些基礎(chǔ)博客沒梳理好,因此耽擱了纠亚。這些UI 組件不論對于初學(xué)者還是有一定開發(fā)經(jīng)驗的同學(xué)來說都是經(jīng)常用到的,但是可能沒有深究其中差異,而網(wǎng)上也沒有統(tǒng)一梳理這方面知識的文章。
本篇文章嘗試用簡單的語言精確描述個中關(guān)聯(lián)與差異谈竿,通過本篇文章你將了解到:
1、Window 與 Window.java/PhoneWindow.java 有啥關(guān)系摸吠?
2空凸、Window 與 View 是如何關(guān)聯(lián)上的?
3寸痢、View 與 ViewGroup 父與子嵌套交錯呀洲?
4、Activity 與 Window啼止、View 如何牽線搭橋道逗?
5、Activity 與 Dialog/PopupWindow/Toast 該怎么選献烦?
6滓窍、Activity 與Fragment 的聯(lián)系與區(qū)別。
7巩那、一個串起來的小故事
1贰您、Window 與 Window.java/PhoneWindow 有啥關(guān)系?
系統(tǒng)里的Window
從最簡單的Android Demo 開始:編寫一個顯示 "Hello World" 的App拢操。
1、定義一個MainActivity舶替。
2令境、MainActivity 指定加載(setContentView(xx))布局文件。
編寫完成并運行到手機上顾瞪,當點擊桌面上該Demo 的圖標后將會顯示"Hello World"字符串舔庶,可以看出僅僅只需要簡單的幾步就可以在手機上顯示一段文字,對于開發(fā)者來說是很簡單陈醒,而簡單的原因是開發(fā)者不需要關(guān)注底層顯示問題惕橙,系統(tǒng)已經(jīng)幫忙我們搞定了這一切。
每一個Activity 啟動時都會向系統(tǒng)申請創(chuàng)建一個Window 用來展示界面钉跷,我們的App是運行在獨立的進程弥鹦,這此處的"系統(tǒng)"指的是系統(tǒng)進程:system_server。
App 進程通過Binder(Android 跨進程通信方式) 告訴系統(tǒng)服務(wù):WMS(WindowManagerService),請為我創(chuàng)建一個Window(窗口)用來顯示我的UI彬坏。
WMS 收到請求后朦促,將會創(chuàng)建WindowState 對象,該對象用來描述Window 的一切屬性栓始,也是WMS 里表示"窗口"的實體务冕。
應(yīng)用里的Window
在App 進程也會涉及到Window,只不過這并不是真正意義上的"窗口"幻赚,它叫:Window.java禀忆,可以看出這是個抽象類,它的唯一實現(xiàn)子類就是咱們熟知的PhoneWindow.java落恼。
Window.java/PhoneWindow.java 作用:
1箩退、將Activity 部分邏輯提取放在Window.java實現(xiàn)。
2领跛、比如設(shè)置狀態(tài)欄乏德、導(dǎo)航欄、標題吠昭、主題等喊括。
3、處理按鍵事件分發(fā)矢棚。
用到Window.java 的組件常見的有Activity與Dialog郑什,它倆都使用DecorView 作為整個ViewTree的根,而PhoneWindow.java 持有DecorView實例蒲肋,也就是說Activity與Dialog 對DecorView的部分操作放在PhoneWindow.java里完成了蘑拯。
網(wǎng)上大部分文章通常舉例說:
Activity 包含Window,Window 包含View兜粘。
從方便理解層次關(guān)系的角度來看上面這句話沒問題申窘,因為從代碼角度出發(fā):Activity 持有PhoneWindow.java 實例,PhoneWindow.java 又持有RootView(DecorView)孔轴,這么看起來就是包含關(guān)系剃法。不過你真要較真的話:
Activity 并不是窗口,Window.java 也沒表示窗口路鹰,它倆就不存在所謂的窗口包含關(guān)系贷洲。
而Window.java/PhoneWindow.java 與系統(tǒng)里的Window 沒有什么直接聯(lián)系,兩者不是一個概念晋柱。
后續(xù)提及的Window沒有特殊說明指的是系統(tǒng)里的Window优构。
2、Window 與 View 是如何關(guān)聯(lián)上的雁竞?
Window 提供給應(yīng)用進程的對象
既然說了Window.java與系統(tǒng)里的Window不是同一個概念钦椭,那么在Activity里的布局文件如何展示到系統(tǒng)里的Window上的呢?
當App 進程請求系統(tǒng)創(chuàng)建Window時,調(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)進程里執(zhí)行的玉凯,addWindow(xx)里會構(gòu)造WindowState势腮。
以上流程調(diào)用結(jié)束,系統(tǒng)里的Window 就創(chuàng)建完畢了漫仆。
現(xiàn)在需要將該Window與應(yīng)用進程關(guān)聯(lián)起來捎拯。
我們知道Android 是事件驅(qū)動的,當提交界面刷新動作后盲厌,這些動作將會被緩存署照,等待屏幕刷新信號到來時才會真正執(zhí)行這些刷新動作,而此處的執(zhí)行入口即為:ViewRootImpl.performTraversals()吗浩。
該方法里調(diào)用了:ViewRootImpl.relayoutWindow(xx)建芙,進而調(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 實例存放在SurfaceControl里阀湿,然后再賦值給應(yīng)用進程里的mSurface變量赶熟。
此時應(yīng)用進程間接擁有了Window 的Surface。
此處涉及到Binder通信陷嘴,更詳細的Binder請移步:Binder"秒懂"系列
應(yīng)用進程繪制到Window上
應(yīng)用進程拿到了WMS的Surface映砖,接下來就需要將界面繪制到該Surface上,問題的重點是如何繪制到Surface上灾挨。
Android 繪制分為軟件繪制與硬件繪制邑退,不論是哪種繪制方式最終都要通過Surface,以軟件繪制為例:
1劳澄、通過Surface.lockCanvas(xx)拿到Canvas對象地技。
2、通過Canvas繪制任意的界面秒拔。
最開始的"Hello World"是個字符串乓土,因此我們可以用TextView來展示它,而TextView本質(zhì)是通過Canvas.drawText("Hello World")來繪制的溯警。
Surface可以理解為一個展示的面,而Canvas則是畫布(繪制各種圖形的API集合)狡相。
通過畫布的各種操作梯轻,最終效果呈現(xiàn)在Surface上。
至此我們知道Window和View的關(guān)聯(lián)過程:
1尽棕、應(yīng)用進程請求WMS 創(chuàng)建Window喳挑。
2、應(yīng)用進程拿到WMS Surface。
3伊诵、應(yīng)用進程通過Surface拿到Canvas单绑。
4、應(yīng)用進程通過將Canvas傳遞給ViewTree的根(RootView)曹宴。
5搂橙、ViewTree將Canvas一層層傳遞給各個ViewGroup/View。
6笛坦、ViewGroup/View 在onDraw(Canvas)里拿到Canvas進行繪制区转。
7、最終效果將呈現(xiàn)在Surface上版扩,也就是說Window 有了內(nèi)容废离。
明顯地可以看出,以上過程與Window.java/PhoneWindow.java/Activity 并無關(guān)系礁芦,我們可以脫離三者將任意想要顯示內(nèi)容顯示在Window上蜻韭。
顯示任意Window 攻略可移步:Window/WindowManager 不可不知之事
3、View 與 ViewGroup 父與子嵌套交錯柿扣?
為什么需要View
當需要往Window上展示界面時肖方,我們除了需要準備繪制的內(nèi)容,如文本窄刘、圖片窥妇。還需要知道繪制在Window的哪個位置,繪制的內(nèi)容展示的尺寸有多大娩践。
這些在Canvas里都有對應(yīng)的方法:
繪制文本活翩、圖片
Canvas.drawText(xx)、Canvas.drawBitmap(xx)翻伺。
繪制的位置
Canvas.translate(xx)
繪制的尺寸
Canvas.clipRect(xx)
雖然都是可以通過Canvas控制材泄,但是若是界面元素很多的話,那么重復(fù)的工作就比較多吨岭,尤其是繪制的位置與尺寸這倆步驟顯然可以抽出作為公共的步驟拉宗。
而View 的作用之一就是封裝了以上三個步驟,就是View里典型的三大步驟:
測量辣辫、擺放旦事、繪制。
當我們需要在Window上展示不同的界面元素時急灭,只需要定義不同的View對象即可姐浮,這樣就方便了許多。
系統(tǒng)提供的文字View(TextView)葬馋,圖片View(ImageView)等即是View的具象化卖鲤。
View 三大過程請移步:View測量/擺放/繪制 終于懂了
為什么需要ViewGroup
再考慮一個場景:想要在Window里展示3個界面元素肾扰,并且是縱向排布的。
這種場景的使用范圍很廣蛋逾,沒必要每次都重新設(shè)置View 之間的排列位置集晚,于是將線性縱向排列拎出來作為一個公共組件,此時就引入了ViewGroup区匣。
ViewGroup 描述了一組View的展示規(guī)則偷拔,系統(tǒng)提供的LinearLayout、FrameLayout等就是ViewGroup的具象化沉颂。
ViewGroup 顧名思義就是個組条摸,其內(nèi)部可以存放View也可以存放ViewGroup,通過嵌套包含铸屉,構(gòu)成了ViewTree钉蒲,最終展示在Window上。
4彻坛、Activity 與 Window顷啼、View 如何牽線搭橋?
Activity 的引入
Window與View 是通過Surface/Canvas 關(guān)聯(lián)上的昌屉,換句話說想要在Window上展示界面元素钙蒙,實際上只需要調(diào)用一個方法:
//textView 表示待展示的View
//layoutParams 表示對textView 布局的約束
windowManager.addView(textView, layoutParams)
windowManager 實例通過獲取系統(tǒng)服務(wù)而得到:
//獲取WindowManager實例,這里的App是繼承自Application
WindowManager wm = (WindowManager) App.getApplication().getSystemService(Context.WINDOW_SERVICE);
添加構(gòu)建單個Window很簡單间驮,試想一下若是添加多個Window躬厌,那么這些Window之間的關(guān)系是如何處理呢?比如Window2顯示后需要將Window1隱藏竞帽,點擊事件該流轉(zhuǎn)到哪個Window上扛施,狀態(tài)欄、導(dǎo)航欄的設(shè)置屹篓,頁面的生命周期是如何確定的等問題疙渣,單靠Window顯得力不從心。
Activity 作為一個獨立頁面的承載者堆巧,擁有完整的生命周期妄荔,當需要展示一個頁面時,我們僅僅只需要將待展示的頁面布局(View)關(guān)聯(lián)到Activity谍肤,最終Window展示的結(jié)果即是我們的布局文件啦租。
因此問題的重點是:
1、Activity 與View 如何關(guān)聯(lián)荒揣?
2刷钢、Activity 與Window 如何關(guān)聯(lián)?
Activity 與 View的關(guān)聯(lián)
通常編寫一個Activity時乳附,需要重寫其onCreate(xx)方法内地,在該方法里調(diào)用:
setContentView(R.layout.activity_main)
注:調(diào)用該方法之前PhoneWindow實例已經(jīng)創(chuàng)建。
Activity 關(guān)聯(lián)了布局文件:R.layout.activity_main赋除,我們統(tǒng)稱為View阱缓。
setContentView(xx) 主要功能如下:
1、創(chuàng)建DecorView举农,作為整個ViewTree的RootView荆针,DecorView下還掛了其它的View/ViewGroup,有個ViewGroup叫做"content"颁糟。
2航背、PhoneWindow持有該DecorView。
3棱貌、將R.layout.activity_main 布局文件加載到內(nèi)存玖媚,實例化為ViewGroup/View。
4婚脱、將實例化后的布局文件加入到DecorView里子布局"content"里今魔。
5、此時一個完整的ViewTree建立完畢障贸。
至此错森,Activity 已經(jīng)關(guān)聯(lián)了PhoneWindow,通過PhoneWindow間接持有了DecorView篮洁。
Activity 到View 的流轉(zhuǎn)更詳細請移步:Android Activity創(chuàng)建到View的顯示過程
Activity 與 Window的關(guān)聯(lián)
Activity 執(zhí)行完成onCreate(xx)后涩维,經(jīng)過"Start"階段到達"Resume"階段,此時界面需要真正展示了袁波。
在"Resume"階段會調(diào)用:
ActivityThread.handleResumeActivity(xx)
該方法里有個重要的操作:
1瓦阐、找到當前的Activity 實例,進而找到所持有的PhoneWindow實例锋叨。
2垄分、通過PhoneWindow實例,找到關(guān)聯(lián)的ViewTree根View:DecorView娃磺。
3薄湿、通過WindowManager.addView(DecorView,layoutparam)將DecorView 添加到Widnow里偷卧。
可以看出豺瘤,Activity 內(nèi)部實現(xiàn)了一些基礎(chǔ)頁面配置(DecorView 里處理了狀態(tài)欄、導(dǎo)航欄听诸、一些主題設(shè)置等)坐求,實現(xiàn)了ViewTree的構(gòu)建(自定義的布局掛到DecorView某個子布局里),實現(xiàn)了將整個ViewTree添加到Window的操作晌梨。
大部分場景下桥嗤,我們僅僅只需要關(guān)注布局文件(R.layout.activity_main)的編寫與邏輯處理即可须妻,剩下的工作交給Activity處理,最終我們想要展示的效果將會在Window里呈現(xiàn)泛领。
更詳細的DecorView分析請移步:Android DecorView 一窺全貌(上)
5荒吏、Activity 與 Dialog/PopupWindow/Toast 該怎么選?
有時候我們并不需要一個完整的頁面渊鞋,僅僅需要一個彈框提示即可绰更,這個時候會考慮使用Dialog/PopupWindow/Toast 等組件。
請記住一個點:不管是什么樣的UI 組件锡宋,最終都需要通過WindowManager.add(xx)添加到Window里儡湾。
Dialog/PopupWindow/Toast 最終都是通過WindowManager.add(xx)加載的,只不過是它們設(shè)定的Window尺寸沒有占滿整個屏幕执俩,而是由外部設(shè)定的Window尺寸徐钠。
它們沒有生命周期,其中Dialog/PopupWindow 依賴于Activity 上下文(Context奠滑,Token限制)丹皱。
當不需要占滿屏幕、UI 簡單宋税、邏輯簡單摊崭、偏重于提示/選擇之類的場景時,Dialog/PopupWindow/Toast 是比較好的選擇杰赛。
至于它們內(nèi)部的區(qū)別呢簸,請移步:Dialog/PopupWindow/Toast 到底該怎么選
6、Activity 與Fragment 的聯(lián)系與區(qū)別乏屯。
有時我們想要在不同的case下顯示不同的頁面根时,例如頁面頂部有幾個Tab欄,新聞辰晕、娛樂蛤迎、學(xué)習(xí)等板塊,點擊不同的tab展示不同的頁面含友。用Activity 作為頁面承載的話有點大材小用替裆,并且過渡沒那么流暢。用Dialog的話因為沒有生命周期管控窘问,一些邏輯沒法閉環(huán)(比如后臺的網(wǎng)絡(luò)請求辆童,數(shù)據(jù)庫加載等)。
此時就需要考慮使用Fragment惠赫。
Fragment 有如下特點:
1把鉴、跟隨Activity 擁有生命周期。
2儿咱、將View 封裝并擁有獨立的處理邏輯庭砍。
3场晶、Fragment 構(gòu)建、切換速度比Activity 快怠缸。
但也有缺點:
1峰搪、生命周期復(fù)雜。
2凯旭、必須依賴于Activity。
更多Fragment 解析請移步:Android Fragment 要你何用使套?
7罐呼、一個串起來的小故事
1、WMS 能夠構(gòu)建并展示窗口侦高,它作為一個公共的服務(wù)嫉柴,需要提供給其它App(應(yīng)用)創(chuàng)建并填充窗口,于是他提供了Surface給其它App使用奉呛。
2计螺、應(yīng)用拿到Surface并從中取出Canvas后就可以繪制任意的圖形了。
3瞧壮、Canvas 繪制需要設(shè)定繪制的起點登馒,繪制的尺寸以及繪制的內(nèi)容,這些都是必經(jīng)之路咆槽,每次展示都需要設(shè)定這些參數(shù)有點冗余陈轿,于是View出現(xiàn)了。
4秦忿、View 封裝了測量麦射、布局、繪制 等操作灯谣,應(yīng)用開發(fā)者只需要關(guān)注如何繪制即可潜秋,甚至繪制都無需過多關(guān)心,比如TextView胎许、ImageView 都是系統(tǒng)封裝好了的峻呛,僅僅需要關(guān)注具體的內(nèi)容即可。
5呐萨、有一些布局的排列比較常見且規(guī)律杀饵,比如線性垂直排列View,這個時候就引入了ViewGroup谬擦,有了它各個View的順序切距、位置編排都能實現(xiàn),甚至我們都不需要關(guān)心這些惨远,比如LinearLayout谜悟、FrameLayout 都是系統(tǒng)封裝好了的排列規(guī)則话肖。
6、此時通過View/ViewGroup 已經(jīng)能夠填充Window的內(nèi)容了葡幸,但還是不夠最筒,因為還是要處理多個Window的交互,這個時候Activity 出現(xiàn)了蔚叨。
7床蜘、Activity 能夠設(shè)定默認的RootView(DecorView),我們僅僅只需要將布局文件關(guān)聯(lián)到Activity就可以有一個統(tǒng)一的展示風(fēng)格蔑水,比如統(tǒng)一的主題邢锯、標題欄等。
8搀别、Activity 承載越來越多的工作量丹擎,為了減輕它的負擔,PhoneWindow.java/Window.java 出現(xiàn)了歇父,能夠幫Activity 處理狀態(tài)欄蒂培、導(dǎo)航欄、事件分發(fā)等一些操作榜苫。
9护戳、Activity 還是太重了,一些場景并不適合单刁,比如只想彈出一個提示框灸异、選擇框等,殺雞焉用牛刀? 于是Dialog/PopupWindow/Toast 出現(xiàn)了羔飞。
10肺樟、而Dialog/PopupWindow/Toast 沒有生命周期,用Activity 直接控制View又增加了Activity的工作量逻淌,于是Fragment 幫助Activity 管理了一些較為獨立的View么伯。
本文基于Android 10.0
若有疑問或是想要了解更細節(jié)的知識,請留言或私信我卡儒。
您若喜歡田柔,請點贊、關(guān)注骨望,您的鼓勵是我前進的動力
持續(xù)更新中硬爆,和我一起步步為營系統(tǒng)、深入學(xué)習(xí)Android
1擎鸠、Android各種Context的前世今生
2缀磕、Android DecorView 必知必會
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ū)動Handler-Message-Looper解析
9遣疯、Android 鍵盤一招搞定
10、Android 各種坐標徹底明了
11凿傅、Android Activity/Window/View 的background
12缠犀、Android Activity創(chuàng)建到View的顯示過
13、Android IPC 系列
14聪舒、Android 存儲系列
15夭坪、Java 并發(fā)系列不再疑惑
16、Java 線程池系列
17过椎、Android Jetpack 前置基礎(chǔ)系列