靈魂畫師,Android繪制流程——Android高級UI

一、前言

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

標簽布局

二、我們的目標是啥

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

  1. 繪制流程從何而起
  2. Activity 的界面結(jié)構(gòu)在哪里開始形成
  3. 繪制流程如何運轉(zhuǎn)起來

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

三、繪制流程從何而起

我們一說到繪制流程,就會想到或是聽過onMeasureonLayout、onDraw這三個方法,但是有沒想過為什么我們開啟一個App或是點開一個Activity,就會觸發(fā)這一系列流程呢?想知道繪制流程從何而起,我們就有必要先解釋 App啟動流程Activity的啟動流程。

我們都知道 ActivityThread 的 main 是一個App的入口。我們來到 main 方法看看他做了什么啟動操作。

ActivityThread 的 main方法是由 ZygoteInit 類中最終通過 RuntimeInit類的invokeStaticMain 方法進行反射調(diào)用。有興趣的童鞋可以自行查閱下,限于篇幅,就不再展開分享。

// ActivityThread 類
public static void main(String[] args) {
    // ...省略不相關(guān)代碼

    // 準備主線程的 Looper
    Looper.prepareMainLooper();

    // 實例化 ActivityThread,用于管理應用程序進程中主線程的執(zhí)行
    ActivityThread thread = new ActivityThread();
    // 進入 attach 方法
    thread.attach(false);

    // ...省略不相關(guān)代碼

    // 開啟 Looper
    Looper.loop();

    // ...省略不相關(guān)代碼

}

進入 main 方法,我們便看到很熟悉的 Handler機制。在安卓中都是以消息進行驅(qū)動,在這里也不例外,我們可以看到先進行 Looper 的準備,在最后開啟 Looper 進行循環(huán)獲取消息,用于處理傳到主線程的消息。

這也是為什么我們在主線程不需要先進行 Looper 的準備和開啟,emmm,有些扯遠了。

回過頭,可以看到夾雜在中間的 ActivityThread 類的實例化并且調(diào)用了 attach 方法。具體代碼如下,我們接著往下走。

// ActivityThread 類
private void attach(boolean system) {
    // ...省略不相關(guān)代碼

    // system 此時為false,進入此分支
    if (!system) {
        // ...省略不相關(guān)代碼

        // 獲取系統(tǒng)的 AMS 服務的 Proxy,用于向 AMS 進程發(fā)送數(shù)據(jù)
        final IActivityManager mgr = ActivityManager.getService();
        try {
            // 將我們的 mAppThread 傳遞給 AMS,AMS 便可控制我們 App 的 Activity
            mgr.attachApplication(mAppThread);
        } catch (RemoteException ex) {
            throw ex.rethrowFromSystemServer();
        }

        // ...省略不相關(guān)代碼
    } else {
        // ...省略不相關(guān)代碼
    }

    // ...省略不相關(guān)代碼

}

// 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,可以進行發(fā)送數(shù)據(jù)
                final IActivityManager am = IActivityManager.Stub.asInterface(b);
                return am;
            }
        };

我們進入attach 方法,方法內(nèi)主要是通過 ActivityManager 的 getService 方法獲取到了 ActivityManagerService(也就是我們所說的AMS) 的 Proxy,達到與AMS 進行跨進程通信的目的。

文中所說的 Proxy 和 Stub,是以系統(tǒng)為我們自動生成AIDL時的類名進行類比使用,方便講解。Proxy 代表著發(fā)送信息,Stub 代表著接收信息。

mgr.attachApplication(mAppThread); 代碼中向 AMS 進程發(fā)送信息,攜帶了一個類型為 ApplicationThread 的 mAppThread 參數(shù)。這句代碼的作用,其實就是把 我們應用的 “控制器” 上交給了 AMS,這樣使得 AMS 能夠來控制我們應用中的Activity的生命周期。為什么這么說呢?我們這就有必要來了解下 ApplicationThread 類的結(jié)構(gòu),其部分代碼如下:

// 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 發(fā)來的信息封裝在 ActivityClientRecord 中,然后發(fā)送給 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 的方法名,我們會驚奇的發(fā)現(xiàn)大多方法名以 scheduleXxxYyyy 的形式命名,而且和我們熟悉的生命周期都挺接近。上面代碼留下了我們需要的方法 scheduleLaunchActivity ,它們包含了我們 Activity 的 onCreate、onStartonResume。

scheduleLaunchActivity 方法會對 AMS 發(fā)來的信息封裝在 ActivityClientRecord 類中,最后通過 sendMessage(H.LAUNCH_ACTIVITY, r); 這行代碼將信息以 H.LAUNCH_ACTIVITY 的信息標記發(fā)送至我們主線程中的 Handler。我們進入主線程的 Handler 實現(xiàn)類 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對象,會進行調(diào)用 Activity 的 onCreate 和 onStart 的生命周期
    Activity a = performLaunchActivity(r, customIntent);

    // Activity 不為空進入
    if (a != null) {
        r.createdConfig = new Configuration(mConfiguration);
        reportSizeConfigurations(r);
        Bundle oldState = r.state;

        // 該方法最終回調(diào)用到 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); 最終會調(diào)用 onCreateonStart 方法。眼見為實,耳聽為虛,我們繼續(xù)進入深入。來到下面這段代碼

// ActivityThread 類
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
    // 省略不相關(guān)代碼

    // 創(chuàng)建 Activity 的 Context
    ContextImpl appContext = createBaseContextForActivity(r);
    Activity activity = null;
    try {
        java.lang.ClassLoader cl = appContext.getClassLoader();
        // ClassLoader 加載 Activity類,并創(chuàng)建 Activity
        activity = mInstrumentation.newActivity(
                cl, component.getClassName(), r.intent);

        // 省略不相關(guān)代碼

    } catch (Exception e) {
       // 省略不相關(guān)代碼
    }

    try {
        // 創(chuàng)建 Application
        Application app = r.packageInfo.makeApplication(false, mInstrumentation);

        // 省略不相關(guān)代碼

        if (activity != null) {
            // 省略不相關(guān)代碼

            // 調(diào)用了 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;
            }

            // 省略不相關(guān)代碼

            // 調(diào)用 Activity 的 onCreate
            if (r.isPersistable()) {
                mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
            } else {
                mInstrumentation.callActivityOnCreate(activity, r.state);
            }

            // 省略不相關(guān)代碼

            if (!r.activity.mFinished) {
                // zincPower 調(diào)用 Activity 的 onStart
                activity.performStart();
                r.stopped = false;
            }

            if (!r.activity.mFinished) {
               // zincPower 調(diào)用 Activity 的 onRestoreInstanceState 方法,數(shù)據(jù)恢復
               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);
                }
            }
            // 省略不相關(guān)代碼
        }

        // 省略不相關(guān)代碼

    } 

    // 省略不相關(guān)代碼

    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);
    // 調(diào)用了 onCreate
    onCreate(icicle);
    mActivityTransitionState.readState(icicle);
    performCreateCommon();
}

// Activity 類
final void performStart() {
    // 省略不相關(guān)代碼

    // 進行調(diào)用 Activity 的 onStart
    mInstrumentation.callActivityOnStart(this);

    // 省略不相關(guān)代碼
}

// Instrumentation 類
public void callActivityOnStart(Activity activity) {
    // 調(diào)用了 Activity 的 onStart
    activity.onStart();
}

進入 performLaunchActivity 方法后,我們會發(fā)現(xiàn)很多我們熟悉的東西,小盆友已經(jīng)給關(guān)鍵點打上注釋,因為不是文章的重點就不再細說,否則篇幅過長。

我們直接定位到 mInstrumentation.callActivityOnCreate 這行代碼。進入該方法,方法內(nèi)會調(diào)用 activityperformCreate 方法,而 performCreate 方法里會調(diào)用到我們經(jīng)常重寫的 Activity 生命周期的 onCreate 方法。??至此,找到了 onCreate 的調(diào)用地方,這里需要立個 FLAG1,因為目標二需要的開啟便是這里,我下一小節(jié)分享,勿急。

