【Android】自定義View-View繪制流程

Android 中 Activity 是作為應(yīng)用程序的載體存在,代表著一個完整的用戶界面拴孤,提供了一個窗口來繪制各種視圖口糕,當(dāng) Activity 啟動時譬胎,我們會通過 setContentView 方法來設(shè)置一個內(nèi)容視圖炫七,這個內(nèi)容視圖就是用戶看到的界面。那么 View 和 activity 是如何關(guān)聯(lián)在一起的呢 钾唬?

Android的UI層級繪制體系

5540252-67dc96d2b0f12b7d.png

上圖是View與Activity之間的關(guān)系万哪,先介紹一下上面這張圖

  • PhoneWindow:每個Activity都會創(chuàng)建一個Window用來承載View的顯示,Window是一個抽象類抡秆,PhoneWindow是Window的唯一實現(xiàn)類奕巍,該類中包含一個DecorView。
  • DecorView:最頂層的View儒士,該View繼承自 FrameLayout的止,它的內(nèi)部包含兩部分,一部分是ActionBar 着撩,另一部分ContentView诅福,
  • ContentView:我們 setContentView() 中傳入的布局,就在該View中加載顯示
  • ViewRootImpl:視圖層次結(jié)構(gòu)的頂部拖叙。一個 Window 對應(yīng)著一個 ViewRootImpl 和 一個DecorView氓润,通過該實例對DecorView進(jìn)行控制,最終通過執(zhí)行ViewRootImpl的performTraversals()開啟整個View樹的繪制薯鳍,

View的加載流程

  • 當(dāng)調(diào)用 Activity 的setContentView 方法后會調(diào)用PhoneWindow 類的setContentView方法
public void setContentView(@LayoutRes int layoutResID) {
    getWindow().setContentView(layoutResID);
    initWindowDecorActionBar();
} 
  • PhoneWindow類的setContentView方法中最終會生成一個DecorView對象
@Override
public void setContentView(int layoutResID) {
    if (mContentParent == null) {
         //在這里生成一個DecorView
        installDecor();
    } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        mContentParent.removeAllViews();
    }
    ...
}


private void installDecor() {
    mForceDecorInstall = false;
    //mDecor  為DecorView
    if (mDecor == null) {
        mDecor = generateDecor(-1);
        mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
        mDecor.setIsRootNamespace(true);
        if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
            mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
        }
    } else {
        mDecor.setWindow(this);
    }
    ...
 }
 
 
protected DecorView generateDecor(int featureId) {
   ...
   // 在這里直接 new 了一個DecorView
   return new DecorView(context, featureId, this, getAttributes());
} 
  • DecorView容器中包含根布局咖气,根布局中包含一個id為content的FrameLayout布局,Activity加載布局的xml最后通過LayoutInflater將xml文件中的內(nèi)容解析成View層級體系挖滤,最后填加到id為content的FrameLayout布局中崩溪。
protected ViewGroup generateLayout(DecorView decor) {
    //做一些窗體樣式的判斷
       ...
     //給窗體進(jìn)行裝飾
    int layoutResource;
    int features = getLocalFeatures();
    // System.out.println("Features: 0x" + Integer.toHexString(features));
    //加載系統(tǒng)布局 判斷到底是加載那個布局
    if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
        layoutResource = R.layout.screen_swipe_dismiss;
        setCloseOnSwipeEnabled(true);
    }  
...
    mDecor.startChanging();
    //將加載到的基礎(chǔ)布局添加到mDecor中
    mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
    //通過系統(tǒng)的content的資源ID去進(jìn)行實例化這個控件
    ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
    if (contentParent == null) {
        throw new RuntimeException("Window couldn't find content container view");
    }

} 

到此,Actvity的繪制完成

View的視圖繪制流程剖析

  • DecorView被加載到Window中

在ActivityThread的 handleResumeActivity() 方法中通過WindowManager將DecorView加載到Window中,通過ActivityThread中一下代碼可以得到應(yīng)征

