死磕Android_View工作原理你需要知道的一切

*本篇文章已授權微信公衆號 guolin_blog (郭霖)獨家發佈

平時在開發安卓的過程中,View是我們用的非常非常多的東西.用戶所看到的一切關於UI的,都是通過View繪製出來展示到屏幕上的.大多數情況下我們僅僅瞭解基本控件的使用方法,我們是無法做出非常複雜炫酷的自定義View的.我們需要掌握View的工作原理:測量、佈局、繪製流程,掌握了這幾個基本的流程我們才能做出更加完美的自定義View.做起來也更加得心應手.當然,View的工作原理,也是大多數面試所必問的知識點.要想了解工作原理,就只能read the fucking code.

/***
 *                                         ,s555SB@@&                          
 *                                      :9H####@@@@@Xi                        
 *                                     1@@@@@@@@@@@@@@8                       
 *                                   ,8@@@@@@@@@B@@@@@@8                      
 *                                  :B@@@@X3hi8Bs;B@@@@@Ah,                   
 *             ,8i                  r@@@B:     1S ,M@@@@@@#8;                 
 *            1AB35.i:               X@@8 .   SGhr ,A@@@@@@@@S                
 *            1@h31MX8                18Hhh3i .i3r ,A@@@@@@@@@5               
 *            ;@&i,58r5                 rGSS:     :B@@@@@@@@@@A               
 *             1#i  . 9i                 hX.  .: .5@@@@@@@@@@@1               
 *              sG1,  ,G53s.              9#Xi;hS5 3B@@@@@@@B1                
 *               .h8h.,A@@@MXSs,           #@H1:    3ssSSX@1                  
 *               s ,@@@@@@@@@@@@Xhi,       r#@@X1s9M8    .GA981               
 *               ,. rS8H#@@@@@@@@@@#HG51;.  .h31i;9@r    .8@@@@BS;i;          
 *                .19AXXXAB@@@@@@@@@@@@@@#MHXG893hrX#XGGXM@@@@@@@@@@MS        
 *                s@@MM@@@hsX#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@&,      
 *              :GB@#3G@@Brs ,1GM@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@B,     
 *            .hM@@@#@@#MX 51  r;iSGAM@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@8     
 *          :3B@@@@@@@@@@@&9@h :Gs   .;sSXH@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@:    
 *      s&HA#@@@@@@@@@@@@@@M89A;.8S.       ,r3@@@@@@@@@@@@@@@@@@@@@@@@@@@r    
 *   ,13B@@@@@@@@@@@@@@@@@@@5 5B3 ;.         ;@@@@@@@@@@@@@@@@@@@@@@@@@@@i    
 *  5#@@#&@@@@@@@@@@@@@@@@@@9  .39:          ;@@@@@@@@@@@@@@@@@@@@@@@@@@@;    
 *  9@@@X:MM@@@@@@@@@@@@@@@#;    ;31.         H@@@@@@@@@@@@@@@@@@@@@@@@@@:    
 *   SH#@B9.rM@@@@@@@@@@@@@B       :.         3@@@@@@@@@@@@@@@@@@@@@@@@@@5    
 *     ,:.   9@@@@@@@@@@@#HB5                 .M@@@@@@@@@@@@@@@@@@@@@@@@@B    
 *           ,ssirhSM@&1;i19911i,.             s@@@@@@@@@@@@@@@@@@@@@@@@@@S   
 *              ,,,rHAri1h1rh&@#353Sh:          8@@@@@@@@@@@@@@@@@@@@@@@@@#:  
 *            .A3hH@#5S553&@@#h   i:i9S          #@@@@@@@@@@@@@@@@@@@@@@@@@A.
 *
 *
 *    又看源碼,看你妹呀!
 */

1. View是從什麼時候開始繪製的?

1.1 先簡單來個demo

新建一個自定義View,名字叫MyView,分別在MyView的onMeasure(),onLayout(),onDraw()打上log.

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    Log.e(TAG, "onMeasure: ---MyView");
}

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    super.onLayout(changed, left, top, right, bottom);
    Log.e(TAG, "onLayout: ---MyView");
}

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    Log.e(TAG, "onDraw: ---MyView");
}

然後在Activity的佈局中添加這個MyView,並在Activity的onCreate(),onStart(),onResume()打上log.

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    Log.e(TAG, "onCreate: ");
}

@Override
protected void onStart() {
    super.onStart();
    Log.e(TAG, "onStart: ");
}

