Android之View的誕生之謎

文章獨家授權公眾號:碼個蛋
更多分享:http://www.cherylgood.cn

前言

  • hello,大家好,平時大家都說自定義view,這次給大家帶來有關view的相關知識,希望你喜歡!
  • 作為一名正在崗位上的Android開發者,工作中常常需要我們使用自定義View去實現一些天馬行空的效果,而作為一名正在尋找工作的Android開發者而言,面試過程中自定義View的相關知識點也是熱門的面試題目之一哦,好東西我們怎么能錯過呢;
  • 之前我們在上一篇Android Touch事件分發機制詳解之由點擊引發的戰爭中講述View的事件分發機制,在里面也講了很多與View相關的知識點。
  • 作為Android開發者,我們應該不斷的豐富自身的知識體系結構,加強Android開發內功的修煉(個人看法:學習Android內部底層一些的知識,可視為內功。而對于api的靈活使用,可視為招式)。
  • 本次我們將來探索自定義View的內功心法之自定義View的死亡三部曲:測量、布局、繪制。
  • 在了解死亡三部曲之前,我們先從上層的視角看下死亡三部曲的執行流程。

我們在了解死亡三部曲之前,先了解下我們activity的布局文件是如何被加載的。

  • 我們的activity中的視圖是什么時候被加載的呢?有個方法你肯定會很眼熟:setContentView(R.layout.main);其實我們的activity就是通過這個方法加載我們的布局文件進行視圖的渲染。那么我們就從他入手吧。

  • 我們進入setContentView(R.layout.main)的源碼看一下,注意代碼中的注視:

    public void setContentView(@LayoutRes int layoutResID) {
        //1、調用getWindow().setContentView(layoutResID);
        //  加載我們的布局資;getWindow實際上是調用了phoneWindow
      getWindow().setContentView(layoutResID);
        //2、
      initWindowDecorActionBar();
      }
    
  • window是什么東東?window是一個抽象類,他只有一個實現類,那就是phoneWindow,phoneWindow是android系統中窗口的頂級類,之前在Android Touch事件分發機制詳解之由點擊引發的戰爭有講到,不了解的可以看下。


  • 我們接著看 getWindow().setContentView(layoutResID);

     @Override  
    public void setContentView(int layoutResID) {  
    //在渲染布局資源前做一些前期準備工作
    //1、 判斷mContentParent是否為null,mContentParent其實
    // 是負責加載我們頁面內容的容器,后面我們會講到
    if (mContentParent == null) {  
    installDecor();  
    } else {  
      //1、如果不為null,說明原來頁面上已經有內容了,
      //  所以我們要移除所有的內容,后面再加載新的內容上去
    mContentParent.removeAllViews();  
    }  
      //調用mLayoutInflater來根據我們的布局資源id渲染視圖
    mLayoutInflater.inflate(layoutResID, mContentParent);  
    .....
    }  
    
  • 在 渲染我們的布局文件前,先調用了installDecor()來初始化mContentParent,之前也說mContentParent是負責加載我們頁面內容的容器,到底是不是呢?我們看下installDecor源碼便知道了:

     private void installDecor() {
    //mDecor是window下的一個內部類,你可以理解成他是window用來填充視圖的容器
    if (mDecor == null) {
    //1、通過 mDecor = generateDecor(); 實例化了DecorView,
    //  而DecorView則是PhoneWindow類的一個內部類,繼承于
    //  FrameLayout;
      mDecor = generateDecor(); 
    mDecor.setDescendantFocusability(
    ViewGroup.FOCUS_AFTER_DESCENDANTS);
      mDecor.setIsRootNamespace(true);
      if (!mInvalidatePanelMenuPosted &&
     mInvalidatePanelMenuFeatures != 0) {     
    mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
      }
    }
    if (mContentParent == null) {
    //2、通過傳入mDecor來初始化mContentParent
      mContentParent = generateLayout(mDecor);
      ...
      } 
    }
    }
    
  • 從2處我們看到mContentParent被創建,那么它是如何被創建的呢,他真的是如我們前面所說負責加載內容部分的父容器么?我們來一探究竟,我們看 mContentParent = generateLayout(mDecor)的源碼:
    protected ViewGroup generateLayout(DecorView decor) {
    // 1、獲得系統當前的style
    TypedArray a = getWindowStyle();
    ...
    if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) {
    //2、如果style是Window_windowNoTitle是true,
    //說明當前的style是沒有標題部分的,則請求移除標題
    requestFeature(FEATURE_NO_TITLE);
    } else if (a.getBoolean(R.styleable.Window_windowActionBar, false)) {
    // 3、同樣,檢查是否需要顯示系統的ActionBar
    requestFeature(FEATURE_ACTION_BAR);
    }
    ...
    //4、下面開始初始化我們的mContentParent了
    int layoutResource;
    int features = getLocalFeatures();
    if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
    layoutResource = R.layout.screen_swipe_dismiss;
    } else if(...){
    ...
    }

      //6、這句就把我們的contentParent實例化了,
      // 這就是我們PhoneWindow. DecorView下的一個
      // view,該view包含了兩個子view,一個是裝在狀
      // 態欄的,一個是我們的布局文件。
      View in = mLayoutInflater.inflate(layoutResource, null);  
      decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); 
      mContentRoot = (ViewGroup) in;
      //7、很熟悉的findViewById是不是?ID_ANDROID_CONTENT定位的其實就是內容不問的布局容器了
      ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
      if (contentParent == null) {
          throw new RuntimeException("Window couldn't find content container view");
      }
      ...
      return contentParent;
    }
    
  • 小小的發現:從上面的代碼我們可以解釋很多開發中的技巧,看下面的代碼,在加載我們的資源文件前,他就檢查了FEATURE_ACTION_BAR和FEATURE_NO_TITLE屬性,所以我們想讓activity全屏或者沒有actionBar的話,必須在setContentView調用之前設置。


  • 接下來我們回到前面
    setContentViewgetWindow().setContentView(layoutResID);方法,繼續看mLayoutInflater.inflate(layoutResID, mContentParent); 這個方法 mContenParent我們已經知道是什么了,然后通過mLayoutInflater.inflate,我們的布局就被渲染出來了。

  • DecorView補充: DecorView是整個ViewTree的最頂層View,我們之前分析過她是是個FrameLayout布局,代表了整個應用的界面。在該布局下面,有標題view和內容view這兩個子元素,而內容view則是上面提到的mContentParent。如下圖:


    DecorView.png
  • 小結:調用setContentView方法,實例化了DecorView, DecorView有兩個子布局,一個是加載頂部狀態欄的,一個是加載我們的內容布局的,activity添加的xml就是內容布局的一個字元素


  • 到目前為止,通過setContentView實例化了DecorView并且加載了設置進來的布局文件。然后,并沒有發現任何與測量、布局、繪制相關的點,可能你會想,我們不會搞錯了吧,其實沒有哦,你們想想,setContentView實在,既然還是不可見的,那我為什么要耗費資源去測量呢,你最終能不能露個臉還說不準呢。虧本的買賣咱不干。其實要想知道什么時候開始執行測量等工作,我們可以看下ActivityThread的源碼,ActivityThread是android用來管理activity的,這家伙知道的肯定多一些。那么我們就來了解下ActivityThread的執行流程。

  • 首先ActivityThread通過調用handleLaunchActivity啟動我們的目標activity,

      private performLaunchActivity (ActivityClientRecord r,Intent customIntent{
      ......
      activity.mCalled = false;
              //1、下面調用了Activity的onCreate方法
              if (r.isPersistable()) {
                  mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
            } else {
                 mInstrumentation.callActivityOnCreate(activity, r.state);
            }
              if (!activity.mCalled) {
                throw new SuperNotCalledException(
                     "Activity " + r.intent.getComponent().toShortString() +
                    " did not call through to super.onCreate()");
            }
      
    }
    
  • 也就是說在performLaunchActivity調用之后,activity的onCreate被調用,我們的資源文件不加載,但是此時還是不可見的,也就還沒有進行側臉之類的事情。

  • 然后我們繼續看ActivityThread.handleResumeActivity的源碼:
    final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward) {
    ......
    //1、可以看到,這里執行了activity的onResume方法
    ActivityClientRecord r = performResumeActivity(token, clearHide);
    if (r != null) {
    final Activity a = r.activity;
    .......
    if (r.window == null && !a.mFinished && willBeVisible) {

           // 2、獲得window對象
          r.window = r.activity.getWindow();
    
          //3、 從window中獲取DecorView對象
          View decor = r.window.getDecorView(); 
          decor.setVisibility(View.INVISIBLE);
    
          //4、從activity中獲得與之關聯的windowManager對象
          ViewManager wm = a.getWindowManager(); 
          WindowManager.LayoutParams l = r.window.getAttributes();
          a.mDecor = decor;
          l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
          l.softInputMode |= forwardBit;
          if (a.mVisibleFromClient) {
              a.mWindowAdded = true;
            //5、終于找到你了,這里將decor與WindowManager關聯上,也就是將我們的decor正式
    //添加到window中,
              wm.addView(decor, l); 
          }
          ......
      }
    }
    }
    