final void handleResumeActivity(IBinder token,
        boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
      ...
      //在此處執(zhí)行Activity的onResume方法
    r = performResumeActivity(token, clearHide, reason);

    if (r != null) {
        final Activity a = r.activity;

        if (localLOGV) Slog.v(
            TAG, "Resume " + r + " started activity: " +
            a.mStartedActivity + ", hideForNow: " + r.hideForNow
            + ", finished: " + a.mFinished);

        final int forwardBit = isForward ?
                WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION : 0;
        boolean willBeVisible = !a.mStartedActivity;
        if (!willBeVisible) {
            try {
                willBeVisible = ActivityManagerNative.getDefault().willActivityBeVisible(
                        a.getActivityToken());
            } catch (RemoteException e) {
                throw e.rethrowFromSystemServer();
            }
        }
        if (r.window == null && !a.mFinished && willBeVisible) {
            //獲取window對象
            r.window = r.activity.getWindow();
            //獲取DecorView
            View decor = r.window.getDecorView();
            decor.setVisibility(View.INVISIBLE);
            //獲取WindowManager斩松,在這里getWindowManager()實質(zhì)上獲取的是ViewManager的子類對象WindowManager
            ViewManager wm = a.getWindowManager();
            WindowManager.LayoutParams l = r.window.getAttributes();
            a.mDecor = decor;
            l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
            l.softInputMode |= forwardBit;
            if (r.mPreserveWindow) {
                a.mWindowAdded = true;
                r.mPreserveWindow = false;
                 //獲取ViewRootImpl對象
                ViewRootImpl impl = decor.getViewRootImpl();
                if (impl != null) {
                    impl.notifyChildRebuilt();
                }
            }
            if (a.mVisibleFromClient && !a.mWindowAdded) {
                a.mWindowAdded = true;
                //在這里WindowManager將DecorView添加到PhoneWindow中
                wm.addView(decor, l);
            }
        } 

總結(jié):在ActivityThread的handleResumeActivity方法中WindowManager將DecorView添加到PhoneWindow中伶唯,addView()方法執(zhí)行時將視圖添加的動作交給了ViewRootImpl處理,最后在ViewRootImpl的performTraversals中開始View樹的繪制

ViewRootImpl的performTraversals()方法完成具體的視圖繪制流程

private void performTraversals() {

    if (!mStopped || mReportNextDraw) {
        ...
        int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
        int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
          ...
         // Ask host how big it wants to be
         //View繪制:開始測量 View的測量時遞歸逐層測量惧盹,由父布局與子布局共同確認(rèn)子View的測量模式抵怎,在子布局測量完畢時確認(rèn)副布局的寬高,
         //在此方法執(zhí)行完畢后才可獲取到View的寬高,否側(cè)獲取的寬高都為0
        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
       }
       
      
    if (didLayout) {
    //開始擺放岭参,該方法是ViewGroup中的方法反惕,例如 LinerLayout...
        performLayout(lp, mWidth, mHeight);
    }
    
    if (!cancelDraw && !newSurface) {
        //開始繪制,執(zhí)行View的onDraw()方法
        performDraw();
    }
} 

下面開始對performMeasure(),performLayout(),performDraw()進(jìn)行解析

  • performMeasure()
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
    Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
    try {
        mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    } finally {
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
} 

通過以上這段代碼,我們可以看到兩個重要的參數(shù) childWidthMeasureSpec演侯,childHeightMeasureSpec,這兩個Int類型的參數(shù)包含了View的測量模式和寬高信息姿染,因此在onMeasure()方法中我們可以通過該參數(shù)獲取到測量模式,和寬高信息,我們在onMeasue中設(shè)置寬高信息也是通過MeasureSpec設(shè)置悬赏,

 */
public static class MeasureSpec {
    //int類型占4個字節(jié)狡汉,其中高2位表示尺寸測量模式,低30位表示具體的寬高信息
    private static final int MODE_SHIFT = 30;
    private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

    /** @hide */
    @IntDef({UNSPECIFIED, EXACTLY, AT_MOST})
    @Retention(RetentionPolicy.SOURCE)
    public @interface MeasureSpecMode {}

    
    //如下所示是MeasureSpec中的三種模式:UNSPECIFIED闽颇、EXACTLY盾戴、AT_MOST 
    //UNSPECIFIED:未指定模式,父容器不限制View的大小兵多,一般用于系統(tǒng)內(nèi)部的測量
    public static final int UNSPECIFIED = 0 << MODE_SHIFT;
    //AT_MOST:最大模式尖啡,對應(yīng)于在xml文件中指定控件大小為wrap_content屬性,子View的最終大小是父View指定的大小值剩膘,并且子View的大小不能大于這個值
    public static final int EXACTLY     = 1 << MODE_SHIFT;
    //EXACTLY :精確模式衅斩,對應(yīng)于在xml文件中指定控件為match_parent屬性或者是具體的數(shù)值,父容器測量出View所需的具體大小
    public static final int AT_MOST     = 2 << MODE_SHIFT;

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

   //獲取寬高信息
    public static int getSize(int measureSpec) {
        return (measureSpec & ~MODE_MASK);
    }
     ...
} 

performMeasure()會繼續(xù)調(diào)用mView.measure()方法

 public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        boolean optical = isLayoutModeOptical(this);
        if (optical != isLayoutModeOptical(mParent)) {
            Insets insets = getOpticalInsets();
            int oWidth  = insets.left + insets.right;
            int oHeight = insets.top  + insets.bottom;
            //根據(jù)原有寬高計算獲取不同模式下的具體寬高值
            widthMeasureSpec  = MeasureSpec.adjust(widthMeasureSpec,  optical ? -oWidth  : oWidth);
            heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
        }
        ...
        if (forceLayout || needsLayout) {
            // first clears the measured dimension flag
            mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;

            resolveRtlPropertiesIfNeeded();

            int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
            if (cacheIndex < 0 || sIgnoreMeasureCache) {
                // measure ourselves, this should set the measured dimension flag back
                //在該方法中子控件完成具體的測量
                onMeasure(widthMeasureSpec, heightMeasureSpec);
                ...
            } 
         ...
    } 

從上述代碼片段中可以看到執(zhí)行到了onMeasure()方法怠褐,如果該控件為View的話畏梆,測量到此結(jié)束,如果是ViewGroup的話奈懒,會繼續(xù)循環(huán)獲取所有子View奠涌,調(diào)用子View的measure方法,下面以LinearLayout為例磷杏,繼續(xù)看

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

LinearLayout通過不同的擺放布局執(zhí)行不同的測量方法铣猩,以measureVertical為例,向下看

void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
    //獲取子View的個數(shù)
    final int count = getVirtualChildCount();
        ...
    //循環(huán)獲取所有子View
    for (int i = 0; i < count; ++i) {
        //獲取子View
        final View child = getVirtualChildAt(i);
        //調(diào)用子View的measure方法
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }
    ....
} 

至此茴丰,View的測量流程結(jié)束

View的layout流程分析

private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
        int desiredWindowHeight) {
  
        final View host = mView;
          // 在此處調(diào)用mView的layout()擺放開始
        host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
     } 

 /*  
 *@param l view 左邊緣相對于父布局左邊緣距離 
 *@param t view 上邊緣相對于父布局上邊緣位置 
 *@param r view 右邊緣相對于父布局左邊緣距離 
 *@param b view 下邊緣相對于父布局上邊緣距離 
 */  