@Override
protected void onResume() {
    super.onResume();
    Log.e(TAG, "onResume: ");
}

運行demo,看一下log:

05-13 22:35:37.130 17346-17346/com.xfhy.demo E/xfhy: onCreate: 
05-13 22:35:37.130 17346-17346/com.xfhy.demo E/xfhy: onStart: 
05-13 22:35:37.130 17346-17346/com.xfhy.demo E/xfhy: onResume: 
05-13 22:35:37.155 17346-17346/com.xfhy.demo E/xfhy: onMeasure: ---MyView
05-13 22:35:37.295 17346-17346/com.xfhy.demo E/xfhy: onLayout: ---MyView
05-13 22:35:37.315 17346-17346/com.xfhy.demo E/xfhy: onMeasure: ---MyView
05-13 22:35:37.315 17346-17346/com.xfhy.demo E/xfhy: onLayout: ---MyView
05-13 22:35:37.315 17346-17346/com.xfhy.demo E/xfhy: onDraw: ---MyView

好了,我們從log中就可以看出,View的繪製其實是從Activity的onResume()之後纔開始的.

1.2 從源碼層看看工作過程

在Activity的啓動過程中,我們知道,在最後那裏,ActivityThread中的handleLaunchActivity,performLaunchActivity,handleResumeActivity,這3個主要的方法完成了Activity的創建到啓動工作.

ps: 如果有不清楚的在網上查閱一下資料,這裏推薦剛哥的書籍:安卓開發藝術探索 第九章

1.2.1 handleLaunchActivity()

下面看一下handleLaunchActivity()方法:

private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent) {
    ...

    if (localLOGV) Slog.v(
        TAG, "Handling launch of " + r);
    
    //分析1 : 這裏是創建Activity,並調用了Activity的onCreate()和onStart()
    Activity a = performLaunchActivity(r, customIntent);

    if (a != null) {
        r.createdConfig = new Configuration(mConfiguration);
        Bundle oldState = r.state;
        //分析2 : 這裏調用Activity的onResume()
        handleResumeActivity(r.token, false, r.isForward,
                !r.activity.mFinished && !r.startsNotResumed);
    }
    ....
}

handleLaunchActivity()主要是爲了調用performLaunchActivity()和handleResumeActivity()

1.2.2 performLaunchActivity()

private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
    ......
    //分析1 : 這裏底層是通過反射來創建的Activity實例
    java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
    activity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent);

    //底層也是通過反射構建Application,如果已經構建則不會重複構建,畢竟一個進程只能有一個Application
    Application app = r.packageInfo.makeApplication(false, mInstrumentation);

    if (activity != null) {
        Context appContext = createBaseContextForActivity(r, activity);
        CharSequence title = r.activityInfo.loadLabel(appContext.getPackageManager());
        Configuration config = new Configuration(mCompatConfiguration);
        if (DEBUG_CONFIGURATION) Slog.v(TAG, "Launching activity "
                + r.activityInfo.name + " with config " + config);
        //分析2 : 在這裏實例化了PhoneWindow,並將該Activity設置爲PhoneWindow的Callback回調,還初始化了WindowManager
        activity.attach(appContext, this, getInstrumentation(), r.token,
                r.ident, app, r.intent, r.activityInfo, title, r.parent,
                r.embeddedID, r.lastNonConfigurationInstances, config);

        //分析3 : 間接調用了Activity的performCreate方法,間接調用了Activity的onCreate方法.
        mInstrumentation.callActivityOnCreate(activity, r.state);
        
        //分析4: 這裏和上面onCreate過程差不多,調用Activity的onStart方法
        if (!r.activity.mFinished) {
            activity.performStart();
            r.stopped = false;
        }
        ....
    }
}

主要過程:

  1. 通過反射來創建的Activity實例
  2. 在這裏實例化了PhoneWindow,並將該Activity設置爲PhoneWindow的Callback回調.建立起Activity與PhoneWindow之間的聯繫.
  3. 調用了Activity的onCreate方法
  4. 調用Activity的onStart方法

對於分析1中的代碼:

public Activity newActivity(ClassLoader cl, String className,
        Intent intent)
        throws InstantiationException, IllegalAccessException,
        ClassNotFoundException {
    //反射->實例化
    return (Activity)cl.loadClass(className).newInstance();
}

對於分析2中的代碼:

