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

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

一、前言

繪製流程可以說是 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 的  onCreateonStart 和  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 的生命週期函式  onCreateonStart 和  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,這裡面就有我們一直提到三個繪製流程方法的起源地。這三個起源地就是我們在上面看到的三個方法  performMeasureperformLayout 、 performDraw 。

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

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

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

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

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

至此,繪製流程的源頭我們便了解清楚了,  onMeasure 、  onLayoutonDraw 三個方法我們會在後面進行詳述並融入在實戰中。

四、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 類@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 為根結點的控制元件樹,方便我們後面繪製流程進行遍歷。

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

終於來到核心節,我們來繼續分析第三節最後說到的三個方法 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)測量模式

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

  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 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、效果圖

靈魂畫師,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進階學習⑥群:345659112)

作者:猛猛的小盆友
連結:
來源:稀土掘金


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69983917/viewspace-2839389/,如需轉載,請註明出處,否則將追究法律責任。

相關文章