public void layout(int l, int t, int r, int b) {
      ...

   //記錄 view 原始位置  
    int oldL = mLeft;
    int oldT = mTop;
    int oldB = mBottom;
    int oldR = mRight;

  //調(diào)用 setFrame 方法 設(shè)置新的 mLeft达皿、mTop、mBottom贿肩、mRight 值峦椰,  
  //設(shè)置 View 本身四個頂點位置  
  //并返回 changed 用于判斷 view 布局是否改變  
    boolean changed = isLayoutModeOptical(mParent) ?
            setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

 //第二步,如果 view 位置改變那么調(diào)用 onLayout 方法設(shè)置子 view 位置 
    if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
        //開始調(diào)用 onLayout  在此處根據(jù)子View的寬高及相關(guān)規(guī)則進(jìn)行擺放
        onLayout(changed, l, t, r, b);
          ...
            }
        }
    }
} 

View的Draw流程分析

private void performDraw() {
        ...
          //調(diào)用draw方法
        draw(fullRedrawNeeded);
        ...
    }
    
    
private void draw(boolean fullRedrawNeeded) {
        ...
     //View的繪制流程調(diào)用的   drawSoftware() 該方法
if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
    return;
}


private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
        boolean scalingRequired, Rect dirty) {
    final Canvas canvas;
       ...
    //初始化畫布
    canvas = mSurface.lockCanvas(dirty);
    ...
    //開始調(diào)用ViewGroup 和  View的draw方法
    mView.draw(canvas);
    ...
}


