靈魂畫師,Android繪製流程——Android高階UI

南方吳彥祖_藍斯發表於2021-10-26

目錄

一、前言

二、我們的目標是啥

三、繪製流程從何而起

四、Activity 的介面結構在哪裡開始形成

五、繪製流程如何運轉起來的

六、實戰

七、寫在最後

一、前言

繪製流程可以說是Android進階中必不可少的一個內容,也是面試中被問得最多的問題之一。這方面優秀的文章也已經是非常之多,但是小盆友今天還是要以自己的姿態來炒一炒這冷飯,或許就是蛋炒飯了?。話不多說,老規矩先上實戰圖,然後開始分享。

標籤佈局

靈魂畫師,Android繪製流程——Android高階UI

二、我們的目標是啥

其實這篇文章,小盆友糾結了挺久,因為繪製流程涉及的東西非常之多,並非一篇文章可以寫完,所以這篇文章我先要確定一些目標,防止因為追查原始碼過深,而迷失於原始碼中,最後導致一無所獲。我們的目標是:

  1. 繪製流程從何而起
  2. Activity 的介面結構在哪裡開始形成
  3. 繪製流程如何運轉起來

接下來我們就一個個目標來 conquer。

三、繪製流程從何而起

我們一說到繪製流程,就會想到或是聽過onMeasureonLayoutonDraw這三個方法,但是有沒想過為什麼我們開啟一個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 的 onCreateonStartonResume

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); 最終會呼叫 onCreateonStart 方法。眼見為實,耳聽為虛,我們繼續進入深入。來到下面這段程式碼

// 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 這行程式碼。進入該方法,方法內會呼叫 activityperformCreate 方法,而 performCreate 方法裡會呼叫到我們經常重寫的 Activity 生命週期的 onCreate 方法。?至此,找到了 onCreate 的呼叫地方,這裡需要立個 FLAG1,因為目標二需要的開啟便是這裡,我下一小節分享,勿急。

回過頭來繼續 performLaunchActivity 方法的執行,會呼叫到 activityperformStart 方法,而該方法又會呼叫到 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 的生命週期函式 onCreateonStartonResume 。按照這一套路,童鞋們可以看看 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();
    
}
複製程式碼

呼叫了 mTraversalRunnablerun 方法之後,會發現也是一連串的方法呼叫,後來到 performTraversals,這裡面就有我們一直提到三個繪製流程方法的起源地。這三個起源地就是我們在上面看到的三個方法 performMeasureperformLayoutperformDraw

而這三個方法會進行如下圖的一個呼叫鏈(?還是手繪,勿噴),從程式碼我們也知道,會按照 performMeasureperformLayoutperformDraw 的順序依次呼叫。

performMeasure 會觸發我們的測量流程,如圖中所示,進入第一層的 ViewGroup,會呼叫 measureonMeasure,在 onMeasure 中呼叫下一層級,然後下一層級的 View或ViewGroup 會重複這樣的動作,進行所有 View 的測量。(這一過程可以理解為書的深度遍歷)

performLayoutperformMeasure 的流程大同小異,只是方法名不同,就不再贅述。

performDraw 稍微些許不同,當前控制元件為ViewGroup時,只有需要繪製背景或是我們通過 setWillNotDraw(false) 設定我們的ViewGroup需要進行繪製時,會進入 onDraw 方法,然後通過 dispatchDraw 進行繪製子View,如此迴圈。而如果為View,自然也就不需要繪製子View,只需繪製自身的內容即可。

靈魂畫師,Android繪製流程——Android高階UI
至此,繪製流程的源頭我們便了解清楚了, onMeasureonLayoutonDraw 三個方法我們會在後面進行詳述並融入在實戰中。

四、Activity 的介面結構在哪裡開始形成

靈魂畫師,Android繪製流程——Android高階UI
上圖是 Activity 的結構。我們先進行大致的描述,然後在進入原始碼體會這一過程。

我們可以清晰的知道一個 Activity 會對應著有一個 Window,而 Window 的唯一實現類為 PhoneWindowPhoneWindow 的初始化是在 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 為根結點的控制元件樹,方便我們後面繪製流程進行遍歷。

五、繪製流程如何運轉起來的

終於來到核心節,我們來繼續分析第三節最後說到的三個方法onMeasureonLayoutonDraw,這便是繪製流程運轉起來的最後一道門閥,是我們自定義控制元件中可操作的部分。我們接下來一個個分析

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)測量模式

類中有三個常量: UNSPECIFIEDEXACTLYAT_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 的是什麼,我們還有兩個問題需要搞清楚:

  1. 這兩個引數值從哪來
  2. 這兩個引數值怎麼使用

這兩個引數值從哪來

藉助下面這張簡圖,設定當前執行的 onMeasure 方法處於B控制元件,則其兩個MeasureSpec值是由其父檢視(即A控制元件)計算得出,計算的規則ViewGroup 有對應的方法,即 getChildMeasureSpec

靈魂畫師,Android繪製流程——Android高階UI
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、效果圖

靈魂畫師,Android繪製流程——Android高階UI
Github入口:傳送門

2、編碼思路

在這種標籤流式佈局的情景中,我們會往控制元件TagFlowLayout中放入標籤TextView(當然也可以是更復雜的佈局,這裡為了方便講清楚思路)。 我們放入四個標籤,分別為 “大Android”、“猛猛的小盆友”、“JAVA”、“ PHP是最好的語言”。

靈魂畫師,Android繪製流程——Android高階UI
我們藉助這張小盆友手繪的流程圖,來講清楚這繪製流程。

(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)。

靈魂畫師,Android繪製流程——Android高階UI

(3) onDraw

這個方法在我們這個控制元件中不需要,因為繪製的任務是由各個子View負責。確切的說 onDraw 在我們的 TagFlowLayout 並不會被呼叫,具體原因我們在前面已經說了,這裡就不贅述了。

3、小結

雖然鋪墊了很多,但是 TagFlowLayout 的程式碼量並不多,這裡也不再貼上出來,需要的進入傳送門。我們只需要在onMeasure 中進行測量,然後將測量的值進行儲存,最後在 onLayout 依賴測量的結果進行擺放即可。

七、寫在最後

距離上篇博文的釋出也有接近三個星期了,這次耗時比較久原因挺多,繪製流程涉及的知識點很多,這裡講述的只是比較接近於我們開發者的部分,所以導致小盆友在寫這篇文章的時候有些糾結。還有另一個原因是小盆友的一些私人事情,需要些時間來平復,但最終也堅持著寫完。如果童鞋們發現有那些欠妥的地方,請留言區與我討論,我們共同進步。如果覺得這碗“蛋炒飯”別有一番滋味,給我一個贊吧。

歡迎加我微信,我們可以進行更多更有趣的交流

靈魂畫師,Android繪製流程——Android高階UI

相關文章