Android 面試官:簡述一下 View 的繪製流程,這個都答不出來就別想拿Offer了

南方吳彥祖_藍斯發表於2020-11-10

前言

作為一名Android開發者肯定明白View的地位,說它佔據半壁江山也不為過,作為基石之一,搞明白它的載入流程是每個開發者都應該去做的,目前網路上很多關於View繪製流程的文章,有些質量也很高,但我還是想以自己的思路出一篇文章。相信讀完你對View的工作機制以及自定義View會有一個全新的認識。

1. View的繪製時機

1.1. 知識儲備

  • Window:每個 Activity都會建立一個 Window用於承載View檢視的顯示, Window是一個抽象類存在了一個唯一實現類 PhoneWindow
  • DecorView:最頂層的View,是一個 FrameLayout子類,最終會被載入到Window當中,它內部只有一個垂直方向的 LinearLayout分為兩部分:
    • TitleBar:螢幕頂部的狀態列
    • ContentView: Activity對應的XML佈局,透過 setContentView設定到 DecorView中。
Android 面試官:簡述一下 View 的繪製流程,這個都答不出來就別想拿Offer了

1.2. Activity、Window、DecorView之間關係

首先來看一下Activity中setContentView原始碼:

 public void setContentView(@LayoutRes int layoutResID) {
        //將xml佈局傳遞到Window當中
        getWindow().setContentView(layoutResID);
        initWindowDecorActionBar();
    }

從程式碼可以看出, ActivitysetContentView實質是將 View傳遞到 WindowsetContentView()方法中, WindowsetContenView會在內部呼叫 installDecor()方法建立 DecorView,看一下它的部分原始碼:

 public void setContentView(int layoutResID) { 
        if (mContentParent == null) {
            //初始化DecorView以及其內部的content
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            mContentParent.removeAllViews();
        }
        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        ...............
        } else {
            //將contentView載入到DecorVoew當中
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }
        ...............
    }
  private void installDecor() {
        ...............
        if (mDecor == null) {
            //例項化DecorView
            mDecor = generateDecor(-1);
            ...............
            }
        } else {
            mDecor.setWindow(this);
        }
       if (mContentParent == null) {
            //獲取Content
            mContentParent = generateLayout(mDecor);
       }  
        ...............
 }
 protected DecorView generateDecor(int featureId) {
        ...............
        return new DecorView(context, featureId, this, getAttributes());
 }

透過 generateDecor()new一個 DecorView,然後呼叫 generateLayout()獲取 DecorViewcontent,最終透過 inflateActivity檢視新增到 DecorView中的 content中,但此時 DecorView還未被新增到 Window中。新增操作需要藉助 ViewRootImpl

ViewRootImpl的作用是用來銜接 WindowManagerDecorView,在 Activity被建立後會透過 WindowManagerDecorView新增到 PhoneWindow中並且建立 ViewRootImpl例項,隨後將 DecorViewViewRootImpl進行關聯,最終透過執行 ViewRootImplperformTraversals()開啟整個View樹的繪製。

關於Activity在何時將DecorView新增到Window以及何時建立 ViewRootImpl,這塊內容牽扯麵比較廣,涉及到Activity啟動流程、ActivityManagerService(AMS)、WindowManagerService(WMS),內容太過於深入加上作者能力有限就不誤人子弟了。如有興趣推薦查閱劉皇叔《Android進階解密》,書中對這方面內容講解還是比較全面的 。

2. 繪製過程

從第一小節可知,View的繪製是從 ViewRootImplperformTraversals()方法開始,從最頂層的 View(ViewGroup)開始逐層對每個 View進行繪製操作,下面來看一下該方法部分原始碼:

private void performTraversals() {
     ...............
    //measur過程
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
     ...............
    //layout過程
    performLayout(lp, desiredWindowWidth, desiredWindowHeight);
     ...............
    //draw過程
    performDraw();}