回過頭來繼續(xù) performLaunchActivity 方法的執(zhí)行,會調(diào)用到 activityperformStart 方法,而該方法又會調(diào)用到 mInstrumentation.callActivityOnStart 方法,最后在該方法內(nèi)便調(diào)用了我們經(jīng)常重寫的 Activity 生命周期的 onStart 方法。??至此,找到了 onStart 的調(diào)用地方。

找到了兩個生命周期的調(diào)用地方,我們需要折回到 handleLaunchActivity 方法中,繼續(xù)往下運行,便會來到 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 的實現(xiàn)類
        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,并且?guī)?WindowManager.LayoutParams
                // 這里面便觸發(fā)真正的繪制流程
                wm.addView(decor, l);
            } else {
                a.onWindowAttributesChanged(l);
            }
        }

    }

    // 省略不相關(guān)代碼
}

performResumeActivity方法最終會調(diào)用到 Activity 的 onResume 方法,因為不是我們該小節(jié)的目標,就不深入了,童鞋們可以自行深入,代碼也比較簡單。至此我們就找齊了我們一直重寫的三個 Acitivity 的生命周期函數(shù) onCreateonStartonResume 。按照這一套路,童鞋們可以看看 ApplicationThread 的其他方法,會發(fā)現(xiàn) Activity 的生命周期均在其中可以找到影子,也就證實了我們最開始所說的 我們將應用 “遙控器” 交給了AMS。而值得一提的是,這一操作是處于一個跨進程的場景。

繼續(xù)往下運行來到 wm.addView(decor, l); 這行代碼,wm 的具體實現(xiàn)類為 WindowManagerImpl,繼續(xù)跟蹤深入,來到下面這一連串的調(diào)用

// 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) {
    // 省略不相關(guān)代碼  

    ViewRootImpl root;
    View panelParentView = null;

    synchronized (mLock) {

        // 省略不相關(guān)代碼

        // 初始化 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) {
            // 省略不相關(guān)代碼            

            // 進入繪制流程
            // tag:進入這一行
            requestLayout();

            // 省略不相關(guān)代碼
        }
    }
}

// ViewRootImpl 類
@Override
public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        checkThread();
        mLayoutRequested = true;
        // tag:進入這一行
        scheduleTraversals();
    }
}

// ViewRootImpl 類
void scheduleTraversals() {
   if (!mTraversalScheduled) {
       mTraversalScheduled = true;
       mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
       // 提交給 編舞者,會在下一幀繪制時調(diào)用 mTraversalRunnable,運行其run
       mChoreographer.postCallback(
               Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
       if (!mUnbufferedInputDispatch) {
           scheduleConsumeBatchedInput();
       }
       notifyRendererOfFramePending();
       pokeDrawLockIfNeeded();
   }
}

中間跳轉(zhuǎn)的方法比較多,小盆友都打上了 // tag:進入這一行 注釋,童鞋們可以自行跟蹤,會發(fā)現(xiàn)最后會調(diào)用到編舞者,即 Choreographer 類的 postCallback方法。Choreographer 是一個會接收到垂直同步信號的類,所以當下一幀到達時,他會調(diào)用我們剛才提交的任務,即此處的 mTraversalRunnable,并執(zhí)行其 run 方法。

值得一提的是通過 Choreographer 的 postCallback 方法提交的任務并不是每一幀都會調(diào)用,而是只在下一幀到來時調(diào)用,調(diào)用完之后就會將該任務移除。簡而言之,就是提交一次就會在下一幀調(diào)用一次。

我們繼續(xù)來看 mTraversalRunnable 的具體內(nèi)容,看看每一幀都做了寫什么操作。

// 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() {

    // 省略不相關(guān)代碼

        if (!mStopped || mReportNextDraw) {

           // 省略不相關(guān)代碼

                // FLAG2
                int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
                int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);

                // 省略不相關(guān)代碼

                // 進行測量
                performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

    // 省略不相關(guān)代碼

        // 進行擺放
        performLayout(lp, mWidth, mHeight);

    // 省略不相關(guān)代碼

    // 布局完回調(diào)
    if (triggerGlobalLayoutListener) {
        mAttachInfo.mRecomputeGlobalAttributes = false;
        mAttachInfo.mTreeObserver.dispatchOnGlobalLayout();
    }

    // 省略不相關(guān)代碼

        // 進行繪制
        performDraw();

}

