Android自定義View的基石——View工作原理總結(jié)

前言
View可以說(shuō)是我們?cè)贏ndroid開(kāi)發(fā)中接觸得最多的一個(gè)類(lèi)了啄寡,雖然不屬于四大組件拾弃,但是發(fā)揮的作用卻一點(diǎn)都不亞于四大組件讯檐,頁(yè)面中的各種控件羡疗、布局都直接或間接地繼承自View,可以說(shuō)View無(wú)處不在别洪。因而了解View的工作原理能讓我們更好地處理開(kāi)發(fā)中的諸多問(wèn)題顺囊,尤其是對(duì)于老生常談的自定義View來(lái)說(shuō),View的工作原理更是必須要掌握的蕉拢。

在進(jìn)入正文之前還是要強(qiáng)調(diào)一下特碳,本文的分析基于Android 9.0(API Level 28)的源碼,不同版本的源碼可能會(huì)有不同晕换,但是基本思路不會(huì)變化太多午乓,可以進(jìn)行參考。此外闸准,本文的篇幅較長(zhǎng)益愈,可以根據(jù)下面的目錄選擇章節(jié)來(lái)查看(無(wú)奈簡(jiǎn)書(shū)不支持生成目錄跳轉(zhuǎn),所以只能將就了o(╥﹏╥)o)。

目錄

  • 1.幾個(gè)相關(guān)類(lèi)
    • 1.1.Window和WindowManager
    • 1.2.DecorView
    • 1.3.ViewRoot
  • 2.準(zhǔn)備階段
    • 2.1.Window的創(chuàng)建
    • 2.2.DecorView的創(chuàng)建
    • 2.3.三大流程的調(diào)用
  • 3.measure階段
    • 3.1.MeasureSpec
      • 3.1.1.MeasureSpec簡(jiǎn)介
      • 3.1.2.如何確定MeasureSpec的值
    • 3.2.LayoutParams
      • 3.2.1.LayoutParams簡(jiǎn)介
      • 3.2.2.View的LayoutParams屬性是何時(shí)設(shè)置的
    • 3.3.measure流程
      • 3.3.1.單一View的measure流程
      • 3.3.2.ViewGroup的measure流程
    • 3.4.補(bǔ)充:MeasureSpec.UNSPECIFIED的應(yīng)用
  • 4.layout階段
    • 4.1.單一View的layout流程
    • 4.2.ViewGroup的layout流程
    • 4.3.getMeasureWidth/getMeasureHeight和getWidth/getHeight的區(qū)別
  • 5.draw階段
    • 5.1.單一View的draw流程
    • 5.2.ViewGroup的draw流程
    • 5.3.ViewGroup的draw()方法調(diào)用問(wèn)題
  • 6.總結(jié)
  • 7.參考文章

1.幾個(gè)相關(guān)類(lèi)

在介紹View的工作原理之前首先要介紹幾個(gè)相關(guān)的類(lèi)蒸其,它們?cè)赩iew的工作流程中扮演了重要的角色敏释,了解它們能讓我們對(duì)于View的工作原理有一個(gè)更全面的認(rèn)識(shí)。

1.1.Window和WindowManager

Window顧名思義是表示一個(gè)窗口摸袁,雖然我們?cè)陂_(kāi)發(fā)中可能很少直接去操作Window钥顽,我目前接觸過(guò)的也僅僅是給Window添加一些Flag的操作,但是Window在Android的視圖體系中其實(shí)是很重要的一環(huán)靠汁,它可以說(shuō)是所有視圖的承載器蜂大,我們最熟悉的Activity的視圖實(shí)際上也是附加在Window上,通過(guò)Window來(lái)管理的蝶怔。Window是一個(gè)抽象類(lèi)奶浦,它只有一個(gè)實(shí)現(xiàn)類(lèi)PhoneWindow,因此我們?cè)诜治鲈创a時(shí)直接看PhoneWindow就可以了踢星。
WindowManager可以譯為窗口管理者澳叉,是外界訪問(wèn)Window的入口,我們可以通過(guò)WindowManager來(lái)操作Window沐悦。WindowManager是一個(gè)接口耳高,它的實(shí)現(xiàn)類(lèi)是WindowManagerImpl

1.2.DecorView

DecorView是最頂層的View所踊,是整個(gè)視圖的根節(jié)點(diǎn),繼承自FrameLayout概荷,因此它也是一個(gè)ViewGroup秕岛。下面以一張圖來(lái)展示可能更直觀一些。

DecorView下包含一個(gè)豎直方向的LinearLayout误证,它的內(nèi)部根據(jù)頁(yè)面主題的不同可能會(huì)有所不同继薛,但是一定會(huì)包含一個(gè)子View,它的id為android.R.id.content愈捅,是一個(gè)FrameLayout遏考,我們調(diào)用setContentView()設(shè)置的布局就是添加到了這個(gè)contentView中。

1.3.ViewRoot

ViewRoot對(duì)應(yīng)于ViewRootImpl蓝谨,是連接WindowManager和DecorView的紐帶灌具,View的measure、layout和draw流程都是通過(guò)ViewRootImpl完成的譬巫。

2.準(zhǔn)備階段

這里將以下幾個(gè)流程稱(chēng)作View工作流程的準(zhǔn)備階段咖楣,可能不是很確切,主要還是為了和我們熟知的measure芦昔、layout诱贿、draw三大流程區(qū)分開(kāi),這一階段完成的工作是Window和DecorView的創(chuàng)建以及對(duì)三大流程的調(diào)用。

2.1.Window的創(chuàng)建

Window的創(chuàng)建時(shí)機(jī)是在ActivityThreadperformLaunchActivity()方法中珠十,在之前Activity的啟動(dòng)流程中也分析過(guò)該方法料扰,我們?cè)賮?lái)簡(jiǎn)單回顧一下:

private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
    // ... 
    java.lang.ClassLoader cl = appContext.getClassLoader();
    // 1.創(chuàng)建Activity對(duì)象
    activity = mInstrumentation.newActivity(
            cl, component.getClassName(), r.intent);
    // ...
  
    // 2.創(chuàng)建Application對(duì)象,如果已經(jīng)創(chuàng)建則不會(huì)重復(fù)創(chuàng)建
    Application app = r.packageInfo.makeApplication(false, mInstrumentation);
    // ...
  
    if (activity != null) {
        // ...
        Window window = null;
        if (r.mPendingRemoveWindow != null && r.mPreserveWindow) {
            window = r.mPendingRemoveWindow;
            r.mPendingRemoveWindow = null;
            r.mPendingRemoveWindowManager = null;
        }
        appContext.setOuterContext(activity);
        // 3.創(chuàng)建PhoneWindow
        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);

        // ...
        // 4.調(diào)用onCreate()方法
        if (r.isPersistable()) {
            mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
        } else {
            mInstrumentation.callActivityOnCreate(activity, r.state);
        }
        // ...
        r.activity = activity;
    }
    // ...
    return activity;
}

該方法內(nèi)部會(huì)依次創(chuàng)建Activity對(duì)象和Application對(duì)象焙蹭,最后通過(guò)Instrumentation對(duì)象調(diào)用Activity的onCreate()方法晒杈,這些都不是我們這里要關(guān)注的,我們只需要分析Activity的attach()方法壳嚎。

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

    // 創(chuàng)建PhoneWindow
    mWindow = new PhoneWindow(this, window, activityConfigCallback);
    // 設(shè)置回調(diào)
    mWindow.setWindowControllerCallback(this);
    mWindow.setCallback(this);
    mWindow.setOnWindowDismissedCallback(this);

    // ...
    
    // 設(shè)置WindowManager
    mWindow.setWindowManager(
            (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
            mToken, mComponent.flattenToString(),
            (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
    if (mParent != null) {
        mWindow.setContainer(mParent.getWindow());
    }
    mWindowManager = mWindow.getWindowManager();
    // ...
}

可以發(fā)現(xiàn)PhoneWindow對(duì)象就是在attach()方法中創(chuàng)建的桐智,之后會(huì)為PhoneWindow設(shè)置相關(guān)回調(diào)并創(chuàng)建WindowManager對(duì)象(實(shí)際上是WindowManagerImpl對(duì)象)。

2.2.DecorView的創(chuàng)建

在Activity的onCreate()方法中我們會(huì)調(diào)用setContentView()來(lái)設(shè)置頁(yè)面的布局烟馅,DecorView的創(chuàng)建就要從該方法來(lái)分析说庭。
Activity的setContentView方法

public void setContentView(@LayoutRes int layoutResID) {
    getWindow().setContentView(layoutResID);
    initWindowDecorActionBar();
}

getWindow()方法獲取到的就是上面創(chuàng)建好的PhoneWindow對(duì)象,我們接著來(lái)看PhoneWindow的setContentView()方法:

@Override
public void setContentView(int layoutResID) {
    if (mContentParent == null) {
        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 {
        mLayoutInflater.inflate(layoutResID, mContentParent);
    }
    mContentParent.requestApplyInsets();
    final Callback cb = getCallback();
    if (cb != null && !isDestroyed()) {
        cb.onContentChanged();
    }
    mContentParentExplicitlySet = true;
}

這里首先會(huì)判斷mContentParent是否為空郑趁,如果為空就調(diào)用installDecor()方法刊驴,否則調(diào)用removeAllViews()方法移除mContentParent的所有子View涕侈。那么這個(gè)mContentParent是什么呢榆鼠,它是一個(gè)ViewGroup砌滞,我們通過(guò)名稱(chēng)可能能夠猜到它就是我們頁(yè)面所展示的內(nèi)容土辩。全局搜索了一下mContentParent的賦值時(shí)機(jī)肛循,發(fā)現(xiàn)只有在installDecor()方法中才會(huì)對(duì)其賦值姥敛,因此這里mContentParent為空乡洼,我們來(lái)看看installDecor()方法:

private void installDecor() {
    mForceDecorInstall = false;
    if (mDecor == null) {
        mDecor = generateDecor(-1);
        // ...
    } else {
        mDecor.setWindow(this);
    }
    if (mContentParent == null) {
        mContentParent = generateLayout(mDecor);
        // ...
    }
}

protected DecorView generateDecor(int featureId) {
    Context context;
    if (mUseDecorContext) {
        Context applicationContext = getContext().getApplicationContext();
        if (applicationContext == null) {
            context = getContext();
        } else {
            context = new DecorContext(applicationContext, getContext());
            if (mTheme != -1) {
                context.setTheme(mTheme);
            }
        }
    } else {
        context = getContext();
    }
    return new DecorView(context, featureId, this, getAttributes());
}

protected ViewGroup generateLayout(DecorView decor) {
    TypedArray a = getWindowStyle();
    // ...
    int layoutResource;
    // 根據(jù)Features(通過(guò)requestFeature()方法添加智袭,可以看做是Window的主題樣式)設(shè)置相應(yīng)的布局
    // 偽代碼
    if () {
        layoutResource =R.layout.xx1;
    } else if () {
        layoutResource =R.layout.xx2;
    } else {
        layoutResource =R.layout.xx;
    }
    // 為DecorView加載布局
    mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);

    ViewGroup contentParent = (ViewGroup) findViewById(ID_ANDROID_CONTENT);
    // ...
    return contentParent;
}

可以看到installDecor()方法主要做的事有兩件:調(diào)用generateDecor()方法創(chuàng)建DecorView变抽,將返回值賦給mDecor础拨;調(diào)用generateLayout()方法創(chuàng)建一個(gè)ViewGroup,賦值給mContentParent绍载。這里的generateDecor()generateLayout()方法都省略了大量代碼诡宗,只保留了最核心的部分。
創(chuàng)建好了DecorView和mContentParent之后击儡,我們回到PhoneWindow的setContentView()方法塔沃,可以發(fā)現(xiàn)這之后調(diào)用了mLayoutInflater.inflate(layoutResID, mContentParent)inflate()方法的作用我們都很熟悉了阳谍,就是根據(jù)我們傳入的布局文件構(gòu)建出View樹(shù)蛀柴,這里調(diào)用的是兩個(gè)參數(shù)的方法,因此會(huì)將創(chuàng)建好的View樹(shù)添加到mContentParent中矫夯。如果不是很清楚inflate()方法幾個(gè)參數(shù)的意義可以查閱網(wǎng)上的相關(guān)文章名扛,或者參考我之前寫(xiě)過(guò)的一篇文章LayoutInflate的使用,這里我就不具體講了〖胙鳎現(xiàn)在我們就清楚了mContentParent是什么了吧肮韧,它是我們setContentView()方法中指定布局的父View,指定的布局會(huì)作為一個(gè)子View添加到mContentParent中。
我一開(kāi)始分析時(shí)有一個(gè)疑問(wèn)弄企,mContentParent是何時(shí)與DecorView產(chǎn)生聯(lián)系的呢超燃,分析到這里好像并沒(méi)有看到諸如mDecor.addView()之類(lèi)的代碼,我們回到generateLayout()方法中拘领,方法內(nèi)部調(diào)用了findViewById()方法獲取到contentParent意乓,并將其賦給mContentParent,而這個(gè)id為content约素,是DecorView中的一個(gè)子View届良,是一個(gè)Framelayout,因此contentParent就是DecorView中的這個(gè)子View圣猎,mContentParent自然也表示這個(gè)子View士葫。generateLayout()方法中會(huì)根據(jù)Window的主題樣式為DecorView加載相應(yīng)的布局,我們就以其中一個(gè)R.layout.screen_simple為例送悔,看看它的布局層級(jí)關(guān)系:
screen_simple.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    android:orientation="vertical">
    <ViewStub android:id="@+id/action_mode_bar_stub"
              android:inflatedId="@+id/action_mode_bar"
              android:layout="@layout/action_mode_bar"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:theme="?attr/actionBarTheme" />
    <FrameLayout
         android:id="@android:id/content"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:foregroundInsidePadding="false"
         android:foregroundGravity="fill_horizontal|top"
         android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>

可以看到最外層是一個(gè)LinearLayout慢显,內(nèi)部有兩個(gè)子View,一個(gè)是ViewStub欠啤,它的id為action_mode_bar_stub荚藻,從名稱(chēng)上大概可以猜出它是頁(yè)面的標(biāo)題欄,會(huì)根據(jù)主題的不同來(lái)選擇是否加載洁段;另一個(gè)子View是id為contentFrameLayout应狱,也就是上面分析中findViewById()方法獲取到的contentParent,因此mContentParent就是這個(gè)FrameLayout祠丝。
看到這里疾呻,我們對(duì)于整個(gè)頁(yè)面的層級(jí)關(guān)系就很清楚了,最外層是一個(gè)DecorView纽疟,它的內(nèi)部有一個(gè)LinearLayout,LinearLayout中有一個(gè)FrameLayout憾赁,我們?cè)?code>setContentView()中指定的布局文件會(huì)被添加到這個(gè)FrameLayout中污朽,當(dāng)然實(shí)際上根據(jù)主題樣式的不同可能會(huì)更復(fù)雜一些,這里只是說(shuō)明最簡(jiǎn)單的一種情況龙考。
值得一提的是蟆肆,AppCompatActivity中的setContentView()方法和Activity的有所不同,簡(jiǎn)單分析一下晦款。
AppCompatActivity的setContentView方法

@Override
public void setContentView(@LayoutRes int layoutResID) {
    getDelegate().setContentView(layoutResID);
}

getDelegate()獲取到的是一個(gè)代理對(duì)象炎功,類(lèi)型為AppCompatDelegateImpl(前幾個(gè)版本的源碼中這里會(huì)根據(jù)SDK版本不同返回不同的對(duì)象如AppCompatDelegateImplNAppCompatDelegateImplV23等缓溅,后面的分析是差不多的)蛇损,之后調(diào)用了AppCompatDelegateImpl的setContentView()方法。

@Override
public void setContentView(int resId) {
    ensureSubDecor();
    ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
    contentParent.removeAllViews();
    LayoutInflater.from(mContext).inflate(resId, contentParent);
    mOriginalWindowCallback.onContentChanged();
}

方法內(nèi)部首先調(diào)用了ensureSubDecor()方法,將返回值賦值給mSubDecor:

private void ensureSubDecor() {
    if (!mSubDecorInstalled) {
        mSubDecor = createSubDecor();
        // ...
    }
}

private ViewGroup createSubDecor() {
    TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);
    // ...
    // 分析1
    mWindow.getDecorView();

    final LayoutInflater inflater = LayoutInflater.from(mContext);
    ViewGroup subDecor = null;

    // 根據(jù)主題為subDecor加載布局
    // 偽代碼
    if () {
        subDecor = (ViewGroup) LayoutInflater.from(themedContext)
                .inflate(R.layout.xx, null);
    } else {

    }

    // ...
    // 分析2
    final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById(
            R.id.action_bar_activity_content);

    final ViewGroup windowContentView = (ViewGroup) mWindow.findViewById(android.R.id.content);
    if (windowContentView != null) {
        // 將windowContentView的子View移除并添加到contentView中
        while (windowContentView.getChildCount() > 0) {
            final View child = windowContentView.getChildAt(0);
            windowContentView.removeViewAt(0);
            contentView.addView(child);
        }

        
        windowContentView.setId(View.NO_ID);
        contentView.setId(android.R.id.content);

        // ...
    }
    
    // 分析3
    mWindow.setContentView(subDecor);
    // ...
    return subDecor;
}

