靈魂畫師,Android繪製流程——Android高階UI
一、前言
繪製流程可以說是 Android進階中必不可少的一個內容,也是面試中被問得最多的問題之一。這方面優秀的文章也已經是非常之多,但是小盆友今天還是要以自己的姿態來炒一炒這冷飯,或許就是蛋炒飯了?。話不多說,老規矩先上實戰圖,然後開始分享。
標籤佈局
二、我們的目標是啥
其實這篇文章,小盆友糾結了挺久,因為繪製流程涉及的東西非常之多,並非一篇文章可以寫完,所以這篇文章我先要確定一些目標,防止因為追查原始碼過深,而迷失於原始碼中,最後導致一無所獲。我們的目標是:
- 繪製流程從何而起
- Activity 的介面結構在哪裡開始形成
- 繪製流程如何運轉起來
接下來我們就一個個目標來 conquer。
三、繪製流程從何而起
我們一說到
繪製流程,就會想到或是聽過
onMeasure
、
onLayout
、
onDraw
這三個方法,但是有沒想過為什麼我們開啟一個App或是點開一個Activity,就會觸發這一系列流程呢?想知道
繪製流程從何而起,我們就有必要先解釋
App啟動流程 和
Activity的啟動流程。
我們都知道 ActivityThread 的
main
是一個App的入口。我們來到
main
方法看看他做了什麼啟動操作。
ActivityThread 的
main
方法是由 ZygoteInit 類中最終透過 RuntimeInit類的invokeStaticMain
方法進行反射呼叫。有興趣的童鞋可以自行查閱下,限於篇幅,就不再展開分享。
// ActivityThread 類public static void main(String[] args) { // ...省略不相關程式碼 // 準備主執行緒的 Looper Looper.prepareMainLooper(); // 例項化 ActivityThread,用於管理應用程式程式中主執行緒的執行 ActivityThread thread = new ActivityThread(); // 進入 attach 方法 thread.attach(false); // ...省略不相關程式碼 // 開啟 Looper Looper.loop(); // ...省略不相關程式碼}
進入
main
方法,我們便看到很熟悉的 Handler機制。在安卓中都是以訊息進行驅動,在這裡也不例外,我們可以看到先進行 Looper 的準備,在最後開啟 Looper 進行迴圈獲取訊息,用於處理傳到主執行緒的訊息。
這也是為什麼我們在主執行緒不需要先進行 Looper 的準備和開啟,emmm,有些扯遠了。
回過頭,可以看到夾雜在中間的 ActivityThread 類的例項化並且呼叫了
attach
方法。具體程式碼如下,我們接著往下走。
// ActivityThread 類private void attach(boolean system) { // ...省略不相關程式碼 // system 此時為false,進入此分支 if (!system) { // ...省略不相關程式碼 // 獲取系統的 AMS 服務的 Proxy,用於向 AMS 程式傳送資料 final IActivityManager mgr = ActivityManager.getService(); try { // 將我們的 mAppThread 傳遞給 AMS,AMS 便可控制我們 App 的 Activity mgr.attachApplication(mAppThread); } catch (RemoteException ex) { throw ex.rethrowFromSystemServer(); } // ...省略不相關程式碼 } else { // ...省略不相關程式碼 } // ...省略不相關程式碼}// ActivityManager 類public static IActivityManager getService() { return IActivityManagerSingleton.get(); }// ActivityManager 類private static final Singleton<IActivityManager> IActivityManagerSingleton = new Singleton<IActivityManager>() { @Override protected IActivityManager create() { // 在這裡獲取 AMS 的binder final IBinder b = ServiceManager.getService(Context.ACTIVITY_SERVICE); // 這裡獲取 AMS 的 proxy,可以進行傳送資料 final IActivityManager am = IActivityManager.Stub.asInterface(b); return am; } };
我們進入
attach
方法,方法內主要是透過 ActivityManager 的
getService
方法獲取到了 ActivityManagerService(也就是我們所說的AMS) 的 Proxy,達到與AMS 進行跨程式通訊的目的。
文中所說的 Proxy 和 Stub,是以系統為我們自動生成AIDL時的類名進行類比使用,方便講解。Proxy 代表著傳送資訊,Stub 代表著接收資訊。
在
mgr.attachApplication(mAppThread);
程式碼中向 AMS 程式傳送資訊,攜帶了一個型別為 ApplicationThread 的
mAppThread
引數。這句程式碼的作用,其實就是把
我們應用的 “控制器” 上交給了 AMS,這樣使得
AMS 能夠來控制我們應用中的Activity的生命週期。為什麼這麼說呢?我們這就有必要來了解下 ApplicationThread 類的結構,其部分程式碼如下:
// ActivityThread$ApplicationThread 類private class ApplicationThread extends IApplicationThread.Stub { // 省略大量程式碼 @Override public final void scheduleLaunchActivity(Intent intent, IBinder token, int ident, ActivityInfo info, Configuration curConfig, Configuration overrideConfig, CompatibilityInfo compatInfo, String referrer, IVoiceInteractor voiceInteractor, int procState, Bundle state, PersistableBundle persistentState, List<ResultInfo> pendingResults, List<ReferrerIntent> pendingNewIntents, boolean notResumed, boolean isForward, ProfilerInfo profilerInfo) { updateProcessState(procState, false); // 會將 AMS 發來的資訊封裝在 ActivityClientRecord 中,然後傳送給 Handler ActivityClientRecord r = new ActivityClientRecord(); r.token = token; r.ident = ident; r.intent = intent; r.referrer = referrer; r.voiceInteractor = voiceInteractor; r.activityInfo = info; r.compatInfo = compatInfo; r.state = state; r.persistentState = persistentState; r.pendingResults = pendingResults; r.pendingIntents = pendingNewIntents; r.startsNotResumed = notResumed; r.isForward = isForward; r.profilerInfo = profilerInfo; r.overrideConfig = overrideConfig; updatePendingConfiguration(curConfig); sendMessage(H.LAUNCH_ACTIVITY, r); } // 省略大量程式碼}
從 ApplicationThread 的方法名,我們會驚奇的發現大多方法名以
scheduleXxxYyyy
的形式命名,而且和我們熟悉的生命週期都挺接近。上面程式碼留下了我們需要的方法
scheduleLaunchActivity
,它們包含了我們 Activity 的
onCreate
、
onStart
和
onResume
。
scheduleLaunchActivity
方法會對 AMS 發來的資訊封裝在 ActivityClientRecord 類中,最後透過
sendMessage(H.LAUNCH_ACTIVITY, r);
這行程式碼將資訊以
H.LAUNCH_ACTIVITY
的資訊標記傳送至我們主執行緒中的 Handler。我們進入主執行緒的 Handler 實現類 H。具體程式碼如下:
// ActivityThread$H 類private class H extends Handler { public static final int LAUNCH_ACTIVITY = 100; // 省略大量程式碼 public void handleMessage(Message msg) { if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what)); switch (msg.what) { case LAUNCH_ACTIVITY: { Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityStart"); final ActivityClientRecord r = (ActivityClientRecord) msg.obj; r.packageInfo = getPackageInfoNoCheck( r.activityInfo.applicationInfo, r.compatInfo); handleLaunchActivity(r, null, "LAUNCH_ACTIVITY"); Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); } // 省略大量程式碼 } } // 省略大量程式碼}
我們從上面的程式碼可以知道訊息型別為
LAUNCH_ACTIVITY
,則會進入
handleLaunchActivity
方法,我們順著往裡走,來到下面這段程式碼
private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent, String reason) { // If we are getting ready to gc after going to the background, well // we are back active so skip it. unscheduleGcIdler(); mSomeActivitiesChanged = true; if (r.profilerInfo != null) { mProfiler.setProfiler(r.profilerInfo); mProfiler.startProfiling(); } // Make sure we are running with the most recent config. handleConfigurationChanged(null, null); if (localLOGV) Slog.v( TAG, "Handling launch of " + r); // Initialize before creating the activity WindowManagerGlobal.initialize(); // 獲得一個Activity物件,會進行呼叫 Activity 的 onCreate 和 onStart 的生命週期 Activity a = performLaunchActivity(r, customIntent); // Activity 不為空進入 if (a != null) { r.createdConfig = new Configuration(mConfiguration); reportSizeConfigurations(r); Bundle oldState = r.state; // 該方法最終回撥用到 Activity 的 onResume handleResumeActivity(r.token, false, r.isForward, !r.activity.mFinished && !r.startsNotResumed, r.lastProcessedSeq, reason); if (!r.activity.mFinished && r.startsNotResumed) { performPauseActivityIfNeeded(r, reason); if (r.isPreHoneycomb()) { r.state = oldState; } } } else { // If there was an error, for any reason, tell the activity manager to stop us. try { ActivityManager.getService() .finishActivity(r.token, Activity.RESULT_CANCELED, null, Activity.DONT_FINISH_TASK_WITH_ACTIVITY); } catch (RemoteException ex) { throw ex.rethrowFromSystemServer(); } } }
我們先看這行程式碼
performLaunchActivity(r, customIntent);
最終會呼叫
onCreate
和
onStart
方法。眼見為實,耳聽為虛,我們繼續進入深入。來到下面這段程式碼
// ActivityThread 類private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) { // 省略不相關程式碼 // 建立 Activity 的 Context ContextImpl appContext = createBaseContextForActivity(r); Activity activity = null; try { java.lang.ClassLoader cl = appContext.getClassLoader(); // ClassLoader 載入 Activity類,並建立 Activity activity = mInstrumentation.newActivity( cl, component.getClassName(), r.intent); // 省略不相關程式碼 } catch (Exception e) { // 省略不相關程式碼 } try { // 建立 Application Application app = r.packageInfo.makeApplication(false, mInstrumentation); // 省略不相關程式碼 if (activity != null) { // 省略不相關程式碼 // 呼叫了 Activity 的 attach activity.attach(appContext, this, getInstrumentation(), r.token, r.ident, app, r.intent, r.activityInfo, title, r.parent, r.embeddedID, r.lastNonConfigurationInstances, config, r.referrer, r.voiceInteractor, window, r.configCallback); // 這個 intent 就是我們 getIntent 獲取到的 if (customIntent != null) { activity.mIntent = customIntent; } // 省略不相關程式碼 // 呼叫 Activity 的 onCreate if (r.isPersistable()) { mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState); } else { mInstrumentation.callActivityOnCreate(activity, r.state); } // 省略不相關程式碼 if (!r.activity.mFinished) { // zincPower 呼叫 Activity 的 onStart activity.performStart(); r.stopped = false; } if (!r.activity.mFinished) { // zincPower 呼叫 Activity 的 onRestoreInstanceState 方法,資料恢復 if (r.isPersistable()) { if (r.state != null || r.persistentState != null) { mInstrumentation.callActivityOnRestoreInstanceState(activity, r.state, r.persistentState); } } else if (r.state != null) { mInstrumentation.callActivityOnRestoreInstanceState(activity, r.state); } } // 省略不相關程式碼 } // 省略不相關程式碼 } // 省略不相關程式碼 return activity; }// Instrumentation 類public void callActivityOnCreate(Activity activity, Bundle icicle, PersistableBundle persistentState) { prePerformCreate(activity); activity.performCreate(icicle, persistentState); postPerformCreate(activity); }// Activity 類final void performCreate(Bundle icicle) { restoreHasCurrentPermissionRequest(icicle); // 呼叫了 onCreate onCreate(icicle); mActivityTransitionState.readState(icicle); performCreateCommon(); }// Activity 類final void performStart() { // 省略不相關程式碼 // 進行呼叫 Activity 的 onStart mInstrumentation.callActivityOnStart(this); // 省略不相關程式碼}// Instrumentation 類public void callActivityOnStart(Activity activity) { // 呼叫了 Activity 的 onStart activity.onStart(); }
進入
performLaunchActivity
方法後,我們會發現很多我們熟悉的東西,小盆友已經給關鍵點打上註釋,因為不是文章的重點就不再細說,否則篇幅過長。
我們直接定位到
mInstrumentation.callActivityOnCreate
這行程式碼。進入該方法,方法內會呼叫
activity
的
performCreate
方法,而
performCreate
方法裡會呼叫到我們經常重寫的 Activity 生命週期的
onCreate
方法。?至此,找到了
onCreate
的呼叫地方,這裡需要立個
FLAG1,因為目標二需要的開啟便是這裡,我下一小節分享,勿急。
回過頭來繼續
performLaunchActivity
方法的執行,會呼叫到
activity
的
performStart
方法,而該方法又會呼叫到
mInstrumentation.callActivityOnStart
方法,最後在該方法內便呼叫了我們經常重寫的 Activity 生命週期的
onStart
方法。?至此,找到了
onStart
的呼叫地方。
找到了兩個生命週期的呼叫地方,我們需要折回到
handleLaunchActivity
方法中,繼續往下執行,便會來到
handleResumeActivity
方法,具體程式碼如下:
// ActivityThread 類final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) { // 省略部分程式碼 r = performResumeActivity(token, clearHide, reason); // 省略部分程式碼 if (r.window == null && !a.mFinished && willBeVisible) { // 將 Activity 中的 Window 賦值給 ActivityClientRecord 的 Window r.window = r.activity.getWindow(); // 獲取 DecorView,這個 DecorView 在 Activity 的 setContentView 時就初始化了 View decor = r.window.getDecorView(); // 此時為不可見 decor.setVisibility(View.INVISIBLE); // WindowManagerImpl 為 ViewManager 的實現類 ViewManager wm = a.getWindowManager(); WindowManager.LayoutParams l = r.window.getAttributes(); a.mDecor = decor; l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION; l.softInputMode |= forwardBit; if (r.mPreserveWindow) { a.mWindowAdded = true; r.mPreserveWindow = false; ViewRootImpl impl = decor.getViewRootImpl(); if (impl != null) { impl.notifyChildRebuilt(); } } if (a.mVisibleFromClient) { if (!a.mWindowAdded) { a.mWindowAdded = true; // 往 WindowManager 新增 DecorView,並且帶上 WindowManager.LayoutParams // 這裡面便觸發真正的繪製流程 wm.addView(decor, l); } else { a.onWindowAttributesChanged(l); } } } // 省略不相關程式碼}
performResumeActivity
方法最終會呼叫到 Activity 的
onResume
方法,因為不是我們該小節的目標,就不深入了,童鞋們可以自行深入,程式碼也比較簡單。至此我們就找齊了我們一直重寫的三個 Acitivity 的生命週期函式
onCreate
、
onStart
和
onResume
。按照這一套路,童鞋們可以看看 ApplicationThread 的其他方法,會發現 Activity 的生命週期均在其中可以找到影子,也就證實了我們最開始所說的
我們將應用 “遙控器” 交給了AMS。而值得一提的是,這一操作是處於一個跨程式的場景。
繼續往下執行來到
wm.addView(decor, l);
這行程式碼,
wm
的具體實現類為
WindowManagerImpl
,繼續跟蹤深入,來到下面這一連串的呼叫
// WindowManagerImpl 類@Overridepublic void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) { applyDefaultToken(params); // tag:進入這一行 mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow); }// WindowManagerGlobal 類public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) { // 省略不相關程式碼 ViewRootImpl root; View panelParentView = null; synchronized (mLock) { // 省略不相關程式碼 // 初始化 ViewRootImpl root = new ViewRootImpl(view.getContext(), display); view.setLayoutParams(wparams); mViews.add(view); mRoots.add(root); mParams.add(wparams); try { // 將 view 和 param 交於 root // ViewRootImpl 開始繪製 view // tag:進入這一行 root.setView(view, wparams, panelParentView); } catch (RuntimeException e) { if (index >= 0) { removeViewLocked(index, true); } throw e; } } }// ViewRootImpl 類public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) { synchronized (this) { if (mView == null) { // 省略不相關程式碼 // 進入繪製流程 // tag:進入這一行 requestLayout(); // 省略不相關程式碼 } } }// ViewRootImpl 類@Overridepublic void requestLayout() { if (!mHandlingLayoutInLayoutRequest) { checkThread(); mLayoutRequested = true; // tag:進入這一行 scheduleTraversals(); } }// ViewRootImpl 類void scheduleTraversals() { if (!mTraversalScheduled) { mTraversalScheduled = true; mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier(); // 提交給 編舞者,會在下一幀繪製時呼叫 mTraversalRunnable,執行其run mChoreographer.postCallback( Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null); if (!mUnbufferedInputDispatch) { scheduleConsumeBatchedInput(); } notifyRendererOfFramePending(); pokeDrawLockIfNeeded(); } }
中間跳轉的方法比較多,小盆友都打上了
// tag:進入這一行
註釋,童鞋們可以自行跟蹤,會發現最後會呼叫到編舞者,即 Choreographer 類的 postCallback方法。Choreographer 是一個會接收到垂直同步訊號的類,所以
當下一幀到達時,他會呼叫我們剛才提交的任務,即此處的
mTraversalRunnable
,並執行其
run
方法。
值得一提的是透過 Choreographer 的 postCallback 方法提交的任務並不是每一幀都會呼叫,而是隻在下一幀到來時呼叫,呼叫完之後就會將該任務移除。簡而言之,就是提交一次就會在下一幀呼叫一次。
我們繼續來看
mTraversalRunnable
的具體內容,看看每一幀都做了寫什麼操作。
// ViewRootImpl 類final TraversalRunnable mTraversalRunnable = new TraversalRunnable();// ViewRootImpl 類final class TraversalRunnable implements Runnable { @Override public void run() { doTraversal(); } }// ViewRootImpl 類void doTraversal() { if (mTraversalScheduled) { mTraversalScheduled = false; mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier); if (mProfile) { Debug.startMethodTracing("ViewAncestor"); } // 進入此處 performTraversals(); if (mProfile) { Debug.stopMethodTracing(); mProfile = false; } } }// ViewRootImpl 類private void performTraversals() { // 省略不相關程式碼 if (!mStopped || mReportNextDraw) { // 省略不相關程式碼 // FLAG2 int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width); int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height); // 省略不相關程式碼 // 進行測量 performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); // 省略不相關程式碼 // 進行擺放 performLayout(lp, mWidth, mHeight); // 省略不相關程式碼 // 佈局完回撥 if (triggerGlobalLayoutListener) { mAttachInfo.mRecomputeGlobalAttributes = false; mAttachInfo.mTreeObserver.dispatchOnGlobalLayout(); } // 省略不相關程式碼 // 進行繪製 performDraw(); }
呼叫了
mTraversalRunnable
的
run
方法之後,會發現也是一連串的方法呼叫,後來到
performTraversals
,這裡面就有我們一直提到三個繪製流程方法的起源地。這三個起源地就是我們在上面看到的三個方法
performMeasure
、
performLayout
、
performDraw
。
而這三個方法會進行如下圖的一個呼叫鏈(?還是手繪,勿噴),從程式碼我們也知道,會按照
performMeasure
、
performLayout
、
performDraw
的順序依次呼叫。
performMeasure
會觸發我們的測量流程,如圖中所示,進入第一層的 ViewGroup,會呼叫
measure
和
onMeasure
,在
onMeasure
中呼叫下一層級,然後下一層級的 View或ViewGroup 會重複這樣的動作,進行所有 View 的測量。(這一過程可以理解為書的深度遍歷)
performLayout
和
performMeasure
的流程大同小異,只是方法名不同,就不再贅述。
performDraw
稍微些許不同,當前控制元件為ViewGroup時,只有需要繪製背景或是我們透過
setWillNotDraw(false)
設定我們的ViewGroup需要進行繪製時,會進入
onDraw
方法,然後透過
dispatchDraw
進行繪製子View,如此迴圈。而如果為View,自然也就不需要繪製子View,只需繪製自身的內容即可。
至此,繪製流程的源頭我們便了解清楚了,
onMeasure
、
onLayout
、
onDraw
三個方法我們會在後面進行詳述並融入在實戰中。
四、Activity 的介面結構在哪裡開始形成
上圖是 Activity 的結構。我們先進行大致的描述,然後在進入原始碼體會這一過程。
我們可以清晰的知道一個 Activity 會對應著有一個 Window,而
Window 的唯一實現類為 PhoneWindow,
PhoneWindow 的初始化是在 Activity 的
attach
方法中,我們前面也有提到
attach
方法,感興趣的童鞋可以自行深入。
在往下一層是一個 DecorView,被 PhoneWindow 持有著, DecorView 的初始化在 setContentView 中,這個我們待會會進行詳細分析。 DecorView 是我們的頂級View,我們設定的佈局只是其子View。
DecorView 是一個 FrameLayout。但在
setContentView
中,會給他加入一個線性的佈局(LinearLayout)。該線性佈局的子View 則一般由 TitleBar 和 ContentView 進行組成。TitleBar 我們可以透過
requestWindowFeature(Window.FEATURE_NO_TITLE);
進行去除,而
ContentView 則是來裝載我們設定的佈局檔案的 ViewGroup 了。
現在我們已經有一個大概的印象,接下來進行詳細分析。在上一節中(FLAG1處),我們最先會進入的生命週期為
onCreate
,在該方法中我們都會寫上這樣一句程式碼
setContentView(R.layout.xxxx)
進行設定佈局。經過上一節我們也知道,真正的繪製流程是在
onResume
之後(忘記的童鞋請倒回去看一下),那麼
setContentView
起到一個什麼作用呢?我進入原始碼一探究竟吧。
進入 Activity 的
setContentView
方法,可以看到下面這段程式碼。getWindow 返回的是一個 Window 型別的物件,而透過Window的官方註釋可以知道其
唯一的實現類為PhoneWindow, 所以我們進入 PhoneWindow 類檢視其
setContentView
方法,這裡值得我們注意有兩行程式碼。我們一一進入,我們先進入
installDecor
方法。
// Activity 類public void setContentView(@LayoutRes int layoutResID) { // getWindow 返回的是 PhoneWindow getWindow().setContentView(layoutResID); initWindowDecorActionBar(); }// Activity 類public Window getWindow() { return mWindow; }// PhoneWindow 類@Overridepublic void setContentView(int layoutResID) { // 此時 mContentParent 為空,mContentParent 是裝載我們佈局的容器 if (mContentParent == null) { // 進行初始化 頂級View——DecorView 和 我們設定的佈局的裝載容器——ViewGroup(mContentParent) installDecor(); } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) { mContentParent.removeAllViews(); } if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) { final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID, getContext()); transitionTo(newScene); } else { // 載入我們設定的佈局檔案 到 mContentParent mLayoutInflater.inflate(layoutResID, mContentParent); } mContentParent.requestApplyInsets(); final Callback cb = getCallback(); if (cb != null && !isDestroyed()) { cb.onContentChanged(); } mContentParentExplicitlySet = true; }
installDecor
方法的作用為初始化了我們的頂級View(即DecorView)和初始化裝載我們佈局的容器(即 mContentParent 屬性)。具體程式碼如下
private void installDecor() { mForceDecorInstall = false; if (mDecor == null) { // 會進行例項化 一個mDecor mDecor = generateDecor(-1); mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS); mDecor.setIsRootNamespace(true); if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) { mDecor.postOnAnimation(mInvalidatePanelMenuRunnable); } } else { mDecor.setWindow(this); } if (mContentParent == null) { // 初始化 mContentParent mContentParent = generateLayout(mDecor); // 省略不相關程式碼}
在
generateDecor
中會進行 DecorView 的建立,具體程式碼如下,較為簡單
protected DecorView generateDecor(int featureId) { Context context; if (mUseDecorContext) { Context applicationContext = getContext().getApplicationContext(); if (applicationContext == null) { context = getContext(); } else { context = new DecorContext(applicationContext, getContext().getResources()); if (mTheme != -1) { context.setTheme(mTheme); } } } else { context = getContext(); } return new DecorView(context, featureId, this, getAttributes()); }
緊接著是
generateLayout
方法,核心程式碼如下,如果我們在
onCreate
方法前透過
requestFeature
進行設定一些特徵,此時的
getLocalFeatures
就會獲取到,並根據其值選擇合適的佈局賦值給
layoutResource
屬性。最後將該佈局資源解析,賦值給 DecorView,緊接著將 DecorView 中 id 為 content 的控制元件賦值給 contentParent,而這個控制元件將來就是裝載我們設定的佈局資源。
protected ViewGroup generateLayout(DecorView decor) { // 省略不相關程式碼 int layoutResource; int features = getLocalFeatures(); // System.out.println("Features: 0x" + Integer.toHexString(features)); if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) { layoutResource = R.layout.screen_swipe_dismiss; setCloseOnSwipeEnabled(true); } else if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) { if (mIsFloating) { TypedValue res = new TypedValue(); getContext().getTheme().resolveAttribute( R.attr.dialogTitleIconsDecorLayout, res, true); layoutResource = res.resourceId; } else { layoutResource = R.layout.screen_title_icons; } // XXX Remove this once action bar supports these features. removeFeature(FEATURE_ACTION_BAR); // System.out.println("Title Icons!"); } else if ((features & ((1 << FEATURE_PROGRESS) | (1 << FEATURE_INDETERMINATE_PROGRESS))) != 0 && (features & (1 << FEATURE_ACTION_BAR)) == 0) { // Special case for a window with only a progress bar (and title). // XXX Need to have a no-title version of embedded windows. layoutResource = R.layout.screen_progress; // System.out.println("Progress!"); } else if ((features & (1 << FEATURE_CUSTOM_TITLE)) != 0) { // Special case for a window with a custom title. // If the window is floating, we need a dialog layout if (mIsFloating) { TypedValue res = new TypedValue(); getContext().getTheme().resolveAttribute( R.attr.dialogCustomTitleDecorLayout, res, true); layoutResource = res.resourceId; } else { layoutResource = R.layout.screen_custom_title; } // XXX Remove this once action bar supports these features. removeFeature(FEATURE_ACTION_BAR); } else if ((features & (1 << FEATURE_NO_TITLE)) == 0) { // If no other features and not embedded, only need a title. // If the window is floating, we need a dialog layout if (mIsFloating) { TypedValue res = new TypedValue(); getContext().getTheme().resolveAttribute( R.attr.dialogTitleDecorLayout, res, true); layoutResource = res.resourceId; } else if ((features & (1 << FEATURE_ACTION_BAR)) != 0) { layoutResource = a.getResourceId( R.styleable.Window_windowActionBarFullscreenDecorLayout, R.layout.screen_action_bar); } else { layoutResource = R.layout.screen_title; } // System.out.println("Title!"); } else if ((features & (1 << FEATURE_ACTION_MODE_OVERLAY)) != 0) { layoutResource = R.layout.screen_simple_overlay_action_mode; } else { // Embedded, so no decoration is needed. layoutResource = R.layout.screen_simple; // System.out.println("Simple!"); } mDecor.startChanging(); // 進行載入 DecorView 的佈局 mDecor.onResourcesLoaded(mLayoutInflater, layoutResource); // 這裡就獲取了裝載我們設定的內容容器 id 為 R.id.content ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT); // 省略不相關程式碼 return contentParent; }
我們折回到
setContentView
方法,來到
mLayoutInflater.inflate(...);
這行程式碼,
layoutResID
為我們設定的佈局檔案,而
mContentParent
就是我們剛剛獲取的id 為 content 的控制元件, 這裡便是把他從 xml 檔案解析成一棵控制元件的物件樹,並且放入在 mContentParent 容器內。
至此我們知道,Activity 的
setContentView
是讓我們佈局檔案從xml “翻譯” 成對應的控制元件物件,
形成一棵以 DecorView 為根結點的控制元件樹,方便我們後面繪製流程進行遍歷。
五、繪製流程如何運轉起來的
終於來到核心節,我們來繼續分析第三節最後說到的三個方法
onMeasure
、
onLayout
、
onDraw
,這便是繪製流程運轉起來的最後一道門閥,是我們自定義控制元件中可操作的部分。我們接下來一個個分析
1、onMeasure
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
要解釋清楚這個方法,我們需要先說明兩個引數的含義和構成。兩個引數都是
MeasureSpec
的型別
MeasureSpec是什麼
MeasureSpec 是一個 32位的二進位制數。高2位為測量模式,即SpecMode;低30位為測量數值,即SpecSize。我們先看下原始碼,從原始碼中找到這兩個值的含義。
以下是 MeasureSpec 類的程式碼(刪除了一些不相關的程式碼)
public static class MeasureSpec { private static final int MODE_SHIFT = 30; // 最終結果為:11 ...(30位) private static final int MODE_MASK = 0x3 << MODE_SHIFT; // 父View 不對 子View 施加任何約束。 子View可以是它想要的任何尺寸。 // 二進位制:00 ...(30位) public static final int UNSPECIFIED = 0 << MODE_SHIFT; // 父View 已確定 子View 的確切大小。子View 的大小便是父View測量所得的值 // 二進位制:01 ...(30位) public static final int EXACTLY = 1 << MODE_SHIFT; // 父View 指定一個 子View 可用的最大尺寸值,子View大小 不能超過該值。 // 二進位制:10 ...(30位) public static final int AT_MOST = 2 << MODE_SHIFT; public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size, @MeasureSpecMode int mode) { // API 17 之後,sUseBrokenMakeMeasureSpec 就為 false if (sUseBrokenMakeMeasureSpec) { return size + mode; } else { return (size & ~MODE_MASK) | (mode & MODE_MASK); } } @MeasureSpecMode public static int getMode(int measureSpec) { return (measureSpec & MODE_MASK); } public static int getSize(int measureSpec) { return (measureSpec & ~MODE_MASK); } }
(1)測量模式
類中有三個常量:
UNSPECIFIED
、
EXACTLY
、
AT_MOST
,他們對應著三種測量模式,具體含義我們在註釋中已經寫了,小盆友整理出以下表格方便我們查閱。
名稱 | 含義 | 數值(二進位制) | 具體表現 |
---|---|---|---|
UNSPECIFIED | 父View不對子View 施加任何約束,子View可以是它想要的任何尺寸 | 00 ...(30個0) | 系統內部使用 |
EXACTLY | 父View已確定子View 的確切大小,子View的大小為父View測量所得的值 | 01 ...(30個0) | 具體數值、match_parent |
AT_MOST | 父View 指定一個子View可用的最大尺寸值,View大小 不能超過該值。 | 10 ...(30個0) | wrap_content |
(2)makeMeasureSpec
makeMeasureSpec
方法,該方法
用於合併測量模式和測量尺寸,將這兩個值合為一個32位的數,高2位為測量模式,低30位為尺寸。
該方法很簡短,主要得益於
(size & ~MODE_MASK) | (mode & MODE_MASK)
的位運算子,但也帶來了一定的理解難度。我們拆解下
-
size & ~MODE_MASK
剔除 size 中的測量模式的值,即將高2位置為00 -
mode & MODE_MASK
保留傳入的模式引數的值,同時將低30位置為 0...(30位0) -
(size & ~MODE_MASK) | (mode & MODE_MASK)
就是 size的低30位 + mode的高2位(總共32位)
至於
&
、
~
、
|
這三個位操作為何能做到如此的騷操作,請移步小盆友的另一博文——
Android位運算簡單講解。(內容很簡短,不熟悉這塊內容的童鞋,強烈推薦瀏覽一下)
(3)getMode
getMode
方法用於獲取我們傳入的
measureSpec
值的高2位,即測量模式。
(4)getSize
getSize
方法用於獲取我們傳入的
measureSpec
值的低30位,即測量的值。
解釋完
MeasureSpec
的是什麼,我們還有兩個問題需要搞清楚:
- 這兩個引數值從哪來
- 這兩個引數值怎麼使用
這兩個引數值從哪來
藉助下面這張簡圖,設定當前執行的
onMeasure
方法處於B控制元件,則其兩個MeasureSpec值是由其父檢視(即A控制元件)計算得出,計算的規則ViewGroup 有對應的方法,即
getChildMeasureSpec
。
getChildMeasureSpec
的具體程式碼如下。我們繼續使用上面的情景, B中所獲得的值,是
A使用自身的MeasureSpec 和 B 的 LayoutParams.width 或 LayoutParams.height 進行計算得出B的MeasureSpec。
// ViewGroup 類public static int getChildMeasureSpec(int spec, int padding, int childDimension) { int specMode = MeasureSpec.getMode(spec); int specSize = MeasureSpec.getSize(spec); int size = Math.max(0, specSize - padding); int resultSize = 0; int resultMode = 0; switch (specMode) { // 父檢視為確定的大小的模式 case MeasureSpec.EXACTLY: /** * 根據子檢視的大小,進行不同模式的組合: * 1、childDimension 大於 0,說明子檢視設定了具體的大小 * 2、childDimension 為 {@link LayoutParams.MATCH_PARENT},說明大小和其父檢視一樣大 * 3、childDimension 為 {@link LayoutParams.WRAP_CONTENT},說明子檢視想為其自己的大小,但 * 不能超過其父檢視的大小。 */ if (childDimension >= 0) { resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // Child wants to be our size. So be it. resultSize = size; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.WRAP_CONTENT) { // Child wants to determine its own size. It can't be // bigger than us. resultSize = size; resultMode = MeasureSpec.AT_MOST; } break; // 父檢視已經有一個最大尺寸限制 case MeasureSpec.AT_MOST: /** * 根據子檢視的大小,進行不同模式的組合: * 1、childDimension 大於 0,說明子檢視設定了具體的大小 * 2、childDimension 為 {@link LayoutParams.MATCH_PARENT}, * -----說明大小和其父檢視一樣大,但是此時的父檢視還不能確定其大小,所以只能讓子檢視不超過自己 * 3、childDimension 為 {@link LayoutParams.WRAP_CONTENT}, * -----說明子檢視想為其自己的大小,但不能超過其父檢視的大小。 */ if (childDimension >= 0) { // Child wants a specific size... so be it resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // Child wants to be our size, but our size is not fixed. // Constrain child to not be bigger than us. resultSize = size; resultMode = MeasureSpec.AT_MOST; } else if (childDimension == LayoutParams.WRAP_CONTENT) { // Child wants to determine its own size. It can't be // bigger than us. resultSize = size; resultMode = MeasureSpec.AT_MOST; } break; case MeasureSpec.UNSPECIFIED: if (childDimension >= 0) { // Child wants a specific size... let him have it resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // Child wants to be our size... find out how big it should // be resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; resultMode = MeasureSpec.UNSPECIFIED; } else if (childDimension == LayoutParams.WRAP_CONTENT) { // Child wants to determine its own size.... find out how // big it should be resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; resultMode = MeasureSpec.UNSPECIFIED; } break; } return MeasureSpec.makeMeasureSpec(resultSize, resultMode); }
我們將這段程式碼整理成表格
子LayoutParams(縱向) \ 父類的SpecMode(橫向) | EXACTLY | AT_MOST | UNSPECIFIED |
---|---|---|---|
dp/px (確定的值) | EXACTLY |
|
|
ChildSize | EXACTLY |
|
|
ChildSize | EXACTLY |
|
|
ChildSize |
|
|
|
MATCH_PARENT | EXACTLY |
|
|
ParentSize | AT_MOST |
|
|
ParentSize | UNSPECIFIED |
|
|
0 |
|
|
|
WRAP_CONTENT | AT_MOST |
|
|
ParentSize | AT_MOST |
|
|
ParentSize | UNSPECIFIED |
|
|
0 |
|
|
|
所以最終,B的
onMeasure
方法獲得的兩個值,便是 父檢視A 對 B 所做的約束建議值。
你可能會有一個疑惑, 頂級DecorView 的約束哪裡來,我們切回 FLAG2 處,在進入
performMeasure
方法時,攜帶的兩個MeasureSpec 是由
WindowManager 傳遞過來的 Window 的 Rect 的寬高 和 Window 的 WindowManager.LayoutParam 共同決定。簡而言之,
DecorView的約束從 Window的引數得來。
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);// 進行測量performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
這兩個引數值怎麼使用
我們上面一直提到的一個詞叫做 “建議”,是因為到達B的兩個維度(橫縱)的 MeasureSpec,不是就已經決定了控制元件B的寬高。
這裡我們可以類比為 父母總是語重心長的跟自己的孩子說,你要怎麼做怎麼做(即算出了子View 的 MeasureSpec),懂事的孩子會知道聽從父母的建議可以讓自己少走彎路(即遵循傳遞下來的MeasureSpec約束),而調皮一點的孩子,覺得打破常規更加好玩(即不管 MeasureSpec 的規則約束)。
按照約定,我們是要遵循父View給出的約束。而B控制元件再進行計算其自己子View的MeasureSpec(如果有子View),子View 會再進行測量 孫View,這樣一層層的測量(這裡能感受到樹結構的魅力了吧?)。
B控制元件完成子View的測量,呼叫
setMeasuredDimension
將自身最終的
測量寬高 進行設定,這樣就完成B控制元件的測量流程就完畢了。
2、onLayout
protected void onLayout(boolean changed, int l, int t, int r, int b)
onLayout
則是進行擺放,這一過程比較簡單,因為我們從
onMeasure
中已經得到各個子View 的寬高。父View 只要按照自己的邏輯負責給定各個子View 的
左上座標 和
右下座標 即可。
3、onDraw
protected void onDraw(Canvas canvas)
繪製流程中,
onDraw
應該說是童鞋們最為熟悉的,只要在
canvas
繪製自身需要繪製的內容便可以。
六、實戰
上一節總結起來,就是我們在面試時總會說的那句話,
onMeasure
負責測量、
onLayout
負責擺放、
onDraw
負責繪製,但理論總是過於空洞,我們現在將理論融入到操作中來。我們用標籤的流式佈局來說明進一步解釋這一切。
1、效果圖
Github入口: 傳送門
2、編碼思路
在這種標籤流式佈局的情景中,我們會往控制元件TagFlowLayout中放入標籤TextView(當然也可以是更復雜的佈局,這裡為了方便講清楚思路)。 我們放入四個標籤,分別為 “大Android”、“猛猛的小盆友”、“JAVA”、“ PHP是最好的語言”。
我們藉助這張小盆友手繪的流程圖,來講清楚這繪製流程。
(1) onMeasure
最開始,控制元件是空的,也就是第一幅小圖。
接著將第一個標籤 “大Android” 放入,此時不超出 TagFlowLayout 的寬,如第二幅小圖所示。
然後將第二個標籤 “猛猛的小盆友” 放入,此時如第三幅小圖所示,超出了 TagFlowLayout 的寬, 所以我們進行換行,將 “猛猛的小盆友” 放入第二行。
在接著將第三個標籤 “JAVA” 放入,此時不超出 TagFlowLayout 的寬,如第四幅小圖所示。
最後把剩下的 “PHP是最好的語言” 也放入,當此時有個問題,即使一行放一個也容不下(第五幅小圖),因為 “ PHP是最好的語言” 的寬已經超出 TagFlowLayout 的寬,所以我們在給 “PHP是最好的語言” 測量的MeasureSpec時,需要進行“糾正”,使其寬度為 TagFlowLayout 的寬,最終形成了第六幅小圖的樣子。
最後還需要將我們測量的結果透過
setMeasuredDimension
設定我們自身的 TagFlowLayout 控制元件的寬高。
(2) onLayout
經過
onMeasure
,TagFlowLayout 心中已經知道自己的
每個孩子的寬高 和
每個孩子要“站”在哪一行,但具體的座標還是需要進行計算。
“大Android” 的標籤比較座標比較容易(我們這裡討論思路的時候不考慮padding和margin),(l1,t1) 就是 (0,0),而 (r1,b1) 則是 (0+ width, 0+height)。
“猛猛的小盆友” 的座標需要依賴 “大Android”,(l2,t2) 則為 (0, 第一行的高度) ,(r2,b2) 為 (自身的Width,第一行的高度+自身的Height)。
“JAVA” 的座標則需要依賴“猛猛的小盆友” 和 “大Android”, (l3,t3) 為 (“猛猛的小盆友”的Width, 第一行的高度) ,(r3,b3) 為 (“猛猛的小盆友”的Width + 自身的Width, 第一行的高度+自身的Height)。
“PHP是最好的語言” 需要依賴前兩行的總高度,具體看座標的計算。 (l4,t4) 為 (0,第一行高+第二行高), (r4,b4) 為 (自身的Width,第一行高+第二行高+自身的Height)。
(3) onDraw
這個方法在我們這個控制元件中不需要,因為繪製的任務是由各個子View負責。確切的說
onDraw
在我們的 TagFlowLayout 並不會被呼叫,具體原因我們在前面已經說了,這裡就不贅述了。
3、小結
雖然鋪墊了很多,但是 TagFlowLayout 的程式碼量並不多,這裡也不再貼上出來,需要的進入
傳送門。我們只需要在
onMeasure
中進行測量,然後將測量的值進行儲存,最後在
onLayout
依賴測量的結果進行擺放即可。
七、寫在最後
距離上篇博文的釋出也有接近三個星期了,這次耗時比較久原因挺多,繪製流程涉及的知識點很多,這裡講述的只是比較接近於我們開發者的部分,所以導致小盆友在寫這篇文章的時候有些糾結。還有另一個原因是小盆友的一些私人事情,需要些時間來平復,但最終也堅持著寫完。如果童鞋們發現有那些欠妥的地方,請留言區與我討論,我們共同進步。如果覺得這碗“蛋炒飯”別有一番滋味,給我一個贊吧。
更多大廠面試資料以及Android資料可以進群免費領取:(Android進階學習⑥群:345659112)
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69983917/viewspace-2839389/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- Android UI繪製流程及原理AndroidUI
- Android高階進階之路【一】Android中View繪製流程淺析AndroidView
- Android進階(五)View繪製流程AndroidView
- Android繪製流程Android
- 【Android進階】RecyclerView之繪製流程(三)AndroidView
- Android View繪製流程AndroidView
- Canvas中的繪圖師講解與實戰——Android高階UICanvas繪圖AndroidUI
- 初·Android View的繪製流程AndroidView
- Android 中 View 繪製流程分析AndroidView
- 所有繪畫的核心靈魂——素描知識(轉)
- Android高階UI系列(2)-DecorViewAndroidUIView
- Android View 繪製流程(Draw) 完全解析AndroidView
- Android View繪製原理:繪製流程排程、測算等AndroidView
- Flutter之UI繪製流程二FlutterUI
- Android原始碼分析之View繪製流程Android原始碼View
- 【Android原始碼】View的繪製流程分析Android原始碼View
- 探究Android View 繪製流程,Canvas 的由來AndroidViewCanvas
- 深入理解 Android 之 View 的繪製流程AndroidView
- 影象操縱大師Xfermode講解與實戰——Android高階UIAndroidUI
- 影像操縱大師Xfermode講解與實戰——Android高階UIAndroidUI
- 高階 Android 工程師的進階之路Android工程師
- Android View繪製流程看這篇就夠了AndroidView
- 靈魂畫手:漫畫圖解 SSH圖解
- Android UI 繪圖基礎AndroidUI繪圖
- [譯]Android 動畫的靈魂—— InterpolatorAndroid動畫
- Android繪製優化(一)繪製效能分析Android優化
- UI繪製流程 Draw Paint基本屬性(四)UIAI
- Word流程圖怎麼畫?如何輕鬆繪製流程圖流程圖
- Android檢視載入流程(6)之View的詳細繪製流程DrawAndroidView
- Android系統原始碼分析--View繪製流程之-inflateAndroid原始碼View
- 繪製流程
- Android豎虛線繪製Android
- Android OpenGLES繪製天空盒Android
- 工程師的靈魂工程師
- 流程是ERP的“靈魂”?(轉)
- 探究 Android View 繪製流程,Activity 的 View 如何展示到螢幕AndroidView
- Android系統原始碼分析--View繪製流程之-setContentViewAndroid原始碼View
- Android系統原始碼分析–View繪製流程之-setContentViewAndroid原始碼View