1. 程式人生 > >Android 介面介紹與繪製優化

Android 介面介紹與繪製優化

Andorid使用者介面框架

這裡寫圖片描述
  Android的使用者介面框架(Android UI Framework)採用MVCModel-View-Controller)模型,為使用者介面提供了處理使用者輸入的控制器(Controller)和顯示介面內容的檢視(View)。其中模型層(Model)是應用程式的核心,資料和程式碼都被儲存在模型中。

MVC模型中的檢視將應用程式的資訊反饋給使用者,可能的反饋方法包括視覺、聽覺或者觸覺等,最常用的就是通過螢幕顯示反饋資訊。

Android介面的基本架構

Android中,介面是由一顆View樹來定義的。View樹的葉子結點是檢視元件,而其他非葉子結點則是容器元件。View

樹的如下:
  
這裡寫圖片描述

View樹的這種樹型結構也為元件的事件管理,如觸控式螢幕、鍵盤點選事件,提供了很多的便利。可以說元件的事件傳遞機制就是沿著樹,從高層向底層傳遞的。

從上圖中可以看出,一個ViewGroup可以擁有多個View,也可以包含多個ViewGroupAndroid這種非常靈活的View層次結構可以形成非常複雜的佈局,開發者可以利用這個特性開發出特別精緻的介面。

當呼叫ActivitysetContentView()方法的時候,可以將一棵樹的根節點或者其子樹的根節點,設定是葉子節點作為引數傳入此方法,這樣系統就獲得了該結點的引數,並將該節點作為根節點來測距和繪製,最終將以該結點為根節點的整棵樹都繪製在介面上。繪製過程中,父節點負責請求其子節點繪製它們自己,子節點可能會請求他們在父節點中的大小和位置,父節點對每個子節點的大小和位置由最終的決定權。

View和ViewGroup

Android的使用者介面都是由檢視元件(View)和容器元件(ViewGroup)組成的。為了便於理解,可以將容器看成是螢幕上的一個矩形的區域,是檢視則是位於該矩形區域內的一個元件。

這裡寫圖片描述

View類是Android API中定義的類,位於android.view包中。一個View類或者其子類的物件中儲存與該物件相關的佈局和內容屬性等,並且可以實現處理包括測距、佈局、繪圖、焦點變換、滾動條以及螢幕區域內的該物件的按鍵和手勢在內的多種功能。View還為Android中的Widget提供服務。WidgetAndroid的系統包,包含了一組View類的可例項化子類。這些子類可以用於繪製互動式螢幕元素。使用Widget

,我們可以快速地建立使用者介面。

ViewGroup類也是Android API中的類,位於android.view包中。值得一提的是,ViewGroup雖然是View的子類,但是與View類有著完全不同的任何和功能。ViewGroup,其實是一組View類物件的集合。實際上,ViewGroup的功能也確實是這樣的,它負責裝載和管理一組View,包括View類的物件和ViewGroup本身。

ViewGroup的一個重要的子類是佈局(Layout)。Layout可以為一組View建立一個結構。

Android介面的組成元素是ViewViewGroup的子類和簡介子類。檢視元件的作用是顯示,而容器元件的作用是管理

座標系

這裡寫圖片描述

View自身的座標

方法 說明
getTop() 獲取View自身頂邊到其父控制元件佈局頂邊的距離。
getLeft() 獲取View自身左邊到其父控制元件佈局左邊的距離。
getTop() 獲取View自身右邊到其父控制元件佈局左邊的距離。
getTop() 獲取View自身底邊到其父控制元件佈局頂邊的距離。

MotionEvent提供的方法

上圖中的圓點,就是我們的觸控點(假定)。觸控事件最終都會由onTouchEvent(MotionEvent event)方法來處理。

方法 說明
getX() 獲取點選事件距離控制元件左邊的距離,即檢視座標
getY() 獲取點選事件距離控制元件頂邊的距離,即檢視座標
getRawX() 獲取點選事件距離整個螢幕左邊的距離,即絕對座標
getRawY() 獲取點選事件距離整個螢幕頂邊的距離,即絕對座標

繪製過程

檢視展示的系統架構,由檢視根結點(RootView)開始,檢視根結點負責連線視窗管理器(WindowManage)與裝飾檢視(DecorView),視窗管理器用於響應使用者事件,而裝飾檢視用於展示特定影象。

  • performTravesals()中包含了以下三個方法:
    • performMeasure()測量
    • performLayout佈局
    • performDraw繪製