public void draw(Canvas canvas) {
 
    drawBackground(canvas);

    //ViewGroup  默認(rèn)是不會調(diào)用OnDraw方法的
    if (!dirtyOpaque) onDraw(canvas);

    //這個方法主要是ViewGroup循環(huán)調(diào)用 drawChild()進(jìn)行對子View的繪制

    dispatchDraw(canvas); 

}



protected void onDraw(Canvas canvas) {
} 

View的onDraw方法只是一個模版汰规,具體實現(xiàn)方式汤功,交由我們這些開發(fā)者去進(jìn)行實現(xiàn)

至此,View的繪制流程完畢

  • requestLayout重新繪制視圖

子View調(diào)用requestLayout方法溜哮,會標(biāo)記當(dāng)前View及父容器滔金,同時逐層向上提交,直到ViewRootImpl處理該事件茂嗓,ViewRootImpl會調(diào)用三大流程餐茵,從measure開始,對于每一個含有標(biāo)記位的view及其子View都會進(jìn)行測量述吸、布局忿族、繪制。

  • invalidate在UI線程中重新繪制視圖

當(dāng)子View調(diào)用了invalidate方法后,會為該View添加一個標(biāo)記位道批,同時不斷向父容器請求刷新错英,父容器通過計算得出自身需要重繪的區(qū)域,直到傳遞到ViewRootImpl中隆豹,最終觸發(fā)performTraversals方法椭岩,進(jìn)行開始View樹重繪流程(只繪制需要重繪的視圖)。

  • postInvalidate在非UI線程中重新繪制視圖

這個方法與invalidate方法的作用是一樣的璃赡,都是使View樹重繪判哥,但兩者的使用條件不同,postInvalidate是在非UI線程中調(diào)用鉴吹,invalidate則是在UI線程中調(diào)用。

最后

一惩琉、面試合集

在這里插入圖片描述

二豆励、源碼解析合集

在這里插入圖片描述

三、開源框架合集

在這里插入圖片描述
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末瞒渠,一起剝皮案震驚了整個濱河市良蒸,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌伍玖,老刑警劉巖嫩痰,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異窍箍,居然都是意外死亡串纺,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進(jìn)店門椰棘,熙熙樓的掌柜王于貴愁眉苦臉地迎上來纺棺,“玉大人,你說我怎么就攤上這事邪狞〉或颍” “怎么了?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵帆卓,是天一觀的道長巨朦。 經(jīng)常有香客問我,道長剑令,這世上最難降的妖魔是什么糊啡? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任,我火速辦了婚禮吁津,結(jié)果婚禮上悔橄,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好癣疟,可當(dāng)我...
    茶點故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布挣柬。 她就那樣靜靜地躺著,像睡著了一般睛挚。 火紅的嫁衣襯著肌膚如雪邪蛔。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天扎狱,我揣著相機(jī)與錄音侧到,去河邊找鬼。 笑死淤击,一個胖子當(dāng)著我的面吹牛匠抗,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播污抬,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼汞贸,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了印机?” 一聲冷哼從身側(cè)響起矢腻,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎射赛,沒想到半個月后多柑,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡楣责,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年竣灌,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片秆麸。...
    茶點故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡帐偎,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出蛔屹,到底是詐尸還是另有隱情削樊,我是刑警寧澤,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布兔毒,位于F島的核電站漫贞,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏育叁。R本人自食惡果不足惜迅脐,卻給世界環(huán)境...
    茶點故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望豪嗽。 院中可真熱鬧谴蔑,春花似錦豌骏、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至钦睡,卻和暖如春蒂窒,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背荞怒。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工洒琢, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人褐桌。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓衰抑,卻偏偏與公主長得像,于是被迫代替她去往敵國和親荧嵌。 傳聞我的和親對象是個殘疾皇子呛踊,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,722評論 2 345

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