這方法大概有幾百行,機智的作者抽出三句精華呈現給大家~~~

  • measure:為測量寬高過程,如果是ViewGroup還要在onMeasure中對所有子View進行measure操作。
  • layout:用於擺放View在ViewGroup中的位置,如果是ViewGroup要在onLayout方法中對所有子View進行layout操作。
  • draw:往View上繪製影像。

示意圖如下:  確實不想畫圖了,從剛哥的書裡拍一張吧~~~

Android 面試官:簡述一下 View 的繪製流程,這個都答不出來就別想拿Offer了

2.1 Measure

performMeasure()原始碼

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
      if (mView == null) {
          return;
      }
      try {
          mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
      } finally {
          Trace.traceEnd(Trace.TRACE_TAG_VIEW);
      }}

可以看出從mView(最頂層ViewGroup)開始進行測量操作,然後逐層遍歷View並執行measure操作。

MeasureSpac

MeasureView繪製三個過程中的第一步,提到 Measure就不得不提 MeasureSpac它是一個32位 int型別數值,高兩位 SpacMode代表測量模式,低30位 SpacSize代表測量尺寸,是View的內部類,原始碼如下:

public class MeasureSpec {
        private static final int MODE_SHIFT = 30;  
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;  
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;  
        public static final int EXACTLY = 1 << MODE_SHIFT;  
        public static final int AT_MOST = 2 << MODE_SHIFT;  
  }

內部也包含三種測量模式:

  • UNSPECIFIED :父佈局不會對子View做任何限制,例如我們常用的 ScrollView就是這種測量模式。
  • EXACTLY :精確數值,比如使用了 match_parent或者xxxdp,表示父佈局已經決定了子 View的大小,通常在這種情況下 View的尺寸就是 SpacSize
  • AT_MOST :自適應,對應 wrap_content子View可以根據內容設定自己的大小,但前提是不能超出父 ViewGroup的寬高。

注意點:

在我們自定義View的過程中都會在onMeasure中進行寬高的測量,這個方法會從父佈局中接收兩個引數 widthMeasureSpacheightMeasureSpac,所以子佈局的寬高大小需要受限於父佈局。

在自定義View寬高測量的過程中,我們需要獲取 MeasurSpac中的寬高和測量模式,自定義 ViewGroup也必須給子View傳遞 MeasurSpac,Android也給我們提供了計算 MeasurSpac 和透過 MeasurSpac 獲取相應值的方式,都位於 MeasurSpac中,具體程式碼如下:

public static class MeasureSpec {
     public static int makeMeasureSpec( int size, int mode) {
            if (sUseBrokenMakeMeasureSpec) {
                return size + mode;
            } else {
                return (size & ~MODE_MASK) | (mode & MODE_MASK);
            }
     }
     public static int getMode(int measureSpec) {
            //noinspection ResourceType
            return (measureSpec & MODE_MASK);
     }
     public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK)
     }}

ViewGroupView對尺寸和模式進行了一次封裝和拆解,其目的是為了減少物件的建立,避免造成不必要的記憶體浪費。

LayoutParams

在剛接觸Android的時候經常有一個疑問,為什麼View設定自己的寬高,還要建立一個 xxx.LayoutParams?前面也提到了,子View的寬高是要受限於父佈局的,所以不能透過 setWidth或者 setHeight直接設定寬高的,另外  LayoutParams的作用不僅如此,比如一個View的父佈局是 RelativeLayout,可以透過設定 RelativeLayout.LayoutParamsabovebelow等屬性來調整在父佈局中的位置。

自定義View寬高測量演示

建立一個類繼承View,重寫其 onMeasure()方法

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
       //預設寬  
       int defaultWidth = 0;    
       //預設高
       int defaultHeight = 0;    
       setMeasuredDimension(
            getDefaultSize(defaultWidth, widthMeasureSpec),  
            getDefaultSize(defaultHeight, heightMeasureSpec));  }