final void attach(Context context, ActivityThread aThread,
        Instrumentation instr, IBinder token, int ident,
        Application application, Intent intent, ActivityInfo info,
        CharSequence title, Activity parent, String id,
        NonConfigurationInstances lastNonConfigurationInstances,
        Configuration config, String referrer, IVoiceInteractor voiceInteractor,
        Window window, ActivityConfigCallback activityConfigCallback) {
    ......

    //實例化PhoneWindow
    mWindow = new PhoneWindow(this, window, activityConfigCallback);
    mWindow.setWindowControllerCallback(this);
    mWindow.setCallback(this);
    mWindow.setOnWindowDismissedCallback(this);
    
    .....
    //還有一些其他的配置代碼

    mWindow.setWindowManager(
            (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
            mToken, mComponent.flattenToString(),
            (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);

    mWindowManager = mWindow.getWindowManager();
    ....
}

對於分析3中的代碼:

//首先是來到Instrumentation的callActivityOnCreate方法
public void callActivityOnCreate(Activity activity, Bundle icicle) {
    prePerformCreate(activity);
    activity.performCreate(icicle);
    postPerformCreate(activity);
}

//然後就來到Activity的performCreate方法
final void performCreate(Bundle icicle) {
    performCreate(icicle, null);
}

final void performCreate(Bundle icicle, PersistableBundle persistentState) {
    ....
    if (persistentState != null) {
        onCreate(icicle, persistentState);
    } else {
        onCreate(icicle);
    }
    ......
}

對於分析4中的代碼:

//Activity
final void performStart() {
    ......
    mInstrumentation.callActivityOnStart(this);
    ......
}

//Instrumentation
public void callActivityOnStart(Activity activity) {
    activity.onStart();
}

1.2.3 handleResumeActivity()

final void handleResumeActivity(IBinder token,
        boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
    .....
    //分析1 : 在其內部調用Activity的onResume方法
    r = performResumeActivity(token, clearHide, reason);

    .....
    r.window = r.activity.getWindow();
    View decor = r.window.getDecorView();
    decor.setVisibility(View.INVISIBLE);
    //獲取WindowManager
    ViewManager wm = a.getWindowManager();
    WindowManager.LayoutParams l = r.window.getAttributes();
    a.mDecor = decor;

    if (a.mVisibleFromClient) {
        .....
        //分析2 : WindowManager添加DecorView
        wm.addView(decor, l);
        ...
    }
    .....

}

細看分析2中的邏輯:

@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
    applyDefaultToken(params);
    mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}

我們看到,方法裏面將邏輯交給了mGlobal,mGlobal是WindowManagerGlobal,WindowManagerGlobal是全局單例.WindowManagerImpl的方法都是由WindowManagerGlobal完成的.我們跟着來到了WindowManagerGlobal的addView方法.

public void addView(View view, ViewGroup.LayoutParams params,
        Display display, Window parentWindow) {
    ....

    ViewRootImpl root;

    synchronized (mLock) {
        root = new ViewRootImpl(view.getContext(), display);
        view.setLayoutParams(wparams);

        .....

        root.setView(view, wparams, panelParentView);
    }
}

在這裏我們看到了,實例化ViewRootImpl,然後建立ViewRootImpl與View的聯繫.跟着進入setView方法

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
    .....
    requestLayout();
    .....
}

@Override
public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        //檢查線程合法性
        checkThread();
        mLayoutRequested = true;
        scheduleTraversals();
    }
}

void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        ......
    }
}

final class TraversalRunnable implements Runnable {
    @Override
    public void run() {
        doTraversal();
    }
}
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();


void doTraversal() {
    if (mTraversalScheduled) {
        ....
        performTraversals();
        ....
    }
}

一路下來,我們來到了熟悉的方法面前:performTraversals方法.

1.2.4 performTraversals()

performTraversals()方法相信大家都已經非常熟悉啦,它是整個View繪製的核心,從measure到layout,再從layout到draw,全部在這個方法裏面完成了,所以這個方法裏面的代碼非常長,這是肯定的.由於本人水平有限,就不每句代碼逐行分析了,我們需要學習的是一個主要的流程.

下面的performTraversals()方法的超精簡代碼,裏面的代碼真的超級超級多,下面是主要流程,也是今天的主角