ensureSubDecor()方法內(nèi)部調(diào)用了createSubDecor()方法淤齐,我們具體分析一下該方法股囊。
分析1、mWindow.getDecorView()
mWindow的類(lèi)型為PhoneWindow更啄,通過(guò)查看PhoneWindow的getDecorView()方法我們可以發(fā)現(xiàn)稚疹,由于此時(shí)mDecor并未被賦值過(guò),因此會(huì)調(diào)用此前分析過(guò)的installDecor()方法祭务,創(chuàng)建DecorView和mContentParent内狗。

@Override
public final View getDecorView() {
    if (mDecor == null || mForceDecorInstall) {
        installDecor();
    }
    return mDecor;
}

之后和generateLayout()方法很類(lèi)似,通過(guò)判斷主題樣式創(chuàng)建出subDecor义锥,加載不同的布局文件柳沙。
分析2、“偷梁換柱”
這里的邏輯還是很巧妙的缨该,涉及到了兩個(gè)View偎行,contentView是subDecor中id為action_bar_activity_content的子View,類(lèi)型為ContentFrameLayout贰拿;另一個(gè)windowContentView的id為android.R.id.content蛤袒,沒(méi)錯(cuò),就是此前分析過(guò)的那個(gè)FrameLayout膨更。之后的操作是將windowContentView的子View移除妙真,添加到contentView中,并將windowContentView的id設(shè)置為View.NO_ID荚守,將contentView的id設(shè)置為android.R.id.content珍德,看上去很像是兩個(gè)View之間的互換,因此我把這個(gè)操作稱(chēng)為“偷梁換柱”矗漾。
分析3锈候、mWindow.setContentView(subDecor)
這里調(diào)用了PhoneWindow的setContentView()方法,將subDecor添加到了mContentParent中敞贡,這里的mContentParent其實(shí)就是上面的windowContentView泵琳,此時(shí)它的id已經(jīng)變成了View.NO_ID。
這時(shí)我們?cè)倩氐紸ppCompatDelegateImpl的setContentView()方法誊役,之后就是根據(jù)我們指定的布局文件構(gòu)建出View樹(shù)并添加到id為android.R.id.content的ViewGroup中获列,即上面的ContentFrameLayout。
我的表述不是很清楚蛔垢,大家可能有些懵了击孩,這都是什么亂七八糟的,我最后總結(jié)一下鹏漆,其實(shí)大體上的流程和Activity的setContentView()方法還是很類(lèi)似的巩梢,不同之處就是在Activity中创泄,我們指定布局文件對(duì)應(yīng)的View樹(shù)會(huì)被添加到FrameLayout中,而對(duì)于AppCompatActivity且改,View樹(shù)會(huì)被添加到一個(gè)ContentFrameLayout中验烧,它們的id均為android.R.id.content,層級(jí)關(guān)系如下又跛,ContentFrameLayout的直接父View是mSubDecor所加載布局的根布局碍拆,對(duì)應(yīng)不同的主題樣式可能不同,這個(gè)根布局的父View就是FrameLayout慨蓝,因此可以看做就是多嵌套了幾層View感混。
其實(shí)這里我就不明白了,我們的Activity都是繼承自AppCompatActivity礼烈,那么相比于繼承自Activity弧满,我們的布局層級(jí)是要復(fù)雜一些的,大家都知道Android開(kāi)發(fā)中是要避免過(guò)多的布局層級(jí)嵌套的此熬,那么AppCompatActivity這樣做的目的是什么呢庭呜?鑒于個(gè)人能力和認(rèn)識(shí)都還很淺顯,想不明白為什么要這樣設(shè)計(jì)犀忱,歡迎大佬們提出自己的見(jiàn)解募谎。

2.3.三大流程的調(diào)用

上面的兩個(gè)流程中已經(jīng)完成了PhoneWindow和DecorView的創(chuàng)建,那么大名鼎鼎的View繪制三大流程又是從何時(shí)開(kāi)始的呢阴汇,就是ActivityThread的handleResumeActivity() 方法数冬,該方法在分析Activity的啟動(dòng)流程時(shí)也分析過(guò),onResume()回調(diào)方法就是經(jīng)由該方法調(diào)用的搀庶。

@Override
public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
                                 String reason) {
    // ...
    // 方法內(nèi)部調(diào)用Activity的onResume()方法
    final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
    // ...
    final Activity a = r.activity;
    // ...
    if (r.window == null && !a.mFinished && willBeVisible) {
        r.window = r.activity.getWindow();
        // 獲取DecorView
        View decor = r.window.getDecorView();
        // 將DecorView設(shè)置為不可見(jiàn)
        decor.setVisibility(View.INVISIBLE);
        // 獲取WindowManager
        ViewManager wm = a.getWindowManager();
        WindowManager.LayoutParams l = r.window.getAttributes();
        // 為Activity的mDecor對(duì)象賦值
        a.mDecor = decor;
        // ...
        if (a.mVisibleFromClient) {
            if (!a.mWindowAdded) {
                a.mWindowAdded = true;
                // 將DecorView添加到Window中
                wm.addView(decor, l);
            } else {
                a.onWindowAttributesChanged(l);
            }
        }
    }
    // ...
    if (!r.activity.mFinished && willBeVisible && r.activity.mDecor != null && !r.hideForNow) {
        // ...
        if (r.activity.mVisibleFromClient) {
            // 將DecorView設(shè)置為可見(jiàn)
            r.activity.makeVisible();
        }
    }
}

該方法主要做了兩件事:調(diào)用performResumeActivity()方法拐纱,進(jìn)而調(diào)用Activity的生命周期回調(diào)onResume();獲取此前創(chuàng)建好的PhoneWindow哥倔、DecorView以及WindowManager對(duì)象秸架,調(diào)用WindowManager的addView()方法將DecorView添加到Window中。前面也說(shuō)過(guò)咆蒿,WindowManager的實(shí)現(xiàn)類(lèi)是WindowManagerImpl东抹,因此我們來(lái)具體看一下WindowManagerImpl的addView()方法都做了些什么。

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

方法內(nèi)部調(diào)用了mGlobal的addView()方法蜡秽,mGlobal的類(lèi)型為WindowManagerGlobal府阀,我們接著看:

public void addView(View view, ViewGroup.LayoutParams params,
                    Display display, Window parentWindow) {
    // ...
    ViewRootImpl root;
    // ...
    // 創(chuàng)建ViewRootImpl對(duì)象
    root = new ViewRootImpl(view.getContext(), display);
  
    view.setLayoutParams(wparams);

    mViews.add(view);
    mRoots.add(root);
    mParams.add(wparams);

    try {
        // 核心代碼
        root.setView(view, wparams, panelParentView);
    } catch (RuntimeException e) {
        // BadTokenException or InvalidDisplayException, clean up.
        if (index >= 0) {
            removeViewLocked(index, true);
        }
        throw e;
    }
}

addView()方法內(nèi)部首先會(huì)創(chuàng)建出ViewRootImpl對(duì)象缆镣,將要添加的View(即DecorView)芽突、ViewRootImpl和布局參數(shù)添加到列表中,最后調(diào)用ViewRootImpl的setView()方法董瞻,我們來(lái)看一下這個(gè)方法寞蚌。

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
    synchronized (this) {
        if (mView == null) {
            mView = view;
            // ...
            requestLayout();
            // ...
        }
    }
}

setView()方法內(nèi)部調(diào)用了requestLayout()方法田巴,這個(gè)方法可能大家在自定義View時(shí)用到過(guò),用于刷新視圖挟秤,不過(guò)需要注意的是壹哺,我們?cè)谧远xView中調(diào)用的requestLayout()方法是在View中定義的,和ViewRootImpl中的邏輯是不一樣的艘刚。接下來(lái)我們就來(lái)看看ViewRootImpl的requestLayout()方法:

@Override
public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        checkThread();
        mLayoutRequested = true;
        scheduleTraversals();
    }
}

方法內(nèi)部又調(diào)用了scheduleTraversals()方法:

void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        // 開(kāi)啟同步屏障機(jī)制
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        // 利用Handler發(fā)送一條異步消息
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        // ...
    }
}

scheduleTraversals()方法內(nèi)部首先執(zhí)行了mHandler.getLooper().getQueue().postSyncBarrier()管宵,這行代碼的作用是開(kāi)啟Handler的同步屏障機(jī)制,關(guān)于Handler的同步屏障機(jī)制攀甚,我這里簡(jiǎn)單解釋一下(因?yàn)槲伊私獾靡膊皇呛芡笍豲(╥﹏╥)o)箩朴,我們都知道Handler處理的消息是放到一個(gè)消息隊(duì)列中的,消息默認(rèn)情況下都是同步的秋度,如果需要發(fā)送異步消息需要使用代碼來(lái)聲明炸庞,同步屏障機(jī)制就使得Looper在從消息隊(duì)列中獲取消息時(shí),只獲取異步消息并進(jìn)行處理荚斯。我可能解釋得不太好埠居,如果想深入了解一下Handler的同步屏障機(jī)制可以自行查找資料,這里推薦一下鴻洋大神WanAndroid上的每日一問(wèn)Handler應(yīng)該是大家再熟悉不過(guò)的類(lèi)了事期,那么其中有個(gè)同步屏障機(jī)制滥壕,你了解多少呢?刑赶。開(kāi)啟了同步屏障后捏浊,調(diào)用了mChoreographer的postCallback()方法,該方法內(nèi)部就是利用了Handler撞叨,發(fā)送了一個(gè)Runable對(duì)象金踪,如果跟蹤源碼的話(huà)可以發(fā)現(xiàn)最后會(huì)把Runable封裝為一個(gè)Message,并將Message設(shè)置為異步消息牵敷,我就不展示了胡岔。清楚了postCallback()方法的原理后,我們就知道了需要分析mTraversalRunnable對(duì)象枷餐,它是一個(gè)Runable對(duì)象靶瘸,類(lèi)型為TraversalRunnable ,在run()方法中調(diào)用了doTraversal()方法毛肋。

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

由于開(kāi)啟了同步屏障怨咪,因此當(dāng)前線程(主線程)的Looper會(huì)優(yōu)先獲取異步消息,即直接執(zhí)行doTraversal()方法润匙。

void doTraversal() {
    if (mTraversalScheduled) {
        mTraversalScheduled = false;
        // 關(guān)閉同步屏障
        mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
        // ...
        performTraversals();
        // ...
    }
}

doTraversal()方法內(nèi)部首先會(huì)關(guān)閉同步屏障機(jī)制诗眨,否則主線程的同步消息就永遠(yuǎn)無(wú)法被處理了,然后調(diào)用了performTraversals()方法孕讳,我們來(lái)看一下:

private void performTraversals() {
    // ...
    // 調(diào)用performMeasure()方法開(kāi)始measure流程
    measureHierarchy(host, lp, res,
            desiredWindowWidth, desiredWindowHeight);
    // ...
    // 開(kāi)始layout流程
    performLayout(lp, mWidth, mHeight);
    // ...
    // 開(kāi)始draw流程
    performDraw();
    // ...
}