Created with Raphaël 2.2.0開始performTravesals()performMeasure()performLayout()performDraw()結束

Android系統顯示原理

Android的顯示過程可以簡單概括為:Android應用程式把經過測量、佈局繪製後的surface快取資料,通過SurfaceFlinger把資料渲染到顯示螢幕上,通過Android的重新整理機制來重新整理資料。也就是說應用層負責繪製,系統層負責渲染,通過程序間通訊把應用層需要繪製的資料傳遞到系統層服務,系統層服務通過重新整理機制把資料更新到螢幕。

繪製原理

繪製任務是由應用發起的,最終通過系統層繪製到硬體螢幕上。也就是說,應用程序繪製好後,通過跨程序通訊機制把需要顯示的內容傳遞到系統層,由系統層的SurfaceFlinger服務繪製到螢幕上。

應用層

Android中每個View繪製中有三個核心步驟,通過MeasureLayout來確定當前需要繪製的View所在的大小和位置,通過繪製(Draw)到Surface,在Android系統中整體的繪圖原始碼是在ViewRootImpperformTraversals()方法,通過這個方法可以看出MeasureLayout都是通過遞迴來獲取View的大小和位置的,並且以深度作為優先順序。可以看出,層級越深,元素越多,耗時也越長優化點)。

方法 說明
Measure 深度優先原則遞迴得到所有檢視(View)的寬、高;獲取當前View的正確寬度childWidthMeasureSpec和高度childHeightMeasureSpec之後,可以呼叫它的成員函式Measure來設定它的大小。如果當前正在測量的子檢視child是一個檢視容器,那麼它又會重複執行操作,直到它的所有子孫檢視的大小都測量完成為止。
Layout 深度優先原則遞迴得到所有檢視(View)的位置;當一個子View在應用程式視窗左上角的位置確定之後,再結合它在前面測量過程中確定的寬度和高度,就可以完全確定它在應用程式視窗中的佈局。
Draw 目前Android支援了兩種繪製方式:軟體繪製(CPU)和硬體加速(GPU),其中硬體加速在Android3.0開始已經全面支援,很明顯,硬體加速在UI的顯示和繪製的效率遠遠高於CPU繪製。

GPU的缺點:
耗電:GPU的功耗與CPU高。
相容問題:某些介面和函式不支援硬體加速。
記憶體大:使用OpenGL的介面至少需要8MB的記憶體。

佈局優化

Android的介面繪製是通過遞迴來繪製介面,因此通過減少Layout層級,減少測量、繪製時間,提高複用性三個方面來優化佈局的時間是非常重要的。

  1. 儘量使用RelativeLayoutLinearLayout
  2. 在佈局層相同的情況下,使用LinearLayout
  3. LinearLayout有時會使巢狀層級變多,應該使用RelativeLayout,使介面儘量扁平化
  4. 使用merge標籤消除多餘的層級。
  5. 如果需要在兩個View之間新增空白,可以使用Space標籤。
    • space標籤是一個空白的標籤,主要用於在兩個View之間新增空白。
    • 而且Space不佔用繪製資源,因為Space控制元件onDraw方法進行了一個空的實現。