private void performTraversals() {
    //分析1 : 這裏面會調用performMeasure開始測量流程
    measureHierarchy(host, lp, res,
                    desiredWindowWidth, desiredWindowHeight);
    //分析2 : 開始佈局流程
    performLayout(lp, mWidth, mHeight);
    //分析3 : 開始繪畫流程
    performDraw();
}

首先,來個主要的流程圖,這個是performTraversals()的大致流程.

View繪製流程

如上面的代碼所示,performTraversals()會依次調用performMeasure、performLayout、performDraw方法,而這三個方法是View的繪製流程的核心所在.

  • performMeasure : 在performMeasure裏面會調用measure方法,然後measure會調用onMeasure方法,而在onMeasure方法中則會對所有的子元素進行measure過程.這相當於完成了一次從父元素到子元素的measure傳遞過程,如果子元素是一個ViewGroup,那麼繼續向下傳遞,直到所有的View都已測量完成.測量完成之後,我們可以根據getMeasureHeight和getMeasureWidth方法獲取該View的高度和寬度.
  • performLayout : performLayout的原理其實是和performMeasure差不多,在performLayout裏面調用了layout方法,然後在layout方法會調用onLayout方法,onLayout又會對所有子元素進行layout過程.由父元素向子元素傳遞,最終完成所有View的layout過程.確定View的4個點: left+top+right+bottom,layout完成之後可以通過getWidth和getHeight獲取View的最終寬高.
  • performDraw : 也是和performMeasure差不多,從父元素從子元素傳遞.在performDraw裏面會調用draw方法,draw方法再調用drawSoftware方法,drawSoftware方法裏面回調用View的draw方法,然後再通過dispatchDraw方法分發,遍歷所有子元素的draw方法,draw事件就這樣一層層地傳遞下去.

2. View 測量流程

2.1 MeasureSpec

在開始進行理解View的測量流程之前,需要先理解MeasureSpec.

MeasureSpec代表的是32位的int值,它的高2位是SpecMode(也是一個int),低30位是SpecSize(也是一個int),SpecMode是測量模式,SpecSize是測量大小. MeasureSpec相當於是兩者的結合. 系統封裝瞭如何從MeasureSpec中提取SpecMode和SpecSize,也封裝了用SpecMode和SpecSize組合成MeasureSpec.

public static class MeasureSpec {
    private static final int MODE_SHIFT = 30;
    private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
    public static final int UNSPECIFIED = 0 << MODE_SHIFT;
    public static final int EXACTLY     = 1 << MODE_SHIFT;
    public static final int AT_MOST     = 2 << MODE_SHIFT;

    public static int makeMeasureSpec(int size,int mode) {
        if (sUseBrokenMakeMeasureSpec) {
            return size + mode;
        } else {
            return (size & ~MODE_MASK) | (mode & MODE_MASK);
        }
    }

    public static int getMode(int measureSpec) {
        return (measureSpec & MODE_MASK);
    }

    public static int getSize(int measureSpec) {
        return (measureSpec & ~MODE_MASK);
    }
}

SpecMode有3種,分別是

  • UNSPECIFIED 父容器對View不會有任何限制,要多大給多大,一般是用在系統內部使用,我們開發的APP用不到.
  • EXACTLY 這種情況對應於match_parent和具體數值這兩種模式,父容器已經檢測出View需要的精確大小.
  • AT_MOST 這種情況對應於wrap_content,父容器指定了一個最大值,View不能超過這個值.

一般來說,View的MeasureSpec由父容器的MeasureSpec和自己的LayoutParams共同決定.因爲有了MeasureSpec纔可以在onMeasure中確定測量寬高.

下面是從源碼中提取出來的摘要信息,後面會詳細看源碼分析,這裏先提取出來

  • 如果View的寬高是固定的值,那麼不管父容器的MeasureSpec是什麼,View的MeasureSpec都是EXACTLY
  • 如果View的寬高是wrap_content,那麼不管父容器的MeasureSpec是EXACTLY還是AT_MOST,最終View的MeasureSpec都是AT_MOST,這裏暫時不用管UNSPECIFIED(我們用不到).而且View最終的大小不能超過父容器的剩餘空間
  • 如果View的寬高是match_parent,那麼要分兩種情況
    • 如果父容器是EXACTLY,那麼View就是EXACTLY
    • 如果父容器是AT_MOST,那麼View也是AT_MOST.

這裏不得不引用一張剛哥書籍裏面的經典表格來表示一下:

