目錄
一、前言
二、我們的目標是啥
三、繪製流程從何而起
四、Activity 的介面結構在哪裡開始形成
五、繪製流程如何運轉起來的
六、實戰
七、寫在最後
一、前言
繪製流程可以說是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 類
@Override
public 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 類
@Override
public 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 類
@Override
public 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位置為00mode & 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 |
EXACTLY 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
依賴測量的結果進行擺放即可。
七、寫在最後
距離上篇博文的釋出也有接近三個星期了,這次耗時比較久原因挺多,繪製流程涉及的知識點很多,這裡講述的只是比較接近於我們開發者的部分,所以導致小盆友在寫這篇文章的時候有些糾結。還有另一個原因是小盆友的一些私人事情,需要些時間來平復,但最終也堅持著寫完。如果童鞋們發現有那些欠妥的地方,請留言區與我討論,我們共同進步。如果覺得這碗“蛋炒飯”別有一番滋味,給我一個贊吧。
歡迎加我微信,我們可以進行更多更有趣的交流