這里省略了大量代碼匠楚,可以看出巍膘,在performTraversals()方法內(nèi)會(huì)依次調(diào)用measureHierarchy()performLayout()芋簿、performDraw()峡懈,進(jìn)而開(kāi)始View的三大流程。
分析到這里与斤,View的繪制準(zhǔn)備階段就算完成了肪康,最后再回顧一下,主要分為三個(gè)階段:

  • Activity的onCreate()方法調(diào)用之前撩穿,創(chuàng)建Window(PhoneWindow)
  • Activity的onCreate()方法中調(diào)用setContentView()方法梅鹦,創(chuàng)建DecorView和contentView(頁(yè)面內(nèi)容根布局),將指定的布局文件加載到contentView中
  • Activity的onResume()方法調(diào)用之后冗锁,將DecorView添加到Window中齐唆,之后依次開(kāi)始View的measure、layout和draw流程

從上面幾個(gè)流程的先后順序我們就能清楚為什么在onResume()方法中或者onResume()方法之前獲取不到View的寬高冻河,就是因?yàn)榇藭r(shí)View還未執(zhí)行measure和layout流程箍邮。

3.measure階段

到這里算是正式進(jìn)入到了View的三大流程,首先要分析的是measure流程叨叙。在分析View的measure流程之前锭弊,我們首先要介紹兩個(gè)相關(guān)的類(lèi):MeasureSpecLayoutParams

3.1.MeasureSpec

3.1.1.MeasureSpec簡(jiǎn)介

關(guān)于MeasureSpec大家可能都很熟悉了擂错,它是由一個(gè)32位int值表示的味滞,高2位表示SpecMode,即測(cè)量模式钮呀,低30位表示SpecSize剑鞍,即測(cè)量尺寸大小。

public static class MeasureSpec {
    private static final int MODE_SHIFT = 30;
    private static final int MODE_MASK = 0x3 << MODE_SHIFT;

    // ...
    
    /**
     * 三種測(cè)量模式
     */
    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;

    /**
     * 根據(jù)測(cè)量尺寸和測(cè)量模式生成MeasureSpec
     */
    public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
                                      @MeasureSpecMode int mode) {
        if (sUseBrokenMakeMeasureSpec) {
            return size + mode;
        } else {
            return (size & ~MODE_MASK) | (mode & MODE_MASK);
        }
    }

    /**
     * 獲得測(cè)量模式
     */
    @MeasureSpecMode
    public static int getMode(int measureSpec) {
        //noinspection ResourceType
        return (measureSpec & MODE_MASK);
    }

    /**
     * 獲得測(cè)量尺寸
     */
    public static int getSize(int measureSpec) {
        return (measureSpec & ~MODE_MASK);
    }

    // ...
}

可以通過(guò)調(diào)用getMode()getSize()方法獲取到測(cè)量模式和測(cè)量尺寸爽醋,方法內(nèi)部就是通過(guò)簡(jiǎn)單的位運(yùn)算保留指定位數(shù)上的數(shù)值蚁署。不由得稱(chēng)贊Android系統(tǒng)開(kāi)發(fā)人員的設(shè)計(jì)巧妙,將兩個(gè)值封裝成了一個(gè)變量蚂四,可以通過(guò)位運(yùn)算獲取相應(yīng)數(shù)值光戈,減少了多余對(duì)象的內(nèi)存分配,其實(shí)Android源碼中很多地方都有類(lèi)似設(shè)計(jì)(比如MotionEvent)遂赠,這里就不多提啦久妆。
MeasureSpec內(nèi)部定義了三種測(cè)量模式:

UNSPECIFIED:父View不會(huì)限制子View的大小,一般用于系統(tǒng)內(nèi)部跷睦,開(kāi)發(fā)中使用很少
EXACTLY:父View能夠確定子View的大小筷弦,對(duì)應(yīng)兩種情況:精確尺寸(dp或px)match_parent
AT_MOST:子View的大小不能超過(guò)父View尺寸,具體尺寸需要由子View自身來(lái)確定送讲,對(duì)應(yīng)wrap_content

雖然我們?cè)陂_(kāi)發(fā)中用到UNSPECIFIED模式的情況不多奸笤,但是了解一下還是有必要的,我在后面會(huì)單獨(dú)介紹一下這個(gè)模式的應(yīng)用哼鬓。

3.1.2.如何確定MeasureSpec的值

MeasureSpec的值是由View自身的LayoutParams父View的MeasureSpec共同確定的。對(duì)于特定的View來(lái)說(shuō),它的MeasureSpec是通過(guò)父View(即ViewGroup)的getChildMeasureSpec()方法得到的殴瘦。
ViewGroup的getChildMeasureSpec方法

/**
 * 獲得子View的MeasureSpec
 *
 * @param spec           父View的MeasureSpec
 * @param padding        父View的padding
 * @param childDimension 子View的LayoutParams指定的寬/高
 * @return
 */
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
    // 獲得父View的測(cè)量模式和測(cè)量尺寸
    int specMode = MeasureSpec.getMode(spec);
    int specSize = MeasureSpec.getSize(spec);
    
    // 父View實(shí)際可用大小
    int size = Math.max(0, specSize - padding);

    int resultSize = 0;
    int resultMode = 0;

    switch (specMode) {
        // 父View的測(cè)量模式為EXACTLY挡育,即match_parent或精確尺寸
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                // 子View的LayoutParams指定為精確的值
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // 子View的LayoutParams指定為MATCH_PARENT
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // 子View的LayoutParams指定為WRAP_CONTENT
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // 父View的測(cè)量模式為AT_MOST,即wrap_content
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // 父View的測(cè)量模式為UNSPECIFIED
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
    }
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

getChildMeasureSpec()方法也驗(yàn)證了子View的MeasureSpec是由父View的MeasureSpec和子View的LayoutParams共同確定的称簿。上面的判斷可能有些復(fù)雜扣癣,不過(guò)別擔(dān)心。已經(jīng)有很多大佬總結(jié)出了表格憨降,看起來(lái)更加直觀一些父虑,下圖摘自Carson_Ho大佬的博客,表中的childSize表示子View的LayoutParams指定的大小授药,parentSize表示父View可用空間的大小士嚎。

我們可以先不去看最后一列UNSPECIFIED的情況,單看前兩列可以找出一定的規(guī)律:

  • 當(dāng)子View的LayoutParams指定為精確數(shù)值時(shí)悔叽,不管父View的測(cè)量模式是什么莱衩,子View的測(cè)量模式均為EXACTLY,測(cè)量尺寸為L(zhǎng)ayoutParams指定的值
  • 當(dāng)子View的LayoutParams指定為match_parent時(shí)娇澎,子View的測(cè)量模式取決于父View笨蚁,即如果父View的測(cè)量模式為EXACTLY,那么子View的測(cè)量模式為EXACTLY趟庄;如果父View的測(cè)量模式為AT_MOST括细,那么子View的測(cè)量模式為AT_MOST,子View的測(cè)量尺寸均為父View可用空間大小
  • 當(dāng)子View的LayoutParams指定為wrap_content時(shí)戚啥,不管父View的測(cè)量模式是什么勒极,子View的測(cè)量模式均為AT_MOST,測(cè)量尺寸為父View可用空間大小

普通View的MeasureSpec是如何獲取的我們已經(jīng)清楚了虑鼎,那么對(duì)于DecorView來(lái)說(shuō)辱匿,它是沒(méi)有父View的,它的MeasureSpec是如何得到的呢炫彩?我們?cè)谏弦还?jié)分析到ViewRootImpl的performTraversals()方法時(shí)匾七,介紹到方法內(nèi)部調(diào)用了measureHierarchy()方法,進(jìn)而調(diào)用performMeasure()方法開(kāi)始View的measure流程江兢,現(xiàn)在我們就來(lái)具體看一下measureHierarchy()方法昨忆。
ViewRootImpl的measureHierarchy方法

private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
                                 final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) {
    int childWidthMeasureSpec;
    int childHeightMeasureSpec;
    boolean windowSizeMayChange = false;
    // ...
    
    childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
    childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
    // ...
    return windowSizeMayChange;
}

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
    if (mView == null) {
        return;
    }
    // ...
    mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

可以看出這里調(diào)用getRootMeasureSpec()方法獲取到childWidthMeasureSpec和childHeightMeasureSpec,之后調(diào)用performMeasure()方法杉允,方法內(nèi)部又調(diào)用了mView的measure()方法邑贴,這個(gè)mView是什么呢席里,我全文檢索了一下mView的賦值時(shí)機(jī),發(fā)現(xiàn)它是在setView()方法中被賦值的拢驾,還記得setView()方法是什么時(shí)候調(diào)用的嗎奖磁,就是在handleResumeActivity()方法中調(diào)用wm.addView(decor,l)這行代碼之后被調(diào)用的,因此這里的mView就是傳過(guò)來(lái)的DecorView繁疤,調(diào)用measure()方法就開(kāi)始了對(duì)DecorView的測(cè)量流程】現(xiàn)在就要關(guān)注childWidthMeasureSpec和childHeightMeasureSpec了,這兩個(gè)值就是DecorView的MeasureSpec稠腊,我們來(lái)看一下獲取到這兩個(gè)值的getRootMeasureSpec()方法:

private static int getRootMeasureSpec(int windowSize, int rootDimension) {
    int measureSpec;
    switch (rootDimension) {
        case ViewGroup.LayoutParams.MATCH_PARENT:
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default:
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
    }
    return measureSpec;
}

邏輯還是比較簡(jiǎn)單的躁染,參數(shù)windowSize傳遞過(guò)來(lái)的值是desiredWindowWidth和desiredWindowHeight,通過(guò)查看源碼可以發(fā)現(xiàn)這兩個(gè)值表示屏幕的寬高尺寸架忌,因此我們可以得出以下結(jié)論:

  • DecorView的LayoutParams指定為MATCH_PARENT時(shí)吞彤,它的測(cè)量模式為EXACTLY,測(cè)量尺寸為屏幕尺寸
  • DecorView的LayoutParams指定為WRAP_CONTENT時(shí)叹放,它的測(cè)量模式為WRAP_CONTENT备畦,測(cè)量尺寸為屏幕尺寸

可以看出,DecorView作為最頂層的View许昨,它的MeasureSpec只取決于自己的LayoutParams參數(shù)懂盐。

3.2.LayoutParams

3.2.1.LayoutParams簡(jiǎn)介

LayoutParams這個(gè)類(lèi)在開(kāi)發(fā)中還是很常見(jiàn)的,顧名思義就是布局參數(shù)糕档,View中定義了一個(gè)LayoutParams類(lèi)型的成員變量莉恼,它的作用就是確定View的寬高,我們平時(shí)在xml布局文件中指定的layout_widthlayout_height屬性就是用于生成LayoutParams速那。需要注意的是俐银,這兩個(gè)屬性的前面都帶上layout前綴,而不是直接使用widthheight來(lái)命名端仰,因此我們要清楚它們的值并不是View的寬高捶惜,也可以說(shuō)它們并不屬于View自身的屬性。
LayoutParams是ViewGroup中的一個(gè)內(nèi)部類(lèi)荔烧,我們看一下它的定義:

public static class LayoutParams {

    public static final int FILL_PARENT = -1; // 已被MATCH_PARENT取代

    public static final int MATCH_PARENT = -1;

    public static final int WRAP_CONTENT = -2;

    @ViewDebug.ExportedProperty(category = "layout", mapping = {
            @ViewDebug.IntToString(from = MATCH_PARENT, to = "MATCH_PARENT"),
            @ViewDebug.IntToString(from = WRAP_CONTENT, to = "WRAP_CONTENT")
    })
    public int width;

    @ViewDebug.ExportedProperty(category = "layout", mapping = {
            @ViewDebug.IntToString(from = MATCH_PARENT, to = "MATCH_PARENT"),
            @ViewDebug.IntToString(from = WRAP_CONTENT, to = "WRAP_CONTENT")
    })
    public int height;

    public LayoutAnimationController.AnimationParameters layoutAnimationParameters;

    /**
     * xml文件中指定的屬性
     */
    public LayoutParams(Context c, AttributeSet attrs) {
        TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_Layout);
        setBaseAttributes(a,
                R.styleable.ViewGroup_Layout_layout_width,
                R.styleable.ViewGroup_Layout_layout_height);
        a.recycle();
    }

    /**
     * 手動(dòng)指定寬高屬性
     */
    public LayoutParams(int width, int height) {
        this.width = width;
        this.height = height;
    }

    /**
     * 指定LayoutParams
     */
    public LayoutParams(LayoutParams source) {
        this.width = source.width;
        this.height = source.height;
    }

    LayoutParams() {
    }

    // ...
}

LayoutParams中定義了幾個(gè)重載構(gòu)造函數(shù)吱七,分別用于xml文件中指定寬高、手動(dòng)指定寬高等場(chǎng)景鹤竭。每個(gè)ViewGroup的子類(lèi)(直接或間接繼承)都有對(duì)應(yīng)的LayoutParams類(lèi)踊餐,比如LinearLayout.LayoutParams,在各自的LayoutParams中可以定義相應(yīng)的布局參數(shù)屬性臀稚。因此不止layout_widthlayout_height這兩個(gè)屬性吝岭,其他以layout開(kāi)頭的屬性(比如layout_weightlayout_margin等等)也都和LayoutParams相關(guān)。

3.2.2.View的LayoutParams屬性是何時(shí)設(shè)置的

了解了LayoutParams的定義后窜管,接下來(lái)需要弄清楚View的LayoutParams屬性是何時(shí)設(shè)置的散劫,我們知道在ViewGroup中添加子View的方式有兩種:xml文件中添加和代碼中添加,我們分別來(lái)看一下這兩種情況幕帆。

  • xml文件中添加View