ps:這裏必須要強烈推薦一波剛哥的安卓開發藝術探索這本書.沒看過這本書,都不敢說自己是學安卓的.去年我作爲萌新買了這本書看了一遍,覺得受益匪淺.今年打算再看一遍,重新梳理一下.好書是值得反覆推敲琢磨的.

2.2 從performMeasure()開始View測量

我們從ViewRootImpl的performTraversals()開始着手,仔細觀察

private void performTraversals() {
    //host爲根視圖,即DecorView
    //desiredWindowWidth是Window的寬度
    measureHierarchy(host, lp, res,
                    desiredWindowWidth, desiredWindowHeight);
}

private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
                                     final Resources res, final int desiredWindowWidth, 									final int desiredWindowHeight) {
    //分析1 : desiredWindowWidth就是Window的寬度,desiredWindowHeight是Window的高度
    childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
    childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
    //分析2 : 開始測量流程
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);                     
}

measure是從上往下執行的,widthMeasureSpec和heightMeasureSpec通常情況下是由父容器傳遞給子視圖的.但是最外層的根視圖,怎麼拿到MeasureSpec呢? 在執行performMeasure方法之前,我們需要拿到最外層的視圖的MeasureSpec,看代碼

private static int getRootMeasureSpec(int windowSize, int rootDimension) {
    int measureSpec;
    switch (rootDimension) {
    //如果是MATCH_PARENT,那麼就是EXACTLY
    case ViewGroup.LayoutParams.MATCH_PARENT:
        // Window can't resize. Force root view to be windowSize.
        measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
        break;
    //如果是WRAP_CONTENT,就是AT_MOST
    case ViewGroup.LayoutParams.WRAP_CONTENT:
        // Window can resize. Set max size for root view.
        measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
        break;
    default:
        //如果是固定的值,也是EXACTLY
        // Window wants to be an exact size. Force root view to be that size.
        measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
        break;
    }
    return measureSpec;
}

最外層的根視圖的MeasureSpec只由自己的LayoutParams決定,做自己的主人,舒服.

既然我們根視圖拿到了MeasureSpec,接下來就要拿自己的MeasureSpec教孩子做人了.

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
    //調用根視圖的measure方法,開始測量流程
    mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

這裏的mView就是DecorView.DecorView是一個FrameLayout,而FrameLayout是一個ViewGroup,而ViewGroup是一個View,這個measure方法就是在View裏面的. 因爲measure是一個final方法,哈哈,所以子類不能覆寫它.

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    ......
    //調用onMeasure方法  
    onMeasure(widthMeasureSpec, heightMeasureSpec);
    ......
}

measure方法裏面有一些檢測是否需要重新onMeasure的代碼,被我略去了.

onMeasure是View裏面的方法,ViewGroup是一個抽象類並且沒有重寫onMeasure.因爲onMeasure方法的實現,每個都是不一樣的,比如LinearLayout和FrameLayout的onMeasure方法肯定是實現邏輯不一樣的.

因爲DecorView是FrameLayout,所以我們看看FrameLayout中的onMeasure.

FrameLayout->onMeasure()

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int count = getChildCount();

    for (int i = 0; i < count; i++) {
        final View child = getChildAt(i);
        if (mMeasureAllChildren || child.getVisibility() != GONE) {
            //分析1 : 遍歷所有子控件,測量每個子控件的大小
                //參數1:View控件
                //參數2:寬MeasureSpec
                //參數3:父容器在寬度上已經用了多少了,因爲FrameLayout的規則是:前面已經放置的View並不會影響後面放置View的寬高,是直接覆蓋到上一個View上的.所以這裏傳0
                //參數4:高MeasureSpec
                //參數5:父容器在高度上已經用了多少了
            measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
        }
    }
    ......

    //分析2 : 測量完所有的子控件的大小之後,才知道自己的大小  這很符合FrameLayout的規則嘛
    setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
            resolveSizeAndState(maxHeight, heightMeasureSpec,
                    childState << MEASURED_HEIGHT_STATE_SHIFT));
    ......
}

FrameLayout的onMeasure方法中會遍歷所有子控件,然後進行所有子控件的大小測量.最後纔來設置自己的大小.注意,onMeasure方法的入參MeasureSpec是從父容器傳過來的,意思就是給你個參考,你自己看着辦吧.

在測量子控件大小的時候會調用ViewGroup的measureChildWithMargins方法,下面是代碼:

protected void measureChildWithMargins(View child,
        int parentWidthMeasureSpec, int widthUsed,
        int parentHeightMeasureSpec, int heightUsed) {
    //獲取子控件的LayoutParams
    final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
    
    //分析1: 計算子控件在寬上的MeasureSpec  
        //參數1:父容器的MeasureSpec
        //參數2:這裏官方的入參名稱是padding,從下面這個傳值的形式來看,顯然是子控件在寬上不能利用的空間(ViewGroup的左右兩邊padding+子控件的左右margin+父容器在寬度上已經使用了並且不能再使用的空間)
        //參數3:子控件想要的寬度
    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                    + widthUsed, lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                    + heightUsed, lp.height);
    
    //分析2: 將measure過程傳遞給子控件  如果子控件又是一個ViewGroup,那麼繼續向下傳遞
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

在measureChildWithMargins方法裏我們首先是看到根據子控件的LayoutParams和父容器的MeasureSpec計算子控件的MeasureSpec,然後將計算出的MeasureSpec通過子控件的measure方法傳遞下去.如果子控件又是一個ViewGroup,那麼它又會重複的measure流程,一直向下傳遞這個過程,直接最後的那個是View爲止.因爲View沒有子控件,它就不能向下傳遞了.

所以我們自定義View(這裏指那種直接繼承自View)的時候,在onMeasure方法裏面,需要根據自身的LayoutParams+父容器的MeasureSpec來計算SpecSize和SpecMode,最後根據業務場景來確定自己的大小(調用setMeasuredDimension來確定大小).

注意了,接下來的getChildMeasureSpec方法就比較重要了

//這裏來自ViewGroup的getChildMeasureSpec方法,無刪減
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
    //根據父容器的MeasureSpec獲取父容器的SpecMode和SpecSize
    int specMode = MeasureSpec.getMode(spec);
    int specSize = MeasureSpec.getSize(spec);
    
    //剩下的size
    int size = Math.max(0, specSize - padding);

    //最終的size和mode
    int resultSize = 0;
    int resultMode = 0;

    switch (specMode) {
    // Parent has imposed an exact size on us
    //父容器有一個確定的大小
    case MeasureSpec.EXACTLY:
        if (childDimension >= 0) {
            //子控件也是確定的大小,那麼最終的大小就是子控件設置的大小,SpecMode爲EXACTLY
            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.
            //子控件想要自己定義大小,但是不能超過剩餘空間 size
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;

    // Parent has imposed a maximum size on us
    case MeasureSpec.AT_MOST:
        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;

    // Parent asked to see how big we want to be
    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;
    }
    //noinspection ResourceType
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

這段代碼對應着下面這段總結

  • 如果View的寬高是固定的值,那麼不管父容器的MeasureSpec是什麼,View的MeasureSpec都是EXACTLY
  • 如果View的寬高是wrap_content,那麼不管父容器的MeasureSpec是EXACTLY還是AT_MOST,最終View的MeasureSpec都是AT_MOST,這裏暫時不用管UNSPECIFIED(我們用不到).而且View最終的大小不能超過父容器的剩餘空間
  • 如果View的寬高是match_parent,那麼要分兩種情況
    • 如果父容器是EXACTLY,那麼View就是EXACTLY
    • 如果父容器是AT_MOST,那麼View也是AT_MOST.

這段代碼對應着上面剛哥總結的那個表格.同時也是measure流程的核心內容.

image

因爲在measureChildWithMargins方法裏我們已經計算出子控件的MeasureSpec,然後通過measure傳遞給子控件了,如果子控件又是一個ViewGroup,那麼它又會重複的measure流程,一直向下傳遞這個過程,直接最後的那個是View爲止.因爲View沒有子控件,它就不能向下傳遞了.到這裏其實我們的View的measure流程已經走完了,哈哈,不知不覺.

下面簡單畫一個流程圖,方便理解上面的流程.

image

2.3 measure小結

從ViewRootImpl的performTraversals方法開始進入View的繪製過程,performTraversals方法裏面會有一個performMeasure方法.這個performMeasure方法是專門拿來測量View的大小的.而且會遍歷整個View樹,全部進行測量.

在performMeasure裏面會調用measure方法,然後measure會調用onMeasure方法,而在onMeasure方法中則會對所有的子元素進行measure過程.這相當於完成了一次從父元素到子元素的measure傳遞過程,如果子元素是一個ViewGroup,那麼繼續向下傳遞,直到所有的View都已測量完成.測量完成之後,我們可以根據getMeasureHeight和getMeasureWidth方法獲取該View的高度和寬度.

3. View 佈局流程

3.1 從performLayout開始佈局

我們還是從ViewRootImpl的performTraversals()開始着手

private void performTraversals() {
    ....
    //開始佈局流程
    performLayout(lp, mWidth, mHeight);
    .....
}

private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
        int desiredWindowHeight) {
    .....
    //這裏的host其實是根視圖(DecorView)
        //參數:left,top,right,bottom  這些位置都是相對於父容器而言的
    host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
    .....
}