一般的自定義View中,如果對寬高沒有特殊需求可直接透過 getDefaultSize()方法獲取,該方法位於View中原始碼如下:

   public static int getDefaultSize(int size, int measureSpec) {
        //預設尺寸
        int result = size;
        //獲取測量模式
        int specMode = MeasureSpec.getMode(measureSpec);
        //獲取尺寸
        int specSize = MeasureSpec.getSize(measureSpec);
        switch (specMode) {
           case MeasureSpec.UNSPECIFIED:
               result = size;
               break;
           case MeasureSpec.AT_MOST:
           case MeasureSpec.EXACTLY:
               result = specSize;
               break;
        }
        return result;
    }

從程式碼分析可知,獲取 modesize後會分別對三種測量模式進行判斷, UNSPECIFIED使用預設尺寸,而 AT_MOSTEXACTLY使用父佈局給出的測量尺寸。尺寸計算完畢後透過 setMeasuredDimension(width,height)設定最終寬高。

2.2 Layout

performLayout()部分原始碼:

 private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
            int desiredWindowHeight) {
        .........
        final View host = mView;
        if (host == null) {
            return;
        }
        host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
        .........}

跟measure類似,同樣是從 mView(最頂層ViewGroup)開始進行 layout操作,隨後逐層遍歷。 layout(l,t,r,b)四個引數分別對應 左上右下的位置,從而確定View在ViewGroup中的位置。下面來看一下 layout()部分原始碼:

public void layout(int l, int t, int r, int b) {
    .......
    //透過setOpticalFrame()和setFrame()老確定四個點的位置
    boolean changed = isLayoutModeOptical(mParent) ? 
    setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
    .......
    //呼叫onLayout(),ViewGroup須重寫此方法
    onLayout(changed, l, t, r, b);
    .......}

結合原始碼可知 layout()會將四個位置引數傳遞給 setOpticalFrame()或者 setFrame(),而 setOpticalFrame()內部會呼叫 setFrame(),所以最終透過 setFrame()確定 ViewViewGroup中的位置。位置確定完畢會呼叫 onLayout(l,t,r,b)對子View進行擺放。

onLayout()

ViewViewGroup在執行完 setFrame()後都會呼叫 onLayout()方法,但上面也有提到該方法的作用是對子View進行位置擺放,所以單一View是不需要重寫此方法。而 ViewGroup會根據自己的特性任意對子View進行擺放。

2.3 Draw

相信很多學習自定義View的同學都是奔著有朝一日自己也實現那些眼花繚亂的效果,起碼我自己就是。我們在手機上看到的那些五彩繽紛的圖片,動畫都是在這個方法內繪製而成。

相比於measure和layout階段,draw階段中View和ViewGroup變得沒那麼緊密了,View的繪製過程中不需要考慮ViewGroup,而ViewGroup也只需觸發子View的繪製方法即可。

performDraw()執行後同樣會從根佈局開始逐層對每個View進行draw操作,在View中繪製操作時透過 draw()進行,來看一下其主要原始碼:

public void draw(Canvas canvas) {
     ........
    // 繪製背景
    drawBackground(canvas);
    // 繪製內容
    onDraw(canvas);
    // 繪製子View
    dispatchDraw(canvas);
    // 繪製裝飾,如scrollBar
    onDrawForeground(canvas)
    ........}

draw()方法中主要包含四部分內容,其中我們開發者只需要關心onDraw(canvas)即可,即自身的內容繪製。

繪製內容簡述

關於繪製內容這部分可利用到的知識點很多,多到可以寫一本書出來,所以僅靠本文全部詳細描述顯然是不現實的。下面我羅列一部分常用內容供大家參考:

  • Canvas:畫布,不管是文字,圖形,圖片都要透過畫布繪製而成
  • Paint:畫筆,可設定顏色,粗細,大小,陰影等等等等,一般配合畫布使用
  • Path:路徑,用於形成一些不規則圖形。
  • Matrix:矩陣,可實現對畫布的幾何變換。

總結