setContentView()方法的分析中我們知道xml文件中添加的View最終是通過(guò)LayoutInflaterinflate()方法來(lái)解析的获搏。

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
    synchronized (mConstructorArgs) {
        // ...
        View result = root;
        int type;
        while ((type = parser.next()) != XmlPullParser.START_TAG &&
                type != XmlPullParser.END_DOCUMENT) {
        }
        // ...
        final String name = parser.getName();
        // ...
       
        final View temp = createViewFromTag(root, name, inflaterContext, attrs);
        ViewGroup.LayoutParams params = null;
        if (root != null) {
            // 調(diào)用generateLayoutParams()方法生成LayoutParams
            params = root.generateLayoutParams(attrs);
            if (!attachToRoot) {
                temp.setLayoutParams(params);
            }
        }

        rInflateChildren(parser, temp, attrs, true);

        if (root != null && attachToRoot) {
            root.addView(temp, params);
        }
      
        if (root == null || !attachToRoot) {
            result = temp;
        }
    }
    // ...
    return result;
}

inflate()方法中有一行代碼是params = root.generateLayoutParams(attrs);generateLayoutParams()方法的作用就是就是根據(jù)xml指定的屬性構(gòu)造出LayoutParams對(duì)象蜓肆。

public LayoutParams generateLayoutParams(AttributeSet attrs) {
    return new LayoutParams(getContext(), attrs);
}

構(gòu)造出LayoutParams對(duì)象后根據(jù)參數(shù)attachToRoot的值有兩種處理邏輯:

  • 如果attachToRoot為true,則會(huì)調(diào)用addView()方法并傳入構(gòu)造好的LayoutParams對(duì)象谋币,addView()方法內(nèi)部會(huì)將LayoutParams對(duì)象設(shè)置給View仗扬,詳細(xì)代碼后面會(huì)展示,這里先這樣記住就好
  • 如果attachToRoot為false蕾额,則會(huì)調(diào)用View的setLayoutParams()方法直接將構(gòu)造好的LayoutParams對(duì)象設(shè)置給View
public void setLayoutParams(ViewGroup.LayoutParams params) {
    if (params == null) {
        throw new NullPointerException("Layout parameters cannot be null");
    }
    // 給mLayoutParams變量賦值
    mLayoutParams = params;
    resolveLayoutParams();
    if (mParent instanceof ViewGroup) {
        ((ViewGroup) mParent).onSetLayoutParams(this, params);
    }
    // 重新執(zhí)行View的繪制流程早芭,measure->layout->draw
    requestLayout();
}
  • 代碼中添加View

我們一般會(huì)使用ViewGroup的addView()方法來(lái)添加子View,就像下面這樣:

LinearLayout llContainer = findViewById(R.id.ll_container);
View child = new View(this);
llContainer.addView(child);

addView()方法有很多個(gè)重載方法诅蝶,上面的代碼我只展示了最簡(jiǎn)單的一個(gè)參數(shù)的情況退个,下面我們就來(lái)具體看一下所有的重載方法。

/**
 * 方法1
 */
public void addView(View child) {
    addView(child, -1);
}

/**
 * 方法2
 */
public void addView(View child, int index) {
    if (child == null) {
        throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
    }
    LayoutParams params = child.getLayoutParams();
    if (params == null) {
        params = generateDefaultLayoutParams();
        if (params == null) {
            throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null");
        }
    }
    addView(child, index, params);
}

/**
 * 方法3
 */
public void addView(View child, int width, int height) {
    final LayoutParams params = generateDefaultLayoutParams();
    params.width = width;
    params.height = height;
    addView(child, -1, params);
}

/**
 * 方法4
 */
@Override
public void addView(View child, LayoutParams params) {
    addView(child, -1, params);
}

/**
 * 方法5
 */
public void addView(View child, int index, LayoutParams params) {
    if (DBG) {
        System.out.println(this + " addView");
    }

    if (child == null) {
        throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
    }
  
    requestLayout();
    invalidate(true);
    addViewInner(child, index, params, false);
}

不難看出调炬,方法1內(nèi)部調(diào)用了方法2语盈,而在方法2內(nèi)部會(huì)先判斷子View是否設(shè)置了LayoutParams屬性,如果沒(méi)有設(shè)置缰泡,就調(diào)用generateDefaultLayoutParams()方法創(chuàng)建出一個(gè)默認(rèn)的LayoutParams對(duì)象刀荒,最后調(diào)用方法5。再看方法3和方法4棘钞,這兩個(gè)方法最終同樣會(huì)調(diào)用方法5缠借,因此我們只需要看方法5就好。
在此之前宜猜,我們先看一下generateDefaultLayoutParams()方法是如何創(chuàng)建出默認(rèn)的LayoutParams對(duì)象的:

protected LayoutParams generateDefaultLayoutParams() {
    return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}

可以看出泼返,ViewGroup默認(rèn)創(chuàng)建出LayoutParams對(duì)象的寬高屬性均為WRAP_CONTENT
回到方法5姨拥,可以看到最后調(diào)用了addViewInner()方法:

private void addViewInner(View child, int index, LayoutParams params,
                          boolean preventRequestLayout) {
    // ...
    if (!checkLayoutParams(params)) {
        params = generateLayoutParams(params);
    }

    if (preventRequestLayout) {
        child.mLayoutParams = params;
    } else {
        child.setLayoutParams(params);
    }
    // ...
}

方法內(nèi)部首先會(huì)調(diào)用checkLayoutParams()方法檢查L(zhǎng)ayoutParams參數(shù)是否合法绅喉,如果不合法就調(diào)用generateLayoutParams()方法構(gòu)造一個(gè)新的LayoutParams對(duì)象,generateLayoutParams()方法我們其實(shí)在上面xml文件中添加View的分析中剛見(jiàn)過(guò)叫乌,不過(guò)這里調(diào)用的是另一個(gè)重載方法霹疫,參數(shù)為L(zhǎng)ayoutParams對(duì)象。

protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
    return  p != null;
}

protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
    return p;
}

之后就是為View設(shè)置LayoutParams屬性了综芥,這里會(huì)判斷傳入的preventRequestLayout參數(shù)值丽蝎,如果為true就直接對(duì)View的mLayoutParams變量賦值;如果為false則調(diào)用setLayoutParams()方法來(lái)給View設(shè)置LayoutParams,這兩種情況的區(qū)別就是setLayoutParams()方法內(nèi)部會(huì)調(diào)用requestLayout()方法來(lái)重新進(jìn)行View的measure屠阻、layout和draw流程红省。可以看到由于addView()方法調(diào)用addViewInner()時(shí)傳入的參數(shù)為false国觉,因此這里會(huì)執(zhí)行setLayoutParams()方法吧恃。額外提一下,ViewGroup中有一個(gè)addViewInLayout()方法麻诀,和addView()方法類(lèi)似痕寓,內(nèi)部也調(diào)用了addViewInner()方法,不過(guò)該方法可以顯式地指定preventRequestLayout參數(shù)的值蝇闭。

3.3.measure流程

前面關(guān)于兩個(gè)類(lèi)的介紹還是比較詳細(xì)的呻率,現(xiàn)在終于進(jìn)入到了measure流程的分析,這里會(huì)分為兩種情況:?jiǎn)我籚iew的measure和ViewGroup的measure呻引,ViewGroup的measure要復(fù)雜一些礼仗,因?yàn)樗粌H需要完成對(duì)自身的measure,還要完成對(duì)所有子View的measure逻悠,我們先分析簡(jiǎn)單的情況——單一View的measure流程元践。

3.3.1.單一View的measure流程

View的measure流程從measure()方法開(kāi)始:

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

可以看到它是一個(gè)final聲明的方法,因此子類(lèi)無(wú)法重寫(xiě)該方法童谒。在方法內(nèi)部又調(diào)用了我們熟悉的onMeasure方法单旁,我們自定義View時(shí)重寫(xiě)的都是該方法。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

方法內(nèi)部調(diào)用了setMeasuredDimension()方法:

protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
    // ...
    setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}

private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
    mMeasuredWidth = measuredWidth;
    mMeasuredHeight = measuredHeight;
    // ...
}

可以看到setMeasuredDimension()方法完成的工作就是為mMeasuredWidth和mMeasuredHeight這兩個(gè)變量賦值饥伊,這兩個(gè)變量表示View的測(cè)量寬高(與實(shí)際寬高有區(qū)別慎恒,View的實(shí)際寬高還取決于layout過(guò)程),我們可以通過(guò)getMeasuredWidth()getMeasuredHeight()方法獲取到View測(cè)量后的寬高尺寸撵渡,即這兩個(gè)變量的低30位融柬。
我們接著來(lái)看View的測(cè)量寬高是如何得到的,即getDefaultSize()方法:

public static int getDefaultSize(int size, int measureSpec) {
    int result = size;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);

    switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
    }
    return result;
}

可以看出當(dāng)View的測(cè)量模式為AT_MOSTEXACTLY時(shí)趋距,View的測(cè)量寬/高等于specSize粒氧,即MeasureSpec中的測(cè)量尺寸;當(dāng)View的測(cè)量模式為UNSPECIFIED時(shí)节腐,View的測(cè)量寬/高等于該方法的第一個(gè)參數(shù)的值外盯,即getSuggestedMinimumWidth()/getSuggestedMinimumHeight()方法的返回值,這里就以getSuggestedMinimumWidth()方法為例翼雀,getSuggestedMinimumHeight()同理饱苟。

protected int getSuggestedMinimumWidth() {
    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}

這里首先判斷了View是否設(shè)置了背景,如果沒(méi)有設(shè)置背景狼渊,返回值為mMinWidth箱熬,它對(duì)應(yīng)于android:minWidth屬性所指定的值类垦,如果沒(méi)有指定則為0;如果View設(shè)置了背景城须,返回值為mMinWidth和mBackground.getMinimumWidth()兩者的最大值蚤认,getMinimumWidth()方法可以獲取到Drawable的原始寬度,但不是所有的Drawable都有原始寬度糕伐,如果沒(méi)有原始寬度砰琢,獲取到的值就為0(上面這段解釋基本上來(lái)自《Android開(kāi)發(fā)藝術(shù)探索》,目前我對(duì)于Drawable的認(rèn)識(shí)還不夠良瞧,想了解更多的話(huà)自行查閱資料吧)陪汽。
這里也引出了一個(gè)問(wèn)題,當(dāng)View的測(cè)量模式為AT_MOST褥蚯,即LayoutParams指定為wrap_content時(shí)挚冤,View的測(cè)量寬/高等于specSize,而從getChildMeasureSpec()方法的分析中我們也得出此時(shí)specSize的值為parentSize遵岩,即父View的可用空間大小你辣,這會(huì)導(dǎo)致wrap_content產(chǎn)生和match_parent一樣的效果巡通,因此我們?cè)谧远xView時(shí)需要重寫(xiě)onMeasure()方法尘执,解決wrap_content的失效問(wèn)題,具體做法也很簡(jiǎn)單宴凉,就是為wrap_content情況指定一個(gè)默認(rèn)的寬高尺寸誊锭,默認(rèn)尺寸可以根據(jù)需要靈活指定,示例代碼如下:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    // 獲取寬度的測(cè)量模式和測(cè)量尺寸
    int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
    // 獲取高度的測(cè)量模式和測(cè)量尺寸
    int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
    int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);

    // 設(shè)置wrap_content的默認(rèn)寬/高值弥锄,可以根據(jù)需要設(shè)置
    if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT && getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT) {
        setMeasuredDimension(200, 200);
    } else if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT) {
        setMeasuredDimension(200, heightSize);
    } else if (getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT) {
        setMeasuredDimension(widthSize, 200);
    }
}

用一張圖總結(jié)一下單一View的measure流程:

View的measure流程.jpg

3.3.2.ViewGroup的measure流程

ViewGroup的measure流程同樣從measure()方法開(kāi)始丧靡,和View是一樣,這里就不展示了籽暇,之后會(huì)調(diào)用onMeasure()方法温治,但是我們會(huì)發(fā)現(xiàn)ViewGroup中并沒(méi)有重寫(xiě)onMeasure()方法,原因其實(shí)也不難理解戒悠,就是因?yàn)槊總€(gè)ViewGroup的布局方式都不一樣熬荆,無(wú)法得出一個(gè)統(tǒng)一的實(shí)現(xiàn)方式,在自定義ViewGroup時(shí)需要根據(jù)想要得到的布局效果來(lái)重寫(xiě)onMeasure()方法绸狐。雖然ViewGroup沒(méi)有提供onMeasure()方法的實(shí)現(xiàn)方式卤恳,但是提供了一個(gè)measureChildren()方法,從方法名也能猜到是用來(lái)測(cè)量ViewGroup的所有子View的寒矿,我們來(lái)看一下這個(gè)方法突琳。

protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
    final int size = mChildrenCount;
    final View[] children = mChildren;
    for (int i = 0; i < size; ++i) {
        final View child = children[i];
        if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
        }
    }
}

方法內(nèi)部遍歷所有的子View,依次調(diào)用measureChild()方法:

protected void measureChild(View child, int parentWidthMeasureSpec,
                            int parentHeightMeasureSpec) {
    final LayoutParams lp = child.getLayoutParams();

    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight, lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom, lp.height);

    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

measureChild()方法首先調(diào)用此前分析過(guò)的getChildMeasureSpec()方法符相,根據(jù)ViewGroup的MeasureSpec和子View自身的LayoutParams確定出子View的MeasureSpec拆融,然后調(diào)用子View的measure()方法,對(duì)子View進(jìn)行測(cè)量,后面的流程就和單一View的measure流程一樣了冠息。我們?cè)谧远xViewGroup時(shí)可以在onMeasure()方法調(diào)用measureChildren()方法完成對(duì)子View的測(cè)量挪凑。
下面以ViewGroup的子類(lèi)LinearLayout為例,分析一下它的measure流程逛艰,加深一下對(duì)ViewGroup的measure流程的理解躏碳。首先來(lái)看onMeasure()方法:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    if (mOrientation == VERTICAL) {
        measureVertical(widthMeasureSpec, heightMeasureSpec);
    } else {
        measureHorizontal(widthMeasureSpec, heightMeasureSpec);
    }
}

onMeasure()方法中會(huì)判斷LinearLayout的布局方向執(zhí)行相應(yīng)的方法,這里就以豎直方向的measureVertical()為例進(jìn)行分析散怖,水平方向是類(lèi)似的菇绵。

