Android之View的誕生之謎

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

前言

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

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

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

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

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


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

     @Override  
    public void setContentView(int layoutResID) {  
    //在渲染布局資源前做一些前期準備工作
    //1颇玷、 判斷mContentParent是否為null,mContentParent其實
    // 是負責(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是負責(zé)加載我們頁面內(nèi)容的容器渣淳,到底是不是呢?我們看下installDecor源碼便知道了:

     private void installDecor() {
    //mDecor是window下的一個內(nèi)部類伴箩,你可以理解成他是window用來填充視圖的容器
    if (mDecor == null) {
    //1入愧、通過 mDecor = generateDecor(); 實例化了DecorView,
    //  而DecorView則是PhoneWindow類的一個內(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)建的呢旁赊,他真的是如我們前面所說負責(zé)加載內(nèi)容部分的父容器么桦踊?我們來一探究竟,我們看 mContentParent = generateLayout(mDecor)的源碼:
    protected ViewGroup generateLayout(DecorView decor) {
    // 1终畅、獲得系統(tǒng)當前的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杖狼、同樣,檢查是否需要顯示系統(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實例化了,
      // 這就是我們PhoneWindow. DecorView下的一個
      // view,該view包含了兩個子view絮识,一個是裝在狀
      // 態(tài)欄的绿聘,一個是我們的布局文件。
      View in = mLayoutInflater.inflate(layoutResource, null);  
      decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); 
      mContentRoot = (ViewGroup) in;
      //7笋除、很熟悉的findViewById是不是斜友?ID_ANDROID_CONTENT定位的其實就是內(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); 這個方法 mContenParent我們已經(jīng)知道是什么了,然后通過mLayoutInflater.inflate酱吝,我們的布局就被渲染出來了也殖。

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


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


  • 到目前為止,通過setContentView實例化了DecorView并且加載了設(shè)置進來的布局文件濒憋。然后何暇,并沒有發(fā)現(xiàn)任何與測量、布局凛驮、繪制相關(guān)的點裆站,可能你會想,我們不會搞錯了吧辐烂,其實沒有哦遏插,你們想想,setContentView實在纠修,既然還是不可見的,那我為什么要耗費資源去測量呢厂僧,你最終能不能露個臉還說不準呢扣草。虧本的買賣咱不干。其實要想知道什么時候開始執(zhí)行測量等工作颜屠,我們可以看下ActivityThread的源碼辰妙,ActivityThread是android用來管理activity的,這家伙知道的肯定多一些甫窟。那么我們就來了解下ActivityThread的執(zhí)行流程密浑。

  • 首先ActivityThread通過調(diào)用handleLaunchActivity啟動我們的目標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)用,我們的資源文件不加載浇衬,但是此時還是不可見的懒构,也就還沒有進行側(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); 
          }
          ......
      }
    }
    }
    

知識補充:

  • Window是一個抽象的概念韵吨,一個Window對應(yīng)一個View和一個ViewRootImpl匿垄;
  • Window和View是通過ViewRootImpl聯(lián)系起來的移宅。
  • ViewRootImpl才是一個View真正實現(xiàn)的動作。
  • WindowManager中也有一個WindowManagerImpl作為實現(xiàn)的類椿疗,負責(zé)具體的操作漏峰。

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

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

     public void addView(View view, ViewGroup.LayoutParams params) {
      //1靖苇、這里通過mGlobal調(diào)用addView進行添加,而mGlobal是什么呢班缰?
      mGlobal.addView(view, params, mDisplay, mParentWindow);
    }
    
  • mGlobal其實是WindowManagerGlobal的一個內(nèi)部實例贤壁,接著看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關(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、這里旨怠,我們看到了很熟悉的一個方法,這就是繪制我們的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 、很開心倦青,開始調(diào)度進行繪制流程了
    scheduleTraversals();
    }
    }

  • ViewRootImpl.scheduleTraversals()調(diào)用后瓮床,系統(tǒng)會發(fā)起一個異步消息,然后在異步消息執(zhí)行過程中調(diào)用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街州、執(zhí)行布局方法
          performLayout(lp, desiredWindowWidth, desiredWindowHeight);
          ...
      }
      if (!cancelDraw && !newSurface) {
       ...
     //5兼丰、開始繪制了哦
              performDraw();
          }
      } 
    

