文章獨家授權(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捆毫。如下圖:
小結(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í)
- 如果你看到這里,我要對你說聲謝謝弄痹,非常感謝你能看完這篇文章