void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
    // 用于記錄豎直方向的總高度
    mTotalLength = 0;
    // ...
    float totalWeight = 0;
    final int count = getVirtualChildCount();
    // ...
    for (int i = 0; i < count; ++i) {
        final View child = getVirtualChildAt(i);
        // ...
        // 對(duì)子View進(jìn)行measure
        measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
                heightMeasureSpec, usedHeight);
        // 獲取子View測(cè)量后的高
        final int childHeight = child.getMeasuredHeight();
        // ...
        final int totalLength = mTotalLength;
        // 每測(cè)量一個(gè)子View,mTotalLength就會(huì)增加镇眷,增加的高度包括子View和高度和豎直方向上的margin
        mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
                lp.bottomMargin + getNextLocationOffset(child));
        // ...
    }

    // ...
    // 計(jì)算豎直方向的padding
    mTotalLength += mPaddingTop + mPaddingBottom;
    int heightSize = mTotalLength;
    heightSize = Math.max(heightSize, getSuggestedMinimumHeight());
    // 完成自身高度的measure
    int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
    heightSize = heightSizeAndState & MEASURED_SIZE_MASK;
    // ...
    // 寬度的measure
    maxWidth += mPaddingLeft + mPaddingRight;
    maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());

    setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
            heightSizeAndState);
    // ...
}

方法很長(zhǎng)咬最,省略了大量代碼,說(shuō)一下簡(jiǎn)單的流程吧欠动,高度上永乌,LinearLayout首先會(huì)遍歷所有子View,調(diào)用measureChildBeforeLayout()方法對(duì)子View進(jìn)行測(cè)量具伍,每測(cè)量一個(gè)子View翅雏,就增加mTotalLength的值,它表示LinearLayout在豎直方向上的總高度人芽,增加的值包括子View的測(cè)量高度和子VIew豎直方向上的margin望几,當(dāng)所有子View測(cè)量完成后,會(huì)計(jì)算LinearLayout自身的padding值萤厅,最后調(diào)用resolveSizeAndState()方法完成對(duì)自身高度的測(cè)量橄抹。寬度上和單一View的測(cè)量類(lèi)似,不需要考慮子View惕味,調(diào)用resolveSizeAndState()完成對(duì)自身寬度的測(cè)量楼誓。方法最后依然是調(diào)用setMeasuredDimension()設(shè)置LinearLayout的測(cè)量寬高。接下來(lái)我們來(lái)看一下LinearLayout測(cè)量自身的方法resolveSizeAndState()名挥,這個(gè)方法是在View中定義的疟羹。

public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
    final int specMode = MeasureSpec.getMode(measureSpec);
    final int specSize = MeasureSpec.getSize(measureSpec);
    final int result;
    switch (specMode) {
        case MeasureSpec.AT_MOST:
            if (specSize < size) {
                // 子View的測(cè)量總高度超過(guò)了LinearLayout可用空間大小
                result = specSize | MEASURED_STATE_TOO_SMALL;
            } else {
                result = size;
            }
            break;
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        case MeasureSpec.UNSPECIFIED:
        default:
            result = size;
    }
    return result | (childMeasuredState & MEASURED_STATE_MASK);
}

可以看出,如果LinearLayout的測(cè)量模式為EXACTLY躺同,那么最終的測(cè)量高度為specSize阁猜,與子View無(wú)關(guān);如果LinearLayout的測(cè)量模式為AT_MOST蹋艺,會(huì)判斷子View的總高度(包括margin剃袍、paddding)是否超過(guò)了LinearLayout豎直方向上的可用空間,如果沒(méi)超過(guò)則最終測(cè)量高度為子View的總高度捎谨,如果超過(guò)了則最終測(cè)量高度為specSize民效,并設(shè)置一個(gè)MEASURED_STATE_TOO_SMALL標(biāo)志憔维。
值得一提的是,LinearLayout的測(cè)量有一種特殊情況畏邢,就是對(duì)于自身的測(cè)量模式為EXACTLY并且子View設(shè)置了layout_weight的情況业扒,這種情況會(huì)在后面重新進(jìn)行一次子View的遍歷和測(cè)量,由于這不是ViewGroup測(cè)量的通用流程舒萎,這里就不細(xì)說(shuō)了程储,感興趣的話(huà)可以查看一下這塊的源碼。
最后用一張圖總結(jié)一下ViewGroup的measure流程臂寝,雖然具體到每個(gè)ViewGroup的measure流程可能會(huì)有所不同章鲤,但是這幾個(gè)步驟是通用的。

ViewGroup的measure流程.jpg

既然ViewGroup和View的measure流程都已經(jīng)分析完了咆贬,我們可以梳理一下一個(gè)頁(yè)面的完整measure流程败徊,首先從ViewRootImpl的performMeasure()方法開(kāi)始對(duì)頂層View——DecorView進(jìn)行測(cè)量,調(diào)用measure()方法掏缎,由于DecorView繼承自FrameLayout皱蹦,可以看做一個(gè)ViewGroup,因此接著會(huì)遍歷DecorVIew的所有子View進(jìn)行測(cè)量眷蜈,如果子View是一個(gè)單一View沪哺,只需要完成自身的測(cè)量,如果子View是一個(gè)ViewGroup端蛆,就又會(huì)重復(fù)上面的步驟凤粗,遍歷該子View下的所有子View進(jìn)行測(cè)量酥泛,之后便是一個(gè)遞歸的過(guò)程丧蘸,最后當(dāng)所有子View的測(cè)量都完成后蒸走,再進(jìn)行DecorVIew自身的測(cè)量。

3.4.補(bǔ)充:MeasureSpec.UNSPECIFIED的應(yīng)用

我們此前介紹UNSPECIFIED模式的時(shí)候基本上是一筆帶過(guò)的,只介紹了該模式下是父View不限制子View大小渤滞,用于系統(tǒng)內(nèi)部,開(kāi)發(fā)中一般很少會(huì)用到因妇,雖然是這樣董朝,我們還是有必要了解一下該模式的常見(jiàn)應(yīng)用場(chǎng)景,可能我們平時(shí)在開(kāi)發(fā)中已經(jīng)接觸過(guò)了腥例,只是沒(méi)有發(fā)現(xiàn)而已辅甥。
ScrollView相信大家都很熟悉了,在使用時(shí)有一個(gè)需要注意的地方燎竖,就是當(dāng)ScrollView的子布局沒(méi)有占滿(mǎn)屏幕高度時(shí)璃弄,它的子View是無(wú)法占滿(mǎn)全屏的,即使設(shè)置了layout_height為match_parent也不管用构回,可能大家都已經(jīng)知道了這個(gè)問(wèn)題夏块,我這里就簡(jiǎn)單展示一下疏咐。布局文件很簡(jiǎn)單,ScrollView嵌套一個(gè)LinearLayout脐供,LinearLayout中有一個(gè)高度為100dp的TextView浑塞。

<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#f00"
        android:orientation="vertical">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="100dp"
            android:text="I can do all things"
            android:textColor="#fff"
            android:textSize="24sp" />
      
    </LinearLayout>
  
</ScrollView>

運(yùn)行效果如下:

可以看出LinearLayout的高度為100dp,并沒(méi)有占滿(mǎn)屏幕政己,但是我們明明設(shè)置了layout_height為match_parent酌壕,其實(shí)不止這樣,即便是layout_height指定了精確數(shù)值(如200dp)也不會(huì)生效歇由。解決方案就是為ScrollView添加android:fillViewport="true"屬性仅孩,運(yùn)行之后發(fā)現(xiàn)LinearLayout可以占滿(mǎn)全屏了。

現(xiàn)在我們從源碼角度分析一下產(chǎn)生這個(gè)問(wèn)題的原因印蓖,看一下ScrollView的onMeasure()方法:
ScrollView的onMeasure方法

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);

    if (!mFillViewport) {
        return;
    }

    final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    if (heightMode == MeasureSpec.UNSPECIFIED) {
        return;
    }

    if (getChildCount() > 0) {
        final View child = getChildAt(0);
        final int widthPadding;
        final int heightPadding;
        final int targetSdkVersion = getContext().getApplicationInfo().targetSdkVersion;
        final FrameLayout.LayoutParams lp = (LayoutParams) child.getLayoutParams();
        if (targetSdkVersion >= VERSION_CODES.M) {
            widthPadding = mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin;
            heightPadding = mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin;
        } else {
            widthPadding = mPaddingLeft + mPaddingRight;
            heightPadding = mPaddingTop + mPaddingBottom;
        }

        final int desiredHeight = getMeasuredHeight() - heightPadding;
        if (child.getMeasuredHeight() < desiredHeight) {
            final int childWidthMeasureSpec = getChildMeasureSpec(
                    widthMeasureSpec, widthPadding, lp.width);
            final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
                    desiredHeight, MeasureSpec.EXACTLY);
            child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        }
    }
}

onMeasure()方法中首先會(huì)判斷mFillViewport的值辽慕,如果為false則直接return,不執(zhí)行后面的邏輯赦肃。從變量名不難猜到這個(gè)mFillViewport就是對(duì)應(yīng)于android:fillViewport屬性溅蛉,默認(rèn)值為false,因此當(dāng)我們沒(méi)有設(shè)置android:fillViewport="true"時(shí)他宛,onMeasure()方法只會(huì)執(zhí)行父類(lèi)的onMeasure()方法船侧。我們先簡(jiǎn)單看一下后面的代碼,首先計(jì)算出ScrollView的可用高度desiredHeight厅各,當(dāng)child.getMeasuredHeight() < desiredHeight镜撩,即子View的測(cè)量高度小于ScrollView的可用高度時(shí),會(huì)將子View高度的測(cè)量模式指定為EXACTLY队塘,測(cè)量尺寸指定為ScrollView的可用高度并進(jìn)行重新測(cè)量袁梗,因此子View的最終測(cè)量高度就是ScrollView的可用高度,對(duì)于上面的例子來(lái)說(shuō)Linearlayout自然就占滿(mǎn)了全屏憔古。
清楚了android:fillViewport="true"屬性為什么可以讓子View占滿(mǎn)全屏后遮怜,我們?cè)賮?lái)分析一下為什么默認(rèn)情況下子View不會(huì)占滿(mǎn)全屏,由于默認(rèn)情況只會(huì)執(zhí)行父類(lèi)的onMeasure()方法鸿市,我們來(lái)看一下ScrollView的父類(lèi)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);
        // ...
        measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
        // ...
    }
    // ...
    setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
            resolveSizeAndState(maxHeight, heightMeasureSpec,
                    childState << MEASURED_HEIGHT_STATE_SHIFT));
    // ...
}

FrameLayout的onMeasure()方法內(nèi)部調(diào)用了measureChildWithMargins()方法來(lái)對(duì)子View進(jìn)行測(cè)量,ScrollView重寫(xiě)了該方法焰情,我們來(lái)看一下:
ScrollView的measureChildWithMargins方法

@Override
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed,
                                       int parentHeightMeasureSpec, int heightUsed) {
    final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                    + widthUsed, lp.width);
    final int usedTotal = mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin +
            heightUsed;
    final int childHeightMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
            Math.max(0, MeasureSpec.getSize(parentHeightMeasureSpec) - usedTotal),
            MeasureSpec.UNSPECIFIED);

    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

可以看出ScrollView在測(cè)量子View時(shí)陌凳,將子VIew高度的測(cè)量模式直接指定為了UNSPECIFIED,還記得我們上面分析過(guò)的LinearLayout的measure過(guò)程嗎内舟,在子View測(cè)量完成后合敦,會(huì)調(diào)用resolveSizeAndState()方法完成自身的測(cè)量,這里再貼一遍代碼谒获。

public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
    final int specMode = MeasureSpec.getMode(measureSpec);
    final int specSize = MeasureSpec.getSize(measureSpec);
    final int result;
    switch (specMode) {
        case MeasureSpec.AT_MOST:
            if (specSize < size) {
                // 子View的測(cè)量總高度超過(guò)了LinearLayout可用空間大小
                result = specSize | MEASURED_STATE_TOO_SMALL;
            } else {
                result = size;
            }
            break;
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        case MeasureSpec.UNSPECIFIED:
        default:
            result = size;
    }
    return result | (childMeasuredState & MEASURED_STATE_MASK);
}

可以看出蛤肌,當(dāng)LinearLayout的測(cè)量模式為UNSPECIFIED時(shí)壁却,LinearLayout的測(cè)量高度為子View的總高度size,因此當(dāng)LinearLayout子View的總高度小于LinearLayout指定的高度時(shí)裸准,LinearLayout的高度不會(huì)生效展东。
看到這里我們清楚了ScrollView子View無(wú)法占滿(mǎn)全屏的原因,也見(jiàn)到了UNSPECIFIED的應(yīng)用場(chǎng)景炒俱,其實(shí)不止ScrollView盐肃,UNSPECIFIED模式在其他的一些可滾動(dòng)的ViewGroup中也有應(yīng)用,比如RecyclerView权悟。和WRAP_CONTENT相比砸王,UNSPECIFIED模式不會(huì)限制View的大小,正是如此峦阁,UNSPECIFIED模式非常適合應(yīng)用到可滾動(dòng)的ViewGroup中谦铃,此時(shí)ViewGroup不必關(guān)心子View的大小是否超出了自身范圍,即時(shí)超出了也可以通過(guò)滾動(dòng)來(lái)查看榔昔。
我們?cè)谧远xView時(shí)該如何處理UNSPECIFIED的情況呢驹闰,這里引用一下每日一問(wèn) 詳細(xì)的描述下自定義 View 測(cè)量時(shí) MesureSpec.UNSPECIFIED中陳小緣大佬的回答,解釋得很好撒会。當(dāng)遇到UNSPECIFIED時(shí)就比較自由了嘹朗,既然尺寸由自己決定,那么可以寫(xiě)死為50诵肛,也可以固定為200屹培,但還是建議結(jié)合實(shí)際需求來(lái)定義,比如ImageView怔檩,它的做法就是:有設(shè)置圖片內(nèi)容(drawable)的話(huà)褪秀,會(huì)直接使用這個(gè)drawable的尺寸,但不會(huì)超過(guò)指定的MaxWidth或MaxHeight珠洗, 沒(méi)有內(nèi)容的話(huà)就是0溜歪;而TextView處理UNSPECIFIED的方式若专,和AT_MOST是一樣的许蓖。