Merge使用注意事項

  1. Merge只能用在佈局XML檔案的根元素。
  2. 使用Merge來載入一個佈局,必須指定一個ViewGroup作為其父元素,並且要設定載入的attachToRoot引數為true(參照inflat(int, ViewGroup, boolean)
  3. 不能在ViewStub中使用Merge標籤。原因就是ViewStubinflate方法中根本沒有attachToRoot的設定。

ViewStub

使用ViewStub標籤提高顯示速度。

在開發中,我們經常會遇到一個問題,在某一個佈局中的子佈局非常多,但是在App中,又不是所有的佈局都需要顯示(部分顯示),開啟這個介面的時候,會根據不同的場景和屬性顯示不同的Layout。例如:一個頁面對不同的使用者,比如未登入使用者、普通使用者、VIP使用者會顯示不同的頁面。又或者說,有些使用者喜歡對APP介面中的某些元素進行隱藏,比如微信的朋友圈入口,這個時候可能就用到了INVISIBLE或者GONE屬性。但是INVISIBLE或者GONE屬性的效率非常的低,原因是即使將元素隱藏了,它們仍然在佈局中,仍會測試和解析這些佈局。

這個時候,就可以使用ViewStub來解決。

ViewStub是一個輕量級的View,它是一個看不見的,並且不佔佈局位置,佔用資源非常小的檢視物件。可以為ViewStub指定一個佈局,載入佈局的使用,只有ViewStub會被初始化,然後當ViewStub被設定為可見的時候,或者呼叫了ViewStub.inflate()的時候,ViewStub所指向的佈局才會被載入和例項化,然後ViewStub的佈局屬性都會傳給它指向的佈局。這樣,就可以使用ViewStub來設定是否顯示某個佈局。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <include
        android:id="@+id/topBar"
        layout="@layout/common_top_bar"
        android:layout_width="match_parent"
        android:layout_height="@dimen/top_bar_height" />

    <ViewStub
        android:id="@+id/viewstub_first"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout="@layout/viewstub_first" />

    <ViewStub
        android:id="@+id/viewstub_second"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout="@layout/viewstub_second" />

    <ViewStub
        android:id="@+id/viewstub_default"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout="@layout/layout_default" />

</LinearLayout>

在呼叫的時候,根據不同的需求切換不同的Layout,這樣可以提高頁面初始化的速度,使用程式碼如下:

View view = view.inflate(R.layout.activity_viewstub, container, false);
switch (choice) {
	case First:
		ViewStub stub1 = view.findViewById(R.id.viewstub_first);
		stub1.inflate();
		break;
	case Second:
		ViewStub stub2 = view.findViewById(R.id.viewstub_second);
		stub2.inflate();
		break;
	default:
		ViewStub stub = view.findViewById(R.id.viewstub_default);
		stub.inflate();
		break;
}

ViewStub顯示有兩種方式,上面的程式碼使用的是inflate方法,也可以直接使用ViewStub.setVisibility(View.Visible)方法。

注意事項

  • 使用ViewStub只能載入一次,之後ViewStub物件會被置為空。換句話說,某個被ViewStub指定的佈局被載入後,就不能再通過ViewStub來控制它了。所以它不適用於需要按需顯示隱藏的情況
  • ViewStub只能用來載入一個佈局檔案,而不是某個具體的View,當然也可以把View寫在某個佈局檔案中。如果想操作一個具體的View,還是使用visibility屬性。
  • ViewStub中不能巢狀Merge標籤。

使用場景

  • 在程式執行期間,某個佈局在載入後,就不會有變化,除非銷燬該頁面重新載入。
  • 想要控制顯示與隱藏的是一個佈局檔案,而非某個View。
  • 因為ViewStub只能Inflate一次,之後就會被置空,無法繼續使用ViewStub來控制佈局。所以需要在執行時不止一次顯示和隱藏某個佈局的時候,使用ViewStub是無法實現的。這時只能使用View的可見性來控制。

延遲載入

延遲載入的功能非常重要,特別是介面中顯示的內容比較多並且所佔空間比較大的時候。在Android應用程式中,可以使用ViewStub實現延遲載入的功能。

當呼叫ViewStubsetVisbility()函式設定為可見或者呼叫inflate初始化該View的時候,ViewStub引用的資源開始初始化,然後引用的資源填充在ViewStub所在的位置上。在沒有呼叫setVisibility(int)函式或者inflate()函式之前,ViewStub一直存在元件樹層級結構中。但是由於ViewStub非常輕量級,所以對效能的影響非常小。

影響佈局效率的主要原因

提高佈局效率的方法總體來說就是減少層級,提高繪製速度和佈局複用。
  影響佈局效率的主要原因如下:

  • 佈局的層級越少,載入速度越快。
  • 減少同一級控制元件的數量,載入速度會變快。
  • 一個控制元件的屬性越少,解析越快。

Android繪製優化

  • 儘量多使用RelativeLayout或者LinearLayout,不要使用絕對佈局AbsoluteLayout。
  • 將可複用的元件抽取出來並通過<include/>標籤使用。
  • 使用<ViewStub/>標籤載入一些不常用的佈局。
  • 使用<merge/>標籤減少佈局的巢狀層次。
  • 儘可能少用wrap_contentwrap_content會增加布局Measure時的計算成本,已知寬高為固定值的時候,不用wrap_content
  • 刪除控制元件中的無用屬性。

附錄

  • 《Android應用效能優化最佳實踐》
    • 羅彧成
  • 《煮酒論Android》
    • 原始人工作室
  • 《Android進階之光》
    • 劉望舒
  • 《Android應用開發實戰詳解》
    • 王翠萍