文章獨(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í)
- 如果你看到這里,我要對你說聲謝謝,非常感謝你能看完這篇文章