4.layout階段

measure流程的作用是對(duì)View的大小進(jìn)行測(cè)量,而layout的作用就是根據(jù)測(cè)量大小確定View的最終位置调衰,簡(jiǎn)單地說(shuō)就是把View放在哪膊爪。和measure流程類(lèi)似,layout流程同樣分為兩種情況:?jiǎn)我籚iew的layout和ViewGroup的layout嚎莉,ViewGroup的layout流程要復(fù)雜一些米酬,因?yàn)樗粌H要進(jìn)行自身的layout,還要對(duì)所有子View進(jìn)行l(wèi)ayout趋箩,我們先來(lái)看看單一View的layout流程赃额。

4.1.單一View的layout流程

View的layout流程從layout()方法開(kāi)始加派,我們來(lái)看一下這個(gè)方法:

public void layout(int l, int t, int r, int b) {
    // ...
    int oldL = mLeft;
    int oldT = mTop;
    int oldB = mBottom;
    int oldR = mRight;

    boolean changed = isLayoutModeOptical(mParent) ?
            setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

    if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
        onLayout(changed, l, t, r, b);
        // ...
    }
    // ...
}

layout()方法內(nèi)部會(huì)根據(jù)isLayoutModeOptical()的返回值調(diào)用setOpticalFrame()方法或setFrame()方法,isLayoutModeOptical()方法會(huì)判斷LAYOUT_MODE_OPTICAL_BOUNDS標(biāo)志位跳芳,它表示一個(gè)布局模式芍锦,從名稱(chēng)上看應(yīng)該是和布局邊界有關(guān),具體作用我也不是很了解飞盆,不過(guò)默認(rèn)情況下都是沒(méi)有設(shè)置該標(biāo)志位的娄琉。這里可以暫且先不去管它的作用,我們會(huì)發(fā)現(xiàn)setOpticalFrame()方法內(nèi)部最終還是會(huì)調(diào)用setFrame()方法吓歇,因此直接來(lái)看setFrame()方法就可以了孽水。

private boolean setOpticalFrame(int left, int top, int right, int bottom) {
    Insets parentInsets = mParent instanceof View ?
            ((View) mParent).getOpticalInsets() : Insets.NONE;
    Insets childInsets = getOpticalInsets();
    return setFrame(
            left   + parentInsets.left - childInsets.left,
            top    + parentInsets.top  - childInsets.top,
            right  + parentInsets.left + childInsets.right,
            bottom + parentInsets.top  + childInsets.bottom);
}

protected boolean setFrame(int left, int top, int right, int bottom) {
    boolean changed = false
    // ...
    if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
        changed = true;
        // ...
        mLeft = left;
        mTop = top;
        mRight = right;
        mBottom = bottom;
        // ...
    }
    return changed;
}

setFrame()方法首先會(huì)判斷根據(jù)mLeft != left || mRight != right || mTop != top || mBottom != bottom,即View的位置是否發(fā)生了改變城看,如果發(fā)生了改變女气,則返回值為true,反之返回值為false测柠。如果View的位置發(fā)生了改變主卫,會(huì)重新為View的四個(gè)頂點(diǎn)位置賦值,對(duì)應(yīng)四個(gè)成員變量mLeft鹃愤、mTop簇搅、mRight和mBottom,關(guān)于這四個(gè)值我們通過(guò)一個(gè)示意圖就可以很清楚了软吐,圖片摘自GcsSloop大佬的博客瘩将。

首次layout 前這四個(gè)變量都沒(méi)有賦過(guò)值,因此這里setFrame()方法會(huì)返回true凹耙,我們回到layout()方法姿现,changed的值就為true,接下來(lái)會(huì)執(zhí)行onLayout()方法肖抱,我們接著來(lái)看onLayout()方法备典。

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}

View中的onLayout()是一個(gè)空方法,沒(méi)有聲明任何邏輯意述,這是因?yàn)?code>layout()方法已經(jīng)確定了View的四個(gè)頂點(diǎn)的位置提佣,而onLayout()方法是用于ViewGroup確定子View的位置,我們會(huì)再來(lái)分析荤崇。
單一View的layout流程就分析完了拌屏,是不是很簡(jiǎn)單,用一張流程圖總結(jié)一下:

View的layout流程.jpg

4.2.ViewGroup的layout流程

ViewGroup的layout流程同樣從layout()方法開(kāi)始术荤,我們來(lái)看一下:

@Override
public final void layout(int l, int t, int r, int b) {
    // ...
    super.layout(l, t, r, b);
    // ...
}

ViewGroup的layout()方法使用final聲明倚喂,因此子類(lèi)無(wú)法重寫(xiě)該方法。layout()方法中調(diào)用了父類(lèi)即View的layout()方法瓣戚,確定了ViewGroup自身的四個(gè)頂點(diǎn)位置端圈,并調(diào)用onLayout()方法焦读,我們來(lái)看一下ViewGroup的onLayout()方法:

@Override
protected abstract void onLayout(boolean changed, int l, int t, int r, int b);

可以發(fā)現(xiàn)ViewGroup的onLayout()方法是一個(gè)抽象方法,因此當(dāng)我們自定義ViewGroup時(shí)需要重寫(xiě)該方法舱权。ViewGroup沒(méi)有實(shí)現(xiàn)onLayout()方法的原因同樣是因?yàn)椴煌腣iewGroup具有不同的布局方式吨灭,無(wú)法得出一個(gè)統(tǒng)一實(shí)現(xiàn)。在自定義ViewGroup中的onLayout()方法中我們需要遍歷所有的子View刑巧,根據(jù)需要的布局方式調(diào)用子View的layout()方法確定子View的位置喧兄。
這樣說(shuō)可能不是很清楚,接下來(lái)我們同樣以LinearLayout為例啊楚,看一下它的layout流程吠冤,加深我們對(duì)ViewGroup的 layout流程的理解。由于ViewGroup的layout()方法無(wú)法被子類(lèi)重寫(xiě)恭理,因此我們直接來(lái)看LinearLayout的onLayout()方法:

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    if (mOrientation == VERTICAL) {
        layoutVertical(l, t, r, b);
    } else {
        layoutHorizontal(l, t, r, b);
    }
}

和LinearLayout的onMeasure()方法類(lèi)似拯辙,onlayout()方法中同樣會(huì)根據(jù)LinearLayout的布局方向執(zhí)行相應(yīng)的布局方法,我們以豎直方向布局為例颜价,分析一下layoutVertical()方法涯保,水平方向同理。

void layoutVertical(int left, int top, int right, int bottom) {
    // ...
    // childTop和childLeft記錄子View的左上位置
    int childTop;
    int childLeft;
    // ...
    final int count = getVirtualChildCount();
    // ...
    for (int i = 0; i < count; i++) {
        final View child = getVirtualChildAt(i);
        // ...
        final int childWidth = child.getMeasuredWidth();
        final int childHeight = child.getMeasuredHeight();

        final LinearLayout.LayoutParams lp =
                (LinearLayout.LayoutParams) child.getLayoutParams();
        // ...

        childTop += lp.topMargin;
        setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                childWidth, childHeight);
        // 每完成一個(gè)子View的layout周伦,childTop就會(huì)增加
        childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);

        i += getChildrenSkipCount(child, i);
    }
}

layoutVertical()方法內(nèi)部遍歷了LinearLayout的所有子View夕春,每次遍歷都調(diào)用setChildFrame()方法,我們來(lái)看一下這個(gè)方法:

private void setChildFrame(View child, int left, int top, int width, int height) {
    child.layout(left, top, left + width, top + height);
}

可以看到setChildFrame()方法其實(shí)就是調(diào)用了子View的layout()方法专挪,完成子View的布局及志。setChildFrame()方法調(diào)用完成后,會(huì)增加childTop的值寨腔,它對(duì)應(yīng)子View的mTop速侈,繼續(xù)下一個(gè)子View的layout,還是比較好理解的迫卢,豎直方向的LinearLayout的子View是一個(gè)接一個(gè)往下放置的倚搬。
總結(jié)一下ViewGroup的layout流程,首先會(huì)調(diào)用layout()方法確定自身的位置乾蛤,之后調(diào)用onLayout()方法每界,遍歷所有的子View,根據(jù)ViewGroup的布局特性依次確定出每個(gè)子View的位置幻捏。流程圖如下所示:

ViewGroup的layout流程.jpg

ViewGroup的layout流程和measure流程還是很相似的盆犁,不過(guò)在順序上有一些區(qū)別,measure是先遍歷子View對(duì)子View進(jìn)行測(cè)量篡九,最后根據(jù)子View的測(cè)量結(jié)果對(duì)ViewGroup自身進(jìn)行測(cè)量;而layout是先確定ViewGroup的位置醋奠,再遍歷子View確定子View的位置榛臼。
最后我們來(lái)梳理一下整個(gè)頁(yè)面的layout過(guò)程伊佃,前面也提到過(guò),頁(yè)面的layout流程從ViewRootImpl的performLayout()方法開(kāi)始沛善。

private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
                           int desiredWindowHeight) {
    // ...
    final View host = mView;
    // ...
    host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
    // ...
}

方法內(nèi)部首先將mView賦值給host航揉,這里的mView我此前也提到過(guò),就是在handleResumeActivity()方法中調(diào)用wm.addView(decor,l)時(shí)傳過(guò)來(lái)的DecorView金刁,因此后面調(diào)用host的layout()方法實(shí)際上就是調(diào)用DecorView的layout()方法帅涂,從這里就開(kāi)始最頂層VIew的layout,而我們知道DecorView繼承自FrameLayout尤蛮,因此這里就是執(zhí)行ViewGroup的layout()方法媳友,之后的步驟我們就清楚了,首先確定出DecorView的位置产捞,然后調(diào)用onlayout()方法遍歷DecorView的子View醇锚,依次調(diào)用子View的layout()方法來(lái)確定子View的位置,如果子View是一個(gè)ViewGroup坯临,還需要接著遍歷子View的所有子View進(jìn)行l(wèi)ayout焊唬。

4.3.getMeasureWidth/getMeasureHeight和getWidth/getHeight的區(qū)別

我在View的measure流程中提到過(guò)measure完成后可以通過(guò)getMeasuredWidth()getMeasuredHeight()方法獲取View的測(cè)量寬高,但是這個(gè)測(cè)量寬高并不等于View的最終實(shí)際寬高看靠,現(xiàn)在就來(lái)解釋一下這個(gè)問(wèn)題赶促。
我們知道View的寬高可以通過(guò)getWidth()getHeight()方法來(lái)獲得,首先來(lái)看一下這幾個(gè)方法的定義挟炬,這里就只對(duì)比getMeasureWidth()getWidth()方法芳杏,getMeasuredHeight()方法和getHeight()方法的區(qū)別同理。

public final int getMeasuredWidth() {
    return mMeasuredWidth & MEASURED_SIZE_MASK;
}

public final int getMeasuredHeight() {
    return mMeasuredHeight & MEASURED_SIZE_MASK;
}

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

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

getMeasuredWidth()獲取到是mMeasuredWidth的低30位辟宗,而mMeasuredWidth是在onMeasure()方法中通過(guò)setMeasuredDimension()賦值的爵赵。getWidth()獲取到的是mRight - mLeft,這兩個(gè)都是在layout()方法中通過(guò)setFrame()賦值的泊脐。這樣看上去兩者獲取到的值好像沒(méi)有什么聯(lián)系空幻,我們可以再回頭看一下layout()方法傳入的left和right參數(shù)的值,就以剛介紹過(guò)的DecorView為例吧容客,left參數(shù)傳入了0秕铛,right參數(shù)傳入了host.getMeasuredWidth(),因此最后計(jì)算出的mRight - mLeft就是getMeasureWidth()方法的返回值缩挑。其實(shí)不止DecorView但两,所有的View在默認(rèn)情況下getWidth()的值和getMeasureWidth()的值都是一樣的,需要注意這里強(qiáng)調(diào)的是默認(rèn)情況下供置,那么什么情況下這兩個(gè)方法的返回值不一樣呢谨湘?
我們可以重寫(xiě)View的layout()方法,就像下面這樣:

@Override
public void layout(int l, int t, int r, int b) {
    super.layout(l, t, r + 100, b + 100);
}

這樣就會(huì)導(dǎo)致getWidth()/getHeight()獲取到的值比getMeasureWidth()/getMeasureHeight()獲取到的值大100px,雖然一般情況下不會(huì)這樣做紧阔,只是為了讓我們更加清楚getWidth()/getHeight()getMeasureWidth()/getMeasureHeight()的區(qū)別坊罢。

5.draw階段

通過(guò)前面的measure和layout兩個(gè)流程,已經(jīng)確定出了View的大小和位置擅耽,接下里就要把View顯示出來(lái)了活孩,draw的作用是就將View繪制到屏幕上。相比于前兩個(gè)流程乖仇,View的繪制流程是最簡(jiǎn)單的憾儒,因?yàn)樵创a的邏輯很少,基本上都要靠我們自己去定義如何繪制乃沙。同樣地起趾,我們分兩種情況進(jìn)行分析,包括單一View的繪制和ViewGroup的繪制崔涂。

5.1.單一View的draw流程

View的繪制流程從draw()方法開(kāi)始阳掐,我們來(lái)看一下這個(gè)方法:

public void draw(Canvas canvas) {
    // ...
    if (!dirtyOpaque) {
        // 第一步、繪制背景
        drawBackground(canvas);
    }
    // ...
    if (!verticalEdges && !horizontalEdges) {
        // 第二步冷蚂、繪制自身內(nèi)容
        if (!dirtyOpaque) onDraw(canvas);
        // 第三步缭保、繪制子View
        dispatchDraw(canvas);
        // ...
        // 第四布、繪制裝飾蝙茶,包括滾動(dòng)條和前景
        onDrawForeground(canvas);
        // ...
        return;
    }
    // ...
}