在performLayout方法裏面調用了DecorView的layout方法,然後我發現:layout方法其實是View這個父類裏面的,然後ViewGroup繼承了View之後重寫了一下,只是調了一下super.layout(l, t, r, b);,相當於實現還是在View的layout裏面.而且ViewGroup的layout方法是final修飾的,意味着子類不能再重寫這個方法了.

//以下是View的layout方法
public void layout(int l, int t, int r, int b) {
    ......
    onLayout(changed, l, t, r, b);
    ......
}

layout方法其實就是調用onLayout方法,如果這裏子控件是一個View的話,那麼onLayout其實是空實現.onLayout在ViewGroup是一個抽象方法,如果是一個ViewGroup的話,比如FrameLayout,那麼onLayout是需要自己實現的.

//View中的定義
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {}

//ViewGroup中的定義,沒錯,這是抽象方法,具體的實現交由實現類去實現
@Override
protected abstract void onLayout(boolean changed, int l, int t, int r, int b);

因爲我們的根視圖是DecorView,也就是FrameLayout,那麼我們來看一下FrameLayout的onLayout實現:

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {   
    //佈局子控件  我沒看懂這個changed參數是拿來幹什麼的,好像並沒有用上(這裏已經是FrameLayout的onLayout方法的全部代碼了)
    layoutChildren(left, top, right, bottom, false /* no force left gravity */);
}

void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
    final int count = getChildCount();
    
    //最左側
    final int parentLeft = getPaddingLeftWithForeground();
    //最右側
    final int parentRight = right - left - getPaddingRightWithForeground();
    //最頂部
    final int parentTop = getPaddingTopWithForeground();
    //最底部
    final int parentBottom = bottom - top - getPaddingBottomWithForeground();

    for (int i = 0; i < count; i++) {
        final View child = getChildAt(i);
        if (child.getVisibility() != GONE) {
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            
            //因爲已經measure流程走完了,所以這裏是能通過getMeasuredWidth方法獲取測量寬度的
            final int width = child.getMeasuredWidth();
            final int height = child.getMeasuredHeight();

            //實際子控件的left
            int childLeft;
            int childTop;

            int gravity = lp.gravity;
            if (gravity == -1) {
                gravity = DEFAULT_CHILD_GRAVITY;
            }

            final int layoutDirection = getLayoutDirection();
            final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
            final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;

            switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                //水平居中
                case Gravity.CENTER_HORIZONTAL:
                    childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
                    lp.leftMargin - lp.rightMargin;
                    break;
                case Gravity.RIGHT:  //子控件在父容器的最右側
                    if (!forceLeftGravity) {
                        childLeft = parentRight - width - lp.rightMargin;
                        break;
                    }
                case Gravity.LEFT: //子控件在父容器的最左側
                default:
                    childLeft = parentLeft + lp.leftMargin;
            }
            
            //豎直方向上的gravity
            switch (verticalGravity) {
                case Gravity.TOP:  //位於父容器的頂部
                    childTop = parentTop + lp.topMargin;
                    break;
                case Gravity.CENTER_VERTICAL:  //垂直居中
                    childTop = parentTop + (parentBottom - parentTop - height) / 2 +
                    lp.topMargin - lp.bottomMargin;
                    break;
                case Gravity.BOTTOM:  //位於父容器底部
                    childTop = parentBottom - height - lp.bottomMargin;
                    break;
                default:
                    childTop = parentTop + lp.topMargin;
            }
            
            //最後給這個子控件一個最終的left,top,right,bottom值
            //把這個子控件放在這裏
            child.layout(childLeft, childTop, childLeft + width, childTop + height);
        }
    }
}

不同的ViewGroup的實現類的onLayout方法實現是不一樣的,是根據自身情況來決定將子控件放在那裏的,比如FrameLayout和LinearLayout的onLayout是不一樣的實現,但是onLayout這個方法最終是將各個子控件有條不紊的放在對應的位置上.