調(diào)用了 mTraversalRunnablerun 方法之后,會發(fā)現(xiàn)也是一連串的方法調(diào)用,后來到 performTraversals,這里面就有我們一直提到三個繪制流程方法的起源地。這三個起源地就是我們在上面看到的三個方法 performMeasure、performLayoutperformDraw 。

而這三個方法會進行如下圖的一個調(diào)用鏈(??還是手繪,勿噴),從代碼我們也知道,會按照 performMeasure、performLayout 、performDraw 的順序依次調(diào)用。

performMeasure 會觸發(fā)我們的測量流程,如圖中所示,進入第一層的 ViewGroup,會調(diào)用 ?measureonMeasure,在 onMeasure 中調(diào)用下一層級,然后下一層級的 View或ViewGroup 會重復這樣的動作,進行所有 View 的測量。(這一過程可以理解為書的深度遍歷)

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

performDraw 稍微些許不同,當前控件為ViewGroup時,只有需要繪制背景或是我們通過 setWillNotDraw(false) 設置我們的ViewGroup需要進行繪制時,會進入 onDraw 方法,然后通過 dispatchDraw 進行繪制子View,如此循環(huán)。而如果為View,自然也就不需要繪制子View,只需繪制自身的內(nèi)容即可。

至此,繪制流程的源頭我們便了解清楚了, onMeasure 、 onLayout、onDraw 三個方法我們會在后面進行詳述并融入在實戰(zhàn)中。

四、Activity 的界面結(jié)構(gòu)在哪里開始形成

上圖是 Activity 的結(jié)構(gòu)。我們先進行大致的描述,然后在進入源碼體會這一過程。

我們可以清晰的知道一個 Activity 會對應著有一個 Window,而 Window 的唯一實現(xiàn)類為 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 了

現(xiàn)在我們已經(jīng)有一個大概的印象,接下來進行詳細分析。在上一節(jié)中(FLAG1處),我們最先會進入的生命周期為onCreate,在該方法中我們都會寫上這樣一句代碼setContentView(R.layout.xxxx) 進行設置布局。經(jīng)過上一節(jié)我們也知道,真正的繪制流程是在 onResume 之后(忘記的童鞋請倒回去看一下),那么 setContentView 起到一個什么作用呢?我進入源碼一探究竟吧。

進入 Activity 的 setContentView 方法,可以看到下面這段代碼。getWindow 返回的是一個 Window 類型的對象,而通過Window的官方注釋可以知道其唯一的實現(xiàn)類為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);

       // 省略不相關(guān)代碼
}

generateDecor 中會進行 DecorView 的創(chuàng)建,具體代碼如下,較為簡單

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 就會獲取到,并根據(jù)其值選擇合適的布局賦值給 layoutResource 屬性。最后將該布局資源解析,賦值給 DecorView,緊接著將 DecorView 中 id 為 content 的控件賦值給 contentParent,而這個控件將來就是裝載我們設置的布局資源。

protected ViewGroup generateLayout(DecorView decor) {

    // 省略不相關(guān)代碼

    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);

    // 這里就獲取了裝載我們設置的內(nèi)容容器 id 為 R.id.content
    ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);

    // 省略不相關(guān)代碼

    return contentParent;
}

我們折回到 setContentView 方法,來到 mLayoutInflater.inflate(...); 這行代碼,layoutResID 為我們設置的布局文件,而 mContentParent 就是我們剛剛獲取的id 為 content 的控件, 這里便是把他從 xml 文件解析成一棵控件的對象樹,并且放入在 mContentParent 容器內(nèi)。

至此我們知道,Activity 的 setContentView 是讓我們布局文件從xml “翻譯” 成對應的控件對象,形成一棵以 DecorView 為根結(jié)點的控件樹,方便我們后面繪制流程進行遍歷。