知識補充:

  • Window是一個抽象的概念,一個Window對應一個View和一個ViewRootImpl;
  • Window和View是通過ViewRootImpl聯系起來的。
  • ViewRootImpl才是一個View真正實現的動作。
  • WindowManager中也有一個WindowManagerImpl作為實現的類,負責具體的操作。

  • 跟到這里,我們來總結一下,activity啟動過程中,在執行handleResumeActivity時將我們的頂層視圖DecorView通過WindowManager掛載到window中。

  • 而WindowManager是個接口類,那么我們看看其實類對象WindowManagerImpl.addView方法

     public void addView(View view, ViewGroup.LayoutParams params) {
      //1、這里通過mGlobal調用addView進行添加,而mGlobal是什么呢?
      mGlobal.addView(view, params, mDisplay, mParentWindow);
    }
    
  • mGlobal其實是WindowManagerGlobal的一個內部實例,接著看WindowManagerGlobal.addView的源碼:

    public void addView(View view, ViewGroup.LayoutParams params,
          Display display, Window parentWindow) {
      ......
      //注意這個對象
      ViewRootImpl root;
      View panelParentView = null;
    
      synchronized (mLock) {
          ......
          //1、通過DecorView獲得上下文以及傳入display實例化一個ViewRootImpl對象
        //也就是說ViewRootImpl與DecorView關聯起來了
          root = new ViewRootImpl(view.getContext(), display); 
          view.setLayoutParams(wparams);
          mViews.add(view);
          mRoots.add(root);
          mParams.add(wparams);
      }
      try {
        //2、這里調用了ViewRootImpl的setView方法,將DecorView與ViewRootImpl產生來關聯。
          root.setView(view, wparams, panelParentView); 
      } catch (RuntimeException e) {
          synchronized (mLock) {
              final int index = findViewLocked(view, false);
              if (index >= 0) {
                  removeViewLocked(index, true);
              }
          }
          throw e;
      }
    }
    
  • 我們繼續看ViewRootImpl.setView方法的源碼

    public final class ViewRootImpl implements ViewParent,  
    public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {  
      synchronized (this) {  
          if (mView == null) {  
              mView = view;  
              ......  
              if (view instanceof RootViewSurfaceTaker) {  
                //1、這里會向系統發出申請,接管屏幕視圖的渲染工作
                  mSurfaceHolderCallback = 
                 ((RootViewSurfaceTaker)view).willYouTakeTheSurface();  
                  if (mSurfaceHolderCallback != null) {  
                      mSurfaceHolder = new TakenSurfaceHolder();  
                      mSurfaceHolder.setFormat(PixelFormat.UNKNOWN);  
                  }  
              }  
    
          //2、這里,我們看到了很熟悉的一個方法,這就是繪制我們的view的入口了
            requestLayout();
              ......  
    
              try {
                  mOrigWindowType = mWindowAttributes.type;
                  mAttachInfo.mRecomputeGlobalAttributes = true;
                  collectViewAttributes();
                //3、通過WindowSession來完成Window的添加過程這是一個IPC的過程,這里就不在深入了。
                  res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                          getHostVisibility(), mDisplay.getDisplayId(),
                          mAttachInfo.mContentInsets, mInputChannel);
              } catch (RemoteException e) {
                  mAdded = false;
                  mView = null;
                  mAttachInfo.mRootView = null;
                  mInputChannel = null;
                  mFallbackEventHandler.setView(null);
                  unscheduleTraversals();
                  setAccessibilityFocus(null, null);
                  throw new RuntimeException("Adding window failed", e);
              } finally {
                  if (restore) {
                      attrs.restore();
                  }
              }
    
              ......  
          }  
      }  
    }  
    
    ......  
    }  
    
  • setView完成的工作很多,如聲明輸入事件的管道,DisplayManager的注冊,view的繪畫,window的添加等等

  • 作為繪制view的入口,我們來看下requestLayout方法
    @Override
    public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
    checkThread();
    mLayoutRequested = true;
    //1 、很開心,開始調度進行繪制流程了
    scheduleTraversals();
    }
    }

  • ViewRootImpl.scheduleTraversals()調用后,系統會發起一個異步消息,然后在異步消息執行過程中調用performTraversals()完成具體的View樹遍歷;

  • 小子,總算是找到你了,我們來看下勝利的果實吧!

     private void performTraversals() {
          ...
      if (!mStopped) {
        //1、獲取頂層布局的childWidthMeasureSpec
          int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);  
        //2、獲取頂層布局的childHeightMeasureSpec
          int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
          //3、測量開始測量
          performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);       
          }
      } 
    
      if (didLayout) {
        //4、執行布局方法
          performLayout(lp, desiredWindowWidth, desiredWindowHeight);
          ...
      }
      if (!cancelDraw && !newSurface) {
       ...
     //5、開始繪制了哦
              performDraw();
          }
      } 
    

總結:

  • 通過上面內容,我們學到了一些小技巧,如移除狀態欄的一些步驟,之前我們可能知道,嗯,是的,要在setContentView前調用requestFeature才可以,通過這次分析,我們之前可能是知道要這樣子做才行,現在我們知道了為什么要這樣子做。是不是寫起代碼來更踏實了呢?
  • 通過這次分析,我們對于activity的創建流程也略知一二,希望對你有幫助
  • 測量、布局、繪制的工作我們放到下一章節進行學習
  • 如果你看到這里,我要對你說聲謝謝,非常感謝你能看完這篇文章
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,401評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,011評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,263評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,543評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,323評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,874評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,968評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,095評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,605評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,551評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,720評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,242評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,961評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,358評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,612評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,330評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,690評論 2 370

推薦閱讀更多精彩內容