我們看到在onLayout方法的最後,調用了子控件的layout方法,其實就是將layout流程向下進行傳遞了.如果子控件還是ViewGroup的話,那麼它又會對它自己所有的子控件進行佈局,放置.最後一層一層的往下,直到全部都layout完成.每個View都知道自己的left,top,right,bottom.這個時候是可以通過View的getWidth和getHeight來獲取最終的寬高的.

下面的View的getWidth和getHeight方法的實現,可以看到,就是通過這四個位置來確定的寬高.

public final int getWidth() {
    return mRight - mLeft;
}

public final int getHeight() {
    return mBottom - mTop;
}

3.2 layout小結

layout主要是爲了確定該控件以及其子控件的位置和大小.在performLayout中,主要是確定每個控件的left+top+right+bottom,performLayout之後它們的位置就已經被確定了,就只剩下最後一步繪製了.

4. View 繪製流程

還是從ViewRootImpl的performTraversals方法開始分析

private void performTraversals() {
    //開始繪畫流程
    performDraw();
}

private void performDraw() {
    ......
    draw(fullRedrawNeeded);
    ......
}

private void draw(boolean fullRedrawNeeded){
    .....
    drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty);
    .....
}

private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
        boolean scalingRequired, Rect dirty) {
    ......
    mView.draw(canvas);
    ......
}

隨着方法的調用深入,發現來到了View的draw方法

public void draw(Canvas canvas) {
    .....

    /*
        注意了這是官方給的註釋,谷歌工程師還真是貼心,把draw步驟寫的詳詳細細,給力,點贊
     * Draw traversal performs several drawing steps which must be executed
     * in the appropriate order:
     *
     *      1. Draw the background
     *      2. If necessary, save the canvas' layers to prepare for fading
     *      3. Draw view's content
     *      4. Draw children
     *      5. If necessary, draw the fading edges and restore layers
     *      6. Draw decorations (scrollbars for instance)
     */

    // Step 1, draw the background, if needed
    //1. 繪製背景
    if (!dirtyOpaque) {
        drawBackground(canvas);
    }

    // skip step 2 & 5 if possible (common case)
    final int viewFlags = mViewFlags;
    boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
    boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
    if (!verticalEdges && !horizontalEdges) {
        // Step 3, draw the content
        //3. 繪製自己的內容
        if (!dirtyOpaque) onDraw(canvas);

        // Step 4, draw the children
        //4. 繪製子控件  如果是View的話這個方法是空實現,如果是ViewGroup則繪製子控件
        dispatchDraw(canvas);

        drawAutofilledHighlight(canvas);

        // Overlay is part of the content and draws beneath Foreground
        if (mOverlay != null && !mOverlay.isEmpty()) {
            mOverlay.getOverlayView().dispatchDraw(canvas);
        }

        // Step 6, draw decorations (foreground, scrollbars)
        //6. 繪製裝飾和前景
        onDrawForeground(canvas);

        // Step 7, draw the default focus highlight
        //7. 繪製默認焦點高亮顯示
        drawDefaultFocusHighlight(canvas);

        if (debugDraw()) {
            debugDrawFocus(canvas);
        }

        // we're done...
        return;
    }
    .....
}

注意到,谷歌工程師將draw的步驟完完全全的寫出來了的.還真是貼心啊.draw的基本步驟如下

  1. 繪製背景
  2. 繪製控件自己本身的內容
  3. 繪製子控件
  4. 繪製裝飾(比如滾動條)和前景

這裏簡單提一下dispatchDraw方法,在這個方法裏面會去調用drawChild方法,在drawChild裏面會調用子控件的draw方法,這相當於完成了draw的傳遞過程,通知子控件去繪製它自己. 然後如果子控件是ViewGroup,它又會重複上面這個遞推.

draw的流程比測量和佈局要簡單一些,但是需要注意的是,View繪製過程是通過dispatchDraw來傳遞的.

5. 結束語

寫一篇深入(可能只是對我來說)的文章真的好不容易,期間遇到了很多坑,也學到了很多.之前其實這部分是學過的.但是隻有當自己去看源碼,一步步分析,輸出成文檔,才真正理解其中的原理,爲什麼代碼要這樣寫.

可能還是太菜了吧,寫這麼一篇水文花了大概4天…

image

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章