五、繪制流程如何運轉(zhuǎn)起來的

終于來到核心節(jié),我們來繼續(xù)分析第三節(jié)最后說到的三個方法onMeasure、onLayoutonDraw,這便是繪制流程運轉(zhuǎn)起來的最后一道門閥,是我們自定義控件中可操作的部分。我們接下來一個個分析

1、onMeasure

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)

要解釋清楚這個方法,我們需要先說明兩個參數(shù)的含義和構(gòu)成。兩個參數(shù)都是 MeasureSpec 的類型

MeasureSpec是什么

MeasureSpec 是一個 32位的二進制數(shù)。高2位為測量模式,即SpecMode;低30位為測量數(shù)值,即SpecSize。我們先看下源碼,從源碼中找到這兩個值的含義。

以下是 MeasureSpec 類的代碼(刪除了一些不相關(guān)的代碼)

public static class MeasureSpec {
    private static final int MODE_SHIFT = 30;
    // 最終結(jié)果為: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)測量模式

類中有三個常量: UNSPECIFIEDEXACTLY 、AT_MOST,他們對應著三種測量模式,具體含義我們在注釋中已經(jīng)寫了,小盆友整理出以下表格方便我們查閱。

名稱 含義 數(shù)值(二進制) 具體表現(xiàn)
UNSPECIFIED 父View不對子View 施加任何約束,子View可以是它想要的任何尺寸 00 ...(30個0) 系統(tǒng)內(nèi)部使用
EXACTLY 父View已確定子View 的確切大小,子View的大小為父View測量所得的值 01 ...(30個0) 具體數(shù)值、match_parent
AT_MOST 父View 指定一個子View可用的最大尺寸值,View大小 不能超過該值。 10 ...(30個0) wrap_content

(2)makeMeasureSpec

makeMeasureSpec 方法,該方法用于合并測量模式和測量尺寸,將這兩個值合為一個32位的數(shù),高2位為測量模式,低30位為尺寸。

該方法很簡短,主要得益于 (size & ~MODE_MASK) | (mode & MODE_MASK) 的位操作符,但也帶來了一定的理解難度。我們拆解下

  • size & ~MODE_MASK 剔除 size 中的測量模式的值,即將高2位置為00
  • mode & MODE_MASK 保留傳入的模式參數(shù)的值,同時將低30位置為 0...(30位0)
  • (size & ~MODE_MASK) | (mode & MODE_MASK) 就是 size的低30位 + mode的高2位(總共32位)

至于 &、~|這三個位操作為何能做到如此的騷操作,請移步小盆友的另一博文——Android位運算簡單講解。(內(nèi)容很簡短,不熟悉這塊內(nèi)容的童鞋,強烈推薦瀏覽一下)

(3)getMode

getMode 方法用于獲取我們傳入的 measureSpec 值的高2位,即測量模式。

(4)getSize

getSize 方法用于獲取我們傳入的measureSpec 值的低30位,即測量的值。

解釋完 MeasureSpec 的是什么,我們還有兩個問題需要搞清楚:

  1. 這兩個參數(shù)值從哪來
  2. 這兩個參數(shù)值怎么使用

這兩個參數(shù)值從哪來

借助下面這張簡圖,設定當前運行的 onMeasure 方法處于B控件,則其兩個MeasureSpec值是由其父視圖(即A控件)計算得出,計算的規(guī)則ViewGroup 有對應的方法,即 getChildMeasureSpec。

getChildMeasureSpec 的具體代碼如下。我們繼續(xù)使用上面的情景, 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:
            /**
             * 根據(jù)子視圖的大小,進行不同模式的組合:
             * 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;
        // 父視圖已經(jīng)有一個最大尺寸限制
        case MeasureSpec.AT_MOST:
            /**
             * 根據(jù)子視圖的大小,進行不同模式的組合:
             * 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的參數(shù)得來。

int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);

// 進行測量
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

這兩個參數(shù)值怎么使用

我們上面一直提到的一個詞叫做 “建議”,是因為到達B的兩個維度(橫縱)的 MeasureSpec,不是就已經(jīng)決定了控件B的寬高。

這里我們可以類比為 父母總是語重心長的跟自己的孩子說,你要怎么做怎么做(即算出了子View 的 MeasureSpec),懂事的孩子會知道聽從父母的建議可以讓自己少走彎路(即遵循傳遞下來的MeasureSpec約束),而調(diào)皮一點的孩子,覺得打破常規(guī)更加好玩(即不管 MeasureSpec 的規(guī)則約束)。

按照約定,我們是要遵循父View給出的約束。而B控件再進行計算其自己子View的MeasureSpec(如果有子View),子View 會再進行測量 孫View,這樣一層層的測量(這里能感受到樹結(jié)構(gòu)的魅力了吧??)。

B控件完成子View的測量,調(diào)用setMeasuredDimension 將自身最終的 測量寬高 進行設置,這樣就完成B控件的測量流程就完畢了。

2、onLayout

protected void onLayout(boolean changed, int l, int t, int r, int b)

onLayout 則是進行擺放,這一過程比較簡單,因為我們從 onMeasure 中已經(jīng)得到各個子View 的寬高。父View 只要按照自己的邏輯負責給定各個子View 的 左上坐標右下坐標 即可。

3、onDraw

protected void onDraw(Canvas canvas)

繪制流程中,onDraw 應該說是童鞋們最為熟悉的,只要在 canvas 繪制自身需要繪制的內(nèi)容便可以。

六、實戰(zhàn)

上一節(jié)總結(jié)起來,就是我們在面試時總會說的那句話,onMeasure負責測量、onLayout負責擺放、onDraw負責繪制,但理論總是過于空洞,我們現(xiàn)在將理論融入到操作中來。我們用標簽的流式布局來說明進一步解釋這一切。

1、效果圖

Github入口:傳送門

2、編碼思路

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

我們借助這張小盆友手繪的流程圖,來講清楚這繪制流程。

(1) onMeasure

最開始,控件是空的,也就是第一幅小圖。

接著將第一個標簽 “大Android” 放入,此時不超出 TagFlowLayout 的寬,如第二幅小圖所示。

然后將第二個標簽 “猛猛的小盆友” 放入,此時如第三幅小圖所示,超出了 TagFlowLayout 的寬, 所以我們進行換行,將 “猛猛的小盆友” 放入第二行。

在接著將第三個標簽 “JAVA” 放入,此時不超出 TagFlowLayout 的寬,如第四幅小圖所示。

最后把剩下的 “PHP是最好的語言” 也放入,當此時有個問題,即使一行放一個也容不下(第五幅小圖),因為 “ PHP是最好的語言” 的寬已經(jīng)超出 TagFlowLayout 的寬,所以我們在給 “PHP是最好的語言” 測量的MeasureSpec時,需要進行“糾正”,使其寬度為 TagFlowLayout 的寬,最終形成了第六幅小圖的樣子。

最后還需要將我們測量的結(jié)果通過 setMeasuredDimension 設置我們自身的 TagFlowLayout 控件的寬高。

(2) onLayout

經(jīng)過 onMeasure ,TagFlowLayout 心中已經(jīng)知道自己的 每個孩子的寬高每個孩子要“站”在哪一行,但具體的坐標還是需要進行計算。

“大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 并不會被調(diào)用,具體原因我們在前面已經(jīng)說了,這里就不贅述了。

3、小結(jié)

雖然鋪墊了很多,但是 TagFlowLayout 的代碼量并不多,這里也不再粘貼出來,需要的進入傳送門。我們只需要在onMeasure 中進行測量,然后將測量的值進行存儲,最后在 onLayout 依賴測量的結(jié)果進行擺放即可。

七、寫在最后

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

更多大廠面試資料以及Android資料可以進群免費領(lǐng)?。海ˋndroid進階學習⑥群:345659112)

作者:猛猛的小盆友
鏈接:https://juejin.cn/post/6844903807860604935
來源:稀土掘金

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容