這里精簡(jiǎn)了一下源碼艺骂,可以直觀地看出View的draw()方法分為四個(gè)步驟(源碼中提到了6個(gè)步驟,另外兩個(gè)可以跳過(guò)的隆夯,這里就不列入了):

  • 調(diào)用drawBackground()方法繪制背景
  • 調(diào)用onDraw()方法繪制自身內(nèi)容
  • 調(diào)用dispatchDraw()方法繪制子View
  • 調(diào)用onDrawForeground()方法繪制裝飾钳恕,包括滾動(dòng)條和前景

下面我們就來(lái)分別看一下這四個(gè)方法。
View的drawBackground方法

private void drawBackground(Canvas canvas) {
    // 獲取背景Drawable
    final Drawable background = mBackground;
    if (background == null) {
        return;
    }
    // 根據(jù)layout流程確定出的四個(gè)頂點(diǎn)位置設(shè)置背景的邊界
    setBackgroundBounds();

    // ...

    // 獲取水平和豎直方向上的滑動(dòng)距離
    final int scrollX = mScrollX;
    final int scrollY = mScrollY;
    // 繪制背景
    if ((scrollX | scrollY) == 0) {
        background.draw(canvas);
    } else {
        canvas.translate(scrollX, scrollY);
        background.draw(canvas);
        canvas.translate(-scrollX, -scrollY);
    }
}

drawBackground()方法首先會(huì)獲取背景Drawable蹄衷,如果沒(méi)有設(shè)置背景則直接返回忧额;如果設(shè)置了背景就調(diào)用Drawable的draw()方法完成背景的繪制,代碼的邏輯還是比較簡(jiǎn)單的愧口,我就不詳細(xì)說(shuō)了睦番。
View的onDraw方法

protected void onDraw(Canvas canvas) {
}

onDraw()方法可以說(shuō)是我們?cè)谧远xView中最熟悉的,View的onDraw()是一個(gè)空方法耍属,需要子類(lèi)自己決定如何進(jìn)行繪制托嚣。
View的dispatchDraw方法

protected void dispatchDraw(Canvas canvas) {

}

View的dispatchDraw()方法同樣是一個(gè)空方法,它的作用是對(duì)子View進(jìn)行繪制厚骗,因此單一View自然無(wú)需實(shí)現(xiàn)該方法示启,我們稍后會(huì)看一下ViewGroup中是如何實(shí)現(xiàn)該方法的。
View的onDrawForeground方法

public void onDrawForeground(Canvas canvas) {
    // 繪制滾動(dòng)條
    onDrawScrollIndicators(canvas);
    onDrawScrollBars(canvas);

    // 繪制前景
    final Drawable foreground = mForegroundInfo != null ? mForegroundInfo.mDrawable : null;
    if (foreground != null) {
        if (mForegroundInfo.mBoundsChanged) {
            mForegroundInfo.mBoundsChanged = false;
            final Rect selfBounds = mForegroundInfo.mSelfBounds;
            final Rect overlayBounds = mForegroundInfo.mOverlayBounds;

            if (mForegroundInfo.mInsidePadding) {
                selfBounds.set(0, 0, getWidth(), getHeight());
            } else {
                selfBounds.set(getPaddingLeft(), getPaddingTop(),
                        getWidth() - getPaddingRight(), getHeight() - getPaddingBottom());
            }

            final int ld = getLayoutDirection();
            Gravity.apply(mForegroundInfo.mGravity, foreground.getIntrinsicWidth(),
                    foreground.getIntrinsicHeight(), selfBounds, overlayBounds, ld);
            foreground.setBounds(overlayBounds);
        }

        foreground.draw(canvas);
    }
}

onDrawForeground()方法用于繪制View的一些裝飾领舰,包括滾動(dòng)條和前景夫嗓,我們一般很少接觸到該方法迟螺,就不具體分析了。
用一張流程圖總結(jié)一下單一View的draw流程:

View的draw流程.jpg

雖然View的繪制流程可以分為以上四步啤月,但是我們?cè)谧远xView中只需要重寫(xiě)onDraw()方法煮仇,按需要進(jìn)行繪制就可以了劳跃。

5.2.ViewGroup的draw流程

ViewGroup的繪制同樣從draw()方法開(kāi)始谎仲,也可分為和View相同的四個(gè)步驟,這里要重點(diǎn)分析一下第三步調(diào)用的dispatchDraw()方法刨仑,ViewGroup重寫(xiě)了該方法郑诺。其他三個(gè)步驟和View是一樣的,這里就不再分析了杉武。

@Override
protected void dispatchDraw(Canvas canvas) {
    // ...
    final int childrenCount = mChildrenCount;
    final View[] children = mChildren;
    // ...
    // 遍歷子View
    for (int i = 0; i < childrenCount; i++) {
        // ...
        final View transientChild = mTransientViews.get(transientIndex);
        if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
                transientChild.getAnimation() != null) {
            // 繪制子View
            more |= drawChild(canvas, transientChild, drawingTime);
        }
        transientIndex++;
        if (transientIndex >= transientCount) {
            transientIndex = -1;
        }
        // ...
    }
    // ...
}

protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
    return child.draw(canvas, this, drawingTime);
}

dispatchDraw()方法內(nèi)部主要做的就是遍歷所有的子View辙诞,依次調(diào)用drawChild方法,drawChild方法內(nèi)部又會(huì)調(diào)用子View的draw()方法轻抱,注意飞涂,這里調(diào)用的draw()方法并不是此前分析過(guò)的那個(gè),它有三個(gè)參數(shù)祈搜。

boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
    // ...
    if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
        mPrivateFlags &= ~PFLAG_DIRTY_MASK;
        dispatchDraw(canvas);
    } else {
        draw(canvas);
    }
    // ...
    return more;
}

這里省略了大量代碼较店,可以看出該方法內(nèi)部會(huì)根據(jù)條件執(zhí)行一個(gè)參數(shù)的draw()方法(執(zhí)行的條件我后面會(huì)分析),后面的流程就和單一View的繪制流程相同了容燕。
總結(jié)一下ViewGroup的draw流程梁呈,整體步驟和單一View的draw流程是一樣的,不同的是ViewGroup重寫(xiě)了dispatchDraw()方法蘸秘,在內(nèi)部遍歷子View并完成子View的繪制官卡。

ViewGroup的draw流程.jpg

最后還是來(lái)梳理一下整個(gè)頁(yè)面的draw流程,從ViewRootImpl的performDraw()方法開(kāi)始:

private void performDraw() {
    // ...
    boolean canUseAsync = draw(fullRedrawNeeded);
    // ...
}

private boolean draw(boolean fullRedrawNeeded) {
    // ...
    if (mAttachInfo.mThreadedRenderer != null && mAttachInfo.mThreadedRenderer.isEnabled()) {
        // ...
        // 開(kāi)啟了硬件加速
        mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this, callback);
    } else {
        // ...
        // 關(guān)閉了硬件加速
        if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset,
                scalingRequired, dirty, surfaceInsets)) {
            return false;
        }
    }
    // ...
    return useAsyncReport;
}

performDraw()方法內(nèi)部又會(huì)調(diào)用draw()方法醋虏,在draw()方法中會(huì)根據(jù)是否開(kāi)啟了硬件加速執(zhí)行相應(yīng)的邏輯寻咒,硬件加速就是通過(guò)引入GPU來(lái)提高繪制和界面刷新的效率,不過(guò)也有可能導(dǎo)致自定義View出現(xiàn)問(wèn)題颈嚼,在API 13(Android 4.0)及以上版本中毛秘,硬件加速是默認(rèn)開(kāi)啟的,我們可以手動(dòng)關(guān)閉硬件加速粘舟。關(guān)于硬件加速我也了解得不多熔脂,這里就不多提了,感興趣的話(huà)可以查閱一下相關(guān)資料柑肴。下面我們就分別看一下開(kāi)啟和關(guān)閉硬件加速的情況下都是如何完成頁(yè)面繪制的吧霞揉。

  • 關(guān)閉硬件加速

關(guān)閉硬件加速的情況下會(huì)執(zhí)行drawSoftware()方法,我們來(lái)看一下這個(gè)方法:

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

方法內(nèi)部會(huì)調(diào)用mView的draw()方法晰骑,這里的mView是DecorView适秩,前面已經(jīng)分析過(guò)了绊序,因此現(xiàn)在進(jìn)入了DecorView的繪制流程践图,接下來(lái)就和ViewGroup的繪制流程一樣了选调,即遍歷DecorView的所有子View,完成子View的繪制上陕,如果子View是一個(gè)ViewGroup則重復(fù)該過(guò)程扬跋,直到所有的子View都繪制完成阶捆。

  • 開(kāi)啟硬件加速

開(kāi)啟硬件加速的情況下會(huì)執(zhí)行mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this, callback),mThreadedRenderer的類(lèi)型為ThreadedRenderer钦听,我們來(lái)看一下ThreadedRenderer的draw()方法:

void draw(View view, AttachInfo attachInfo, DrawCallbacks callbacks,
          FrameDrawingCallback frameDrawingCallback) {
    // ...
    updateRootDisplayList(view, callbacks);
    // ...
}

private void updateRootDisplayList(View view, DrawCallbacks callbacks) {
    // ...
    updateViewTreeDisplayList(view);
    // ...
}

private void updateViewTreeDisplayList(View view) {
    view.mPrivateFlags |= View.PFLAG_DRAWN;
    view.mRecreateDisplayList = (view.mPrivateFlags & View.PFLAG_INVALIDATED)
            == View.PFLAG_INVALIDATED;
    view.mPrivateFlags &= ~View.PFLAG_INVALIDATED;
    view.updateDisplayListIfDirty();
    view.mRecreateDisplayList = false;
}

經(jīng)過(guò)一系列調(diào)用最終會(huì)調(diào)用updateViewTreeDisplayList()方法洒试,這里傳入的view為DecorView,方法內(nèi)部會(huì)根據(jù)view是否設(shè)置了PFLAG_INVALIDATED標(biāo)志位來(lái)給成員變量mRecreateDisplayList賦值朴上,由于DecorView沒(méi)有設(shè)置該標(biāo)志位垒棋,因此mRecreateDisplayList的值為false。接下來(lái)會(huì)調(diào)用updateDisplayListIfDirty()方法痪宰,它定義在View中叼架,我們來(lái)看一下:

public RenderNode updateDisplayListIfDirty() {
    final RenderNode renderNode = mRenderNode;
    // ...
    if ((mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == 0
            || !renderNode.isValid()
            || (mRecreateDisplayList)) {
        if (renderNode.isValid()
                && !mRecreateDisplayList) {
            // 不需要重新進(jìn)行繪制
            mPrivateFlags |= PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID;
            mPrivateFlags &= ~PFLAG_DIRTY_MASK;
            dispatchGetDisplayList();

            return renderNode;
        }

        // 需要重新進(jìn)行繪制
        mRecreateDisplayList = true;

        try {
            // ...
            if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
                // 如果設(shè)置了PFLAG_SKIP_DRAW標(biāo)志位,執(zhí)行dispatchDraw()方法
                DecorView(canvas);
                drawAutofilledHighlight(canvas);
                if (mOverlay != null && !mOverlay.isEmpty()) {
                    mOverlay.getOverlayView().draw(canvas);
                }
                if (debugDraw()) {
                    debugDrawFocus(canvas);
                }
            } else {
                // 沒(méi)有設(shè)置PFLAG_SKIP_DRAW標(biāo)志位衣撬,執(zhí)行draw()方法
                draw(canvas);
            }
            // ...
        } finally {
            renderNode.end(canvas);
            setDisplayListProperties(renderNode);
        }
    } else {
        mPrivateFlags |= PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID;
        mPrivateFlags &= ~PFLAG_DIRTY_MASK;
    }
    return renderNode;
}

我們先來(lái)看一下第一個(gè)if判斷的幾個(gè)條件:

  • mPrivateFlags & PFLAG_DRAWING_CACHE_VALID == 0

由于DecorView沒(méi)有設(shè)置PFLAG_DRAWING_CACHE_VALID標(biāo)志位乖订,因此該條件滿(mǎn)足。

  • !renderNode.isValid()

isValid()方法字面意思上是判斷renderNode是否有效淮韭,那么什么時(shí)候是有效的呢垢粮?我們會(huì)發(fā)現(xiàn)updateDisplayListIfDirty()方法最后調(diào)用了renderNode.end(canvas),點(diǎn)進(jìn)這個(gè)end()方法看一下:

/**
 * Ends the recording for this display list. A display list cannot be
 * replayed if recording is not finished. Calling this method marks
 * the display list valid and {@link #isValid()} will return true.
 *
 * @see #start(int, int)
 * @see #isValid()
 */
public void end(DisplayListCanvas canvas) {
    long displayList = canvas.finishRecording();
    nSetDisplayList(mNativeRenderNode, displayList);
    canvas.recycle();
}

從注釋Calling this method marks the display list valid and isValid() will return true中可以看出當(dāng)調(diào)用了end()方法后靠粪,isValid()會(huì)返回true蜡吧,由于此時(shí)是頁(yè)面首次繪制,還沒(méi)有調(diào)用過(guò)end()方法占键,因此isValid()返回false昔善,!renderNode.isValid()為true,該條件滿(mǎn)足畔乙。

  • mRecreateDisplayList

上面也說(shuō)過(guò)了君仆,由于設(shè)置PFLAG_INVALIDATED標(biāo)志位,此時(shí)DecorView的mRecreateDisplayList值為false牲距,該條件不滿(mǎn)足返咱。
由于滿(mǎn)足了兩個(gè)條件,因此會(huì)進(jìn)入到第一個(gè)if判斷中牍鞠,接下來(lái)又是一個(gè)if判斷咖摹,判斷條件是renderNode.isValid() && !mRecreateDisplayList ,根據(jù)上面的分析难述,該條件不滿(mǎn)足萤晴,因此不會(huì)執(zhí)行if中的邏輯吐句。接下來(lái)會(huì)判斷是否設(shè)置了PFLAG_SKIP_DRAW標(biāo)志位,關(guān)于這個(gè)標(biāo)志位的作用我后面會(huì)分析店读,這里先記住ViewGroup默認(rèn)情況下都會(huì)設(shè)置這個(gè)標(biāo)志位嗦枢,由于DecorView就是一個(gè)ViewGroup,會(huì)設(shè)置該標(biāo)志位屯断,因此會(huì)執(zhí)行dispatchDraw()方法文虏,遍歷所有子View,完成對(duì)子View的繪制裹纳,如果子View是一個(gè)ViewGroup則接著遍歷下面的子View择葡,直到所有子View都完成繪制紧武。

