<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                合規國際互聯網加速 OSASE為企業客戶提供高速穩定SD-WAN國際加速解決方案。 廣告
                # 第6章 深入理解控件(ViewRoot)系統(節選) 本章主要內容: + 介紹創建窗口的新的方法以及WindowManager的實現原理 + 探討ViewRootImpl的工作方式 + 討論控件樹的測量、布局與繪制 + 討論輸入事件在控件樹中的派發 + 介紹PhoneWindow的工作原理以及Activity窗口的創建方式 本章涉及的源代碼文件名及位置: + ContextImpl.java frameworks/base/core/java/android/app/ContextImpl.java + WindowManagerImpl.java frameworks/base/core/java/android/view/WindowManagerImpl.java + WindowManagerGlobal.java frameworks/base/core/java/android/view/WindowManagerGlobal.java + ViewRootImpl.java frameworks/base/core/java/android/view/ViewRootImpl.java + View.java frameworks/base/core/java/android/view/View.java + ViewGroup.java frameworks/base/core/java/android/view/ViewGroup.java + TabWidget.java frameworks/base/core/java/android/widget/TabWidget.java + HardwareRenderer.java frameworks/base/core/java/android/view/HardwareRenderer.java + FocusFinder.java frameworks/base/core/java/android/view/FocusFinder.java + Activity.java frameworks/base/core/java/android/app/Activity.java + PhoneWindow.java frameworks/base/policy/src/com/android/internal/policy/impl/PhoneWindow.java + Window.java frameworks/base/core/java/android/view/Window.java + ActivityThread.java frameworks/base/core/java/android/app/ActivityThread.java ## 6.1 初識Android的控件系統 第4章和第5章分別介紹了窗口的兩個最核心的內容:顯示與用戶輸入,同時也介紹了在Android中顯示一個窗口并接受輸入事件的最基本的方法。但是這種方法過于基本,不便于使用。直接使用Canvas繪制用戶界面以及使用InputEventReceiver處理用戶輸入是一件非常繁瑣惱人的工作,因為你不得不親歷親為以下復雜的工作: + 測量各個UI元素(一段文字、一個圖片)的顯示尺寸與位置。 + 對各個UI元素進行布局計算與繪制。 + 當顯示內容需要發生變化時進行重繪。出于效率考慮,你必須保證重繪區域盡可能地小。 + 分析InputEventReceiver所接收的事件的類型,并確定應該由哪個UI元素響應這個事件。 + 需要處理來自WMS的很多與窗口狀態相關的回調。 所幸Android的控件系統使得這些事情不需要我們親歷親為。 自1983年蘋果公司發布第一款搭載圖形用戶界面(GUI)操作系統的個人電腦Lisa以來的三十多年里,圖形用戶界面已經發展得相當成熟。無論是運行于桌面系統還是Web,每一個面向圖形用戶界面的開發工具包(SDK)都至少內置實現了用戶和開發者所公認的一套UI元素,盡管名稱可能有所差異。例如文本框、圖片框、列表框、組合框、按鈕、單選按鈕、多選按鈕,等等。Android的控件系統不僅延續了對各種標準UI元素的支持,還針對移動平臺的操作特點增加了使用更加方便、種類更加豐富的一系列新型的UI元素。 注意 在Android中,一個UI元素被稱為一個視圖(View),然而,筆者認為控件才是UI元素的更貼切的名字。因為UI元素不僅僅是為了向用戶顯示一些內容,更重要的是它們響應用戶的輸入并進行相應的工作。本書后續部分將以控件來稱呼UI元素(View)。 另外,本章的目的并不是介紹如何使用各種Android控件,而是介紹Android控件系統的工作原理。本章要求讀者至少應了解使用Android控件的基本知識。 讀者所熟知的Activity、各種對話框、彈出菜單、狀態欄與導航欄等等都是基于這套控件系統實現的。因此控件系統將是繼WMS與IMS兩大系統服務之后的又一個需要我們攻克的目標。 ### 6.1.1 另一種創建窗口的方法 在這一小節里將介紹另外一種創建窗口的方法,并以此為切入點來開始對Android控件系統的探討。 這個例子將會在屏幕中央顯示一個按鈕,它會浮在所有應用之上,直到用戶點擊它為止。市面上某些應用的懸浮窗就是如此實現的。 + 首先,讀者使用Eclipse建立一個新的Android工程,并新建一個Service。然后在這個Service中增加如下代碼: ``` // 將按鈕作為一個窗口添加到WMS中 private void installFloatingWindow() { ??? // ① 獲取一個WindowManager實例 ??? finalWindowManager wm = ??????????????????? (WindowManager)getSystemService(Context.WINDOW_SERVICE); ??? // ② 新建一個按鈕控件 ??? finalButton btn = new Button(this.getBaseContext()); ???btn.setText("Click me to dismiss!"); ??? // ③ 生成一個WindowManager.LayoutParams,用以描述窗口的類型與位置信息 ???LayoutParams lp = createLayoutParams(); ??? // ④ 通過WindowManager.addView()方法將按鈕作為一個窗口添加到系統中 ???wm.addView(btn, lp); ???btn.setOnClickListener(new View.OnClickListener() { ???????@Override ???????public void onClick(View v) { ?????????? ?// ⑤當用戶點擊按鈕時,將按鈕從系統中刪除 ???????????wm.removeView(btn); ???????????stopSelf(); ??????? } ??? }); } ??? privateLayoutParams createLayoutParams() { ???????LayoutParams lp = new WindowManager.LayoutParams(); ???????lp.type = LayoutParams.TYPE_PHONE; ???????lp.gravity = Gravity.CENTER; ???????lp.width = LayoutParams.WRAP_CONTENT; ???????lp.height = LayoutParams.WRAP_CONTENT; ???????lp.flags = LayoutParams.FLAG_NOT_FOCUSABLE ???????????????| LayoutParams.FLAG_NOT_TOUCH_MODAL; ???????return lp; ??? } ``` + 然后在新建的Service的onStartCommand()函數中增加對installFloatingWindow()的調用。 + 在應用程序的主Activity的onCreate()函數中調用startService()以啟動這個服務。 + 在應用程序的AndroidManifest.xml中增加對權限android.permission.SYSTEM_ALERT_WINDOW的使用聲明。 當完成這些工作之后,運行這個應用即可得到如圖6-1所示的效果。一個名為“Clickme to dismiss!”的按鈕浮在其他應用之上。而點擊這個按鈕后,它便消失了。 ![](https://box.kancloud.cn/2016-03-01_56d567b369ff0.png) 圖 6 - 1浮動窗口例子的運行效果 讀者可以將本例與第4章的例子SampleWindow做一個對比。它們的實現效果是大同小異的。而然,本章的這個例子無論是從最終效果、代碼量、API的復雜度或可讀性上都有很大的優勢。這得益于對控件系統的使用。在這里,控件Button托管了窗口的繪制過程,并且將輸入事件封裝為了更具可讀性的回調。并且添加窗口時所使用的WindowManager實例掩蓋了客戶端與WMS交互的復雜性。更重要的是,本例所使用的接口都來自公開的API,也就是說可以脫離Android源碼進行編譯。這無疑會帶來更方便的開發過程以及更好的程序兼容性。 因此,除非需要進行很底層的窗口控制,使用本例所介紹的方法向系統中添加窗口是最優的選擇。 ### 6.1.2 控件系統的組成 從這個例子中可以看到在添加窗口過程中的兩個關鍵組件:Button和WindowManager。Button是控件的一種,繼承自View類。不只Button,任何一個繼承自View類的控件都可以作為一個窗口添加到系統中去。WindowManager其實是一個繼承自ViewManager的接口,它提供了添加/刪除窗口,更新窗口布局的API,可以看作是WMS在客戶端的代理類。不過WindowManager的接口與WMS的接口相差很大,幾乎已經無法通過WindowManager看到WMS的模樣。這也說明了WindowManager為了精簡WMS的接口做過大量的工作。這部分內容也是本章的重點。 因此控件系統便可以分為繼承自View類的一系列控件類與WindowManager兩個部分。 ## 6.2 深入理解WindowManager WindowManager的主要功能是提供簡單的API使得使用者可以方便地將一個控件作為一個窗口添加到系統中。本節將探討它工作原理。 ### 6.2.1 WindowManager的創建與體系結構 首先需要搞清楚WindowManager是什么。 準確的說,WindowManager是一個繼承自ViewManager的接口。ViewManager定義了三個函數,分別用于添加/刪除一個控件,以及更新控件的布局。 ViewManager接口的另一個實現者是ViewGroup,它是容器類控件的基類,用于將一組控件容納到自身的區域中,這一組控件被稱為子控件。ViewGroup可以根據子控件的布局參數(LayoutParams)在其自身的區域中對子控件進行布局。 讀者可以將WindowManager與ViewGroup進行一下類比:設想WindowManager是一個ViewGroup,其區域為整個屏幕,而其中的各個窗口就是一個一個的View。WindowManager通過WMS的幫助將這些View按照其布局參數(LayoutParams)將其顯示到屏幕的特定位置。二者的核心工作是一樣的,因此WindowManager與ViewGroup都繼承自ViewManager。 接下來看一下WindowManager接口的實現者。本章最開始的例子通過Context.getSystemService(Context.WINDOW_SERVICE)的方式獲取了一個WindowManager的實例,其實現如下: ``` [ContextImpl.java-->ContextImpl.getSystemService()] public Object getSystemService(String name) { ??? // 獲取WINDOW_SERVICE所對應的ServiceFetcher ???ServiceFetcher fetcher = SYSTEM_SERVICE_MAP.get(name); ??? // 調用fetcher.getService()獲取一個實例 ??? returnfetcher == null ? null : fetcher.getService(this); } ``` Context的實現者ContextImpl在其靜態構造函數中初始化了一系列的ServiceFetcher來響應getSystemService()的調用并創建對應的服務實例。看一下WINDOW_SERVICE所對應的ServiceFetcher的實現: ``` [ContextImpl.java-->ContextImpl.static()] registerService(WINDOW_SERVICE, newServiceFetcher() { ???????????public Object getService(ContextImpl ctx) { ???????????????// ① 獲取Context中所保存的Display對象 ???????????????Display display = ctx.mDisplay; ???????????????/* ② 倘若Context中沒有保存任何Display對象,則通過DisplayManager獲取系統 ??????????????????**主屏幕所對應的Display對象** */ ???????????????if (display == null) { ???????????????????DisplayManager dm = ??????????????????????????? (DisplayManager)ctx.getOuterContext().getSystemService( ???????????????????????????????????????????????????????????????????Context.DISPLAY_SERVICE); ???????????????????display = dm.getDisplay(Display.DEFAULT_DISPLAY); ???????????????} ???????????????// ③ 使用Display對象作為構造函數創建一個WindowManagerImpl對象并返回 ???????????????return new WindowManagerImpl(display); ???????????}}); ``` 由此可見,通過Context.getSystemService()的方式獲取的WindowManager其實是WindowManagerImpl類的一個實例。這個實例的構造依賴于一個Display對象。第4章介紹過DisplayContent的概念,它在WMS中表示一塊的屏幕。而這里的Display對象與DisplayContent的意義是一樣的,也用來表示一塊屏幕。 再看一下WindowManagerImpl的構造函數: ``` [WindowManagerImpl.java-->WindowManagerImpl.WindowManagerImpl()] ??? publicWindowManagerImpl(Display display) { ???????this(display, null); ??? } ??? privateWindowManagerImpl(Display display, Window parentWindow) { ???????mDisplay = display; ???????mParentWindow = parentWindow; ??? } ``` 其構造函數實在是出奇的簡單,僅僅初始化了mDisplay與mParentWindow兩個成員變量而已。從這兩個成員變量的名字與類型來推斷,它們將決定通過這個WindowManagerImpl實例所添加的窗口的歸屬。 說明 WindowManagerImpl的構造函數引入了一個Window類型的參數parentWindow。Window類是什么呢?以Activity為例,一個Activity顯示在屏幕上時包含了標題欄、菜單按鈕等控件,但是在setContentView()時并沒有在layout中放置它們。這是因為Window類預先為我們準備好了這一切,它們被稱之為窗口裝飾。除了產生窗口裝飾之外,Window類還保存了窗口相關的一些重要信息。例如窗口ID(IWindow.asBinder()的返回值)以及窗口所屬Activity的ID(即AppToken)。在6.6.1 介將會對這個類做詳細的介紹。 也許在WindowManagerImpl的addView()函數的實現中可以找到更多的信息。 ``` [WindowManagerImpl.java-->WindowManagerImpl.addView()] ??? publicvoid addView(View view, ViewGroup.LayoutParams params) { ???????mGlobal.addView(view, params, mDisplay, mParentWindow); ??? } ``` WindowManagerImpl.addView()將實際的操作委托給一個名為mGlobal的成員來完成,它隨著WindowManagerImpl的創建而被初始化: ``` ??? privatefinal WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance(); ``` 可見mGlobal的類型是WindowManagerGlobal,而且WindowManagerGlobal是一個單例模式——即一個進程中最多僅有一個WindowManagerGlobal實例。所有WindowManagerImpl都是這個進程唯一的WindowManagerGlobal實例的代理。 此時便對WindowManager的結構體系有了一個清晰的認識,如圖6-2所示。 ![](https://box.kancloud.cn/2016-03-01_56d567b37e6da.png) 圖 6 - 2 WindowManager的結構體系 + ViewManager接口:WindowManager體系中最基本的接口。WindowManager繼承自這個接口說明了WindowManager與ViewGroup本質上的一致性。 + WindowManager接口:WindowManager接口繼承自ViewManager接口的同時,根據窗口的一些特殊性增加了兩個新的接口。getDefaultDisplay()用以得知這個WindowManager的實例會將窗口添加到哪個屏幕上去。而removeViewImmediate()則要求WindowManager必須在這個調用返回之前完成所有的銷毀工作。 + WindowManagerImpl類:WindowManager接口的實現者。它自身沒有什么實際的邏輯,WindowManager所定義的接口都是交由WindowManagerGlobal完成的。但是它保存了兩個重要的只讀成員,它們分別指明了通過這個WindowManagerImpl實例所管理的窗口將被顯示在哪個屏幕上,以及將會作為哪個窗口的子窗口。因此在一個進程中,WindowManagerImpl的實例可能有多個。 + WindowManagerGlobal類:它沒有繼承上述任何一個接口,但它是WindowManager的最終實現者。它維護了當前進程中所有已經添加到系統中的窗口的信息。另外,在一個進程中僅有一個WindowManagerGlobal的實例。 在理清了WindowManager的結構體系后,便可以探討WindowManager是如何完成窗口管理的。其管理方式體現在其對ViewManager的三個接口的實現上。為了簡潔起見,我們將直接分析WindowManagerGlobal中的實現。 ### 6.2.2 通過WindowManagerGlobal添加窗口 參考WindowManagerGlobal.addView()的代碼: ``` [WindowManagerGlobal.java-->WindowManagerGlobal.addView()] ??? publicvoid addView(View view, ViewGroup.LayoutParams params, ?? ?????????Display display, Window parentWindow){ ??????? ......// 參數檢查 ???????final WindowManager.LayoutParams wparams =(WindowManager.LayoutParams)params; ??????? /* ① 如果當前窗口需要被添加為另一個窗口的附屬窗口(子窗口),則需要讓父窗口視自己的情況 ????????????對當前窗口的布局參數(LayoutParams)進行一些修改 */ ??????? if(parentWindow != null) { ???????????parentWindow.adjustLayoutParamsForSubWindow(wparams); ??????? } ???????ViewRootImpl root; ??????? ViewpanelParentView = null; ???????synchronized (mLock) { ??????????? ...... ???????????// WindowManager不允許同一個View被添加兩次 ???????????int index = findViewLocked(view, false); ???????????if (index &gt;= 0) { throw new IllegalStateException("......");} ???????????// ② 創建一個ViewRootImpl對象并保存在root變量中 ???????????root = new ViewRootImpl(view.getContext(), display); ???????????view.setLayoutParams(wparams); ???????????/* ③ 將作為窗口的控件、布局參數以及新建的ViewRootImpl以相同的索引值保存在三個 ??????????????**數組中。**到這步為止,我們可以認為完成了窗口信息的添加工作 */ ???????????mViews[index] = view; ???? ???????mRoots[index] = root; ???????????mParams[index] = wparams; ??????? } ??????? try{ ???????????/* **④ 將作為窗口的控件設置給ViewRootImpl。**這個動作將導致ViewRootImpl向WMS ???????????????添加新的窗口、申請Surface以及托管控件在Surface上的重繪動作。這才是真正意義上 ??????????????? 完成了窗口的添加操作*/ ???????????root.setView(view, wparams, panelParentView); ??????? }catch (RuntimeException e) { ...... } ??? } ``` 添加窗口的代碼并不復雜。其中的關鍵點有: + 父窗口修改新窗口的布局參數。可能修改的只有LayoutParams.token和LayoutParams.mTitle兩個屬性。mTitle屬性不必贅述,僅用于調試。而token屬性則值得一提。回顧一下第4章的內容,每一個新窗口必須通過LayoutParams.token向WMS出示相應的令牌才可以。在addView()函數中通過父窗口修改這個token屬性的目的是為了減少開發者的負擔。開發者不需要關心token到底應該被設置為什么值,只需將LayoutParams丟給一個WindowManager,剩下的事情就不用再關心了。父窗口修改token屬性的原則是:如果新窗口的類型為子窗口(其類型大于等于LayoutParams.FIRST_SUB_WINDOW并小于等于LayoutParams.LAST_SUB_WINDOW),則LayoutParams.token所持有的令牌為其父窗口的ID(也就是IWindow.asBinder()的返回值)。否則LayoutParams.token將被修改為父窗口所屬的Activity的ID(也就是在第4章中所介紹的AppToken),這對類型為TYPE_APPLICATION的新窗口來說非常重要。從這點來說,當且僅當新窗的類型為子窗口時addView()的parentWindow參數才是真正意義上的父窗口。這類子窗口有上下文菜單、彈出式菜單以及游標等等,在WMS中,這些窗口對應的WindowState所保存的mAttachedWindow既是parentWindow所對應的WindowState。然而另外還有一些窗口,如對話框窗口,類型為TYPE_APPLICATION,?并不屬于子窗口,但需要AppToken作為其令牌,為此parentWindow將自己的AppToken賦予了新窗口的的LayoutParams.token中。此時parentWindow便并不是嚴格意義上的父窗口了。 + 為新窗口創建一個ViewRootImpl對象。顧名思義,ViewRootImpl實現了一個控件樹的根。它負責與WMS進行直接的通訊,負責管理Surface,負責觸發控件的測量與布局,負責觸發控件的繪制,同時也是輸入事件的中轉站。總之,ViewRootImpl是整個控件系統正常運轉的動力所在,無疑是本章最關鍵的一個組件。 + 將控件、布局參數以及新建的ViewRootImpl以相同的索引值添加到三個對應的數組mViews、mParams以及mRoots中,以供之后的查詢之需。控件、布局參數以及ViewRootImpl三者共同組成了客戶端的一個窗口。或者說,在控件系統中的窗口就是控件、布局參數與ViewRootImpl對象的一個三元組。 注意 筆者并不認同將這個三元組分別存儲在三個數組中的設計。如果創建一個WindowRecord類來統一保存這個三元組將可以省去很多麻煩。 另外,mViews、mParams以及mRoots這三個數組的容量是隨著當前進程中的窗口數量的變化而變化的。因此在addView()以及隨后的removeView()中都伴隨著數組的新建、拷貝等操作。鑒于一個進程所添加的窗口數量不會太多,而且也不會很頻繁,所以這些時間開銷是可以接受的。不過筆者仍然認為相對于數組,ArrayList或CopyOnWriteArrayList是更好的選擇。 + 調用ViewRootImpl.setView()函數,將控件交給ViewRootImpl進行托管。這個動作將使得ViewRootImpl向WMS添加窗口、獲取Surface以及重繪等一系列的操作。這一步是控件能夠作為一個窗口顯示在屏幕上的根本原因! 總體來說,WindowManagerGlobal在通過父窗口調整了布局參數之后,將新建的ViewRootImpl、控件以及布局參數保存在自己的三個數組中,然后將控件交由新建的ViewRootImpl進行托管,從而完成了窗口的添加。WindowManagerGlobal管理窗口的原理如圖6-3所示。 ![](https://box.kancloud.cn/2016-03-01_56d567b393c1a.png) 圖 6 - 3 WindowManagerGlobal的窗口管理 ### 6.2.3 更新窗口的布局 ViewManager所定義的另外一個功能就是更新View的布局。在WindowManager中,則是更新窗口的布局。窗口的布局參數發生變化時,如LayoutParams.width從100變為了200,則需要將這個變化通知給WMS使其調整Surface的大小,并讓窗口進行重繪。這個工作在WindowManagerGlobal中由updateViewLayout()函數完成。 ``` [WindowManagerGlobal.java-->WindowManagerGlobal.updateViewLayout()] ??? publicvoid updateViewLayout(View view, ViewGroup.LayoutParams params) { ??????? ......// 參數檢查 ???????final WindowManager.LayoutParams wparams =(WindowManager.LayoutParams)params; ??????? // 將布局參數保存到控件中 ???????view.setLayoutParams(wparams); ???????synchronized (mLock) { ???????????// 獲取窗口在三個數組中的索引 ???????????int index = findViewLocked(view, true); ???????????ViewRootImpl root = mRoots[index]; ?????????? ?// 更新布局參數到數組中 ???????????mParams[index] = wparams; ???????????// 調用ViewRootImpl的setLayoutParams()使得新的布局參數生效 ???????????root.setLayoutParams(wparams, false); ??????? } ??? } ``` 更新窗口布局的工作在WindowManagerGlobal中是非常簡單的,主要是保存新的布局參數,然后調用ViewRootImpl.setLayoutParams()進行更新。 ### 6.2.3 刪除窗口 接下來探討窗口的刪除操作。在了解了WindowManagerGlobal管理窗口的方式后應該可以很容易地推斷出刪除窗口所需要做的工作: + 從3個數組中刪除此窗口所對應的元素,包括控件、布局參數以及ViewRootImpl。 + 要求ViewRootImpl從WMS中刪除對應的窗口(IWindow),并釋放一切需要回收的資源。 這個過程十分簡單,這里就不引用相關的代碼了。只是有一點需要說明一下:要求ViewRootImpl從WMS中刪除窗口并釋放資源的方法是調用ViewRootImpl.die()函數。因此可以得出這樣一個結論:ViewRootImpl的生命從setView()開始,到die()結束。 ### 6.2.4 WindowManager的總結 經過前文的分析,相信讀者對WindowManager的工作原理有了深入的認識。 + 鑒于窗口布局和控件布局的一致性,WindowManager繼承并實現了接口ViewManager。 + 使用者可以通過Context.getSystemService(Context.WINDOW_SERVICE)來獲取一個WindowManager的實例。這個實例的真實類型是WindowManagerImpl。WindowManagerImpl一旦被創建就確定了通過它所創建的窗口所屬哪塊屏幕?哪個父窗口? + WindowManagerImpl除了保存了窗口所屬的屏幕以及父窗口以外,沒有任何實質性的工作。窗口的管理都交由WindowManagerGlobal的實例完成。 + WindowManagerGlobal在一個進程中只有一個實例。 + WindowManagerGlobal在3個數組中統一管理整個進程中的所有窗口的信息。這些信息包括控件、布局參數以及ViewRootImpl三個元素。 + 除了管理窗口的上述3個元素以外,WindowManagerGlobal將窗口的創建、銷毀與布局更新等任務交付給了ViewRootImpl完成。 說明 在實際的應用開發過程中,有時會在logcat的輸出中遇到有關WindowLeaked的異常輸出。WindowLeaked異常發生與WindowManagerGlobal中,其原因是Activity在destroy之前沒有銷毀其附屬窗口,如對話框、彈出菜單等。 如此看來,WindowManager的實現仍然是很輕量的。窗口的創建、銷毀與布局更新都指向了一個組件:ViewRootImpl。 ## 6.3 深入理解ViewRootImpl ViewRootImpl實現了ViewParent接口,作為整個控件樹的根部,它是控件樹正常運作的動力所在,控件的測量、布局、繪制以及輸入事件的派發處理都由ViewRootImpl觸發。另一方面,它是WindowManagerGlobal工作的實際實現者,因此它還需要負責與WMS交互通信以調整窗口的位置大小,以及對來自WMS的事件(如窗口尺寸改變等)作出相應的處理。 本節將對ViewRootImpl的實現做深入的探討。 ### 6.3.1 ViewRootImpl的創建及其重要的成員 ViewRootImpl創建于WindowManagerGlobal的addView()方法中,而調用addView()方法的線程即是此ViewRootImpl所掌控的控件樹的UI線程。ViewRootImpl的構造主要是初始化了一些重要的成員,事先對這些重要的成員有個初步的認識對隨后探討ViewRootImpl的工作原理有很大的幫助。其構造函數代碼如下: ``` [ViewRootImpl.java-->ViewRootImpl.ViewRootImpl()] public ViewRootImpl(Context context, Displaydisplay) { ??? /* ① 從WindowManagerGlobal中獲取一個IWindowSession的實例。它是ViewRootImpl和 ????? WMS進行通信的代理 */ ??? mWindowSession= WindowManagerGlobal.getWindowSession(context.getMainLooper()); ??? // **②保存參數display**,在后面setView()調用中將會把窗口添加到這個Display上 ??? mDisplay= display; ???CompatibilityInfoHolder cih = display.getCompatibilityInfo(); ???mCompatibilityInfo = cih != null ? cih : new CompatibilityInfoHolder(); ??? /* **③ 保存當前線程到mThread。**這個賦值操作體現了創建ViewRootImpl的線程如何成為UI主線程。 ????? 在ViewRootImpl處理來自控件樹的請求時(如請求重新布局,請求重繪,改變焦點等),會檢 ????? 查發起請求的thread與這個mThread是否相同。倘若不同則會拒絕這個請求并拋出一個異常*/ ??? mThread= Thread.currentThread(); ??? ...... ??? /* **④ mDirty用于收集窗口中的無效區域。**所謂無效區域是指由于數據或狀態發生改變時而需要進行重繪 ????? 的區域。舉例說明,當應用程序修改了一個TextView的文字時,TextView會將自己的區域標記為無效 ????? 區域,并通過invalidate()方法將這塊區域收集到這里的mDirty中。當下次繪制時,TextView便 ????? 可以將新的文字繪制在這塊區域上 */ ??? mDirty =new Rect(); ???mTempRect = new Rect(); ??? mVisRect= new Rect(); ??? /* **⑤ mWinFrame,描述了當前窗口的位置和尺寸。**與WMS中WindowState.mFrame保持著一致 */ ???mWinFrame = new Rect(); ??? /* ⑥ 創建一個W類型的實例,W是IWindow.Stub的子類。即它將在WMS中作為新窗口的ID,以及接 ????? 收來自WMS的回調*/ ??? mWindow= new W(this); ??? ...... ??? /* **⑦ 創建mAttachInfo。**mAttachInfo是控件系統中很重要的對象。它存儲了此當前控件樹所以貼附 ?? ???的窗口的各種有用的信息,并且會派發給控件樹中的每一個控件。這些控件會將這個對象保存在自己的 ?????mAttachInfo變量中。mAttachInfo中所保存的信息有WindowSession,窗口的實例(即mWindow), ????? ViewRootImpl實例,窗口所屬的Display,窗口的Surface以及窗口在屏幕上的位置等等。所以,當 ????? 要需在一個View中查詢與當前窗口相關的信息時,非常值得在mAttachInfo中搜索一下 */ ???mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display,this, mHandler, this); ??? /* **⑧ 創建FallbackEventHandler。**這個類如同PhoneWindowManger一樣定義在android.policy ????? 包中,其實現為PhoneFallbackEventHandler。FallbackEventHandler是一個處理未經任何人 ????? 消費的輸入事件的場所。在6.5.4節中將會介紹它 */ ???mFallbackEventHandler =PolicyManager.makeNewFallbackEventHandler(context); ??? ...... ??? /* ⑨ 創建一個依附于當前線程,即主線程的Choreographer,用于通過VSYNC特性安排重繪行為 */ ??? mChoreographer= Choreographer.getInstance(); ??? ...... } ``` 在構造函數之外,還有另外兩個重要的成員被直接初始化: + mHandler,類型為ViewRootHandler,一個依附于創建ViewRootImpl的線程,即主線程上的,用于將某些必須主線程進行的操作安排在主線程中執行。mHandler與mChoreographer的同時存在看似有些重復,其實它們擁有明確不同的分工與意義。由于mChoreographer處理消息時具有VSYNC特性,因此它主要用于處理與重繪相關的操作。但是由于mChoreographer需要等待VSYNC的垂直同步事件來觸發對下一條消息的處理,因此它處理消息的及時性稍遜于mHandler。而mHandler的作用,則是為了將發生在其他線程中的事件安排在主線程上執行。所謂發生在其他線程中的事件是指來自于WMS,由繼承自IWindow.Stub的mWindow引發的回調。由于mWindow是一個Binder對象的Bn端,因此這些回調發生在Binder的線程池中。而這些回調會影響到控件系統的重新測量、布局與繪制,因此需要此Handler將回調安排到主線程中。 說明 mHandler與mThread兩個成員都是為了單線程模型而存在的。Android的UI操作不是線程安全的,而且很多操作也是建立在單線程的假設之上(如scheduleTraversals())。采用單線程模型的目的是降低系統的復雜度,并且降低鎖的開銷。 + mSurface,類型為Surface。采用無參構造函數創建的一個Surface實例。mSurface此時是一個沒有任何內容的空殼子,在 WMS通過relayoutWindow()為其分配一塊Surface之前尚不能實用。 + mWinFrame、mPendingContentInset、mPendingVisibleInset以及mWidth,mHeight。這幾個成員存儲了窗口布局相關的信息。其中mWinFrame、mPendingConentInsets、mPendingVisibleInsets與窗口在WMS中的Frame、ContentInsets、VisibleInsets是保持同步的。這是因為這3個成員不僅會作為 relayoutWindow()的傳出參數,而且ViewRootImpl在收到來自WMS的回調IWindow.Stub.resize()時,立即更新這3個成員的取值。因此這3個成員體現了窗口在WMS中的最新狀態。與mWinFrame中的記錄窗口在WMS中的尺寸不同的是,mWidth/mHeight記錄了窗口在ViewRootImpl中的尺寸,二者在絕大多數情況下是相同的。當窗口在WMS中被重新布局而導致尺寸發生變化時,mWinFrame會首先被IWindow.Stub.resize()回調更新,此時mWinFrame便會與mWidth/mHeight產生差異。此時ViewRootImpl即可得知需要對控件樹進行重新布局以適應新的窗口變化。在布局完成后,mWidth/mHeight會被賦值為mWinFrame中所保存的寬和高,二者重新統一。在隨后分析performTraversals()方法時,讀者將會看到這一處理。另外,與mWidth/mHeight類似,ViewRootImpl也保存了窗口的位置信息Left/Top以及ContentInsets/VisibleInsets供控件樹查詢,不過這四項信息被保存在了mAttachInfo中。 ViewRootImpl的在其構造函數中初始化了一系列的成員變量,然而其創建過程仍未完成。僅在為其指定了一個控件樹進行管理,并向WMS添加了一個新的窗口之后,ViewRootImpl承上啟下的角色才算完全確立下來。因此需要進一步分析ViewRootImpl.setView()方法。 ``` [ViewRootImp.java-->ViewRootImpl.setView()] public void setView(View view,WindowManager.LayoutParams attrs, View panelParentView) { ???synchronized (this) { ??????? if (mView == null) { ???????????// **① mView保存了控件樹的根** ???????????mView = view; ???????????...... ???????????// ②mWindowAttributes保存了窗口所對應的LayoutParams ???????????mWindowAttributes.copyFrom(attrs); ?????? ?????...... ???????????/* 在添加窗口之前,先通過requestLayout()方法在主線程上安排一次“遍歷”。所謂 ?????????????“遍歷”是指ViewRootImpl中的核心方法performTraversals()。這個方法實現了對 ??????????????控件樹進行測量、布局、向WMS申請修改窗口屬性以及重繪的所有工作。由于此“遍歷” ??????????????操作對于初次遍歷做了一些特殊處理,而來自WMS通過mWindow發生的回調會導致一些屬性 ??????????????發生變化,如窗口的尺寸、Insets以及窗口焦點等,從而有可能使得初次“遍歷”的現場遭 ??????????????到破壞。因此,需要在添加窗口之前,先發送一個“遍歷”消息到主線程。 ?????????????? 在主線程中向主線程的Handler發送消息如果使用得當,可以產生很精妙的效果。例如本例 ??????????????中可以實現如下的執行順序:添加窗口->初次遍歷->處理來自WMS的回調 */ ???????????requestLayout(); ???????????/***③ 初始化mInputChannel。**參考第五章,InputChannel是窗口接受來自InputDispatcher ?????????????的輸入事件的管道。 注意,僅當窗口的屬性inputFeatures不含有 ?????????????INPUT_FEATURE_NO_INPUT_CHANNEL時才會創建InputChannel,否則mInputChannel ?????????????為空,從而導致此窗口無法接受任何輸入事件 */ ???????????if ((mWindowAttributes.inputFeatures ???????????????????& WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) { ???????????????mInputChannel = new InputChannel(); ???????????} ???????????try { ???????????????...... ???????????????/* 將窗口添加到WMS中。完成這個操作之后,mWindow已經被添加到指定的Display中去 ?????????????????而且mInputChannel(如果不為空)已經準備好接受事件了。只是由于這個窗口沒有進行 ?????????????????過relayout(),因此它還沒有有效的Surface可以進行繪制 */ ???????????????res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes, ??????????????????????? getHostVisibility(), mDisplay.getDisplayId(), ???????????????????????mAttachInfo.mContentInsets, mInputChannel); ???????????} catch (RemoteException e) {......} finally { ...... } ???????????...... ???????????if (res &lt; WindowManagerGlobal.ADD_OKAY) { ???????????????// 錯誤處理。窗口添加失敗的原因通常是權限問題,重復添加,或者tokeen無效 ???????????} ???????????...... ??????????? /*④ 如果mInputChannel不為空,則創建mInputEventReceiver,用于接受輸入事件。 ?????????????注意第二個參數傳遞的是Looper.myLooper(),即mInputEventReceiver將在主線程上 ?????????????觸發輸入事件的讀取與onInputEvent()。這是應用程序可以在onTouch()等事件響應中 ?????????????直接進行UI操作等根本原因。 ?????????? ?*/ ???????????if (mInputChannel != null) { ???????????????...... ???????????????mInputEventReceiver = new WindowInputEventReceiver(mInputChannel, ???????????????????????????Looper.myLooper()); ???????????} ??????????/* ViewRootImpl將作為參數view的parent。所以,ViewRootImpl可以從控件樹中任何一個 ????????????控件開始,通過回溯getParent()的方法得到 */ ???????????view.assignParent(this); ??????????? ...... ??????? } ??? } } ``` 至此,ViewRootImpl所有重要的成員都已經初始化完畢,新的窗口也已經添加到WMS中。ViewRootImpl的創建過程是由構造函數和setView()方法兩個環節構成的。其中構造函數主要進行成員的初始化,setView()則是創建窗口、建立輸入事件接收機制的場所。同時,觸發第一次“遍歷”操作的消息已經發送給主線程,在隨后的第一次“遍歷”完成后,ViewRootImpl將會完成對控件樹的第一次測量、布局,并從WMS獲取窗口的Surface以進行控件樹的初次繪制工作。 在本節的最后,通過圖 6 – 4對ViewRootImpl中的重要成員進行了分類整理。 ![](https://box.kancloud.cn/2016-03-01_56d567b3adac7.png) 圖 6 - 4 ViewRootImpl中的主要成員 ### 6.3.2 控件系統的心跳:performTraversals() ViewRootImpl在其創建過程中通過requestLayout()向主線程發送了一條觸發“遍歷”操作的消息,“遍歷”操作是指performTraversals()方法。它的性質與WMS中的performLayoutAndPlaceSurfacesLocked()類似,是一個包羅萬象的方法。ViewRootImpl中接收到的各種變化,如來自WMS的窗口屬性變化,來自控件樹的尺寸變化、重繪請求等都引發performTraversals()的調用,并在其中完成處理。View類及其子類中的onMeasure()、onLayout()以及onDraw()等回調也都是在performTraversals()的執行過程中直接或間接地引發。也正是如此,一次次的performTraversals()調用驅動著控件樹有條不紊地工作著,一旦此方法無法正常執行,整個控件樹都將處于僵死狀態。因此,performTraversals()函數可謂是ViewRootImpl的心跳。 由于布局的相關工作是此方法中最主要的內容,為了簡化分析,并突出此方法的工作流程,本節將以布局的相關工作為主線進行探討。待完成了這部分內容的分析之后,龐大的performTraversals()方法將不再那么難以馴服,讀者便可以輕易地學習其他的工作了。 #### 1.performTraversals()的工作階段 performTraversals()是Android 源碼中最龐大的方法之一,因此在正式探討它的實現之前最好先將其劃分為以下幾個工作階段作為指導。 + 預測量階段。這是進入performTraversals()方法后的第一個階段,它會對控件樹進行第一次測量。測量結果可以通過mView. getMeasuredWidth()/Height()獲得。在此階段中將會計算出控件樹為顯示其內容所需的尺寸,即期望的窗口尺寸。在這個階段中,View及其子類的onMeasure()方法將會沿著控件樹依次得到回調。 + 布局窗口階段。根據預測量的結果,通過IWindowSession.relayout()方法向WMS請求調整窗口的尺寸等屬性,這將引發WMS對窗口進行重新布局,并將布局結果返回給ViewRootImpl。 + 最終測量階段。預測量的結果是控件樹所期望的窗口尺寸。然而由于在WMS中影響窗口布局的因素很多(參考第4章),WMS不一定會將窗口準確地布局為控件樹所要求的尺寸,而迫于WMS作為系統服務的強勢地位,控件樹不得不接受WMS的布局結果。因此在這一階段,performTraversals()將以窗口的實際尺寸對控件進行最終測量。在這個階段中,View及其子類的onMeasure()方法將會沿著控件樹依次被回調。 + 布局控件樹階段。完成最終測量之后便可以對控件樹進行布局了。測量確定的是控件的尺寸,而布局則是確定控件的位置。在這個階段中,View及其子類的onLayout()方法將會被回調。 + 繪制階段。這是performTraversals()的最終階段。確定了控件的位置與尺寸后,便可以對控件樹進行繪制了。在這個階段中,View及其子類的onDraw()方法將會被回調。 說明 很多文章都傾向于將performTraversals()的工作劃分為測量、布局與繪制三個階段。然而筆者認為如此劃分隱藏了WMS在這個過程中的地位,并且沒能體現出控件樹對窗口尺寸的期望、WMS對窗口尺寸做最終的確定,最后以WMS給出的結果為準再次進行測量的協商過程。而這個協商過程充分體現了ViewRootImpl作為WMS與控件樹的中間人的角色。 接下來將結合代碼,對上述五個階段進行深入的分析。 #### 2.預測量與測量原理 本節將探討performTraversals()將以何種方式對控件樹進行預測量,同時,本節也會對控件的測量過程與原理進行介紹。 ##### 預測量參數的候選 預測量也是一次完整的測量過程,它與最終測量的區別僅在于參數不同而已。實際的測量工作在View或其子類的onMeasure()方法中完成,并且其測量結果需要受限于來自其父控件的指示。這個指示由onMeasure()方法的兩個參數進行傳達:widthSpec與heightSpec。它們是被稱為MeasureSpec的復合整型變量,用于指導控件對自身進行測量。它有兩個分量,結構如圖6-5所示。 ![](https://box.kancloud.cn/2016-03-01_56d567b3c170c.png) 圖 6 - 5 MeasureSpec的結構 其1到30位給出了父控件建議尺寸。建議尺寸對測量結果的影響依不同的SPEC_MODE的不同而不同。SPEC_MODE的取值取決于此控件的LayoutParams.width/height的設置,可以是如下三種值之一。 + MeasureSpec.UNSPECIFIED (0):表示控件在進行測量時,可以無視SPEC_SIZE的值。控件可以是它所期望的任意尺寸。 + MeasureSpec.EXACTLY (1):表示子控件必須為SPEC_SIZE所制定的尺寸。當控件的LayoutParams.width/height為一確定值,或者是MATCH_PARENT時,對應的MeasureSpec參數會使用這個SPEC_MODE。 + MeasureSpec.AT_MOST (2):表示子控件可以是它所期望的尺寸,但是不得大于SPEC_SIZE。當控件的LayoutParams.width/height為WRAP_CONTENT時,對應的MeasureSpec參數會使用這個SPEC_MODE。 Android提供了一個MeasureSpec類用于組合兩個分量成為一個MeasureSpec,或者從MeasureSpec中分離任何一個分量。 那么ViewRootImpl會如何為控件樹的根mView準備其MeasureSpec呢? 參考如下代碼,注意desiredWindowWidth/Height的取值,它們將是SPEC_SIZE分量的候選。另外,這段代碼分析中也解釋了與測量無關,但是比較重要的代碼段。 ``` [ViewRootImpl.java-->ViewRootImpl.performTraversals()] private void performTraversals() { ??? // 將mView保存在局部變量host中,以此提高對mView的訪問效率 ??? finalView host = mView; ??? ...... ??? // 聲明本階段的主角,這兩個變量將是mView的SPEC_SIZE分量的候選 ??? intdesiredWindowWidth; ??? intdesiredWindowHeight; ??? ....... ??? Rectframe = mWinFrame; // 如上一節所述,mWinFrame表示了窗口的最新尺寸 ??? if(mFirst) { ??????? /*mFirst表示了這是第一次遍歷,此時窗口剛剛被添加到WMS,此時窗口尚未進行relayout,因此 ??? ??????mWinFrame中沒有存儲有效地窗口尺寸 */ ??????? if(lp.type == WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL) { ??????????? ......// 為狀態欄設置desiredWindowWidth/Height,其取值是屏幕尺寸 ??????? }else { ??????????? //① 第一次“遍歷”的測量,采用了應用可以使用的最大尺寸作為SPEC_SIZE的候選 ???????????DisplayMetrics packageMetrics = ???????????????mView.getContext().getResources().getDisplayMetrics(); ???????????desiredWindowWidth = packageMetrics.widthPixels; ???????????desiredWindowHeight = packageMetrics.heightPixels; ??????? } ?????? ?/* 由于這是第一次進行“遍歷”,控件樹即將第一次被顯示在窗口上,因此接下來的代碼填充了 ?????????mAttachInfo中的一些字段,然后通過mView發起了dispatchAttachedToWindow()的調用 ????????? 之后每一個位于控件樹中的控件都會回調onAttachedToWindow() */ ??????? ...... ??? } else { ??????? // ② 在非第一次遍歷的情況下,會采用窗口的最新尺寸作為SPEC_SIZE的候選 ???????desiredWindowWidth = frame.width(); ???????desiredWindowHeight = frame.height(); ??????? /* 如果窗口的最新尺寸與ViewRootImpl中的現有尺寸不同,說明WMS側單方面改變了窗口的尺寸 ????????? 這將產生如下三個結果 */ ??????? if(desiredWindowWidth != mWidth || desiredWindowHeight != mHeight) { ?? ?????????// 需要進行完整的重繪以適應新的窗口尺寸 ???????????mFullRedrawNeeded = true; ???????????// 需要對控件樹進行重新布局 ???????????mLayoutRequested = true; ???????????/* 控件樹有可能拒絕接受新的窗口尺寸,比如在隨后的預測量中給出了不同于窗口尺寸的測量結果 ?????????????產生這種情況時,就需要在窗口布局階段嘗試設置新的窗口尺寸 */ ???????????windowSizeMayChange = true; ??????? } ??? } ??? ...... ??? /* 執行位于RunQueue中的回調。RunQueue是ViewRootImpl的一個靜態成員,即是說它是進程唯一 ????? 的,并且可以在進程的任何位置訪問RunQueue。在進行多線程任務時,開發者可以通過調用View.post() ????? 或View.postDelayed()方法將一個Runnable對象發送到主線程執行。這兩個方法的原理是將 ??? ??Runnable對象發送到ViewRootImpl的mHandler去。當控件已經加入到控件樹時,可以通過 ?????AttachInfo輕易獲取這個Handler。而當控件沒有位于控件樹中時,則沒有mAttachInfo可用,此時 ????? 執行View.post()/PostDelay()方法,Runnable將會被添加到這個RunQueue隊列中。 ????? 在這里,ViewRootImpl將會把RunQueue中的Runnable發送到mHandler中,進而得到執行。所以 ????? 無論控件是否顯示在控件樹中,View.post()/postDelay()方法都是可用的,除非當前進程中沒有任何 ????? 處于活動狀態的ViewRootImpl */ ???getRunQueue().executeActions(attachInfo.mHandler); ??? booleanlayoutRequested = mLayoutRequested && !mStopped; ??? /* 僅當layoutRequested為true時才進行預測量。 ?????layoutRequested為true表示在進行“遍歷”之前requestLayout()方法被調用過。 ?????requestLayout()方法用于要求ViewRootImpl進行一次“遍歷”并對控件樹重新進行測量與布局 */ ??? if(layoutRequested) { ???????final Resources res = mView.getContext().getResources(); ??????? if(mFirst) { ??????????? ......// 確定控件樹是否需要進入TouchMode,本章將在6.5.1節介紹 TouchMode ??????? }else { ??????????? /*檢查WMS是否單方面改變了ContentInsets與VisibleInsets。注意對二者的處理的差異, ?????????????ContentInsets描述了控件在布局時必須預留的空間,這樣會影響控件樹的布局,因此將 ?????????????insetsChanged標記為true,以此作為是否進行控件布局的條件之一。而VisibleInsets則 ?????????????描述了被遮擋的空間,ViewRootImpl在進行繪制時,需要調整繪制位置以保證關鍵控件或區域, ?????????????如正在進行輸入的TextView等不被遮擋,這樣VisibleInsets的變化并不會導致重新布局, ?????????????所以這里僅僅是將VisibleInsets保存到mAttachInfo中,以便繪制時使用 */ ???????????if (!mPendingContentInsets.equals(mAttachInfo.mContentInsets)) { ???????????????insetsChanged = true; ???????????} ???????????if (!mPendingVisibleInsets.equals(mAttachInfo.mVisibleInsets)) { ???????????????mAttachInfo.mVisibleInsets.set(mPendingVisibleInsets); ???????????} ??????????? /*當窗口的width或height被指定為WRAP_CONTENT時,表示這是一個懸浮窗口。 ?????????????此時會對desiredWindowWidth/Height進行調整。在前面的代碼中,這兩個值被設置 ?????????????被設置為窗口的當前尺寸。而根據MeasureSpec的要求,測量結果不得大于SPEC_SIZE。 ?????????????然而,如果這個懸浮窗口需要更大的尺寸以完整顯示其內容時,例如為AlertDialog設置了 ?????????????一個更長的消息內容,如此取值將導致無法得到足夠大的測量結果,從而導致內容無法完整顯示。 ?????????????因此,對于此等類型的窗口,ViewRootImpl會調整desiredWindowWidth/Height為此應用 ?????????????可以使用的最大尺寸 */ ???????????if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT ???????????????????|| lp.height == ViewGroup.LayoutParams.WRAP_CONTENT) { ???????????????// 懸浮窗口的尺寸取決于測量結果。因此有可能需要向WMS申請改變窗口的尺寸。 ???????????????windowSizeMayChange = true; ???????????????if (lp.type == WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL) { ???????????????????// ???????????????} else { ???????????????????// ③ 設置懸浮窗口SPEC_SIZE的候選為應用可以使用的最大尺寸 ???????????????????DisplayMetrics packageMetrics = res.getDisplayMetrics(); ???????????????????desiredWindowWidth = packageMetrics.widthPixels; ???????????????????desiredWindowHeight = packageMetrics.heightPixels; ???????????????} ???????????} ??????? } ??????? // **④ 進行預測量。**通過measureHierarchy()方法以desiredWindowWidth/Height進行測量 ??????? windowSizeMayChange |=measureHierarchy(host, lp, res, ???????????????desiredWindowWidth, desiredWindowHeight); ??? } ??? // 其他階段的處理 ??? ...... } ``` 由此可知,預測量時的SPEC_SIZE按照如下原則進行取值: + 第一次“遍歷”時,使用應用可用的最大尺寸作為SPEC_SIZE的候選。 + 此窗口是一個懸浮窗口,即LayoutParams.width/height其中之一被指定為WRAP_CONTENT時,使用應用可用的最大尺寸作為SPEC_SIZE的候選。 + 在其他情況下,使用窗口最新尺寸作為SPEC_SIZE的候選。 最后,通過measureHierarchy()方法進行測量。 ##### 測量協商 measureHierarchy()用于測量整個控件樹。傳入的參數desiredWindowWidth與desiredWindowHeight在前述代碼中根據不同的情況作了精心的挑選。控件樹本可以按照這兩個參數完成測量,但是measureHierarchy()有自己的考量,即如何將窗口布局地盡可能地優雅。 這是針對將LayoutParams.width設置為了WRAP_CONTENT的懸浮窗口而言。如前文所述,在設置為WRAP_CONTENT時,指定的desiredWindowWidth是應用可用的最大寬度,如此可能會產生如圖6-6左圖所示的丑陋布局。這種情況較容易發生在AlertDialog中,當AlertDialog需要顯示一條比較長的消息時,由于給予的寬度足夠大,因此它有可能將這條消息以一行顯示,并使得其窗口充滿了整個屏幕寬度,在橫屏模式下這種布局尤為丑陋。 倘若能夠對可用寬度進行適當的限制,迫使AlertDialog將消息換行顯示,則產生的布局結果將會優雅得多,如圖6-6右圖所示。但是,倘若不分清紅皂白地對寬度進行限制,當控件樹真正需要足夠的橫向空間時,會導致內容無法顯示完全,或者無法達到最佳的顯示效果。例如當一個懸浮窗口希望盡可能大地顯示一張照片時就會出現這樣的情況。 ![](https://box.kancloud.cn/2016-03-01_56d567b3cfebc.png) 圖 6 - 6 丑陋的布局與優雅的布局 那么measureHierarchy()如何解決這個問呢?它采取了與控件樹進行協商的辦法,即先使用measureHierarchy()所期望的寬度限制嘗試對控件樹進行測量,然后通過測量結果來檢查控件樹是否能夠在此限制下滿足其充分顯示內容的要求。倘若沒能滿足,則measureHierarchy()進行讓步,放寬對寬度的限制,然后再次進行測量,再做檢查。倘若仍不能滿足則再度進行讓步。 參考代碼如下: ``` [ViewRootImpl.java-->ViewRootImpl.measureHierarchy()] private boolean measureHierarchy(final View host,final WindowManager.LayoutParams lp, ???????final Resources res, final int desiredWindowWidth, ??????? final int desiredWindowHeight) { ??? intchildWidthMeasureSpec; // 合成后的用于描述寬度的MeasureSpec ??? intchildHeightMeasureSpec; // 合成后的用于描述高度的MeasureSpec ??? booleanwindowSizeMayChange = false; // 表示測量結果是否可能導致窗口的尺寸發生變化 ??? booleangoodMeasure = false; // goodMeasure表示了測量是否能滿足控件樹充分顯示內容的要求 ??? // 測量協商僅發生在LayoutParams.width被指定為WRAP_CONTENT的情況下 ??? if(lp.width == ViewGroup.LayoutParams.WRAP_CONTENT) { ??????? /* **① 第一次協商。**measureHierarchy()使用它最期望的寬度限制進行測量。這一寬度限制定義為 ???????? 一個系統資源。可以在frameworks/base/core/res/res/values/config.xml找到它的定義 */ ???????res.getValue(com.android.internal.R.dimen.config_prefDialogWidth,mTmpValue, true); ??????? intbaseSize = 0; ??????? // 寬度限制被存放在baseSize中 ??????? if(mTmpValue.type == TypedValue.TYPE_DIMENSION) { ???????????baseSize = (int)mTmpValue.getDimension(packageMetrics); ??????? } ??????? if(baseSize != 0 && desiredWindowWidth &gt; baseSize) { ???????????// 使用getRootMeasureSpec()函數組合SPEC_MODE與SPEC_SIZE為一個MeasureSpec ???????????childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width); ?? ?????????childHeightMeasureSpec = ?????????????????????????? getRootMeasureSpec(desiredWindowHeight,lp.height); ??????????? //**②第一次測量。**由performMeasure()方法完成 ???????????performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); ???????????/* 控件樹的測量結果可以通過mView的getmeasuredWidthAndState()方法獲取。如果 ?????????????控件樹對這個測量結果不滿意,則會在返回值中添加MEASURED_STATE_TOO_SMALL位 */ ???????????if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) ??????????????????????????????????????????????? ==0) { ???????????????goodMeasure = true; // 控件樹對測量結果滿意,測量完成 ???????????} else { ???????????????// **③ 第二次協商。**上次測量結果表明控件樹認為measureHierarchy()給予的寬度太小, ?????????????????在此適當地放寬對寬度的限制,使用最大寬度與期望寬度的中間值作為寬度限制 */ ???????????????baseSize = (baseSize+desiredWindowWidth)/2; ???????????????childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width); ???????????????// **④ 第二次測量** ???????????????performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); ???????????????// 再次檢查控件樹是否滿足此次測量 ???????????????if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) ????????????????????????????????????????????????????????????????????????????????== 0) { ???????????????????goodMeasure = true; // 控件樹對測量結果滿意,測量完成 ???????????????} ????? ??????} ??????? } ??? } ??? if(!goodMeasure) { ??????? /* **⑤ 最終測量。**當控件樹對上述兩次協商的結果都不滿意時,measureHierarchy()放棄所有限制 ????????? 做最終測量。這一次將不再檢查控件樹是否滿意了,因為即便其不滿意,measurehierarchy()也沒 ????????? 有更多的空間供其使用了 */ ???????childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth,lp.width); ???????childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight,lp.height); ???????performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); ??????? /* 最后,如果測量結果與ViewRootImpl中當前的窗口尺寸不一致,則表明隨后可能有必要進行窗口 ????????? 尺寸的調整 */ ??????? if(mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) ??????? { ???????????windowSizeMayChange = true; ??????? } ??? } ??? // 返回窗口尺寸是否可能需要發生變化 ??? returnwindowSizeMayChange; } ``` 顯然,對于非懸浮窗口,即當LayoutParams.width被設置為MATCH_PARENT時,不存在協商過程,直接使用給定的desiredWindowWidth/Height進行測量即可。而對于懸浮窗口,measureHierarchy()可以連續進行兩次讓步。因而在最不利的情況下,在ViewRootImpl的一次“遍歷”中,控件樹需要進行三次測量,即控件樹中的每一個View.onMeasure()會被連續調用三次之多,如圖6-7所示。所以相對于onLayout(),onMeasure()方法的對性能的影響比較大。 ![](https://box.kancloud.cn/2016-03-01_56d567b3e199c.png) 圖 6 - 7 協商測量的三次嘗試 接下來通過performMeasure()看控件樹如何進行測量。 ##### 測量原理 performMeasure()方法的實現非常簡單,它直接調用mView.measure()方法,將measureHierarchy()給予的widthSpec與heightSpec交給mView。 看下View.measure()方法的實現: ``` [View.java-->View.measure()] public final void measure(int widthMeasureSpec,int heightMeasureSpec) { ??? /* 僅當給予的MeasureSpec發生變化,或要求強制重新布局時,才會進行測量。 ????? 所謂強制重新布局,是指當控件樹中的一個子控件的內容發生變化時,需要進行重新的測量和布局的情況 ????? 在這種情況下,這個子控件的父控件(以及其父控件的父控件)所提供的MeasureSpec必定與上次測量 ????? 時的值相同,因而導致從ViewRootImpl到這個控件的路徑上的父控件的measure()方法無法得到執行 ????? 進而導致子控件無法重新測量其尺寸或布局。因此,當子控件因內容發生變化時,從子控件沿著控件樹回溯 ????? 到ViewRootImpl,并依次調用沿途父控件的requestLayout()方法,在這個方法中,會在 ?????mPrivateFlags中加入標記PFLAG_FORCE_LAYOUT,從而使得這些父控件的measure()方法得以順利 ????? 執行,進而這個子控件有機會進行重新測量與布局。這便是強制重新布局的意義 */ ??? if ((mPrivateFlags& PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT || ???????????widthMeasureSpec != mOldWidthMeasureSpec || ???????????heightMeasureSpec != mOldHeightMeasureSpec) { ??????? /* **① 準備工作。**從mPrivateFlags中將PFLAG_MEASURED_DIMENSION_SET標記去除。 ???????? PFLAG_MEASURED_DIMENSION_SET標記用于檢查控件在onMeasure()方法中是否通過 ???????? 調用setMeasuredDimension()將測量結果存儲下來 */ ???????mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET; ???????...... ??????? /* **② 對本控件進行測量** 每個View子類都需要重載這個方法以便正確地對自身進行測量。 ????????? View類的onMeasure()方法僅僅根據背景Drawable或style中設置的最小尺寸作為 ????????? 測量結果*/ ???????onMeasure(widthMeasureSpec, heightMeasureSpec); ??????? /* ③ 檢查onMeasure()的實現是否調用了setMeasuredDimension() ?????????setMeasuredDimension()會將PFLAG_MEASURED_DIMENSION_SET標記重新加入 ?????????mPrivateFlags中。之所以做這樣的檢查,是由于onMeasure()的實現可能由開發者完成, ????????? 而在Android看來,開發者是不可信的 */ ??????? if((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) ???????????????????????????????????? !=PFLAG_MEASURED_DIMENSION_SET) { ???????????throw new IllegalStateException(......); ??????? } ?????? // ④ 將PFLAG_LAYOUT_REQUIRED標記加入mPrivateFlags。這一操作會對隨后的布局操作放行 ???????mPrivateFlags |= PFLAG_LAYOUT_REQUIRED; ??? } ??? // 記錄父控件給予的MeasureSpec,用以檢查之后的測量操作是否有必要進行 ???mOldWidthMeasureSpec = widthMeasureSpec; ???mOldHeightMeasureSpec = heightMeasureSpec; } ``` 從這段代碼可以看出,View.measure()方法沒有實現任何測量算法,它的作用在于引發onMeasure()的調用,并對onMeasure()行為的正確性進行檢查。另外,在控件系統看來,一旦控件執行了測量操作,那么隨后必須進行布局操作,因此在完成測量之后,將PFLAG_LAYOUT_REQUIRED標記加入mPrivateFlags,以便View.layout()方法可以順利進行。 onMeasure()的結果通過setMeasuredDimension()方法盡行保存。setMeasuredDimension()方法的實現如下: ``` [View.java-->View.setMeasuredDimension()] protected final void setMeasuredDimension(intmeasuredWidth, int measuredHeight) { ??? /* ① 測量結果被分別保存在成員變量mMeasuredWidth與mMeasuredHeight中 ???mMeasuredWidth = measuredWidth; ? ??mMeasuredHeight = measuredHeight; ??? // ② 向mPrivateFlags中添加PFALG_MEASURED_DIMENSION_SET,以此證明onMeasure()保存了測量結果 ???mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET; } ``` 其實現再簡單不過。存儲測量結果的兩個變量可以通過getMeasuredWidthAndState()與getMeasuredHeightAndState()兩個方法獲得,就像ViewRootImpl.measureHierarchy()中所做的一樣。此方法雖然簡單,但需要注意,與MeasureSpec類似,測量結果不僅僅是一個尺寸,而是一個測量狀態與尺寸的復合整、變量。其0至30位表示了測量結果的尺寸,而31、32位則表示了控件對測量結果是否滿意,即父控件給予的MeasureSpec是否可以使得控件完整地顯示其內容。當控件對測量結果滿意時,直接將尺寸傳遞給setMeasuredDimension()即可,注意要保證31、32位為0。倘若對測量結果不滿意,則使用View.MEASURED_STATE_TOO_SMALL | measuredSize 作為參數傳遞給setMeasuredDimension()以告知父控件對MeasureSpec進行可能的調整。 既然明白了onMeasure()的調用如何發起,以及它如何將測量結果告知父控件,那么onMeasure()方法應當如何實現的呢?對于非ViewGroup的控件來說其實現相對簡單,只要按照MeasureSpec的原則如實計算其所需的尺寸即可。而對于ViewGroup類型的控件來說情況則復雜得多,因為它不僅擁有自身需要顯示的內容(如背景),它的子控件也是其需要測量的內容。因此它不僅需要計算自身顯示內容所需的尺寸,還有考慮其一系列子控件的測量結果。為此它必須為每一個子控件準備MeasureSpec,并調用每一個子控件的measure()函數。 由于各種控件所實現的效果形形色色,開發者還可以根據需求自行開發新的控件,因此onMeasure()中的測量算法也會變化萬千。不從Android系統實現的角度仍能得到如下的onMeasure()算法的一些實現原則: + 控件在進行測量時,控件需要將它的Padding尺寸計算在內,因為Padding是其尺寸的一部分。 + ViewGroup在進行測量時,需要將子控件的Margin尺寸計算在內。因為子控件的Margin尺寸是父控件尺寸的一部分。 + ViewGroup為子控件準備MeasureSpec時,SPEC_MODE應取決于子控件的LayoutParams.width/height的取值。取值為MATCH_PARENT或一個確定的尺寸時應為EXACTLY,WRAP_CONTENT時應為AT_MOST。至于SPEC_SIZE,應理解為ViewGroup對子控件尺寸的限制,即ViewGroup按照其實現意圖所允許子控件獲得的最大尺寸。并且需要扣除子控件的Margin尺寸。 + 雖然說測量的目的在于確定尺寸,與位置無關。但是子控件的位置是ViewGroup進行測量時必須要首先考慮的。因為子控件的位置即決定了子控件可用的剩余尺寸,也決定了父控件的尺寸(當父控件的LayoutParams.width/height為WRAP_CONTENT時)。 + 在測量結果中添加MEASURED_STATE_TOO_SMALL需要做到實事求是。當一個方向上的空間不足以顯示其內容時應考慮利用另一個方向上的空間,例如對文字進行換行處理,因為添加這個標記有可能導致父控件對其進行重新測量從而降低效率。 + 當子控件的測量結果中包含MEASURED_STATE_TOO_SMALL標記時,只要有可能,父控件就應當調整給予子控件的MeasureSpec,并進行重新測量。倘若沒有調整的余地,父控件也應當將MEASURED_STATE_TOO_SMALL加入到自己的測量結果中,讓它的父控件嘗試進行調整。 + ViewGroup在測量子控件時必須調用子控件的measure()方法,而不能直接調用其onMeasure()方法。直接調用onMeasure()方法的最嚴重后果是子控件的PFLAG_LAYOUT_REQUIRED標識無法加入到mPrivateFlag中,從而導致子控件無法進行布局。 綜上所述,測量控件樹的實質是測量控件樹的根控件。完成控件樹的測量之后,ViewRootImpl便得知了控件樹對窗口尺寸的需求。 ##### 確定是否需要改變窗口尺寸 接下來回到performTraversals()方法。在ViewRootImpl.measureHierarchy()執行完畢之后,ViewRootImpl了解了控件樹所需的空間。于是便可確定是否需要改變窗口窗口尺寸以便滿足控件樹的空間要求。前述的代碼中多處設置windowSizeMayChange變量為true。windowSizeMayChange僅表示有可能需要改變窗口尺寸。而接下來的這段代碼則用來確定窗口是否需要改變尺寸。 ``` [ViewRootImpl.java-->ViewRootImp.performTraversals()] private void performTraversals() { ??? ......// 測量控件樹的代碼 ??? /* 標記mLayoutRequested為false。因此在此之后的代碼中,倘若控件樹中任何一個控件執行了 ?????requestLayout(),都會重新進行一次“遍歷” */ ?? ?if (layoutRequested) { ??? ????mLayoutRequested = false; ??? } ??? // 確定窗口是否確實需要進行尺寸的改變 ??? booleanwindowShouldResize = layoutRequested && windowSizeMayChange ???????&& ((mWidth != host.getMeasuredWidth() || mHeight !=host.getMeasuredHeight()) ???????????|| (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT && ???????????????????frame.width() &lt; desiredWindowWidth && frame.width() !=mWidth) ???????????|| (lp.height == ViewGroup.LayoutParams.WRAP_CONTENT && ???????????????????frame.height() &lt; desiredWindowHeight && frame.height() !=mHeight)); } ``` 確定窗口尺寸是否確實需要改變的條件看起來比較復雜,這里進行一下總結,先介紹必要條件: + layoutRequested為true,即ViewRootImpl.requestLayout()方法被調用過。View中也有requestLayout()方法。當控件內容發生變化從而需要調整其尺寸時,會調用其自身的requestLayout(),并且此方法會沿著控件樹向根部回溯,最終調用到ViewRootImp.requestLayout(),從而引發一次performTraversals()調用。之所以這是一個必要條件,是因為performTraversals()還有可能因為控件需要重繪時被調用。當控件僅需要重繪而不需要重新布局時(例如背景色或前景色發生變化時),會通過invalidate()方法回溯到ViewRootImpl,此時不會通過performTraversals()觸發performTraversals()調用,而是通過scheduleTraversals()進行觸發。在這種情況下layoutRequested為false,即表示窗口尺寸不需發生變化。 + windowSizeMayChange為true,如前文所討論的,這意味著WMS單方面改變了窗口尺寸而控件樹的測量結果與這一尺寸有差異,或當前窗口為懸浮窗口,其控件樹的測量結果將決定窗口的新尺寸。 在滿足上述兩個條件的情況下,以下兩個條件滿足其一: + 測量結果與ViewRootImpl中所保存的當前尺寸有差異。 + 懸浮窗口的測量結果與窗口的最新尺寸有差異。 注意ViewRootImpl對是否需要調整窗口尺寸的判斷是非常小心的。第4章介紹WMS的布局子系統時曾經介紹過,調整窗口尺寸所必須調用的performLayoutAndPlaceSurfacesLocked()函數會導致WMS對系統中的所有窗口新型重新布局,而且會引發至少一個動畫幀渲染,其計算開銷相當之大。因此ViewRootImpl僅在必要時才會驚動WMS。 至此,預測量階段完成了。 ##### 總結 這一階段的工作內容是為了給后續階段做參數的準備并且其中最重要的工作是對控件樹的預測量,至此ViewRootImpl得知了控件樹對窗口尺寸的要求。另外,這一階段還準備了后續階段所需的其他參數: + viewVisibilityChanged。即View的可見性是否發生了變化。由于mView是窗口的內容,因此mView的可見性即是窗口的可見性。當這一屬性發生變化時,需要通過通過WMS改變窗口的可見性。 LayoutParams。預測量階段需要收集應用到LayoutParams的改動,這些改動一方面來自于WindowManager.updateViewLayout(),而另一方面則來自于控件樹。以SystemUIVisibility為例,View.setSystemUIVisibility()所修改的設置需要反映到LayoutParams中,而這些設置確卻保存在控件自己的成員變量里。在預測量階段會通過ViewRootImpl.collectViewAttributes()方法遍歷控件樹中的所有控件以收集這些設置,然后更新LayoutParams。
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看