總結(jié):

  • 通過上面內(nèi)容,我們學(xué)到了一些小技巧唆缴,如移除狀態(tài)欄的一些步驟鳍征,之前我們可能知道,嗯面徽,是的艳丛,要在setContentView前調(diào)用requestFeature才可以,通過這次分析趟紊,我們之前可能是知道要這樣子做才行氮双,現(xiàn)在我們知道了為什么要這樣子做。是不是寫起代碼來更踏實了呢霎匈?
  • 通過這次分析戴差,我們對于activity的創(chuàng)建流程也略知一二,希望對你有幫助
  • 測量唧躲、布局造挽、繪制的工作我們放到下一章節(jié)進行學(xué)習(xí)
  • 如果你看到這里,我要對你說聲謝謝弄痹,非常感謝你能看完這篇文章
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市嵌器,隨后出現(xiàn)的幾起案子肛真,更是在濱河造成了極大的恐慌,老刑警劉巖爽航,帶你破解...
    沈念sama閱讀 216,591評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蚓让,死亡現(xiàn)場離奇詭異,居然都是意外死亡讥珍,警方通過查閱死者的電腦和手機历极,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來衷佃,“玉大人趟卸,你說我怎么就攤上這事∈弦澹” “怎么了锄列?”我有些...
    開封第一講書人閱讀 162,823評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長惯悠。 經(jīng)常有香客問我邻邮,道長,這世上最難降的妖魔是什么克婶? 我笑而不...
    開封第一講書人閱讀 58,204評論 1 292
  • 正文 為了忘掉前任筒严,我火速辦了婚禮丹泉,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘鸭蛙。我一直安慰自己摹恨,他們只是感情好,可當我...
    茶點故事閱讀 67,228評論 6 388
  • 文/花漫 我一把揭開白布规惰。 她就那樣靜靜地躺著睬塌,像睡著了一般。 火紅的嫁衣襯著肌膚如雪歇万。 梳的紋絲不亂的頭發(fā)上揩晴,一...
    開封第一講書人閱讀 51,190評論 1 299
  • 那天,我揣著相機與錄音贪磺,去河邊找鬼硫兰。 笑死,一個胖子當著我的面吹牛寒锚,可吹牛的內(nèi)容都是我干的劫映。 我是一名探鬼主播,決...
    沈念sama閱讀 40,078評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼刹前,長吁一口氣:“原來是場噩夢啊……” “哼泳赋!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起喇喉,我...
    開封第一講書人閱讀 38,923評論 0 274
  • 序言:老撾萬榮一對情侶失蹤祖今,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后拣技,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體千诬,經(jīng)...
    沈念sama閱讀 45,334評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,550評論 2 333
  • 正文 我和宋清朗相戀三年膏斤,在試婚紗的時候發(fā)現(xiàn)自己被綠了徐绑。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,727評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡莫辨,死狀恐怖傲茄,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情衔掸,我是刑警寧澤烫幕,帶...
    沈念sama閱讀 35,428評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站敞映,受9級特大地震影響较曼,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜振愿,卻給世界環(huán)境...
    茶點故事閱讀 41,022評論 3 326
  • 文/蒙蒙 一捷犹、第九天 我趴在偏房一處隱蔽的房頂上張望弛饭。 院中可真熱鬧,春花似錦萍歉、人聲如沸侣颂。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,672評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽憔晒。三九已至,卻和暖如春蔑舞,著一層夾襖步出監(jiān)牢的瞬間拒担,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,826評論 1 269
  • 我被黑心中介騙來泰國打工攻询, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留从撼,地道東北人。 一個月前我還...
    沈念sama閱讀 47,734評論 2 368
  • 正文 我出身青樓钧栖,卻偏偏與公主長得像低零,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子拯杠,可洞房花燭夜當晚...
    茶點故事閱讀 44,619評論 2 354

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