文章從四個方面總結了View的繪製流程: 繪製時機, 寬高測量, 位置擺放, 影像繪製,因為側重於流程所以只是把這四部分的精華給拎出來分享給大家,起到一個拋磚引玉的作用,想要透徹理解啟動流程、玩轉自定義View還需要對各部分知識系統的學習。

如何系統學習Android呢?

這裡今天給大家分享一份Android進階學習資料,主要為安卓相關知識點及面試資料為主,在這個PDF中,透過 詳解各大網際網路公司的 Android 常見面試題為主線,從面試的角度帶你介紹必備知識點,以及該知識點在專案中的實際應用。

幫你在現在的基礎上,重新梳理和建立 Android 開發的知識體系。無論是你短期內想提升 Android 內功實力,突破自己工作中的能力瓶頸,還是準備參加 Android 面試,都會在這個PDF中有所收穫。一些基礎不好的,這裡也有一份安卓基礎資料包,幫助鞏固基礎。

以下是這份PDF主要內容

  • Android 核心技術:介紹 Android 開發中常用的核心技術,比如自定義 View、Handler,以及一些開源框架的原理實現,來夯實你的底層能力。只有底層能力足夠出色,之後的進階之路才會更加輕鬆。
  • 常見問題剖析:介紹一些專案中常見的疑難問題,使你能夠對現有專案做出合理的重構最佳化。

1、確定好方向,梳理成長路線圖

不用多說,相信大家都有一個共識:無論什麼行業,最牛逼的人肯定是站在金字塔端的人。所以,想做一個牛逼的程式設計師,那麼就要讓自己站的更高,成為技術大牛並不是一朝一夕的事情,需要時間的沉澱和技術的積累。

關於這一點,在我當時確立好Android方向時,就已經開始梳理自己的成長路線了,包括技術要怎麼系統地去學習,都列得非常詳細。

Android 面試官:簡述一下 View 的繪製流程,這個都答不出來就別想拿Offer了

知識梳理完之後,就需要進行查漏補缺,所以針對這些知識點,我手頭上也準備了不少的電子書和筆記,這些筆記將各個知識點進行了完美的總結:

Android 面試官:簡述一下 View 的繪製流程,這個都答不出來就別想拿Offer了

2、透過原始碼來系統性地學習

只要是程式設計師,不管是Java還是Android,如果不去閱讀原始碼,只看API文件,那就只是停留於皮毛,這對我們知識體系的建立和完備以及實戰技術的提升都是不利的。

真正最能鍛鍊能力的便是直接去閱讀原始碼,不僅限於閱讀各大系統原始碼,還包括各種優秀的開源庫。

Android 面試官:簡述一下 View 的繪製流程,這個都答不出來就別想拿Offer了
《486頁超全面Android開發相關原始碼精編解析》

3、閱讀前輩的一些技術筆記

Android 面試官:簡述一下 View 的繪製流程,這個都答不出來就別想拿Offer了
《960全網最全Android開發筆記》

4、刷題備戰,直通大廠

歷時半年,我們整理了這份市面上最全面的安卓面試題解析大全
包含了騰訊、百度、小米、阿里、樂視、美團、58、360、新浪、搜狐等一線網際網路公司面試被問到的題目。熟悉本文中列出的知識點會大大增加透過前兩輪技術面試的機率。

如何使用它?

1.可以透過目錄索引直接翻看需要的知識點,查漏補缺。
2.五角星數表示面試問到的頻率,代表重要推薦指數

Android 面試官:簡述一下 View 的繪製流程,這個都答不出來就別想拿Offer了
《379頁Android開發面試寶典》

以上文章中的資料,均可以免費分享給大家來學習,無論你是零基礎還是工作多年,現在開始就不會晚。

以上內容均放在了開源專案: github  中已收錄,大家可以自行獲取。

學習技術是一條慢長而艱苦的道路,不能靠一時激情,也不是熬幾天幾夜就能學好的,必須養成平時努力學習的習慣。所以: 貴在堅持!

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69983917/viewspace-2733351/,如需轉載,請註明出處,否則將追究法律責任。

相關文章