5.3.ViewGroup的draw()方法調(diào)用問(wèn)題

首先介紹幾個(gè)Android中常見(jiàn)的位運(yùn)算剃氧,有助于我們更好地理解源碼:

a | b:為a添加標(biāo)志位b
(a & b) != 0:判斷a是否有標(biāo)志位b
a & ~b:為a清除標(biāo)志位b
a^b:取出a與b的不同部分

感嘆一下,位運(yùn)算在Android中還是很常見(jiàn)的阻星,尤其是在View的源碼中朋鞍,熟悉上面這個(gè)幾個(gè)位運(yùn)算操作對(duì)我們閱讀源碼還是有很大幫助的。
下面進(jìn)入正題妥箕,當(dāng)我們的自定義View繼承自ViewGroup時(shí)會(huì)遇到一個(gè)問(wèn)題滥酥,默認(rèn)情況下draw()方法和onDraw()方法都不會(huì)被調(diào)用,只會(huì)調(diào)用了dispatchDraw()方法畦幢,可以自己嘗試一下坎吻,我這里就不展示了。我們下面就來(lái)分析一下原因宇葱,首先來(lái)看上面分析過(guò)的三個(gè)參數(shù)的draw()方法:

boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
    // ...
    if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
        mPrivateFlags &= ~PFLAG_DIRTY_MASK;
        dispatchDraw(canvas);
    } else {
        draw(canvas);
    }
    // ...
    return more;
}

這個(gè)方法是在父View遍歷子VIew依次調(diào)用drawChild()方法后被調(diào)用的瘦真,可以很明顯地看出當(dāng)滿(mǎn)足(mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW條件時(shí),執(zhí)行dispatchDraw(canvas)方法黍瞧,不滿(mǎn)足條件就執(zhí)行一個(gè)參數(shù)的draw()方法诸尽,進(jìn)而執(zhí)行onDraw()方法。mPrivateFlags是View中定義的一個(gè)全局變量印颤,用于存儲(chǔ)各種標(biāo)志位您机,上面的條件就是判斷mPrivateFlags是否設(shè)置了PFLAG_SKIP_DRAW標(biāo)志位。既然ViewGroup默認(rèn)情況下不會(huì)執(zhí)行draw()方法年局,那么肯定是設(shè)置了PFLAG_SKIP_DRAW標(biāo)志位际看,是在什么時(shí)候設(shè)置的呢?我們發(fā)現(xiàn)在ViewGroup的構(gòu)造方法中調(diào)用了initViewGroup()方法:

public ViewGroup(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    super(context, attrs, defStyleAttr, defStyleRes);

    initViewGroup();
    initFromAttributes(context, attrs, defStyleAttr, defStyleRes);
}

接著來(lái)看initViewGroup()方法:

private void initViewGroup() {
    // ViewGroup doesn't draw by default
    if (!debugDraw()) {
        setFlags(WILL_NOT_DRAW, DRAW_MASK);
    }
    // ...
}

從注釋ViewGroup doesn't draw by default中也能看出ViewGroup默認(rèn)情況下的確不會(huì)調(diào)用draw()方法矢否,在initViewGroup()方法內(nèi)部執(zhí)行了setFlags(WILL_NOT_DRAW, DRAW_MASK)仲闽,從方法名可以看出是設(shè)置了一個(gè)標(biāo)志位,我們接下來(lái)看一下setFlags()方法:

void setFlags(int flags, int mask) {
    // ...
    int old = mViewFlags;
    // 設(shè)置標(biāo)志位
    mViewFlags = (mViewFlags & ~mask) | (flags & mask);
    // 判斷標(biāo)志位是否改變
    int changed = mViewFlags ^ old;
    if (changed == 0) {
        return;
    }
    int privateFlags = mPrivateFlags;
    // ...
    if ((changed & DRAW_MASK) != 0) {
        // 設(shè)置了DRAW_MASK模式的標(biāo)志位
        if ((mViewFlags & WILL_NOT_DRAW) != 0) {
            // 設(shè)置了WILL_NOT_DRAW標(biāo)志位
            if (mBackground != null
                    || mDefaultFocusHighlight != null
                    || (mForegroundInfo != null && mForegroundInfo.mDrawable != null)) {
                // 如果設(shè)置了背景兴喂、焦點(diǎn)高亮背景或者前景蔼囊,就移除PFLAG_SKIP_DRAW標(biāo)志位
                mPrivateFlags &= ~PFLAG_SKIP_DRAW;
            } else {
                // 如果沒(méi)有設(shè)置背景焚志、焦點(diǎn)高亮背景或者前景,就設(shè)置PFLAG_SKIP_DRAW標(biāo)志位
                mPrivateFlags |= PFLAG_SKIP_DRAW;
            }
        } else {
            // 沒(méi)有設(shè)置WILL_NOT_DRAW標(biāo)志位畏鼓,移除PFLAG_SKIP_DRAW標(biāo)志位
            mPrivateFlags &= ~PFLAG_SKIP_DRAW;
        }
        requestLayout();
        invalidate(true);
    }
    // ...
}

setFlags()方法有兩個(gè)參數(shù):flags和mask酱酬,flags就是要設(shè)置的標(biāo)志位,mask表示標(biāo)志位的位置云矫,mPrivateFlags和mask進(jìn)行按位與運(yùn)算可以得到該mask對(duì)應(yīng)的標(biāo)志位膳沽,舉個(gè)例子,執(zhí)行了setFlags(WILL_NOT_DRAW, DRAW_MASK)后让禀,通過(guò)mPrivateFlags & DRAW_MASK就可以得到WILL_NOT_DRAW這個(gè)標(biāo)志位挑社。這里省略了大量代碼,只保留了和DRAW_MASK相關(guān)的部分巡揍,其實(shí)View的可見(jiàn)狀態(tài)VISIBLE痛阻、INVISIBLE和GONE也是通過(guò)標(biāo)志位來(lái)實(shí)現(xiàn)的,感興趣的話(huà)可以看一看腮敌≮宓保可以看出,當(dāng)View設(shè)置了WILL_NOT_DRAW標(biāo)志位糜工,并且沒(méi)有設(shè)置背景弊添、焦點(diǎn)高亮背景或者前景(后面統(tǒng)稱(chēng)為背景)的情況下,會(huì)設(shè)置PFLAG_SKIP_DRAW標(biāo)志位捌木,由于ViewGroup默認(rèn)情況下是沒(méi)有設(shè)置背景的油坝,因此會(huì)設(shè)置PFLAG_SKIP_DRAW標(biāo)志位,不會(huì)執(zhí)行draw()方法刨裆,當(dāng)然也不會(huì)執(zhí)行onDraw()方法澈圈。
如果想讓ViewGroup的draw()方法被執(zhí)行要怎么做呢?從上面的分析中也能看出崔拥,只要ViewGroup移除了WILL_NOT_DRAW標(biāo)志位或者設(shè)置了背景极舔,就會(huì)移除PFLAG_SKIP_DRAW標(biāo)志位,使得draw()方法被調(diào)用链瓦,下面我們就看一下具體該怎么做拆魏。

  • 移除WILL_NOT_DRAW標(biāo)志位

View中有一個(gè)setWillNotDraw()方法,我們來(lái)看一下:

public void setWillNotDraw(boolean willNotDraw) {
    setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}

setWillNotDraw()方法內(nèi)部會(huì)根據(jù)傳入的willNotDraw參數(shù)調(diào)用setFlags()方法來(lái)設(shè)置或移除WILL_NOT_DRAW標(biāo)志位慈俯,通過(guò)調(diào)用setWillNotDraw(false) 就可以移除WILL_NOT_DRAW標(biāo)志位渤刃,使得ViewGroup的draw()方法得到調(diào)用。

  • 為ViewGroup設(shè)置背景(包括背景贴膘、焦點(diǎn)高亮背景和前景)

這里就以設(shè)置背景的setBackgroundDrawable()方法為例分析卖子,設(shè)置焦點(diǎn)高亮背景(對(duì)應(yīng)setDefaultFocusHighlight()方法)和設(shè)置前景(對(duì)應(yīng)setForeground()方法)類(lèi)似。

public void setBackgroundDrawable(Drawable background) {
    // ...
    if (background != null) {
        // ...
        // 設(shè)置背景刑峡,移除PFLAG_SKIP_DRAW標(biāo)志位
        if ((mPrivateFlags & PFLAG_SKIP_DRAW) != 0) {
            mPrivateFlags &= ~PFLAG_SKIP_DRAW;
            requestLayout = true;
        }
    } else {
        mBackground = null;
        // 取消背景洋闽,設(shè)置PFLAG_SKIP_DRAW標(biāo)志位
        if ((mViewFlags & WILL_NOT_DRAW) != 0
                && (mDefaultFocusHighlight == null)
                && (mForegroundInfo == null || mForegroundInfo.mDrawable == null)) {
            mPrivateFlags |= PFLAG_SKIP_DRAW;
        }
        // ...
    }
    // ...
}

當(dāng)設(shè)置了背景后玄柠,mPrivateFlags會(huì)移除PFLAG_SKIP_DRAW標(biāo)志位,因此可以通過(guò)設(shè)置背景的方式來(lái)使得ViewGroup的draw()方法得到執(zhí)行诫舅。
通過(guò)以上兩種方式就可以調(diào)用的ViewGroup的draw()方法了羽利,從而使得onDraw()方法也會(huì)被調(diào)用。在開(kāi)發(fā)中我們還是要考慮實(shí)際需求刊懈,因?yàn)閂iewGroup本身只是一個(gè)容器这弧,一般情況下是不需要繪制自身內(nèi)容的,默認(rèn)情況設(shè)置了PFLAG_SKIP_DRAW標(biāo)志位也是出于系統(tǒng)優(yōu)化的考慮虚汛,如果需要在onDraw()中繪制內(nèi)容時(shí)再通過(guò)以上兩種方式移除PFLAG_SKIP_DRAW標(biāo)志位匾浪,或是直接在dispatchDraw()方法中進(jìn)行繪制都可以。

6.總結(jié)

這篇文章其實(shí)算是知識(shí)點(diǎn)總結(jié)卷哩,篇幅很長(zhǎng)蛋辈,很多地方我都想盡可能涵蓋多一些知識(shí)點(diǎn)而不是直接一筆帶過(guò),前前后后整理了差不多一個(gè)月時(shí)間吧殉疼,期間閱讀了很多優(yōu)秀的文章梯浪,對(duì)于我自己來(lái)說(shuō)收獲還是很大的,讓我更加系統(tǒng)地認(rèn)識(shí)了View的工作原理瓢娜,源碼的閱讀能力也有了一定的提升。自定義View這塊對(duì)于我來(lái)說(shuō)一直是一塊難啃的骨頭礼预,了解View的工作原理算是打好了堅(jiān)實(shí)的基礎(chǔ)眠砾,紙上得來(lái)終覺(jué)淺,絕知此事要躬行托酸,想要真正要提高自己的自定義View水平還是要親自寫(xiě)幾個(gè)實(shí)例來(lái)練習(xí)褒颈。
由于自身水平的原因,對(duì)于文章中分析得不正確的地方励堡,歡迎大家交流指出谷丸。

7.參考文章

《Android開(kāi)發(fā)藝術(shù)探索》
Android:一篇文章帶你完全梳理自定義View工作流程!
死磕Android_View工作原理你需要知道的一切

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末应结,一起剝皮案震驚了整個(gè)濱河市刨疼,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌鹅龄,老刑警劉巖揩慕,帶你破解...
    沈念sama閱讀 206,968評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異扮休,居然都是意外死亡迎卤,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)玷坠,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)蜗搔,“玉大人劲藐,你說(shuō)我怎么就攤上這事≌疗啵” “怎么了瘩燥?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,220評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)不同。 經(jīng)常有香客問(wèn)我厉膀,道長(zhǎng),這世上最難降的妖魔是什么二拐? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,416評(píng)論 1 279
  • 正文 為了忘掉前任服鹅,我火速辦了婚禮,結(jié)果婚禮上百新,老公的妹妹穿的比我還像新娘企软。我一直安慰自己,他們只是感情好饭望,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布仗哨。 她就那樣靜靜地躺著,像睡著了一般铅辞。 火紅的嫁衣襯著肌膚如雪厌漂。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,144評(píng)論 1 285
  • 那天斟珊,我揣著相機(jī)與錄音苇倡,去河邊找鬼。 笑死囤踩,一個(gè)胖子當(dāng)著我的面吹牛旨椒,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播堵漱,決...
    沈念sama閱讀 38,432評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼综慎,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了勤庐?” 一聲冷哼從身側(cè)響起示惊,我...
    開(kāi)封第一講書(shū)人閱讀 37,088評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎埃元,沒(méi)想到半個(gè)月后涝涤,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,586評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡岛杀,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評(píng)論 2 325
  • 正文 我和宋清朗相戀三年阔拳,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,137評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡糊肠,死狀恐怖辨宠,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情货裹,我是刑警寧澤嗤形,帶...
    沈念sama閱讀 33,783評(píng)論 4 324
  • 正文 年R本政府宣布,位于F島的核電站弧圆,受9級(jí)特大地震影響赋兵,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜搔预,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評(píng)論 3 307
  • 文/蒙蒙 一霹期、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧拯田,春花似錦历造、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,333評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至鸭轮,卻和暖如春臣淤,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背张弛。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,559評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工荒典, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人吞鸭。 一個(gè)月前我還...
    沈念sama閱讀 45,595評(píng)論 2 355
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像覆糟,于是被迫代替她去往敵國(guó)和親刻剥。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容