Android之View的誕生之謎

文章獨(dú)家授權(quán)公眾號:碼個(gè)蛋
更多分享:http://www.cherylgood.cn

前言

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

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

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

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

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


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

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

     private void installDecor() {
    //mDecor是window下的一個(gè)內(nèi)部類,你可以理解成他是window用來填充視圖的容器
    if (mDecor == null) {
    //1、通過 mDecor = generateDecor(); 實(shí)例化了DecorView,
    //  而DecorView則是PhoneWindow類的一個(gè)內(nèi)部類,繼承于
    //  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被創(chuàng)建,那么它是如何被創(chuàng)建的呢,他真的是如我們前面所說負(fù)責(zé)加載內(nèi)容部分的父容器么?我們來一探究竟,我們看 mContentParent = generateLayout(mDecor)的源碼:
    protected ViewGroup generateLayout(DecorView decor) {
    // 1、獲得系統(tǒng)當(dāng)前的style
    TypedArray a = getWindowStyle();
    ...
    if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) {
    //2、如果style是Window_windowNoTitle是true,
    //說明當(dāng)前的style是沒有標(biāo)題部分的,則請求移除標(biāo)題
    requestFeature(FEATURE_NO_TITLE);
    } else if (a.getBoolean(R.styleable.Window_windowActionBar, false)) {
    // 3、同樣,檢查是否需要顯示系統(tǒng)的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實(shí)例化了,
      // 這就是我們PhoneWindow. DecorView下的一個(gè)
      // view,該view包含了兩個(gè)子view,一個(gè)是裝在狀
      // 態(tài)欄的,一個(gè)是我們的布局文件。
      View in = mLayoutInflater.inflate(layoutResource, null);  
      decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); 
      mContentRoot = (ViewGroup) in;
      //7、很熟悉的findViewById是不是?ID_ANDROID_CONTENT定位的其實(shí)就是內(nèi)容不問的布局容器了
      ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
      if (contentParent == null) {
          throw new RuntimeException("Window couldn't find content container view");
      }
      ...
      return contentParent;
    }
    
  • 小小的發(fā)現(xiàn):從上面的代碼我們可以解釋很多開發(fā)中的技巧,看下面的代碼,在加載我們的資源文件前,他就檢查了FEATURE_ACTION_BAR和FEATURE_NO_TITLE屬性,所以我們想讓activity全屏或者沒有actionBar的話,必須在setContentView調(diào)用之前設(shè)置。


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

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


    DecorView.png
  • 小結(jié):調(diào)用setContentView方法,實(shí)例化了DecorView, DecorView有兩個(gè)子布局,一個(gè)是加載頂部狀態(tài)欄的,一個(gè)是加載我們的內(nèi)容布局的,activity添加的xml就是內(nèi)容布局的一個(gè)字元素


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

  • 首先ActivityThread通過調(diào)用handleLaunchActivity啟動我們的目標(biāo)activity,

      private performLaunchActivity (ActivityClientRecord r,Intent customIntent{
      ......
      activity.mCalled = false;
              //1、下面調(diào)用了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調(diào)用之后,activity的onCreate被調(diào)用,我們的資源文件不加載,但是此時(shí)還是不可見的,也就還沒有進(jìn)行側(cè)臉之類的事情。

  • 然后我們繼續(xù)看ActivityThread.handleResumeActivity的源碼:
    final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward) {
    ......
    //1、可以看到,這里執(zhí)行了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中獲得與之關(guān)聯(lián)的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關(guān)聯(lián)上,也就是將我們的decor正式
    //添加到window中,
              wm.addView(decor, l); 
          }
          ......
      }
    }
    }
    

知識補(bǔ)充:

  • Window是一個(gè)抽象的概念,一個(gè)Window對應(yīng)一個(gè)View和一個(gè)ViewRootImpl;
  • Window和View是通過ViewRootImpl聯(lián)系起來的。
  • ViewRootImpl才是一個(gè)View真正實(shí)現(xiàn)的動作。
  • WindowManager中也有一個(gè)WindowManagerImpl作為實(shí)現(xiàn)的類,負(fù)責(zé)具體的操作。

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

  • 而WindowManager是個(gè)接口類,那么我們看看其實(shí)類對象WindowManagerImpl.addView方法

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

    public void addView(View view, ViewGroup.LayoutParams params,
          Display display, Window parentWindow) {
      ......
      //注意這個(gè)對象
      ViewRootImpl root;
      View panelParentView = null;
    
      synchronized (mLock) {
          ......
          //1、通過DecorView獲得上下文以及傳入display實(shí)例化一個(gè)ViewRootImpl對象
        //也就是說ViewRootImpl與DecorView關(guān)聯(lián)起來了
          root = new ViewRootImpl(view.getContext(), display); 
          view.setLayoutParams(wparams);
          mViews.add(view);
          mRoots.add(root);
          mParams.add(wparams);
      }
      try {
        //2、這里調(diào)用了ViewRootImpl的setView方法,將DecorView與ViewRootImpl產(chǎn)生來關(guān)聯(lián)。
          root.setView(view, wparams, panelParentView); 
      } catch (RuntimeException e) {
          synchronized (mLock) {
              final int index = findViewLocked(view, false);
              if (index >= 0) {
                  removeViewLocked(index, true);
              }
          }
          throw e;
      }
    }
    
  • 我們繼續(xù)看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、這里會向系統(tǒng)發(fā)出申請,接管屏幕視圖的渲染工作
                  mSurfaceHolderCallback = 
                 ((RootViewSurfaceTaker)view).willYouTakeTheSurface();  
                  if (mSurfaceHolderCallback != null) {  
                      mSurfaceHolder = new TakenSurfaceHolder();  
                      mSurfaceHolder.setFormat(PixelFormat.UNKNOWN);  
                  }  
              }  
    
          //2、這里,我們看到了很熟悉的一個(gè)方法,這就是繪制我們的view的入口了
            requestLayout();
              ......  
    
              try {
                  mOrigWindowType = mWindowAttributes.type;
                  mAttachInfo.mRecomputeGlobalAttributes = true;
                  collectViewAttributes();
                //3、通過WindowSession來完成Window的添加過程這是一個(gè)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 、很開心,開始調(diào)度進(jìn)行繪制流程了
    scheduleTraversals();
    }
    }

  • ViewRootImpl.scheduleTraversals()調(diào)用后,系統(tǒng)會發(fā)起一個(gè)異步消息,然后在異步消息執(zhí)行過程中調(diào)用performTraversals()完成具體的View樹遍歷;

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

     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、執(zhí)行布局方法
          performLayout(lp, desiredWindowWidth, desiredWindowHeight);
          ...
      }
      if (!cancelDraw && !newSurface) {
       ...
     //5、開始繪制了哦
              performDraw();
          }
      } 
    

總結(jié):

  • 通過上面內(nèi)容,我們學(xué)到了一些小技巧,如移除狀態(tài)欄的一些步驟,之前我們可能知道,嗯,是的,要在setContentView前調(diào)用requestFeature才可以,通過這次分析,我們之前可能是知道要這樣子做才行,現(xiàn)在我們知道了為什么要這樣子做。是不是寫起代碼來更踏實(shí)了呢?
  • 通過這次分析,我們對于activity的創(chuàng)建流程也略知一二,希望對你有幫助
  • 測量、布局、繪制的工作我們放到下一章節(jié)進(jìn)行學(xué)習(xí)
  • 如果你看到這里,我要對你說聲謝謝,非常感謝你能看完這篇文章
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容