文章獨家授權公眾號:碼個蛋
更多分享: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的創建流程也略知一二,希望對你有幫助
- 測量、布局、繪制的工作我們放到下一章節進行學習
- 如果你看到這里,我要對你說聲謝謝,非常感謝你能看完這篇文章